25 Oct 2015

Let’s start off with yet another one of these ‘progress has been a little slow lately’ disclaimers. Recently I’ve had a few too many different things that have been sucking up time (mostly enjoyable things by the way :-), which means time spent on 2k14: The Game has been dialed back even further. From the slow pace of development and the dearth of updates it may seem progress has stalled, but that’s not the case. I’m still trudging along, slowly but surely ;-). With a bit of luck the amount of interesting things I can do with just a tiny bit of development time will go up exponentially with all the generic engine pieces falling into place.

The topic for this post is servo’s, a feature I’ve added over the past couple of weeks. It’s a nice and small feature that took minimal effort to implement, but will provide plenty of opportunities to add more dynamics to game.

What’s a ‘servo’ anyway?

In mechanical and engineering, the term ‘servo’ is typically used to refer to some kind of control system that incorporates sensor feedback to drive some actuator. Wikipedia page about servomechanisms says ;-). In electrical engineering, and specifically when dealing with radiographic controllers or robotics, servo is often used as a shorthand for ‘servo motor’: an actuator that (for example) changes the position of a control surface of an RC plane, some robot part, etc.

Neither of these two interpretations is exactly how I use the term for 2k14: The Game though, so we can safely ignore most of what the linked Wikipedia pages have to say ;-). In the context of 2k14: The Game, a servo is a bit of a mash-up between a mechanical servomechanism and a radio servo, without any form of feedback loop. For lack of a better word, I use ‘servo’ to indicate an object that generates values over time, according to some kind of periodic signal function. A good way to illustrate the servo concept is by taking a look at the class diagram for the implementation.

Implementation details

From the diagram the simplicity of the servo concept is immediately clear: we have a named object (K14Servo) that can be stepped with a time delta, at each step producing a one-dimensional signal value that changes according to a periodic signal function, as represented by the K14Signal classes.

The servo value is cached to prevent unnecessary re-calculation when the servo is queried multiple times without being stepped. To track the position at which the signal value was sampled, the servo stores a position value, which is normalized to the signal period, ranging between 0 and 1. To allow servo’s that only run during some preset interval, start and stop properties can be set. The start and stop timestamp are just a hint for the game engine to determine when to step the servo and when to destroy it, the K14Servo itself has no concept of the current ‘time of day’, it only knows the relative position at which the signal value was sampled.

The signal functions themselves are what determines the values produced by the servo, and are implemented by classes deriving from K14Servo. Every signal has a period in wall-clock time, which is used by the K14Servo class to normalize the (wall clock) time delta’s passed to its step method. To sample the signal, K14Signal subclasses have to provide a valueAtPosition method, which takes the normalized position at which to sample the signal. Just like the K14Servo class, K14Signal has no concept of the current time of day.

Signals can be combined into a compound signal, which concatenates the signal functions of its components and has a period that is the sum of the sub-signal periods. This way more interesting signals can be constructed, such as a block-wave (a ‘high’ uniform signal followed by a ‘low’ uniform signal), or a linear up-down signal.

On the K14Game side, a simple dictionary mapping servo names to K14Servo instances has been added, along with a method addServo to add a new servo to the game. Any servo with a start and stop interval that contains the current game timestamp is stepped on every iteration of the game loop. There’s no removeServo method: servo’s are automatically deleted when the game timestamp goes outside the servo interval. To create infinitely running servo’s, -inf and inf can be used as the servo start and stop values.

To allow Lua scripts to create and query servo’s, some binding code has been added to K14ScriptableGame. The createServo class of the Lua base class Game takes the servo name, start and stop values, and a Lua table that describes servo signal (or signals, in case of a compound), which are parametrized differently depending on the signal type. I’ll leave the details to the imagination of the reader. A method Game.queryServo(name) has been added to (surprise) query a named servo, returning NaN if the servo does not exist.

Last but not least, the state of all active servo’s has been added to K14GameSnapshot, and restoring a K14Game from a snapshot will re-create the servo’s to the exact state at the time the snapshot was made. This way action replays will have the same behavior when servo’s were active at the time the replay was captured.

Servo use cases

So what do we do with these servo’s? If you think about it for a while, there’s a surprising amount of use cases, basically anything that has a temporal component to it. I’ll list just the first few things that come to mind here, but I expect I will come up with many other uses along the way.

  • Controlling animation, particularly non-linear sprite frame cycling
  • Any kind of periodic visual effect, such as twinkling stars, pulsating colors, etc
  • Controlling the position and speed of entities, e.g. modulating a preset path with a sinusoidal signal to get a nice wiggle to their movement
  • Moving parts of the planet, for example creating some kind of hatch-like surface section that opens and closes along a linear up-down signal when some condition is triggered (shooting a button, for instance)
  • As a source of predictable semi-random sequences that can be shared between multiple entities and reliably stored/restored to a snapshot
  • To drive entity behavior, for example a turret that fires bursts of projectiles at angles following a signal function
  • This list is already pretty long and I still have plenty use cases left ;-)

To demonstrate how servo’s can be used, I added some Lua code to our test planet that creates a few servo’s and uses them in different ways. Specifically, I created a linear up-down signal that controls the color of the planet surface, and three debug entities (represented by white crosses) which move left to right on the screen, while the y-coordinate of each of them is set using a different servo (linear up-down, block wave, an sinusoidal):

All that was needed to add the customizations seen in the video were a few lines of Lua code. To illustrate how easy it is to create and use servo’s to add new functionality and behaviors, here’s the relevant Lua code snippets that add the customizations:

-- Initialize planet
function Planet0:init()

  -- ...

  local color_up = { type=Game.SignalType["LINEAR"], period=4, offset=0, inclination=1 }
  local color_down = { type=Game.SignalType["LINEAR"], period=8, offset=1, inclination=-1 }

  game:createServo("PULSATE_COLOR", { color_up, color_down }, 0, math.huge)

  local move_0_up = { type=Game.SignalType["LINEAR"], period=0.5, offset=0, inclination=1 }
  local move_0_down = { type=Game.SignalType["LINEAR"], period=0.5, offset=1, inclination=-1 }

  game:createServo("MOVE_0", { move_0_up, move_0_down }, 0, math.huge)

  local move_1_low = { type=Game.SignalType["UNIFORM"], period=0.5, value=-1 }
  local move_1_high = { type=Game.SignalType["UNIFORM"], period=0.5, value=1 }

  game:createServo("MOVE_1", { move_1_low, move_1_high }, 0, math.huge)

  local move_2 = { type=Game.SignalType["SINUSOIDAL"], period=1, phase=0, amplitude=1 }

  game:createServo("MOVE_2", { move_2 }, 0, math.huge)

  -- ...

-- Planet step function
function Planet0:step(dt)

  local pulsate_color = self:getGame():queryServo("PULSATE_COLOR")

  self:setTint({r=1, g=pulsate_color, b=1-pulsate_color, a=1})

  local dy_0 = self:getGame():queryServo("MOVE_0")
  local dy_1 = self:getGame():queryServo("MOVE_1")
  local dy_2 = self:getGame():queryServo("MOVE_2")

  self.debug_entity_0:setPosition({ x=-10 + (4*game:getElapsed()) % 20, y=12 + dy_0*2})
  self.debug_entity_1:setPosition({ x=-10 + (4*game:getElapsed() + 2) % 20, y=12 + dy_1*2})
  self.debug_entity_2:setPosition({ x=-10 + (4*game:getElapsed() + 4) % 20, y=12 + dy_2*2})

Future improvements

An obvious future improvement would be to add some way to add more interesting signal shapes. To prevent having to write custom K14Signal subclasses for every signal function, I’m thinking about adding some kind of signal function modifier, that will multiply either the value or the position of a signal with some other function. For instance, an ease-in or ease-out effect could be created by scaling the position of a linear signal by an exponential function, and a bounce effect could be created by scaling the value of a sinusoidal signal with some kind of decreasing function. I’ll have to think about this some more.

Another interesting improvement would be to keep some history of previous servo values, for example in a ring buffer. This way animation effects such as a particle trail behind an entity could be programmed by taking the last few samples of a sinusoidal servo. To make things even more interesting, multiple servo’s could be combined to produce an n-dimensional servo, for example to create a 3D vortex-effect by using the value of one servo as an x/y coordinate, and the other one as a z-coordinate. For our 2D game this may not be useful, but we could also a 2-dimensional servo to e.g. create circular paths for entities.

Next steps

The thing I would really like to start working on now is refactoring the planet representation, to allow creating more flexible planet topologies. Together with the servo system this should allow creating planets with more interesting gameplay logic, for example sections of the planet opening and closing in reaction to some kind event.

Development scoreboard

Implementing the servo classes was pretty straightforward and only took about 4 hours, for a total of ~242 hours. Native code SLOC count went up by 499 to 6403, which is a lot considering the low development effort, but can be explained because almost all of these lines are boilerplate for defining the signal classes and their serialization functions. Lua SLOC count remains unchanged at 649.