Обработка сущностей в игре - PullRequest
11 голосов
/ 06 ноября 2010

В качестве небольшого упражнения я пытаюсь написать очень маленький, простой игровой движок, который обрабатывает только сущности (перемещение, базовый ИИ и т. Д.)

Как таковой, я пытаюсь думать о том, как игра обрабатывает обновления для всех сущностей, и я немного запутался (возможно, потому, что я поступаю неправильно)

Поэтому я решил опубликовать этот вопрос здесь, чтобы показать вам мой нынешний образ мышления и посмотреть, сможет ли кто-нибудь предложить мне лучший способ сделать это.

В настоящее время у меня есть класс CEngine, который берет указатели на другие классы, которые ему нужны (например, класс CWindow, класс CEntityManager и т. Д.)

У меня есть игровой цикл, который в псевдокоде будет выглядеть следующим образом (в классе CEngine)

while(isRunning) {
    Window->clear_screen();

    EntityManager->draw();

    Window->flip_screen();

    // Cap FPS
}

Мой класс CEntityManager выглядел так:

enum {
    PLAYER,
    ENEMY,
    ALLY
};

class CEntityManager {
    public:
        void create_entity(int entityType); // PLAYER, ENEMY, ALLY etc.
        void delete_entity(int entityID);

    private:
        std::vector<CEntity*> entityVector;
        std::vector<CEntity*> entityVectorIter;
};

И мой класс CEntity выглядел так:

class CEntity() {
    public:
        virtual void draw() = 0;
        void set_id(int nextEntityID);
        int get_id();
        int get_type();

    private:
        static nextEntityID;
        int entityID;
        int entityType;
};

После этого я бы, например, создал классы для врага и дал ему спрайт, его собственные функции и т. Д.

Например:

class CEnemy : public CEntity {
    public:
        void draw(); // Implement draw();
        void do_ai_stuff();

};

class CPlayer : public CEntity {
    public:
        void draw(); // Implement draw();
        void handle_input();
};

Все это работало нормально только для рисования спрайтов на экране.

Но потом я столкнулся с проблемой использования функций, которые существуют в одной сущности, но не в другой.

В приведенном выше примере с псевдокодом do_ai_stuff (); и handle_input ();

Как вы можете видеть из моего игрового цикла, есть вызов EntityManager-> draw (); Это просто повторялось по entityVector и вызывало draw (); функция для каждой сущности - которая работала нормально, поскольку все сущности имеют draw (); функция.

Но потом я подумал, а что, если это игровая сущность, которая должна обрабатывать ввод? Как это работает?

Я не пробовал, но я предполагаю, что я не могу просто пройти через цикл, как я делал с функцией draw (), потому что у сущностей, подобных врагам, не будет функции handle_input ().

Я мог бы использовать оператор if для проверки entityType, например:

for(entityVectorIter = entityVector.begin(); entityVectorIter != entityVector.end(); entityVectorIter++) {
    if((*entityVectorIter)->get_type() == PLAYER) {
        (*entityVectorIter)->handle_input();
    }
}

Но я не знаю, как обычно люди пишут эти вещи, поэтому я не уверен, как лучше это сделать.

Я много писал здесь и не задавал никаких конкретных вопросов, поэтому поясню, что я ищу здесь:

  • Хорошо ли, как я выложил / спроектировал мой код, и это практично?
  • Есть ли лучший, более эффективный способ для обновления моих сущностей и вызова функций, которых могут не иметь другие сущности?
  • Является ли использование enum для отслеживания типа сущностей хорошим способом идентификации сущностей?

Ответы [ 5 ]

10 голосов
/ 06 ноября 2010

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

Обычно вы видите что-то вроде этого

class CEntity {
  public:
     virtual void draw() {};  // default implementations do nothing
     virtual void update() {} ;
     virtual void handleinput( const inputdata &input ) {};
}

class CEnemy : public CEntity {
  public:
     virtual void draw(); // implemented...
     virtual void update() { do_ai_stuff(); }
      // use the default null impl of handleinput because enemies don't care...
}

class CPlayer : public CEntity {
  public:
     virtual void draw(); 
     virtual void update();
     virtual void handleinput( const inputdata &input) {}; // handle input here
}

и затем менеджер сущностей проходит и вызывает update (), handleinput () и draw () для каждой сущности в мире.

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

Один - хранить , например, входные данные в глобальном (или как элемент глобального интерфейса, или как единое целое и т. Д.). Затем переопределите функцию update () врагов, чтобы они делали do_ai_stuff (). и update () игроков, чтобы он обрабатывал ввод, опрашивая глобальные.

Другим вариантом является использование некоторого варианта шаблона Listener , так что все, что заботится о вводе, наследуется от общего класса слушателей, и вы регистрируете всех этих слушателей с помощью InputManager. Затем менеджер ввода вызывает каждого слушателя по очереди каждый кадр:

class CInputManager
{
  AddListener( IInputListener *pListener );
  RemoveListener( IInputListener *pListener );

  vector<IInputListener *>m_listeners;
  void PerFrame( inputdata *input ) 
  { 
     for ( i = 0 ; i < m_listeners.count() ; ++i )
     {
         m_listeners[i]->handleinput(input);
     }
  }
};
CInputManager g_InputManager; // or a singleton, etc

class IInputListener
{
   virtual void handleinput( inputdata *input ) = 0;
   IInputListener() { g_InputManager.AddListener(this); }
   ~IInputListener() { g_InputManager.RemoveListener(this); }
}

class CPlayer : public IInputListener
{
   virtual void handleinput( inputdata *input ); // implement this..
}

И есть другие, более сложные способы сделать это. Но все эти работы, и я видел каждый из них в чем-то, что на самом деле поставляется и продается.

8 голосов
/ 06 ноября 2010

Вы должны искать компоненты, а не наследование для этого. Например, у меня в двигателе (упрощенно):

class GameObject
{
private:
    std::map<int, GameComponent*> m_Components;
}; // eo class GameObject

У меня есть различные компоненты, которые делают разные вещи:

class GameComponent
{
}; // eo class GameComponent

class LightComponent : public GameComponent // represents a light
class CameraComponent : public GameComponent // represents a camera
class SceneNodeComponent : public GameComponent // represents a scene node
class MeshComponent : public GameComponent // represents a mesh and material
class SoundComponent : public GameComponent // can emit sound
class PhysicsComponent : public GameComponent // applies physics
class ScriptComponent : public GameComponent // allows scripting

Эти компоненты могут быть добавлены к игровому объекту, чтобы вызвать поведение. Они могут связываться через систему обмена сообщениями, а вещи, которые требуют обновления во время основного цикла, регистрируют прослушиватель кадров. Они могут действовать независимо и могут быть безопасно добавлены / удалены во время выполнения. Я считаю, что это очень расширяемая система.

РЕДАКТИРОВАТЬ: Извинения, я немного уточню это, но я сейчас в середине чего-то:)

6 голосов
/ 06 ноября 2010

Вы также можете реализовать эту функцию с помощью виртуальной функции:

class CEntity() {
    public:
        virtual void do_stuff() = 0;
        virtual void draw() = 0;
        // ...
};

class CEnemy : public CEntity {
    public:
        void do_stuff() { do_ai_stuff(); }
        void draw(); // Implement draw();
        void do_ai_stuff();

};

class CPlayer : public CEntity {
    public:
        void do_stuff() { handle_input(); }
        void draw(); // Implement draw();
        void handle_input();
};
1 голос
/ 06 ноября 2010

В целом, ваш код в порядке, как отмечали другие.

Чтобы ответить на ваш третий вопрос: в коде, который вы нам показали, вы не используете тип enum, за исключением создания. Там это кажется нормальным (хотя мне интересно, не проще ли читать методы createPlayer (), createEnemy () и т. Д.). Но как только у вас есть код, который использует if или даже переключается, чтобы делать разные вещи в зависимости от типа, вы нарушаете некоторые принципы OO. Затем вы должны использовать возможности виртуальных методов, чтобы убедиться, что они делают то, что должны. Если вам нужно «найти» объект определенного типа, вы можете также сохранить указатель на ваш специальный объект игрока прямо при его создании.

Вы можете также рассмотреть возможность замены идентификаторов необработанными указателями, если вам просто нужен уникальный идентификатор.

Пожалуйста, рассмотрите это как подсказки, которые МОГУТ быть подходящими в зависимости от того, что вам действительно нужно.

1 голос
/ 06 ноября 2010

1 Небольшая вещь - зачем менять идентификатор организации?Обычно это константа и инициализируется во время построения, и вот так:

class CEntity
{ 
     const int m_id;
   public:
     CEntity(int id) : m_id(id) {}
}

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


Добавить ко всем

Самый простой способ - просто добавить все методы в базовый интерфейс и реализовать их какno-op в классах, которые его не поддерживают.Это может звучать как плохой совет, но это приемлемая денормализация, если существует очень мало методов, которые не применяются, и вы можете предположить, что набор методов не будет значительно расти с будущими требованиями.

Вы можетедаже реализовать базовый вид «механизма обнаружения», например

 class CEntity
 {
   public:
     ...
     virtual bool CanMove() = 0;
     virtual void Move(CPoint target) = 0;
 }

Не переусердствовать! Легко начать этот путь, а затем придерживаться его, даже когда он создает огромный беспорядоквашего кода.Это может быть приукрашено как «преднамеренная денормализация иерархии типов» - но в конце концов это просто хак, который позволяет быстро решить несколько проблем, но быстро причиняет боль при росте приложения.


True Type discovery

с использованием и dynamic_cast, вы можете безопасно привести ваш объект от CEntity к CFastCat.Если сущность на самом деле является CReallyUnmovableBoulder, результатом будет нулевой указатель.Таким образом, вы можете проверять объект на предмет его действительного типа и реагировать на него соответствующим образом.

CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
   fastCat->Meow();

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

// -----BAD BAD BAD BAD Code -----
CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
   fastCat->Meow();

CBigDog * bigDog = dynamic_cast<CBigDog *>(entity) ;
if (bigDog != 0)
   bigDog->Bark();

CPebble * pebble = dynamic_cast<CPebble *>(entity) ;
if (pebble != 0)
   pebble->UhmWhatNoiseDoesAPebbleMake();

Это обычно означает, что ваши виртуальные методы не выбраны тщательно.


Интерфейсы

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

class IMovable
{
   virtual void SetSpeed() = 0;
   virtual void SetTarget(CPoint target) = 0;
   virtual CPoint GetPosition() = 0;
   virtual ~IMovable() {}
}

class IAttacker
{
   virtual int GetStrength() = 0;
   virtual void Attack(IAttackable * target) = 0;
   virtual void SetAnger(int anger) = 0;
   virtual ~IAttacker() {}
}

Ваши разные объекты наследуются от базового класса и одного или нескольких интерфейсов:

class CHero : public CEntity, public IMovable, public IAttacker 

И снова вы можете использовать dynamic_cast для поиска интерфейсовна любом объекте.

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


Шаблон посетителя

The шаблон посетителя требует большого набора текста, но он позволяет добавлять функциональность к классам без изменения этих классов.

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

Это может справиться спрактически каждое требование к изменениям (при условии, что ваши сущности хорошо учтены).

(я только ссылаюсь на него, потому что большинству людей требуется время, чтобы обернуть вокруг него голову, и я бы не рекомендовал использовать его, если вы не испытали ограничений других методов)

...