This is the second part of my series on game engine design. I’ve spent some time throwing around various ideas about the principal entities that a game engine should be concerned with and how they relate to each other. I’m going to hold off on any class diagrams or specific examples for the time being. This is all a thought exercise until the next entry, where I’ll analyze some specific examples and start generating some pseudo code for how I would like those use cases to look from the game developer’s perspective.
Let’s get the obvious one out of the way first. Rules have a couple of parts that I plan to treat separately. Any rules, in its simplest form, consists of a trigger and a response. All rules can be broken down and expressed in this form, and in terms of responsibilities at the code level the division makes sense. These two concepts are troublemakers. They tend to need access to every system and object in your game to do their jobs, which gets messy fast.
A trigger’s responsibility is to recognize a scenario and report that recognition to the system. They must be informed of state changes and have the ability to poll systems for information.
A response has the responsibility of executing a change to the game’s entities. They require the ability to manipulate the relationship between objects and game systems, change object properties and potentially trigger non-game functionality (metrics reporting, client/server protocol, etc)
The Infamous GameObject
Making a Katamari of System Data
There’s a common problem in game development. You have various systems that each need their own object-specific data, and you have your game object. In most cases, the game object class rolls through your code like a Katamari picking up odds and ends from every minor subsystem and feature in your game. Avoiding the Katamari game object anti-pattern is probably one of the best things that a game engine can strive for. If this is allowed to happen, the game object class becomes monolothic and inevitable naming collisions result in awkward or confusing variable and method names. How often have you seen duplicate position variables to handle world coordinates vs screen coordinates?
Divide and Conquer
There’s actually no compelling reason to store all of the data for a single conceptual object in one class. It feels natural to associate a single software object with the single conceptual object that it represents, but it’s neither necessary nor wise. A better system is to create multiple views of an object. A PhysicsView might have a bounding box, mass, x-velocity, and y-velocity properties, for example. A PlayerStatsView class might hold the player’s health and energy. Neither system needs to be aware of the other or the data it uses.
We’ve separated our systems from each other and sequestered their related data, so we need a method to synchronize data between the Views which actually represent the same or related information. I’ve identified two ways of doing this which I’m planning to test out in my initial implementation.
If a value is encapsulated in an object, rather than being a native type that is passed by value, you can share that reference between the View classes. This solution performs well and is easy to implement, but it only works in situations where the value should be exactly the same from the perspective of both systems.
The second method of synchronization is through explicit synchronization classes which are fully aware of the two (or more) Views that they will synchronize. These are much less flexible in terms of how they are integrated. They must make an assumption about the order in which systems are updated. However, they are flexible in terms of how values can related to each other.
I have shied away from dynamic objects for a long time because I associate them with Actionscript 1.0 debugging nightmares. It’s time to dust it off and employ the technique where it makes sense. If we separate all of our object data, we still need a centralized way to access that data from responses and triggers without having to hunt around in a dozen places for it. Enter Flash’s “Atom” type, denoted by an asterisk (*).
If we store references to all of the views on the atomic object, we can still benefit from the speed advantage of strict typing, because any performance-sensitive system works with a strongly-type View class. At the same time, we don’t need to maintain a game object class with variables defined for every possible type of data that an object might need to hold.
An additional benefit is that every game object only has to carry the data that it actually needs with it. You won’t be stuck creating infinite subclasses of GameObject to add specific capabilities. Composition over inheritance.
Objects At Rest
It’s very important to note that with this system, objects have no functions to call. Views should not have any function either, aside from those which report state information. The great thing about this type of system is that when your objects have no behaviour, you can test the behaviour a lot more easily, and mix behaviours a lot more freely, just by pointing the appropriate system at your game objects.
Most of the time, games need to create several similar or identical objects. This is pretty straightforward if you’re just subclassing left and right, but if we’re using composition there’s a little more legwork involved. The game engine will need a templating system which can take care of creating the appropriate Views and assigning them to the game object, as well as applying any initial rules and plugging the game object into the appropriate systems.
That’s our cast of characters for the time being. It will be a little while before I can get to part three; crunch time is upon me for two projects at once and I want to keep working away on AScalpel, but I’ll have it up as soon as it’s ready.