Scorecard & gameplay events

04 May 2015

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:

  1. 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?

  2. 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.

  3. 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 the K14FuelPod step would be impossible without extending the K14FuelPod 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.

https://www.youtube.com/embed/PnZIYN1vEd8

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.