12

作为一个小练习,我正在尝试编写一个非常小的、简单的游戏引擎,它只处理实体(移动、基本 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(); 和句柄输入();

从我的游戏循环中可以看出,有一个对 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();
    }
}

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

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

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

5 回答 5

13

您已经非常接近大多数游戏的实际操作方式(尽管性能专家脾气暴躁的 Mike Acton经常抱怨这一点)。

通常你会看到这样的东西

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() 以便它通过轮询全局来进行输入处理。

另一个是使用监听器模式的一些变体,这样所有关心输入的东西都继承自一个通用的监听器类,然后你用一个 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..
}

还有其他更复杂的方法。但是所有这些工作,我已经在实际运输和销售的东西中看到了它们中的每一个。

于 2010-11-06T11:20:59.620 回答
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

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

编辑:抱歉,我会稍微充实一下,但我现在正处于一些事情的中间:)

于 2010-11-06T11:25:09.383 回答
7

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

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();
};
于 2010-11-06T11:03:15.667 回答
2

1一件小事——为什么要更改实体的 ID?通常,这是在构造过程中保持不变并初始化的,就是这样:

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

对于其他事情,有不同的方法,选择取决于有多少特定于类型的函数(以及你可以复制它们的程度)。


添加到所有

最简单的方法是将所有方法添加到基接口,并在不支持它的类中将它们实现为无操作。这听起来可能是个坏建议,但如果有很少的方法不适用,那么这是一种可接受的非规范化,并且您可以假设方法集不会随着未来的需求而显着增长。

你甚至可以实现一种基本的“发现机制”,例如

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

不要过度!以这种方式开始很容易,然后坚持下去,即使它会产生大量混乱的代码。它可以被美化为“类型层次结构的故意非规范化” - 但最终它只是一个 hack,可以让您快速解决一些问题,但当应用程序增长时很快就会受到伤害。


真类型发现

使用 and dynamic_cast,您可以安全地将对象从CEntityto 转换为CFastCat。如果实体实际上是 a 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 探测任何实体上的接口。

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


访客模式

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

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

这可以应对几乎所有的变更需求(前提是您的实体本身是经过充分考虑的)。

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

于 2010-11-06T11:35:13.263 回答
1

一般来说,正如其他人所指出的那样,您的代码还不错。

回答您的第三个问题:在您向我们展示的代码中,除了创建之外,您不使用枚举类型。那里似乎没问题(尽管我想知道“createPlayer()”,“createEnemy()”方法等是否更容易阅读)。但是一旦你的代码使用 if 甚至 switch 来根据类型做不同的事情,那么你就违反了一些 OO 原则。然后,您应该使用虚拟方法的力量来确保他们做他们必须做的事情。如果您必须“找到”某种类型的对象,您不妨在创建它时存储一个指向您的特殊玩家对象的指针。

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

请考虑这些提示,根据您的实际需要可能是合适的。

于 2010-11-06T11:38:45.350 回答