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)
-- ...
end
-- 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})
end
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.