Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling entities in a game

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?
like image 473
Lucas Avatar asked Nov 06 '10 10:11

Lucas


People also ask

What are entities in a game?

In this architecture, an entity is a general type for objects relevant to your game. Entities can represent objects that are crucial to gameplay, such as player and enemy characters, or objects that merely exist in the game world without interacting with the player, such as animated decorations in a level.

How does entity component system work?

An Entity Component System (ECS) architecture separates identity (entities), data (components), and behaviour (systems). The architecture focuses on the data. Systems transform the data from an input state to an output state by reading streams of component data, which are indexed by entities.


2 Answers

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.

like image 161
Crashworks Avatar answered Sep 18 '22 02:09

Crashworks


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)

like image 27
peterchen Avatar answered Sep 18 '22 02:09

peterchen