处理游戏中的实体

发布于 2024-09-30 21:38:12 字数 2264 浏览 0 评论 0原文

作为一个小练习,我试图编写一个非常小、简单的游戏引擎,只处理实体(移动、基本 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->draw(); 的调用。 这只是迭代了entityVector并调用了draw();每个实体的函数 - 由于所有实体都有一个draw(),所以效果很好;功能。

但转念一想,如果是一个需要处理输入的玩家实体怎么办? 这是如何运作的?

我还没有尝试过,但我认为我不能像使用draw()函数那样循环遍历,因为像敌人这样的实体不会有handle_input()函数。

我可以使用 if 语句来检查实体类型,如下所示:

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

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

我在这里写了很多,但没有提出任何具体问题,所以我将澄清我在这里寻找的内容:

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

As a small exercise I am trying to write a very small, simple game engine that just handles entities (moving, basic AI etc.)

As such, I am trying to think about how a game handles the updates for all of the entities, and I am getting a little bit confused (Probably because I am going about it in the wrong way)

So I decided to post this question here to show you my current way of thinking about it, and to see if anyone can suggest to me a better way of doing it.

Currently, I have a CEngine class which take pointers to other classes that it needs (For example a CWindow class, CEntityManager class etc.)

I have a game loop which in pseudo code would go like this (Within the CEngine class)

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

    EntityManager->draw();

    Window->flip_screen();

    // Cap FPS
}

My CEntityManager class looked like this:

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

And my CEntity class looked like this:

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

After that, I would create classes for example, for an enemy, and give it a sprite sheet, its own functions etc.

For example:

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

All of this worked fine for just drawing sprites to the screen.

But then I came to the problem of using functions which exist in one entity, but not in another.

In the above pseudo code example, do_ai_stuff(); and handle_input();

As you can see from my game loop, there is a call to EntityManager->draw();
This just iterated through the entityVector and called the draw(); function for each entity - Which worked fine seeing as all entities have a draw(); function.

But then I thought, what if it is a player entity that needs to handle input?
How does that work?

I haven't tried but I assume that I can't just loop through as I did with the draw() function, because entities like enemies won't have a handle_input() function.

I could use an if statement to check the entityType, like so:

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

But I don't know how people normally go about writing this stuff so I'm not sure of the best way to do it.

I wrote a lot here and I didn't ask any concrete questions, so I will clarify what I am looking for here:

  • Is the way I have laid out/designed my code ok, and is it practical?
  • Is there a better more efficient way for me to update my entities and call functions that other entities may not have?
  • Is using an enum to keep track of an entities type a good way to identify entities?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

抱猫软卧 2024-10-07 21:38:12

你已经非常接近大多数游戏的实际做法了(尽管脾气暴躁的性能专家 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() 函数,以便他们执行_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..
}

并且还有其他更复杂的方法来实现它。但所有这些都有效,而且我已经在实际发货和销售的产品中看到了它们中的每一个。

You're getting pretty close to the way most games actually do it (although performance expert curmudgeon Mike Acton often gripes about that).

Typically you'd see something like this

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
}

and then the entity manager goes through and calls update(), handleinput(), and draw() on each entity in the world.

Of course, having a whole lot of these functions, most of which do nothing when you call them, can get pretty wasteful, especially for virtual functions. So I've seen some other approaches too.

One is to store eg the input data in a global (or as a member of a global interface, or a singleton, etc). Then override the update() function of enemies so they do_ai_stuff(). and the update() of the players so that it does the input handling by polling the global.

Another is to use some variation on the Listener pattern, so that everything that cares about input inherits from a common listener class, and you register all those listeners with an InputManager. Then the inputmanager calls each listener in turn each frame:

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

And there are other, more complicated ways of going about it. But all of those work and I've seen each of them in something that actually shipped and sold.

只为守护你 2024-10-07 21:38:12

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

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

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

编辑:抱歉,我会稍微充实一下,但我现在正在做某事:)

You should look in to components, rather than inheritance for this. For example, in my engine, I have (simplified):

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

I have various components that do different things:

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

These components can be added to a game object to induce behavior. They can communicate through a messaging system, and things that require updating during the main loop register a frame listener. They can act independently and be safely added/removed at runtime. I find this a very extensible system.

EDIT: Apologies, I will flesh this out a bit, but I am in the middle of something right now :)

静谧幽蓝 2024-10-07 21:38:12

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

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

You could realize this functionality by using virtual function as well:

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();
};
暮色兮凉城 2024-10-07 21:38:12

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

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


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

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


访问者模式

访问者模式需要大量输入,但它允许您可以向类添加功能而不修改这些类。

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

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

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

1 A small thing - why would you change the ID of an entity? Normally, this is constant and initialized during construction, and that's it:

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

For the other things, there are different approaches, the choice depends on how many type-specific functions are there (and how well you can repdict them).


Add to all

The most simple method is just add all methods to the base interface, and implement them as no-op in classes that don't support it. That might sound like bad advise, but is an acceptabel denormalization, if there are very few methods that don't apply, and you can assume the set of methods won't significantly grow with future requirements.

You mayn even implement a basic kind of "discovery mechanism", e.g.

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

Do not overdo! It's easy to start this way, and then stick to it even when it creates a huge mess of your code. It can be sugarcoated as "intentional denormalization of type hierarchy" - but in the end it's jsut a hack that lets you solve a few problems quickly, but quickly hurts when the application grows.


True Type discovery

using and dynamic_cast, you can safely cast your object from CEntity to CFastCat. If the entity is actually a CReallyUnmovableBoulder, the result will be a null pointer. That way you can probe an object for its actual type, and react to it accordingly.

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

That mechanism works well if there is only little logic tied to type-specific methods. It's not a good solution if you end up with chains where you probe for many types, and act accordingly:

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

That usually means your virtual methods aren't chosen carefully.


Interfaces

Above can be extended to interfaces, when the type-specific functionality isn't single methods, but groups of methods. They aren#t supported very well in C++, but it's bearable. E.g. your objects have different features:

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

Your different objects inherit from the base class and one or more interfaces:

class CHero : public CEntity, public IMovable, public IAttacker 

And again, you can use dynamic_cast to probe for interfaces on any entity.

That's quite extensible, and usually the safest way to go when you are unsure. It's a bit mroe verbose than above solutions, but can cope quite well with unexpected future changes. Factoring functionality into interfaces is not easy, it takes some experience to get a feel for it.


Visitor pattern

The visitor pattern requires a lot of typing, but it allows you to add functionality to classes without modifying those classes.

In your context, that means you can build your entity structure, but implement their activities separately. This is usually used when you have very distinct operations on your entities, you can't freely modify the classes, or adding the functionality to the classes would strongly violate the single-responsibility-principle.

This can cope with virtually every change requirement (provided your entities themselves are well-factored).

(I'm only linking to it, because it takes most people a while to wrap their head around it, and I would not recommend to use it unless you have experienced the limitations of other methods)

将军与妓 2024-10-07 21:38:12

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

回答你的第三个问题:在你向我们展示的代码中,除了创建之外,你不使用类型枚举。看起来没问题(尽管我想知道“createPlayer()”、“createEnemy()”方法等是否不会更容易阅读)。但是,一旦您的代码使用 if 甚至 switch 来根据类型执行不同的操作,那么您就违反了一些面向对象的原则。然后,您应该使用虚拟方法的强大功能来确保它们执行其必须执行的操作。如果您必须“查找”某种类型的对象,您不妨在创建它时存储一个指向特殊播放器对象的指针。

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

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

In general, your code is pretty ok, as others have pointed out.

To answer your third question: In the code you showed us, you don't use the type enum except for creation. There it seems ok (although I wonder if a "createPlayer()", "createEnemy()" method and so on woudn't be easier to read). But as soon as you have code that uses if or even switch to do different things based on the type, then you are violating some OO principles. You should then use the power of virtual methods to assure they do what they have to. If you have to "find" an object of a certain type, you might as well store a pointer to your special player object right when you create it.

You might also consider replacing the IDs with the raw pointers if you just need a unique ID.

Please consider these as hints that MIGHT be appropriate depending on what you actually need.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文