Basic game structure is a topic that many people have trouble with, yet it somehow gets overlooked. In this lesson, I will show you exactly how to set up and structure code for commercial games.
We're going to use Leadwerks 3 in this lesson because it makes C++ game development faster, but the ideas here are applicable to any programming environment.
Direct vs. Component Programming
Although the component-based script system Leadwerks supports is a convenient way to get simple demos running quickly with Leadwerks, it can be limiting when you try to make a full game. Fortunately Leadwerks supports direct programming, in both C++ and Lua. This gives a lot more power and control than component-based systems. To really take advantage of this power, we need to understand some basic principles on how to set up and structure our game.
We start with a base class for all objects in our game. We'll call this the Node class, and derive it from the Leadwerks::Object class. The Node class is not an entity, but it has an entity as a member. Think of a Node as your own game object that is associated with an entity.
For this lesson we'll create an imaginary class called Foo derived from the Node class. The Foo class could represent an enemy, an NPC, a bullet, a grenade, or anything else. We can use the same structure for all of these things. The Foo class has one function called Update. This is where all our game code that updates that single instance of this class would go. This code could control the trajectory of a bullet, the AI of an enemy, or anything else. The point is all the code that controls that object is compartmentalized into this function, and it gets called over and over again, for each instance of the class.
In order to keep track of each instance of the Foo class, let's use a standard C++ list. This is listed in the header file as a static member, so that each instance of the class can access this list:
static std::list<Foo*> list;
Each instance of the Foo class will also have a list iterator so we can remove it from the list when it's deleted:
In the Foo() constructor, the object will add itself to the list of all objects in this class:
it = list.begin();
And in the destructor, we will use the iterator to remove the object from the list:
WARNING Removing the iterator from a list can cause a crash if this object is deleted while your code is iteratoring through a list. To avoid this problem, you can add the object to be deleted to a queue of objects to be deleted in your main application loop. However, this is beyond the scope of this lesson, which is only meant to convey a simple program structure.
Why do we need a list of all the instances of our Foo class? Well, this means we can now iterate through each one, at any point in our program. This is very powerful because it means we can create new instances of the Foo class at any time, and our game will adjust to keep them all running, without hard-coding a lot of specific behavior. Iterating through the list is done with the following code:
//First we declare an iterator so we can cycle through our loop
for (it = Foo::List.begin(); it!=Foo::List.end(); it++)
//The Foo object is gotten with (*it)
//Alternatively, you could declare a Foo* variable and set it to this value
//Foo* foo = *it;
This code should go somewhere in your main game loop. You'll end up with a loop like that for each class your game uses, if it's a class that needs to be continuously updated. (Some types of objects can just sit there until something happens to make them react. For those situations, I recommend using a collision callback or other means to activate them.) So your main loop will look something like this now:
We're going to do one more thing to make our code a little cleaner. Because we'll probably end up with a dozen or more classes by the time our game code is done, we can take that loop and put it into a static function:
You might wonder why I didn't just create a list in the Node class, and have an Update function there. After all, any class derived from the Node class could override that function, and a single loop could be used to update all game objects. There's two reasons we don't do that.
First, not all of our game objects need an Update function to be called each frame. Iterating through hundreds or thousands of unnecessary objects would hurt our performance for no good reason. We can put an Update function in a base Enemy class, however, and have both goblins and trolls use the same Update loop, when Enemy::UpdateEach() is called.
Second, we want to control the order and time at which each class is updated. Some classes work best when they are updated at the beginning of the loop. Some work best when they are updated between the call to World::Update() and World::Render(). It's different for each class, depending on what you make them do, and we want to leave room for ourselves to experiment and not get locked into a design that can't be easily changed when needed. We could try working around this by setting a priority for each class, so objects are updated in a specified order, but I wouldn't do this. In my opinion, this is the point where your structure is done and you should think about structuring the classes for your game, and filling in their code.
So what does the Foo:Update() function do that's so important, anyways? Foo::Update() presently does nothing, but it does everything. This is where your game code goes. We can use this structure for AI, bullets, rockets, explosions, enemies, tanks, planes, ninjas, pirates, robots, or giant enemy crabs that shoot laser beams out of their eyes. In fact we can also use the same structure for those laser beam objects the crab is emitting!
The main point of this is to show how to graduate from writing simple single-file demos, and to start thinking in terms of classes. Your game code should be written in such a way that it doesn't matter how many objects there are of each class, when they get created, or when they get destroyed. Each class Update() function is written from the point of view of that single object, looking out at the world around it. This simple concept can be used to make just about any type of game, from first-person shooters to MMOs.
The image for this article was provided by oppenheimer.