Обработка объектов в игре

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

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

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

В настоящее время у меня есть класс 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();
    }
}

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

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

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

Ответ 1

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

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

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..
}

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

Ответ 2

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

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

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

EDIT: Извиняюсь, я это немного испортил, но сейчас я нахожусь в центре чего-то:)

Ответ 3

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

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();
};

Ответ 4

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

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

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


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

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

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

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

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


Открытие True Type

и 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();

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


Интерфейсы

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

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 для проверки интерфейсов на любом объекте.

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


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

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

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

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

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

Ответ 5

В общем, ваш код довольно хорошо, как указывали другие.

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

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

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