A quick update, for a change :-). With only a few hours development time I was able to make some changes that have a relatively large visible impact on the playability of the game.
Scorecard rendering
Using the font label renderer I implemented last week, adding a very preliminary scorecard was a breeze. At this point the score card is just a few text labels that show the number of lives, game score, and player fuel level, but it instantly makes the game screen look more like… well… a game screen ;-)
To be able to position the labels for the scorecard, I added parameters to the
label renderer createLabel
method to specify X and Y reference points for the
label to create. The reference point values determine how the label position
is interpreted and can have values such as left, right, center (for the x
reference point) and top, bottom, center, baseline (for the y reference point).
When the label is rendered, the translation to apply to the label geometry can
be easily calculated from the label width and height.
At this point the the quads that make up the label text are re-submitted to the
GPU using glDrawElements
every frame, which is fine if the combined number
of quads over all visible labels is small, but still wasteful, especially if
the label geometry rarely changes. Somewhere down the line I will change the
label rendering code to use vertex buffer objects, and only re-submit the
label geometry when it changes.
Gameplay events
Something that had been sticking out like a sore thumb for quite a while was
the unclear separation between entity (local) and game (global) state
manipulation inside the entity-, and game update methods. Most of the game
logic was more or less arbitrarily sprinkled over the update methods of the
entity classes, with no way for other entities or the K14Game
instance itself
to observe them. For example, each entity implemented its own hit-detection to
remove itself from the game world when hit by a projectile, without
notifying the K14Game
instance of its demise, e.g to allow it to adjust the
player score.
There are multiple reasons why it’s desirable to keep a clear separation between local state manipulations such as changing the orientation of the player ship in response to user inputs, and global manipulations such as adding projectiles or removing entities:
-
Without centralized handling of global gameplay events, complex interactions between different entities can quickly becomes an ungodly mess of dependent code distributed over multiple classes. What if multiple entities of different types need to be removed from the game world in response to some condition occurring in the
step
method of a third entity, but only if some compound condition involving all three entities holds? Which of the three entity types implements the logic? Or do we distribute it over all three? What if a fourth entity type is added to which the same logic should also apply? -
Global state changes could trigger audio or video events, for example a sound may be played when a projectile is fired, or the screen may flash if an entity is hit. These are not the kinds of operations you want to implement inside the entity classes themselves.
-
Decentralized global state changes make it hard or impossible to introduce new game rules or change existing ones using scriptable
K14Game
logic. Suppose we would want to add a game rule where an extra life is awarded when the player picks up all fuel pods without accidentally destroying one by shooting it. Trying to implement this rule locally inside theK14FuelPod step
would be impossible without extending theK14FuelPod
class with global state that track for all removed fuel pods were shot or depleted. Now change the gameplay rule to only apply if the player also destroyed all turrets on the planet. You get the idea… ;-)
There are other reasons why centralized global state changes are The Right Thing™ to do, but the three reasons above should be enough to get the point across. What’s needed is an MVC-like architecture where entities are considered part of the model (M), the renderer is the view (V), and somewhere in between should sit a controller (C) to coordinate global state changes.
Naturally (this being an iOS application, after all), I already separated input
events, game loop control and view transitions from the game loop using an
UIViewController
, which in our case is a GLKit GLKViewController
. This controller
is not the right place for game logic though, it’s strictly a view controller in the
traditional MVC-pattern sense. It implements application-level logic, not gameplay,
and we would like to be able to swap it out for something different, e.g. a view
controller that implements a control scheme more suitable for keyboard and mouse. Instead
I chose to promote the K14Game
class from a simple data structure with a step
method
to update the K14Planet
, to a controller class through which all global game state
changes have to go.
For easy communication between model classes such as K14Entity
subclasses, and our
newfound controller class K14Game
, and to prevent blowing up the K14Game
public
interface with a separate method for every possible gameplay event, I decided to
use a simple message queue to which events represented by a new class K14GameplayEvent
can be posted using a method K14Game postEvent
. The event class is a very simple
data class that contains only an event type, the originating entity, and a dictionary
for additional event parameters. I didn’t subclass K14GameplayEvent
for different
event types, as the set of events is diverse, will grow in the future, and should
be extendable from a possible scripting layer later on.
The event queue is processed whenever the K14Game step
method is called, by means
of a single function with a switch statement that implements all global gameplay
logic. For the current set of gameplay events the event processing function looks
like this:
/** Process unhandled gameplay events. */
for (K14GameplayEvent *event in self.events)
{
switch (event.type)
{
case K14GameplayEventTypeEntityHit:
{
[event.origin kill];
// Increase game score if the projectile that killed the entity originated from the player
K14Entity *projectile_origin = event.parameters[@(K14GameplayEventParameterProjectileOrigin)];
if (projectile_origin.id == self.planet.ship.id)
{
self.player.score += event.origin.killScore;
}
break;
}
case K14GameplayEventTypeFuelPodDepleted:
{
K14FuelPod *fuel_pod = (K14FuelPod *) event.origin;
[fuel_pod kill];
self.player.score += fuel_pod.depletionScore;
break;
}
case K14GameplayEventTypePlayerHit:
{
if (self.player.lives > 0)
{
[self.planet.ship respawnAtPosition:self.initialShipSnapshot.position
angle:self.initialShipSnapshot.angle];
[self.planet.orb respawnAtPosition:self.initialOrbSnapshot.position
angle:self.initialOrbSnapshot.angle];
self.player.lives = self.player.lives - 1;
}
else
{
[self stop];
}
break;
}
case K14GameplayEventTypeProjectileLaunched:
{
float angle = [event.parameters[@(K14GameplayEventParameterAngle)] floatValue];
float velocity = [event.parameters[@(K14GameplayEventParameterVelocity)] floatValue];
NSString *projectileClass = event.parameters[@(K14GameplayEventParameterClassName)];
[self.planet createProjectileOriginatingFromEntity:event.origin angle:angle velocity:velocity className:projectileClass];
break;
}
case K14GameplayEventTypeOrbLifted:
{
K14Ship *ship = self.planet.ship;
K14Orb *orb = self.planet.orb;
orb.bodyType = K14EntityBodyTypeDynamic;
[ship joinWithEntity:orb usingDistanceJointWithMaxDistance:ship.tractorBeamRange];
break;
}
default:
break;
}
}
[self.events removeAllObjects];
As you can see from the snippet above, the event processing loop is also where the number of lives
remaining is decreased when the player is hit, and the game is stopped when no lives are left. Just
to illustrate what the ‘other side’ of the gameplay event system looks like, here’s the step
method
for the K14Turret
entity.
self.lastBulletFired += dt;
if (self.lastBulletFired > (1.0f / self.firingRate))
{
[self.planet.game postEvent:[K14GameplayEvent projectileLaunchedEventWithOrigin:self
angle:self.angle + M_PI_2
velocity:self.bulletVelocity
projectileClass:K14Bullet.entityClass]];
self.lastBulletFired = 0.0f;
}
[super step:dt];
An additional benefit of using and explicit, discrete representation of gameplay
events, is that we can easily wrap them in a K14ActionReplayEvent
and add them to
action replays. This is particularly helpful to be able to reproduce enemy entity
behavior such as turrets shooting, which may happen non-deterministically while playing.
Instead of serializing the conditions that determine such events (e.g. the random seed
at the beginning of the game), we can simply serialize the event itself.
At this point, there is still a grey area of gameplay events that is not handled
centrally, such as decreasing the capacity of a fuel pod while it is being tractored,
or starting the K14Reactor
countdown timer based on the reactor hit count. The occurrence
of these events should probably still be determined by the classes themselves to allow
customizable entity-specific behavior, but at the very least the entity should actualize
the non-local game state through a gameplay event. The fuel pod can decide if and by
how much it should decrease its capacity, but it should pass this information
to the K14Game
instance so it can increase the player fuel level, for example. I’m
even contemplating to take this reasoning even further, and even apply it to strictly
local state changes such as setting the player position and angle, applying thrust,
etc. This would not only allow action replays that don’t depend on simulating input
events, but it would also allow for recording entities that can change their local
properties based on other factors than input events, e.g. a turret that can move. I’m
still on the fence if I need and want to take the idea this far.
Video
The obligatory video to show off the scorecard with live score and fuel statistics. Never mind the ugly font and the lack of chrome, this is just a quick hack to see if the font rendering and game state update is working as expected.
Next steps
I’m not sure yet what to add next. Maybe I’ll extend the gameplay event handling and integrate it with the action replay functionality. Or maybe I’ll implement distance field rendering for font labels. I may even start experimenting with Lua integration, to be able to define planets using Lua scripts.
Development scoreboard
Everything discussed in this post took about 6 hours to implement, for a total of ~169 hours. SLOC count is now at 2456, an increase of 141 lines.