There
are a lot of articles about games. Most of these are about particular
aspects of a game like rendering or physics. All engines, however, have
a binding structure that ties all aspects of the game together. Usually
there is a base class (Object, Actor or Entity are common names) that
all objects in the game derive from, but very little is written on the
subject. Only very recently a couple of talks on game|tech have briefly
touched on the subject [Bloom], [Butcher], [Stelly]. Still, choosing a
structure to build your game on is very important. The end user might
not “see” the difference between a good and a bad structure, but this
choice will affect many aspects of the development process. A good
structure will reduce risk and increase the efficiency of the team.
When we started Shellshock: Nam ‘67 (SSN67)
at Guerrilla Games we were looking for a structure that would be
flexible enough to handle all of our ideas, yet strict enough to force
us into a structured way of working. The development of our first game Killzone
was already well underway so we had a good opportunity to look at their
structure and improve on it. After a couple of weeks the base structure
for our game had been designed and over the course of the next 2 years
a lot changed but our basic structure remained more or less the same.
The core design decisions we made at the start of SSN67 are:
The system needs to be (mostly) data driven
It should use the well known Model-View-Controller pattern
The game simulation should run at a fixed update frequency to ensure consistent behavior on all platforms (PS2, XBOX and PC)
In the remainder of this article these points will be worked out to show how they were implemented in SSN67.
Shellshock Nam ‘67
Entities
In SSN67
the base class for all game objects is called Entity. We make a clear
distinction between objects that have game state and those that don't.
For example, the static world geometry and its collision model are not
Entities, neither is a moving cloud in the sky. The player can't change
the state of these objects so they are not an Entity. An oil barrel,
for example, is an Entity because when it explodes it can harm the
player and therefore influences the game state.
Using
a definition like this makes it very easy to separate objects that are
important to the game from objects that are not. Our streaming system,
for example, can stream textures and other rendering data in and out
without affecting the game state. Another system that benefits is the
save game system. The state of an Entity is saved and on reload all
current Entities are destroyed and replaced by Entities from the save
game. Because all static geometry is unaffected saving and loading is
very quick and the resulting save game is small. To go back to our
example of a moving cloud: In SSN67 clouds are not Entities and therefore not saved. If you look carefully you can see this.
Most of the Entities in SSN67
need a position and orientation in the world so our Entity by default
contains a 4x4 matrix. For those Entities that do not need a position
we just accept the overhead.
Data Driven
Data driven systems use data instead of code to determine the properties of a system where possible. In SSN67
we used the EntityResource class as base class for Entity properties.
EntityResources are defined in a text file and edited by hand. The
following example shows what the configuration for an NPC could look
like:
CoreObject Class=HumanoidResource
{
Name = Ramirez
Model = models/soldiers/ramirez
Speed = 5
…
}
When
these text files are loaded by our serialization system the
corresponding objects are automatically created and each variable is
mapped onto a member variable. After an EntityResource is loaded we
validate the range of each variable and check if all variables together
form a correct configuration. If this is not the case the game will
display an error and refuse to run until the problem is corrected.
In SSN67
we do not allow Entities to be created without an EntityResource. Using
an EntityResource makes sure that a specific object acts the same in
all levels. This generally reduces the amount of bug fixing when the
code for an Entity changes, because there is no need to test every
level but only the EntityResources that are affected. We use a simple
folder structure to store our EntityResources with one object per file.
The
Factory pattern [DP] is used to create an Entity from its
EntityResource. This means that if you have an EntityResource you can
create a corresponding Entity ready to be used in game. We use the
factory mechanism, for example, in our Maya plug-in. Level designers
can create an Entity in a level by selecting the corresponding
EntityResource file. Maya uses the factory to create an Entity for it
and allows the level designer to position it. Entities that need
specific (dynamic) behavior are controlled from our scripting language
LUA [LUA]. The properties that can be set from LUA usually boil down to
giving high level commands to the AI so that most of the configuration
of an Entity is still contained in the EntityResource. Supporting a new
Entity class in Maya is as simple as recompiling the Maya plug-in.
Model-View-Controller
The
Model-View-Controller pattern [DP] is used in many areas. In a word
processor, for example, the Controller handles mouse and keyboard input
and translates these to simple actions. The Model manages the text and
executes the actions dictated by the Controller. The View is
responsible for drawing (a portion of) the text on screen and
communicates with the display drivers.
In
a game we can use the same pattern. In our case the Model is called
Entity, the View is called EntityRepresentation and the Controller is
called Controller. We will now discuss the roles of each of these
classes in game.
The Model (Entity)
Entities
try to keep a minimal state of the object. Any state that is only
needed for visualization is not part of the Entity. Most Entities in SSN67 are simple state machines.
Ideally
the Entity should not know about its animations but unfortunately most
animations affect the collision volume of the Entity and therefore the
game state. Still, we try to keep Entities and their animations as
loosely coupled as possible. A walking humanoid, for example, looks at
its EntityResource for its maximum walk speed and not at the walk
animation. The EntityResource precalculates the speed of the walk
animation and speeds up / slows down the animation based on the current
speed. This makes it possible to use a simple model for a walking
humanoid (linear movement) while the animation always matches the
current speed perfectly.
The View (EntityRepresentation)
The EntityRepresentation is responsible for drawing the Entity and all of its effects.
Examples of things that an EntityRepresentation manages are:
3D models
Particles
Decals
Screen filters
Sound
Controller rumble
Camera shake
An
EntityRepresentation can look at but not change the state of the
Entity. The current state of an Entity is usually enough to calculate
the state of the EntityRepresentation. In some cases this is not
practical however. For example, when a human gets hit we want a blood
spray from the wound. Simply looking at the previous health and the
current health does not tell us where the enemy was hit. For these
cases we use a messaging system. All messages derive from a class
called EntityEvent, which contains an ID that indicates the type of the
message. The getting hit message also contains the type and amount of
damage and where the hit occurred. The Entity fills in the structure
and sends it to a message handler in the EntityRepresentation. It won't
receive a response to this message so the communication is one way
only.
To
achieve a game object that looks realistic the timing of effects is
usually critical and needs a lot of code to manage. By separating this
code from the essential game logic we reduce the risk of bugs. Another
advantage is that if all objects follow the rules, the game state is
not dependent on any of the EntityRepresentations and the game can run
without them. This principle can be very useful for creating a
dedicated multiplayer server where graphical output is not needed.
The Controller
The
Controller provides abstract input for an Entity. For every Entity that
supports Controllers we create a base class that defines the
controllable parts of the Entity. From this version we can derive an AI
and a player version of the Controller. For example, a Humanoid has a
HumanoidController and we derive a HumanoidAIController and a
HumanoidPlayerController from this controller. The player version of
the controller can be further split up to provide mouse and keyboard
support (for PC) or joystick support (for XBOX and PS2).
The
role of the Controller is to specify what the Entity should do. The
Controller can't directly change the state of the Entity. Every update
the Controller is polled by the Entity and the Entity tries to follow
the Controllers instructions.
For
example, the Controller would never call a MoveTo() function on a
Humanoid but instead the Humanoid would poll the Controller's
GetDesiredSpeed() function and then try to reach this speed while
performing collision detection to make sure not to clip through walls.
The
Controller's functions are more abstract than IsButtonPressed() or
GetJoystickAxisX(), this to make it easier for the AI to use the
Controller.
To
make a Humanoid interact with a mounted gun, for example, the
HumanoidController implements a function called GetUseObject(). This
function is polled every update by the Humanoid. The AI uses reasoning
to determine if it is beneficial to use a specific mounted gun. When it
chooses to use a mounted gun it walks to the correct location and
returns the mounted gun through GetUseObject(). When the player stands
next to a mounted gun the HumanoidPlayerController detects that the
mounted gun is in range and handles the logic of the use menu. The HUD
takes care of drawing the menu. When the selection is made it is
returned through GetUseObject(). Next time the Humanoid polls the
HumanoidController it will start up the mount process. The
HumanoidController is disabled and the mounted gun receives a
MountedGunAIController or MountedGunPlayerController so that it can be
used.
A Simple Example
We will illustrate the Model-View-Controller mechanism with an example of a tank.
The tank is split up in an Entity (Tank), EntityRepresentation (TankRepresentation) and a Controller (TankController).
The
TankController has functions like GetAcceleration(),
GetSteerDirection(), GetAimTarget() and Fire(). The AI steers the tank
using a TankAIController, the player uses a TankPlayerController.
The
Tank keeps track of the physics state of the vehicle. It looks at the
controller and applies forces to the physical model and responds to
collisions. It keeps track of damage state, turret direction and
implements firing logic.
The
TankRepresentation draws the tank. It creates sparks particles and
corresponding sound when the tank collides. It plays the engine sound
and changes its pitch when the speed of the tank changes. It creates
smoke particles when the tank fires and produces track marks when the
tank is driving. It can also fine tune the position / rotation of the
tracks so that they follow the terrain exactly (without influencing the
collision volume of the tank).
An Example of Simplifying the Game State
When a human fires a gun, the bullets exit from the muzzle of the gun. In SSN67
this sometimes led to problems. Every weapon had its own aiming
animations that were slightly different. These differences led to
inconsistencies with our precalculated cover positions for the AI. The
AI would sometimes think that they could fire from a specific location
but when they got to that position and tried they would shoot into a
rock.
To
improve the situation we separated the logic of firing from the visual
effect (the tracers). The bullets now exit from a fixed offset (roughly
where the shoulder is) based only on the current position and stance of
the humanoid. The tracers are handled by the EntityRepresentation and
come from the muzzle of the gun (which is taken from the animation) and
move in the same direction as the bullet. They slowly converge towards
the real path of the bullet so that the impact point and time for the
bullet and tracer is the same.
For
the player it turned out that with our 3rd person camera it sometimes
looked like you could fire over a piece of cover where in reality you
couldn't. We tweaked the firing offsets for the player so that he was
shooting from eye height instead of shoulder height and fixed the
problem.
This
simple model in the Entity is easy to use for the AI and guarantees
consistency throughout many bits of code that are related to aiming and
firing. It trades complicated code throughout the system for
complicated code localized in the EntityRepresentation where it can
only influence the EntityRepresentation itself.
Excellent article! I wish this sort of thing was more prevalent in the industry... sadly, I tend to see kitchen sink objects more often than ones with this clean separation between model, view, and controller. I had been advocating the entity/controller separation for years, but I never thought to consider gamelogic representation as a separate concept from enginemodel representation... I like that better. Thanks for writing this.