12

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?
tshepang
  • 12,111
  • 21
  • 91
  • 136
Lucas
  • 10,476
  • 7
  • 39
  • 40

5 Answers5

13

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.

Crashworks
  • 40,496
  • 12
  • 101
  • 170
  • 4
    Man, the guy in that link was truly terrible. – Puppy Nov 06 '10 at 11:44
  • 6
    Maybe, but he's also why Ratchet & Clank never went below 60fps. – Crashworks Nov 06 '10 at 11:54
  • 1
    @DeadMG: well, he points out some real issues, in terms of performance and design. He also seems to have an irrational dislike of C++, and squeezed in some completely pointless rants as well (like the one against references). Still, filter out the bullshit, and the performance advice is pretty solid. – jalf Nov 06 '10 at 13:24
  • 1
    @jalf: Performance advice is only solid after a profile. I'm not claiming that code was the pinnacle of design - it was also pretty terrible. But the rant also wasn't exactly rational and pointing out the real problems. – Puppy Nov 06 '10 at 14:02
  • 1
    @DeadMG: profiling isn't a substitute for understanding your code though. Once you've profiled and found the bottlenecks, you **also** need to understand *why* it is slow. And before profiling, you also need some kind of understanding of how to structure your code and data, because you don't want to find yourself in a situation where the profiler just tells you "your code is screwed, start over from scratch". There are some fundamental assumptions that are pretty hard to fix once you've profiled them and found them to be a problem. – jalf Nov 06 '10 at 14:23
  • @jalf: I'm not saying that it is a substitute. What I am saying is that what he seems to be ranting about a good portion of the time most definitely seem to me to be micro optimizations best left to the compiler or only addressed after a very solid profile. – Puppy Nov 06 '10 at 14:32
  • 1
    @DeadMG: some of it, yes (but then remember that the code he's looking at was intended to be library code to be used by third parties in high-performance apps. And they won't be easily able to fix the library after profiling it. It has to be efficient *to begin with*. And many (not all) of his optimizations are specifically ones that the compiler **cannot** fix for you, and ones which, taken together, may add up to something like an order of magnitude difference in performance for the functions shown. (Of course, profiling is necessary to determine the performance impact on the app as a whole) – jalf Nov 06 '10 at 23:18
  • Keep in mind that the code is specifically intended for code which **does** typically end up being a performance bottleneck. Frustum culling and visibility tests *do* end up taking a lot of CPU time in games, so it may not be a bad idea to keep performance in mind when designing the code. ;) – jalf Nov 06 '10 at 23:21
  • @jalf: Maybe I just don't recognize the code in question, having never written anything to do with planes or frustrums or anysuch. – Puppy Nov 06 '10 at 23:33
  • @DeadMG: Mike is sort of coming from a different world regarding perf and cost. The tasks he looks at -- frustum cull, animation -- are the sort of operations that need to execute tens of thousands of units per millisecond. This sort of thing needs to be designed correctly from the beginning, because a profiler will quickly get you to a bad local maximum if you don't understand the core design problems. Also, he spends every day looking at a profiler, and has built a lot of experience as to what works and what's problematic. – Crashworks Nov 07 '10 at 01:08
  • @Crashworks: It's not just Mike that's banging the performance drum. When C++ became popular in console dev c 1998, cache misses weren't so deadly but nowadays, the CPU vs memory performance gap is widening so dramatically OO is feeling the brunt of the backlash ( right or wrong ) – zebrabox Nov 08 '10 at 00:10
  • 2
    @zebrabox: In Cell development we call -> the "cache miss operator". – Crashworks Nov 08 '10 at 00:16
  • @Crashworks : As someone who works for SCE, that made me smile :) – zebrabox Nov 08 '10 at 20:34
8

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 :)

Moo-Juice
  • 38,257
  • 10
  • 78
  • 128
7

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();
};
Flinsch
  • 4,296
  • 1
  • 20
  • 29
  • 1
    I'd prefer "update()" as a name compared to "do_stuff()", but I agree in your point! – Philipp Nov 06 '10 at 11:32
  • I would never name a function/method "do_stuff" or something similar. I just adopted the naming of "do_ai_stuff" to a more generic name on the fast. So, I agree with you, too! There are a lot more potentials to further improve the design anyway. ;) – Flinsch Nov 06 '10 at 11:56
2

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)

peterchen
  • 40,917
  • 20
  • 104
  • 186
  • dynamic_cast<>s can be problematic because they tend to be very, very slow ( a full microsecond or more apiece ). In a game where you have thousands of entities and 16.6 milliseconds to run a frame, it adds up. – Crashworks Nov 06 '10 at 11:55
  • @Crashworks: Thanks for pointing out - but I'd test my compiler before ruling them out to quickly. It should not be a problem for "true type discovery" above (unless your compiler pessimizes that case - in which case the mechanism is easy to implement). For a very complex hierarchy, a few thousand cycles may be possible, but with a limited mechanism limited set of entities, a much faster custom implementation is possible. (Yeah, it sucks....) Anyway, there's a reason there are different methods. – peterchen Nov 06 '10 at 12:20
  • I'm with Crashworks. I've never come across dynamic_cast used in console runtimes as it requires RTTI to be enabled and usually has too high a performance cost. Most console devs roll their own C++ customised reflection/introspection system – zebrabox Nov 07 '10 at 17:49
1

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.

Philipp
  • 11,549
  • 8
  • 66
  • 126
  • Out of interest, why is differentiating based on type a violation of OO principles? – Ell Jun 16 '11 at 23:45
  • in OO, you try to encapsulate behavior in objects. So instead of having if's and switch's, you rather call a virtual method and because it is within an object of the currently active type, this method can do what should be done. This is more flexible and more type-safe (consider adding a new type in the hierarchy, you get compile-time safety vs. runtime problems) – Philipp Jun 17 '11 at 13:06
  • I think understand the use of virtual methods, but why does allowing other objects to know another object's type violate encapsulation? Having an object store its type (or use dynamic_cast) just makes it easier to store an array of objects with a common ancestor, or is that just bad design? – Ell Jun 17 '11 at 18:46
  • don't know what you mean exactly. I just think that having 'if (x is of type A) foo1(); else if (x is of type B) foo2();' usually looks like bad design. – Philipp Jun 18 '11 at 10:44
  • Yes, it does look like bad design, but it can also make things easier at times. I'll look into it later :) – Ell Jun 18 '11 at 13:31