ClanLib logo

Designing a game engine, ClanLib API overview

Abstract:

The purpose of this document is to introduce techniques useful when designing a game engine.

Engine requirements

One of the most important aspects of writing a game is to have a solid framework, usually refered as the game engine. Although the requirements for the framework varies from game to game, there are still a lot of thing that can be generalized.

Basic framework requirements:

  • System to run the game.
  • System to draw the game.
  • Object to object communication.

In order to solve those problems, we first need two basic classes: a World and a GameObject interface.

The World is the main container class for the complete game. It simply represents everything that exist in the game. The World knows how to run the game, it knows how to draw the game, it knows how to do networking. Actually, everything that operates above invidual objects belong here.

The GameObject interface represents a generic object in the world. Typically it will contain virtual functions that can run the object, draw the object and, if it's network game, also function that is used to communicate across the net.

A minimal World and GameObject may look like this:

    class World
    {
    public:
    	World();
    	~World();
    
    	void show(); // draw the world.
    	void update(float time_elapsed); // run the world.
    	
    	CL_ResourceManager *get_resources();
    
    private:
    	std::list<GameObject *> objects;
    	Map *map;
    	CL_ResourceManager *resources;
    };
    
    class GameObject
    {
    public:
    	GameObject(World *world);
    	virtual ~GameObject() { return; }
    	
    	virtual void show()=0; // draw the object.
    	virtual void update(float time_elapsed)=0; // run the object.
    	
    	virtual bool get_destroy_flag() { return destroy_flag; }
    
    protected:
    	World *get_world();
    	virtual void set_destroy_flag() { destroy_flag = true; }
    	
    private:
    	World *world;
    	bool destroy_flag;
    };
    

This is the typical interface that is used in the ClanGame demo games. Often we just don't call the world World. For instance, in the pacman game it is just part of the application class.

When the world is asked to show itself (when show() is called), it will first ask the map to draw itself. Then it walk through the list of objects, asking each of them to draw themself (by calling show()).

The same tactic is used for the update() function. In the update cycle, it will check the destroy flag of each object, and, if set, it'll remove the object from the list and delete it.

Note that for your particular game, show() may accept all kinds of information - especially clipping information is often passed. Likewise there doesn't have to be difference between the map and other objects. It all depends on the particular game.

In this case, update() takes a float time_elapsed as parameter. It value indicates how long time has passed since the last time update() was called. If your game needs to be turn based, you will probably prefer to pass something else, or nothing at all.

Object to object communication.

Imagine the following situation. We're creating a Pacman game, there are ghosts and then the pacman. In the Pacman demo, this is implemented as two GameObject inheriated objects; Pacman and Ghost.

But there's a problem. What if we want the ghosts to talk to each other, how do we know what GameObject instance is a Ghost and which is a Pacman?

The solution is either to use runtime type information (RTTI), doing dynamic_cast's from GameObject to Ghost, or to add several lists to the world, each one belonging to their object:

    std::list<GameObject *> objects;
    std::list<Ghost *> ghosts;
    

Which method is best? Well, that's up to you to judge. :-)



Questions or comments, write to the ClanLib mailing list.