Wednesday, April 20, 2011

Bucket-based object management

After much work, I finally got PVS (Potential Visibility Set) culling working in the engine, so now I'm turning to game logic matters. I've been thinking about how best to manage game objects and the relationships between them. As one can imagine, it's not easy, but then again, what is?

In Game Challenge 18 (that 2D shooter thingy), my game object management was kind of hard to follow and disorganized. The game logic had two lists: one for players, one for all other game objects. Except not really. Any bullets spawned by firing weapons would get added there, but the weapons themselves were completely managed by the player (in fact, the game logic had no idea of their existence). So if you wanted to get the weapon or some info about it, you had to hunt down its owner. And you better hope the player properly manages the weapon's lifetime. I didn't have too many different kinds of game objects, but I imagine if I added more it would get even more convoluted.

So, this time around I decided to centralize things better. All game objects will be managed by the Scene class. This way, it's easy to get access to everything that needs updating or interacting with, and game objects aren't dependent on any particular class to call on their services. For example, weapons no longer need players to call Update() on them.

This does, however, bring up the problem of lifetime management and pointer validation. For example, let's say Player A was tracking Player B's health. Player A could store Player B's position in the game object list, and use it to call some function of Player B to get his health. Then Player B leaves the game. The game logic removes Player B from the list and frees his memory. That position in the list is then used to store another game object. What's going to happen when Player A tries to get Player B's health next frame? Unless you have something in place to counteract this problem, the answer is "who knows"?

One good way to solve this is through the use of handles. Instead of passing around game object pointers or positions in the list, give them handles instead. Now, if Player A wants to access Player B, he sends his request to the Scene class, passing in the handle. The Scene class will then be able to check if Player B still exists, and return NULL if he doesn't.

So I've made a structure that looks like this:

class ObjectHandle
{
  unsigned long objectID;
  unsigned long allocationID;
};

and a Scene method called GetGameObject(), which takes a handle and returns a game object pointer. The objectID is used as an index to the array. If there's no object there, it will be NULL at that location. Otherwise, there will be a valid game object at that location.

But what if the game object at that slot had been deleted, and the slot used to store another game object? You'd be getting a pointer to the wrong object! Here's where the allocationID variable comes in. When a game object gets added to the array, it gets a number that uniquely identifies that game object. If that object gets deleted and replaced with another one, it will have a different allocation ID assigned to it. Therefore, GetGameObject() will also compare the handle's allocation ID with that of the object in the slot. If they don't match, the original game object here has been replaced with another one, and NULL is returned to the caller.

The whole idea of centralizing game object management raises issues, however, when game objects' Update() functions are dependent on the results of other game objects' Update() functions. For example, imagine you had four soldiers riding in a vehicle, and they were all carrying weapons. You can't determine the position of the weapons until you know the position of the soldiers. And you can't do THAT until you know the position of the vehicle. If you do it in an arbitrary order, you may get the order (Weapon, Vehicle, Soldier), for example. With this order, the weapon would be updating based on data that's a frame old! The result would be the weapon's position lagging behind everything else by one frame. In order to get the proper behavior, we need (Vehicle, Soldier, Weapon). But how do we enforce this order?

One way to do it is to force update order at the Scene class level, based on type. The Scene will first call Update() on all the vehicles only, then all the soldiers only, then all the weapons only. This works for simpler games with only a few types of objects, but quickly gets very messy in more complex games. Why? Because now the Scene class not only needs to be aware of every type of game object, but iterate through each group separately! If you had 50 different kinds of game objects, your Scene would need 50 for loops just to update everything every frame! Not pretty.

An alternative (and more flexible) way is to sort objects into a small number of "buckets" (borrowing the term from a book called Game Engine Architecture). A bucket is essentially a list of game objects. The Scene::AddGameObject() function takes an optional parameter that specifies what bucket to add the object to. If you need to, you can later change the bucket with Scene::ChangeBucket(). Updating everything is simply a matter of iterating through each bucket and calling Update().

Why is this better than the type-based approach? Because the Scene class no longer needs to be aware of an object's type; it just blindly adds it to the bucket. You can get away with a small number of buckets, too (I'm starting with 5). The reason is that most objects aren't dependent on the updates of others. The weapon a player's holding isn't dependent on the rubber ducky another player is holding, so they can coexist in the same bucket. And I can't imagine an inter-object relationship more than 5 levels deep, can you? The disadvantage, however, is that game objects need to be aware of their position in inter-object relationships. If there's a weapon in the first bucket and a player picks it up, the player will need to explicitly move the weapon to the second bucket to preserve proper update order.

There's a lot more I want to say about how I'm doing game objects, but I'll close this post for now.

No comments:

Post a Comment