处理游戏中的实体

时间:2010-11-06 10:57:46

标签: c++ game-engine entity-system

作为一个小练习,我试图编写一个非常小巧,简单的游戏引擎,只处理实体(移动,基本AI等)。

因此,我试图考虑游戏如何处理所有实体的更新,我有点困惑(可能是因为我以错误的方式处理它)

所以我决定在这里发布这个问题,向你展示我目前的思考方式,看看是否有人可以向我建议一个更好的方法。

目前,我有一个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-&gt; 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();
    }
}

但我不知道人们通常会怎么写这些东西,所以我不确定最好的方法。

我在这里写了很多,我没有问任何具体的问题,所以我会澄清我在这里寻找的东西:

  • 我的设计/设计代码是否正常,是否实用?
  • 我是否有更好的方式更新我的实体并调用其他实体可能没有的功能?
  • 使用枚举来跟踪实体类型是识别实体的好方法吗?

5 个答案:

答案 0 :(得分:10)

你已经非常接近大多数游戏的实际操作方式(尽管性能专家谴责Mike Acton often gripes about that)。

通常你会看到像这样的东西

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 pattern上使用一些变体,以便关注输入的所有内容都继承自公共侦听器类,并使用InputManager注册所有这些侦听器。然后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..
}

还有其他更复杂的方法。但所有这些工作,我已经看到他们每个人实际发货和销售。

答案 1 :(得分:8)

您应该查看组件,而不是继承。例如,在我的引擎中,我有(简化):

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

可以将这些组件添加到游戏对象中以诱导行为。它们可以通过消息传递系统进行通信,而在主循环期间需要更新的东西会注册一个帧监听器。它们可以独立运行,并在运行时安全地添加/删除。我发现这是一个非常可扩展的系统。

编辑:道歉,我会稍微充实一下,但我现在处于中间状态:)

答案 2 :(得分:6)

您也可以通过使用虚拟功能来实现此功能:

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

答案 3 :(得分:1)

1 小事 - 为什么要更改实体的ID?通常情况下,这是恒定的并在构造期间初始化,就是这样:

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

不要过度使用!以这种方式启动很容易,即使它造成了大量的代码,也要坚持下去。它可以被糖衣作为“类型层次结构的故意非规范化” - 但最终它可以让你快速解决一些问题,但是当应用程序增长时会很快受到伤害。


真实类型发现

使用和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探测任何实体上的接口。

这是非常可扩展的,当你不确定时,这通常是最安全的方式。它比上述解决方案有点冗长,但可以很好地应对未来意想不到的变化。将功能保存到界面中很容易,需要一些经验来感受它。


访客模式

visitor pattern需要大量输入,但它允许您在不修改这些类的情况下向类添加功能。

在您的上下文中,这意味着您可以构建实体结构,但可以单独实施其活动。这通常在您对实体进行非常不同的操作时使用,您无法自由修改类,或者向类中添加功能会严重违反单一责任原则。

这可以应对几乎所有的变更要求(假设您的实体本身都是很好的因素)。

(我只是链接到它,因为大多数人需要一段时间才能绕过它,除非你遇到其他方法的限制,否则我不建议使用它)

答案 4 :(得分:1)

一般来说,你的代码非常好,正如其他人所指出的那样。

回答第三个问题:在您向我们展示的代码中,除了创建之外,您不使用类型枚举。似乎没问题(虽然我想知道“createPlayer()”,“createEnemy()”方法等是否更容易阅读)。但是只要你有代码使用if甚至切换到基于类型做不同的事情,那么你违反了一些OO原则。然后,您应该使用虚拟方法的强大功能来确保他们完成所需的操作。如果你必须“找到”某种类型的对象,你可以在创建时立即存储指向你的特殊玩家对象的指针。

如果您只需要一个唯一的ID,您也可以考虑用原始指针替换ID。

请根据您的实际需要,将这些视为可能适当的提示。