Freeze/unfreeze

25 Jan 2015

The year 2014 has come and gone, and I’m still here. Let’s start off with the following:

  • No, 2k14: The Game is not done yet
  • Yes, I will continue working on it
  • No, I will not be changing its name to 2k15: The Game ;-)

I’m not going to spend much time talking about the dearth of updates over the last few months this time, so without further ado, let’s talk about the stuff I did work on since the last update, which is quite a lot!

Preliminary player controls

With planet setup, the game loop and basic planet and entity rendering in place, the time was right to add some preliminary player controls, a first step towards actual gameplay. The first controls implemented were thrust, tractor beam, and ship rotation. The game engine does not support shooting yet, so I left it out for the time being.

Ship rotation was implemented using device tilt, by means of the iOS CoreMotion framework. Polling device attitude using CoreMotion is trivial, the framework does all the heavy lifting: combining the gyroscope, accelerometer and compass sensor data, filtering it to remove sensor jitter, and continuously calibrating the neutral position. From the application perspective, you simply query a shared (singleton) CMMotionManager instance, to get the pitch, yaw and roll angles of the device relative to its neutral position. The following figure taken from Apple’s Event Handling Guide for iOS illustrates how the pitch (rotation around X-axis), roll (rotation around Y-axis) and yaw (rotation around Z-axis) angles relate to device attitude:

The CMMotionManager can use polling or periodic callbacks, I used polling from the step selector of the K14GLViewController that drives the game main loop, which is more efficient and less of a hassle than periodic callbacks. For now, the device pitch (rotation along X axis) angle is copied directly to the tilt property of the K14Inputs instance accessible through the K14Game class.

The K14Ship step selector reads the tilt angle on each game update, and translates it to ship rotation. A few factors affect this translation: the tilt dead zone to clip small tilt angles to zero, the maximum tilt speed in degrees per second, and a minimum and maximum tilt scaling factor to distribute tilt speed over the the range of tilt angles outside of the tilt dead zone. In code, the tilt-to-rotation translation is implemented as follows:

-(void) step: (NSTimeInterval) dt
{
  // Handle rotation
  if (abs(self.planet.game.inputs.tilt) > TILT_DEAD_ZONE)
  {
    float tilt_factor = 
      TILT_FACTOR_MIN + 
      MIN(fabs(self.planet.game.inputs.tilt / TILT_SCALING), TILT_FACTOR_MAX - TILT_FACTOR_MIN);

    float tilt_angle = dt * TILT_SPEED * tilt_factor;
    
    self.angle = (game.inputs.tilt > 0.0f ? self.angle + tilt_angle : self.angle - tilt_angle);
  }

  // ...
}

Since my intention was just to have some simple proof-of-concept gameplay, I didn’t really spend much time fine-tuning the tilt parameters to improve the ship controls. Initial results suggest that the fact that I’m only using a single device attitude angle (pitch) may be a little too simplistic, as it’s very hard to figure out how to move the device to start or stop rotation, or to relate tilt speed to the amount of device motion. My gut feeling says I probably want to consider the device yaw angle and device acceleration to determine ship rotation. I will have to get back to this at a later stage.

Besides tilt controls, I also needed thrust and tractor beam controls. These are basically toggle inputs that can be activated and deactivated through the setInputActive selector of the K14Inputs input handler class. The only thing missing was the touch input handling to call this selector. I added a class K14TouchArea, which is created with a rectangle in normalized window coordinates (ranging from -1.0 to 1.0 in x and y directions), and the input ID of the toggle input (defined by an enumeration type). Any number of these touch areas can be added to a K14Game instance. The view controller (K14GLViewController) implements the basic UIKit touchesBegan and touchesEnded handlers to detect touches that start inside one of the touch areas, and call the K14Inputs setInputActive selector with the corresponding input ID, disabling the input when the touch ends. The renderer will also retrieve the list of touch areas, and render them as translucent boxes, to indicate where to touch the screen. Just like the tilt controls, the thrust and tractor beam controls haven’t been tuned to improve gameplay yet. No controls for shooting have been added either.

Freezing/unfreezing game state

What I initially set out to implement after the basic player controls was recording and playing back actual on-device gameplay. All the bits and pieces required to record and simulate input events have been in place since way back when I needed a way to reproduce gameplay features for testing. Adding real-world, on-device gameplay recording and playback should be a simple matter of capturing all control state before every game update, right? Not so fast…

In principle, the available infrastructure was sufficient to be able to register control state, and to ‘execute’ the replay by actuating the recorded input events in sequence, at the timestamps they were recorded. There was one big limitation though: since no planet state was stored with replays, they could only be started from t=0, ie: the initial planet state. A replay starting from an arbitrary point in time was impossible, and if the initial planet state depended on any external configuration, user input or random state, the replay would be useless, as it did not store anything about the planet itself or its entities. The most obvious solution to increase the utility of action replays would be to store the initial planet state along with the replay: surface, gravity vector, entities and their initial states, the player position, velocity, angle, everything.

The realization that virtually every piece of planet and game state could affect the reproducibility of the gameplay encoded by the action replay, turned my attention to another aspect of games on mobile devices: unexpected interruptions by other applications, phone calls etc, the possibility of the OS (in our case iOS) deciding to swap out your application to free memory for other processes, and the ability to freeze and restore game state to prevent losing progress. I hadn’t listed it on my to-do list yet, but freezing and unfreezing are absolutely essential for mobile games, unless you don’t mind pissing off the player by trashing their progress whenever the game is interrupted. So instead of concentrating only on restoring the initial planet state for replays, I went a step further and went for full game state freeze/unfreeze functionality.

When I implemented planet and entity rendering, I introduced the concept of planet- and entity snapshot classes: read-only carbon-copies of the planet and entity state required for rendering. The most natural thing seemed would be to re-use them for freezing and unfreezing game state. For rendering I only needed snapshot classes K14EntitySnapshot (containing entity state common to all entity types, such as position, size, sprite frame, etc), K14PlanetSnapshot (a snapshot of the planet surface) and K14ShipSnapshot (containing some additional ship-specific state required for rendering, such as whether the tractor beam is active). Extending this approach for full game state freeze/unfreeze functionality would imply adding snapshot classes for all entities, since almost every entity has its own set of custom properties. Fuel pods have a fuel level, turrets have a timestamp when they last fired, reactors have a hit count and countdown timer etc. This did not seem like an attractive solution, as it would effectively mean the hierarchy of snapshot classes would have to be a one-on-one mapping of the hierarchy of K14Entity classes, with lots of boilerplate, duplication of properties, etc. Every time a new entity type were to be added, a new snapshot class would be required, and every time an entity property were added, it would have to be added to its snapshot class as well. In addition to being cumbersome, this would also be very limiting for extending the set of entities using scriptable entities at some point in the future.

Instead, I decided to limit the set of snapshot classes to only the following four: K14GameSnapshot, K14PlanetSnaphshot, K14PlayerSnapshot and K14EntitySnapshot, ie: no class hierarchy of entity snapshots. To allow serializing/deserializing and rendering of entity-type specific properties, the K14EntitySnapshot class was extended with a customProperties dictionary, holding name-value pairs to which K14Entity subclasses can add custom properties when creating a snapshot of their state. The K14EntitySnapshot class itself only stores properties common to all entity types. The following code snippet of the K14Ship snapshot selector illustrates the idea:

-(K14EntitySnapshot *) snapshot
{
  K14EntitySnapshot *snapshot = [K14EntitySnapshot snapshotWithEntity:self];
  
  snapshot.customProperties[@(K14ShipPropertiesTractorBeamActive)] = @(self.tractorBeamActive);
  snapshot.customProperties[@(K14ShipPropertiesThrustActive)] = @(self.thrustActive);
  snapshot.customProperties[@(K14ShipPropertiesOrbAttached)] = @(self.orbAttached);
  snapshot.customProperties[@(K14ShipPropertiesOrbLifted)] = @(self.orbLifted);
  
  if (self.orbAttached)
  {
    snapshot.customProperties[@(K14ShipPropertiesOrb)] = [K14EntitySnapshot snapshotWithEntity:self.orb];
  }
  
  return snapshot;
}

For the actual serialization of game state, all snapshot classes implement the Cocoa NSCoding protocol, through initWithCoder and encodeWithCoder. To restore game state from an deserialized snapshot, the K14Game, K14Planet, K14Player and all K14Entity classes implement a factory method restoreWithSnapshot, which takes a snapshot instance, and the parent class for the instance in the game state hierarchy (ie: a K14Gameinstance for a K14Planet, or a K14Planet instance for a K14Entity).

Properties common to all entities (position, velocity, sprite frame, etc) are initialized from the snapshot by the K14Entity restoreFromSnapshot factory method, which besides property initialization also allocates the subclass instance by means of the initWithPlanet:id initializer all K14Entity subclasses have to implement. For base class K14Entity and subclass K14Ship, the restoreFromSnapshot factory methods look like this:

// K14Entity
+(instancetype) restoreFromSnapshot: (K14EntitySnapshot *) snapshot withPlanet: (K14Planet *) planet;
{
  K14Entity *entity = [[self alloc] initWithPlanet:planet id:snapshot.id];
  
  entity.lastUpdateTick = snapshot.lastUpdateTick;
  
  entity.state = snapshot.state;
  
  entity.position = snapshot.position;
  entity.angle = snapshot.angle;
  
  entity.velocity = snapshot.velocity;
  entity.angularVelocity = snapshot.angularVelocity;
  
  entity.spriteFrame = snapshot.spriteFrame;
  
  return entity;
}

// K14Ship
+(instancetype) restoreFromSnapshot: (K14EntitySnapshot *) snapshot withPlanet: (K14Planet *) planet
{
  K14Ship *ship = [super restoreFromSnapshot:snapshot withPlanet:planet];
  
  ship.thrustActive = [snapshot.customProperties[@(K14ShipPropertiesThrustActive)] boolValue];
  ship.tractorBeamActive = [snapshot.customProperties[@(K14ShipPropertiesTractorBeamActive)] boolValue];
  ship.orbAttached = [snapshot.customProperties[@(K14ShipPropertiesOrbAttached)] boolValue];
  ship.orbLifted = [snapshot.customProperties[@(K14ShipPropertiesOrbLifted)] boolValue];
  
  return ship;
}

Entity lifecycle callbacks

A chicken-and-egg problem occurs when restoring planet entities from a game snapshot, and initializing the game state to exactly reflect its state when it was frozen. Some of the entity classes have references to other planet entities, for example the K14Ship entity can have a reference to the entity the player was tractoring, or to the orb if it was lifted. Additionally, state such as the Box2D joint between the ship and the orb may have to be re-created after the entities are restored. When deserializing the ship entity though, the tractored entity or the orb might not have been restored yet. One workaround would be to enforce a strict deserialization order for entities, but that’s not the solution I’ve chosen. Instead, I introduced a callback method postCreate that entity classes can implement, which will be called automatically for all entities after a K14Planet instance is fully restored from a snapshot. In the future, additional of these lifecycle callback methods could be added, e.g. for pre-freeze processing.

A temporary level select screen

To be able to easily record, play and debug action replays, and to start moving towards a framework for jumping in and out of the planet simulation, I threw together a simple ‘level select’ screen. The level select screen is a simple UITableView with a custom UITableViewCell, backed by a view controller to fill the table view and react to touch interaction.

The table view cell is populated from the available K14Planet subclasses, one row for each planet class. I added a singleton array registeredPlanets to the base class K14Planet, to which the Class object for all derived planet classes are added. Planet registration is the responsibility of the planet subclasses right now, implemented by calling the static method registerPlanet from the subclass load selector, which is automatically called by Objective-C exactly once when the class is loaded from the application bundle. The level select view controller simply mirrors the registeredPlanets through its UITableViewData protocol selectors.

Video

Here’s a video to illustrate the freeze/unfreeze and replay capture/playback functionality in action. The video shows the temporary level select screen, which lists the two planets that are currently implemented. The second planet already has a stored replay, which was taken while the game running on the device: all movement and input action executed by the replay where captured from the iPhone tilt sensors and touchscreen. You could say this is the very first actual ‘gameplay’ video of the game ;-)

The first planet did not have an associated replay, before replaying the on-device gameplay the video shows starting the planet, capturing the gameplay, then replaying it.

Note that the video is only intended to show how freezing/unfreezing and replaying works, not as an example of the gameplay itself. There’s a few things going on that are obviously wrong from a gameplay perspective, such as the orb being hit by a turrent, and bounced against the planet surface without killing the player. Additionally, rendering is still very basic and lacking.

Next steps

From here, I’m going to add and properly implement additional gameplay features, for example shooting, lifting just the orb instead of the complete orb + pedestal assembly, etc. There are many other loose ends that will have to be fixed along the way, most importantly memory leaks when starting/stopping a planet, erratic behavior when running the same replay multiple times, and the list goes on. I’ll tackle them one by one whenever they start to annoy me too much ;-)

Development scoreboard

Implementing the NSCoding serialization, the factory methods to restore (unfreeze) a game from a snapshot, the required supporting functionality to execute replays, and the temporary level select screen, took quite a few hours. So many that I’ve actually lost track a little. I estimate it must have been around 30 hours, of which an not insignificant amount were spent trying to figure out how to auto-layout the level select table. This brings total development time to about 140 hours, rounded up for convenience. The SLOC count increased quite a bit as well, serializing all these entity properties requires a lot of code. Total SLOC count is now 1622.