After adding JSON-based planet definitions and support for multiple planet
components, the renderer needed to be adapted to be able to render multiple
component polygons. I initially implemented the communication of game state
between the game engine and the renderer by means of the snapshot classes
K14GameSnapshot
, K14PlanetSnapshot
and K14EntitySnapshot
, which were also
used for freezing/unfreezing game state and to represent the start state of
action replays. At the time, this seemed like a good idea: the goal was to
decouple the renderer from the game engine, to allow moving rendering to a separate
thread at some point in the future and maybe run the renderer and the game logic
at different frequencies. To achieve this goal, some form of double-buffered
game state was required, and since we already had to make carbon-copies of
the game state for action replays and freezing/unfreezing, why not use the
same set of snapshot classes for all these things?
So I set out to modify the K14PlanetSnapshot
class, replacing the previous
representation of the planet topology (a single edge-list defining the planet
surface), by a list of planet components represented as an array of
K14PlanetComponentSnapshot
instances, carbon-copies of each
K14PlanetComponent
. The planet component data included the component name,
vertices, edges, its attachment points, visual attributes such as its color,
etc. Basically anything you would need to render and/or serialize/deserialize
the planet component.
In the process of making the necessary changes, a feeling was creeping up to me
that I was repeating the same kind of code a little too often. Planet
components, for example, were now represented by the K14PlanetDefinition
class (for initial planet state), the K14PlanetComponentSnapshot
class (for
run-time read-only snapshots for rendering and freezing/unfreezing the game),
and by the K14PlanetComponent
class (the modifiable in-memory representation
of the component). To efficiently render the planet component, the renderer
would take the planet component snapshot, triangulate it, and create vertex and
index buffers that could be reused on subsequent rendering passes, adding even
more code that depended on the data representation of planet components.
Because game state (e.g. player stats for the score board) and
entities (sprites, visual effects) were also passed to the renderer by means of
the snapshot classes, the same observations applied to almost every other part
of the rendering code: many parts of the code were sharing the same lowest
common denominator data structures (the K14...Snapshot
classes), effectively
creating a tight coupling between unrelated components of the game. Any time
a property was added, removed or changed that was used by either the core engine
or the renderer, code in all these components needed to be touched.
All this was spoiling the fun, draining gumption, and
slowing progress. It was starting to become obvious that abusing the snapshot
classes for multiple purposes (serializing the game, and communicating game
state to the renderer) was a bad example of code reuse. The disadvantages
were far outweighing the advantage of reusing the snapshot classes for
multiple purposes:
- Any piece of game state that needed to be serialized and/or rendered has to be represented (at least) twice: in a run-time, in-memory class, and in a snapshot class.
- On every frame, all game state included in the snapshot classes has to be copied before sending the snapshot to the renderer, including game state irrelevant to rendering.
- The generic nature of the snapshot classes (carbon-copies of the game state), makes them unsuitable for incremental updates. If only 1% of the game state changed, a carbon copy of the complete game state still needs to be made for every frame rendered.
- Sharing data structures between (ideally unrelated) components promotes tight coupling, and actually decreases reusability of the more complex components (e.g. the renderer) in favor of reusing the simple components (the snapshot classes).
Render buffers
To untangle this knot before it got entangled even further, I decided to remove
the concept of ‘snapshot classes’ altogether, and to create a strict barrier
between the renderer and the game state. To pass state over this barrier, a
new class K14RenderBuffer
has been implemented, to which ‘render commands’
can be added that represent low-level rendering operations. Examples of render
commands are K14RenderRectangleCommand
, K14RenderSpriteCommand
or
K14RenderTextCommand
.
Render commands for specific rendering operations only carry state required for
rendering, and are otherwise completely decoupled from game engine state. For
example rendering a sprite using a K14RenderSpriteCommand
involves setting up
only the absolute minimum of state required by the renderer: the sprite
position, size, rotation, its texture, and in the future possibly properties
and flags to indicate visual effects and such. Likewise, a rectangle render
command contains just the rectangle position, size and color, and nothing more.
This means that, for example, the renderer does not know if it is rendering the
rectangle for a touch area, or a rectangle representing a projectile.
Obviously, the renderer needs some additional facilities to allow more complex
rendering, for example to enforce a certain drawing order for render commands.
For this purpose, the base class for all render commands (unsurprisingly called
K14RenderCommand
) adds a layer
property that indicates drawing order, a
type
property that indicates the command type, and a renderBits
bitfield
property that allows grouping and querying render commands in various ways.
Examples of render bits that can currently be set are
K14RenderCommandBitsEntity
or K14RenderCommandBitsOSD
, which are used to
filter sprite-, and rectangle render commands for entities and projectiles, and
to draw all OSD elements on top of everything else using normalized device
coordinates instead of world coordinates. Similarly, the renderer needs to be
able to uniquely identify and relate render commands between frames, to be able
to setup and re-use internal structures that only need to be updated if the
underlying render command changes. An example of this is the planet surface
polygon, which is currently rendered using an OpenGL vertex buffer object
(VBO), but rarely changes. For this purpose render commands have a string-type
id (name
) property that can be used to relate render commands between
different frames, and a timestamp
property that indicates when the command
was created.
The game loop and renderer use render command name and timestamp to implement a
simple copy-on-write approach: before each frame the view
controller that drives the game loop takes the render buffer for the previous
frame, and only updates it for render commands that should be changed or
removed. This is done in a way that ensures the renderer does not see the
changes until the old buffer can be swapped for the updated buffer, by copying
the K14RenderBuffer
instance, which effectively only copies the underlying container
holding pointers to the (immutable) render commands. Looking from the other
side, the renderer can use the command name and timestamp to relate them to
internal state that may be reused or needs to be released. For example a
polygon render command named planet_component_1
may be related to a polygon
VBO. As long as the command stays in the render buffer with the same timestamp, the
VBO can be rendered directly. If the same command shows up with a new timestamp
the VBO can be recreated or updated, and if the command disappears from the
render buffer, the VBO can be released.
In the future, the K14RenderBuffer
command may get some more advanced ways to
query and sort render commands based on a combination of the command type,
command bits and the render layer, which would hypothetically allow different
rendering backends (OpenGL ES, Metal, software rendering, …) to process the
render commands in a way that best matches the target device. I’m pretty sure I
will never actually need this for performance or portability, but it was easy
to add and may in the future simplify (or actually, further ‘dumb down’) the
renderer code. Right now the renderer is very ad-hoc, and encodes knowledge
about what you will always find in the render buffer for this game: it first
gets all polygon commands (which will always be planet components) and renders
them, then gets all sprite render commands (which will always be entities),
followed by all rectangle commands with the ‘entity’ bit set (always
projectiles), and last but not least all remaining commands (rectangle and
text) for the OSD elements. This could be simplified a lot using smart queries
and sorting, to draw all render commands from a simple switch
statement with
correct drawing order and minimal OpenGL state changes.
Serialization
Serialization and deserialization of game state is now implemented directly on
the K14Game
, K14Planet
and K14Entity
classes by means of NSCoding
. Where
previously, a K14...Snapshot
instance implementing NSCoding
was created from
the persistent game state, the full object graph including parent-child pointer
relations (e.g. game ↔ planet and planet ↔ entity) is now serialized to
an NSArchive
from the K14Game encodeWithCoder:
method, letting NSCoding
figure out how to deal with weak pointers (using decodeConditionalObject
) etc. This
also obviated the need for initWithSnapshot
initializers for the game, planet
and entity classes, which had to ‘parse’ the snapshot and recreate the
game state object graph from them. Instead, deserialization is now implemented
using the bog-standard initWithCoder
initializers you’ll find in any iOS/OS X
program using NSCoding
.
Entity-Component model for scripting
Having untangled rendering and serialization, I felt like extending my ‘refactoring run’ a little longer, and changed the way scriptability of games, planets and entities was implemented. I had been reading up on entity-component systems (ECS), an architectural pattern often used in game engines (e.g. Unity) and more complex, high-performance games, and borrowed some ECS concepts to simplify some things in the 2k14: The Game code.
Entity-Component Systems are typically employed to enforce decoupling of entity data and behavior, to allow adding (possibly runtime) pluggable data or behavior, or to be able to improve performance by means of improved data locality by being a better fit for data-oriented design. A full discussion of entity-component systems is outside the scope of this post, so I’ll refer to the linked articles for a more in-depth treatment. The executive summary is as follows: instead of using object-oriented techniques like inheritance and polymorphism to extend and/or specialize the properties or behavior of entities in a game engine, an ECS architecture prefers composition, defining entities by a list of their components. In its purest form, an ECS treats entities as ‘empty shells’ without any data or behavior, represented for example by just a numerical id. Any data or behavior is added to the entity by associating its id with a set of components that implement said data or behavior. For example a ‘position component’ could add a coordinate pair to indicate the location of the entity, a ‘physics component’ could add its physical properties and behavior, an ‘AI component’ could add pathfinding, a ‘graphics component’ sprites and animation, and so on. Obviously this is a bit of an oversimplification that skips over the intricacies of implementing an entity-component system. For example if and how components should be able to share and advertise data, how to add components to entities, how to query the components (capabilities) of entities, etc. The articles linked above provide much more details related to the implementation of entity-component systems. It should also be noted that it’s perfectly possible (and often desirable) to take a hybrid approach, and add the most basic properties and behaviors of entities (e.g. position, size, etc) directly to the entity data structure (which could be a struct, or a traditional OOP class), and only use components for more complex, optional or pluggable data/behavior.
While I wasn’t (and still aren’t) convinced a full-blown/pure ECS architecture
makes sense for the kind of game I’m making, the idea resonates with me, and I
did recognize some of the problems entity-component architectures try to solve
in the way I implemented the scripting layer of 2k14: The Game. Previously,
scripting abilities for games, planets and entities were implemented by
subclassing K14Game
, K14Planet
and K14Entity
, overriding any parts of
these base classes that should be handled by scripting code. The forced
dependency of the K14Scriptable...
classes on their base class introduced
annoying initialization order dependencies, control-flow ping-ponging between
the scriptable classes and the base classes when updating game state, the
requirement to track both the native class type (K14Planet
or
K14ScriptablePlanet
, for example) and the ‘subclass’ type (Lua class name for
scriptable entities, none for non-scriptable entities), etc. Another deficiency
of this architecture was that it would surely get me in even more trouble
later, if I ever wanted to mix and match scriptable and non-scriptable entities
with different (possibly optional) capabilities, requiring some form of
multiple inheritance (or actually, a facsimile of it, since Objective-C does
not natively support multiple inheritance). Not a nice prospect.
Anyway, I won’t bore anyone with more details, because none of this is concerned with problems related to the game itself, all these problems were self-imposed by limitations of the implementation language and an architecture that did not match the problem domain very well. Let’s focus on the code changes I made to fix or avoid code kludges resulting from insisting on using classic OOP inheritance.
I took a bit of an ‘ECS-lite’ approach to get rid of the derived
K14Scriptable...
classes, replacing them by ‘scripting component classes’ as
you could find them in a ‘real’ ECS architecture. I stopped there though, and
didn’t create components for other aspects of entities. Entities are still
represented by the relatively ‘rich’ OOP class K14Entity
, which directly
implements all entity data and behavior, except its scripting capabilities. The
scripting component is now always present, but in theory could be ‘attached’ to
an entity instance after creating it, if needed even in the middle of a game.
Appropriate hooks were added in e.g. the K14Entity step
method, to call the
script component step
method if an entity has one. When the entity is
serialized, advertised properties of the Lua class that implements the entity
are retrieved through the scripting component and sent to the NSCoder
as
key-value pairs. When deserializing the instance, the scripting component can
simply be re-created, creating a fresh Lua instance for the entity, which is
restored to its original state using the serialized Lua properties.
I applied the same concept to the K14Game
and K14Planet
classes, even
though they would have no part in a ‘real’ entity-component system, as they are
neither entities nor components. I also didn’t reduce references from the
scripting components to the scripted K14Entity
to numerical id’s as you would
usually do in a pure ECS, using some kind of ‘component manager’ to retrieve
entities by id. Instead, I simply used weak pointers from the scripting
component to its K14Entity
. Summarizing, basically the only thing I used
from ECS architecture is using composition instead of inheritance ;-)
It may seem like I spent a lot of time describing such a simple change from inheritance to composition for entities, but it cannot be understated in how many places this simplified the code. OOP is a useful programming paradigm and inheritance can be a powerful tool, but like any other powerful tool, when used blindly or careless, it will get you into trouble. I may have only used one aspect of ECS architectures (favor composition over inheritance) and still don’t benefit from any of its other advantages because I ignored others (data-oriented design, getting rid of a class to represent entities completely, replacing it by just a numerical id), but I already feel like the quality of one of the most important parts of the core engine of the game has improved a lot. I will definitely apply the lessons learned when adding additional core-engine capabilities to entities in the future.
Adding it all up
I’ve put together two simplified architecture diagrams to illustrate the ‘before’ and ‘after’ situations, which may be useful to put all of the above in perspective. First the ‘before’ architecture:
And the architecture after introducing render buffers, getting rid of the
snapshot classes, and replacing the scripting classes K14Scriptable...
by scripting component classes K14Lua...
:
These should speak for themselves if you’ve read all of the above, so I’ll leave most of their interpretation as an exercise for the reader ;-). One thing that deserves mention is the color-coding in the diagrams. Using the red (core engine), green (rendering) and blue (scripting) boxes, I’ve tried to indicate the areas where these three components of the game overlap (in functionality or representation), or have some kind of internal (practical, implementation-related) dependency on each other, for the ‘before’ and ‘after’ situations. The way these boxes are drawn is not very scientific, maybe not even completely correct, but they paint a relatively accurate picture of the entanglement between the main components in the ‘before’ situation, and the clean separation of concerns in the ‘after’ situation.
Next steps
Now that I feel the core parts of the game are implemented in a clean and sane way, I finally feel like I can safely start adding additional complexity, in the form of new features. As mentioned before, the first thing will be modifiable planet surfaces controlled by switches and servo’s. Another minor thing that has been on my list for quite a while is adding support for serializing and re-creating joints, when freezing the game, or when creating/replaying action replays. Right now, joints such as the ship-orb joint are not serialized, which means any replay that starts from a state where the orb is lifted will be useless, just like looping a replay that ends while the orb is lifted. I will try to fix this ‘on the side’.
Development scoreboard
Compared to the total development time so far, I feel like I spent an enormous
amount of time pulling the old code apart and re-assembling it without the
snapshot classes and the K14Scriptable...
game, planet and entity subclasses.
I estimate about 20 hours, but I could be an hour or two off. I prefer to err
on the safe side, so I’ll round up, and quote total development time at ~275
hours. Native code SLOC count went up by 769 lines compared to before I
implemented planet definitions, for a total of 7172. Most
of the extra code can be attributed to reading, parsing and serializing planet
definitions. I was a little disappointed with this number, as I had hoped that
removing the snapshot classes would actually reduce the SLOC count so much it
would offset the line count added by new functionality, but as it turns out the
boilerplate required to implement the render buffer and the render commands is
about the same amount of code as was removed by deprecating the snapshot
classes. Lua SLOC count is at 628, down from 649, which can be attributed to
removing some debug/test code, nothing new was added.