суббота, 24 сентября 2011 г.

[Slick] Статья номер 4. Игра, основанная на сущностях (Entity).

В этой статье рассмотрим способ построения игры из набора сущностей.

 Что такое сущность (Entity)

 Сущность в игре - это объект, существующий в созданном игровом мире. Это означает, что практически всё в игровом мире является сущностями, начиная от игрока и заканчивая таблицей рекордов. Некоторые сущности могут быть видимыми, другие могут быть подвижными, но все они (даже невидимые) являются частями игрового мира.


 Для чего нужны сущности

 Если вы никогда не пробовали делать большие игры, то вероятнее всего вы, не долго думая, создадите очень большой метод update/render, в котором для управления отрисовкой/обновлением будет использовано большое количество конструкций if-then-else, чем обеспечите себе большую головную боль. Помещая всё, что есть в игре, в сущности мы решаем эту проблему.
 Но не спешите определить все сущности в отдельных классах - это не самая лучшая идея.

  Сценарий 1. Давайте сделаем всё в разных классах.

 То есть, мы создаём интерфейс Entity, от которого будем наследовать все игровые объекты. Допустим, нам нужна некоторая функциональность, например, способность объекта перемещаться, для чего мы создаём новый интерфейс, наследуя его от Entity, и уже на его основе создаём классы для таких объектов, как автомобиль, пуля или игрок.
 Затем мы должны будем добавить еще функциональности в класс игрока, и еще... И в конце мы получим целый "клубок" из функциональностей. Очевидно, что многие из унаследованных функциональностей никогда не будут задействованы.

  Сценарий 2. Компоненты.

 В нашем случае мы создадим простой класс Entity, который будет содержать несколько псевдо-индивидуальных компонентов.
 Каждый компонент будет отвечать за определённую часть общей функциональности Entity; например, будет компонент позиционирования, который отвечает за хранение информации о положении Entity, компонент перемещения, отвечающий за применение некоторых правил перемещения объекта и передающий информацию в компонент позиционирования, компонент отрисовки, который, по информации полученной из изменённого компонента позиционирования, рисует объект в новой позиции. Компоненты названы псевдо-индивидуальными, потому что иногда они должны взаимодействовать друг с другом для выполнения общей работы. Возникает вопрос: зачем же нам нужны именно компоненты? Допустим, вы захотели поменять метод управления перемещением вашего объекта с клавиатуры на мышь. При использовании данной системы вы просто меняете компонент, отвечающий за перемещение, не меняя ничего больше. Также, когда вы разработаете еще несколько компонентов, вы сможете использовать их в других компонентах, вместо того, чтобы переносить код этого компонента в другие классы.

 Сущности состоящие из компонентов

  Хорошо, теперь пора приступать к использованию компонентов в программе.

 Давайте объявим компонент и сущность. Сущность - это нечто, что существует в игровом мире. Она может быть видима или невидима. Она может взаимодействовать с миром, а может быть абсолютно независима от него. Компонент - это небольшая часть функциональности сущности, которая либо взаимодействует, либо не взаимодействует с другими компонентами, при этом подразумевается, что любой компонент может быть использован самостоятельно, и не требует знания о других компонентах (правда, это не является обязательным условием). Небольшое замечание перед там, как перейти к рассмотрению кода: в качестве ID у меня используется строка, если вам это не подходит (по причине уменьшения быстродействия), можете поменять по своему усмотрению.

 Компонент

 Каждый компонент должен знать своего владельца и должен иметь только одного владельца. Каждый компонент должен реализовывать определённый интерфейс. Пример такого интерфейса смотрите ниже.

package com.foxhole.engine.component;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.state.StateBasedGame;
 
import com.foxhole.engine.entity.Entity;
 
public abstract class Component {
 
    protected String id;
    protected Entity owner;
 
    public String getId()
    {
        return id;
    }
 
    public void setOwnerEntity(Entity owner)
    {
     this.owner = owner;
    }
 
    public abstract void update(GameContainer gc, StateBasedGame sb, int delta);
}

 Как вы можете видеть, это очень простой интерфейс, в котором доступен только один метод - update. Также видно, что компонент знает своего владельца и может непосредственно влиять на него.

 Сущность

 Итак, что такое сущность (Entity)? Вот перечень простых вещей, которые должна иметь сущность.

 Во-первых, сущность будет знать своё положение, поворот и масштаб в игровом мире. Во-вторых, будет список компонентов, которые создадут функциональность сущности. В-третьих, сущность будет иметь два известных компонента, используемых для ускорения выполнения, это компонент отрисовки и компонент обработки столкновений (конечно, если он необходим).

package com.foxhole.engine.entity;
 
import java.util.ArrayList;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
import com.foxhole.engine.component.Component;
import com.foxhole.engine.component.render.RenderComponent;
 
public class Entity {
 
    String id;
 
    Vector2f position;
    float scale;
    float rotation;
 
    RenderComponent renderComponent = null;
 
    ArrayList<Component> components = null;
 
    public Entity(String id)
    {
        this.id = id;
 
        components = new ArrayList<Component>();
 
        position = new Vector2f(0,0);
        scale = 1;
        rotation = 0;
    }
 
    public void AddComponent(Component component)
    {
        if(RenderComponent.class.isInstance(component))
            renderComponent = (RenderComponent)component;
 
        component.setOwnerEntity(this);
 components.add(component);
    }
 
    public Component getComponent(String id)
    {
        for(Component comp : components)
 {
     if ( comp.getId().equalsIgnoreCase(id) )
         return comp;
 }
 
 return null;
    }
 
    public Vector2f getPosition()
    {
 return position;
    }
 
    public float getScale()
    {
 return scale;
    }
 
    public float getRotation()
    {
 return rotation;
    }
 
    public String getId()
    {
     return id;
    }
 
    public void setPosition(Vector2f position) {
 this.position = position;
    }
 
    public void setRotation(float rotate) {
        rotation = rotate;
    }
 
    public void setScale(float scale) {
 this.scale = scale;
    }
 
    public void update(GameContainer gc, StateBasedGame sb, int delta)
    {
        for(Component component : components)
        {
            component.update(gc, sb, delta);
        }
    }
 
    public void render(GameContainer gc, StateBasedGame sb, Graphics gr)
    {
        if(renderComponent != null)
            renderComponent.render(gc, sb, gr);
    }
}

 В этом примере, сущность сама по себе не имеет чего-либо очень сложного, на самом деле подход Сущность-Компонент прост с виду, а вся остальная работа выполняется компонентами.

 Компонент отрисовки - это особый вид компонента, реализуемый следующим классом:

package com.foxhole.engine.component.render;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.state.StateBasedGame;
 
import com.foxhole.engine.component.Component;
 
public abstract class RenderComponent extends Component {
 
    public RenderComponent(String id)
    {
 this.id = id;
    }
 
    public abstract void render(GameContainer gc, StateBasedGame sb, Graphics gr);
}

 Это простой компонент, который позволяет вам использовать метод render. Не правда ли, всё очень просто? Теперь необходим небольшой пример, как его использовать.

 Создание простого тестового примера

 А теперь давайте сделаем повтор первого урока (01 - A Basic Slick Game), там где самолёт и ландшафт. Для этого мы должны просто добавить в наши сущности два новых компонента: один компонент для отрисовки изображения и другой для реализации движения самолёта.

  TopDownMovementComponent


 Этот компонент должен копировать движение с видом сверху, которое было описано в уроке "самолёт и ландшафт".

package com.foxhole.engine.component.movement;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Input;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
import com.foxhole.engine.component.Component;
 
public class TopDownMovement extends Component {
 
 float direction;
 float speed;
 
 public TopDownMovement( String id )
 {
  this.id = id;
 }
 
 public float getSpeed()
 {
  return speed;
 }
 
 public float getDirection()
 {
  return direction;
 }
 
 @Override
 public void update(GameContainer gc, StateBasedGame sb, int delta) {
 
  float rotation = owner.getRotation();
  float scale = owner.getScale();
  Vector2f position = owner.getPosition();
 
  Input input = gc.getInput();
 
        if(input.isKeyDown(Input.KEY_A))
        {
         rotation += -0.2f * delta;
        }
 
        if(input.isKeyDown(Input.KEY_D))
        {
         rotation += 0.2f * delta;
        }
 
        if(input.isKeyDown(Input.KEY_W))
        {
            float hip = 0.4f * delta;
 
            position.x += hip * java.lang.Math.sin(java.lang.Math.toRadians(rotation));
            position.y -= hip *java.lang.Math.cos(java.lang.Math.toRadians(rotation));
        }
 
  owner.setPosition(position);
  owner.setRotation(rotation);
  owner.setScale(scale);
 }
 
}

 Как вы можете видеть, этот компонент не заботится ни о чём, кроме обработки движения. Этот компонент может быть присоединён как к простому изображению, так и к 3D-модели, то есть, к чему угодно. Он просто определяет текущую позицию сущности и изменяет её в соответствии с правилами перемещения при виде сверху.

 ImageRenderComponent


 Этот компонент делает следующее: получает текущие позицию, масштаб и угол поворота от своей сущности и применяет их к известной картинке.

 То есть, этот компонент берёт на себя заботу об изображении. Где бы не находилась родительская сущность, какой бы ни был её масштаб и угол поворота это компонент должен знать всё это и соответствующим образом отображать картинку.

package com.foxhole.engine.component.render;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
public class ImageRenderComponent extends RenderComponent {
 
 Image image;
 
 public ImageRenderComponent(String id, Image image)
 {
  super(id);
  this.image = image;
 }
 
 @Override
 public void render(GameContainer gc, StateBasedGame sb, Graphics gr) {
  Vector2f pos = owner.getPosition();
  float scale = owner.getScale();
 
  image.draw(pos.x, pos.y, scale);
 }
 
 @Override
 public void update(GameContainer gc, StateBasedGame sb, int delta) {
  image.rotate(owner.getRotation() - image.getRotation());
 }
 
}

 Чтобы получить изображение, двигающееся по правилам вида сверху достаточно было добавить компонент перемещения и компонент отображения картинки. Здорово, не правда ли?

 Окончательный код


 Вот повтор первого урока с использованием новых сущностей.

package slick.path2glory;
 
import org.newdawn.slick.AppGameContainer;
import org.newdawn.slick.BasicGame;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Vector2f;
 
import com.foxhole.engine.component.movement.TopDownMovement;
import com.foxhole.engine.component.render.ImageRenderComponent;
import com.foxhole.engine.entity.Entity;
 
public class Intermission1SlickBasicGame extends BasicGame{
 
    Entity plane = null;
    Entity land = null;
 
 
    public Intermission1SlickBasicGame()
    {
        super("Slick2D Path2Glory - SlickBasicGame");
    }
 
    @Override
    public void init(GameContainer gc)
   throws SlickException {
     land = new Entity("land");
 
        land.AddComponent( new ImageRenderComponent("PlaneRender", new Image("/data/land.jpg")) );
 
        plane = new Entity("plane");
        plane.AddComponent( new ImageRenderComponent("PlaneRender", new Image("/data/plane.png")) );
        plane.AddComponent( new TopDownMovement("PlaneMovement") );
        plane.setPosition(new Vector2f(400, 300));
    }
 
    @Override
    public void update(GameContainer gc, int delta)
   throws SlickException
    {
     land.update(gc, null, delta);
     plane.update(gc, null, delta);
    }
 
    public void render(GameContainer gc, Graphics gr)
   throws SlickException
    {
     land.render(gc, null, gr);
     plane.render(gc, null, gr);
    }
 
    public static void main(String[] args)
   throws SlickException
    {
         AppGameContainer app =
   new AppGameContainer( new Intermission1SlickBasicGame() );
 
         app.setDisplayMode(800, 600, false);
         app.start();
    }
}

 Как можно заметить, здесь есть пара изменений, во-первых в инициализации

public void init(GameContainer gc)
   throws SlickException {
     land = new Entity("land");
 
        land.AddComponent( new ImageRenderComponent("PlaneRender", new Image("/data/land.jpg")) );
 
        plane = new Entity("plane");
        plane.AddComponent( new ImageRenderComponent("PlaneRender", new Image("/data/plane.png")) );
        plane.AddComponent( new TopDownMovement("PlaneMovement") );
        plane.setPosition(new Vector2f(400, 300));
    }

 Как вы можете видеть, мы просто создаём сущности, а затем, как в ЛЕГО, мы добавляем к ним компоненты, каждый из которых добавит новую функциональности для данной сущности.

 Также вы можете видеть, что компонент отображения картинки задействован и в ландшафте и в самолёте. Это возможно из-за того, что этот компонент не делает ничего, кроме вывода статической картинки.

 Наконец, ещё одно необходимое уточнение, так как напрямую из кода примера этого не видно. В методе update нам достаточно всего лишь вызвать методы update каждой из сущностей, а в методе render - одноимённые методы сущностей. Предвижу ваш вопрос: А что, если будет сотня сущностей? Что ж, в этом случае достаточно все сущности поместить в массив, после чего появится возможность перебирать в цикле все элементы массива и вызывать у каждого объекта соответствующий метод. Или же изучите следующий учебный материал про SlickOut.

 Ну вот и всё... Теперь вы знаете простой способ сборной функциональности для игровых объектов.

 В следующих учебных материалах мы добавим больше функциональности в эту систему, добавив больше типов перемещения, компоненты, реализующие искусственный интеллект, скрипты и, конечно же, будет проверка столкновений между объектами.

Источник: сайт Wiki-документации проекта Slick: http://slick.cokeandcode.com/wiki/doku.php?id=entity_tutorial

2 комментария:

  1. Воу, чертовски круто!
    Пользуясь этим приёмом можно идеально структурировать проект, а это практически бесценно, если планируется его поддержка или доработка!
    Жаль, что таких годных статей не особенно много на просторах сети

    ОтветитьУдалить