SDLx::Tween - SDL Perl XS Tweening Library
# simple linear tween
use SDLx::Tween::Timeline;
$sdlx_app = SDLx::App->new(...);
# timeline gets its ticks from the SDLx app controller move handler
$timeline = SDLx::Tween::Timeline->new(app => $sdlx_app);
$xy = [0, 0]; # tween the position stored in this array ref
# create tweens from the timeline, setting the target ($xy), the
# duration (1 second), and the final requested value ([640, 480])
$tween = $timeline->tween(on => $xy, t => 1_000, to => [640, 480]);
# tween will not do anything until started
$tween->start;
# xy will now be tweened between [0, 0] and [640, 480] for 1 second
# and then the tween will stop
$sdlx_app->run;
# tween accessors
$ticks = $tween->get_cycle_start_time;
$bool = $tween->is_active;
$ticks = $tween->get_duration;
# tween methods
$tween->set_duration($new_duration_in_ticks); # hasten/slow a tween
$tween->start($optional_ideal_cycle_start_time_in_ticks);
$tween->stop;
$tween->pause($optional_ideal_pause_time_in_ticks);
$tween->resume($optional_ideal_resume_time_in_ticks);
$tween->tick($now_in_ticks); # called internally by timeline or SDLx app
# tweening an integer get/set accessor
$tween = $timeline->tween(
on => [radius => $circle], # set $circle->radius
t => 4_000, # tween duration
from => 100, # initial value
to => 300, # final value
round => 1, # round values before setting
bounce => 1, # reverse when repeating
forever => 1, # continue forever
ease => 'p3_in_out', # use easing function
);
# tweening 2D position using a non-linear path, repeat 4 times
$tween = $timeline->tween(
on => [xy => $circle], # set values in a xy array ref field
t => 4_000, # tween duration
to => [640,480], # final value, initial taken from $xy
repeat => 4, # repeat tween 4 times
path => 'sine', # use a sine path
path_args => { # sine path needs path_args
{amp => 100, freq => 2},
}
);
# tail behavior makes one position follow another at given speed
# the behavior makes the tail follow the head
# unlike other tweens, tails only allow the following 3 contructor args
$tail = $timeline->tail(
speed => 50/1000, # advance a distance of 50 pixels a sec
head => $head, # position to follow array ref
tail => $tail, # position to set array ref
);
SDLx::Tween
is a library for tweening Perl SDL elements. It lets you to move game objects (GOBs) around in various ways, rotate and scale things, animate sprites and colors, make GOBs spawn at a given rate, and generally bring about changes in the game over time. It lets you do these things declaratively, without writing complex SDLx::Controller
move_handlers()
.
See "WHY" and "FEATURES" for an introduction to tweening, or continue for the technical details.
The timeline is the tween factory. It is created with an SDLx::App
:
$timeline = SDLx::Tween::Timeline->new(sdlx_app => $app);
Now you can create tweens through the timeline, and control the cycle of all the timeline controlled tweens through the timeline: start/stop/pause/resume
. You can create as many timelines as you like to control different tween groups.
A tween is a behavior of some game property vs. time. It sets the target game property using a proxy, according to some path, at a time computed by an easing function, which obeys certain cycle rules, for a given duration.
A tween is created from a timeline, which passes tick()
to the tween from the SDLx::App
move_handler
. Once you create the tween you can control its cycle: set ideal cycle times, start/stop, pause/resume, and change cycle duration.
The simplest tween uses the method proxy and calls a game method with the tweened value. It uses the default linear path to tween a value between a range given by the range
arg. Speed of change will be constant, because the tween uses the default linear easing function.
The simplest tween, animating a turret turning 180 degrees in 1 second:
$tween = $timeline->tween(
on => [angle => $turret],
t => 1_000,
range => [0, pi],
);
A tween has a duration and a cycle start time. These define its cycle. Several tween constructor arguments help to control the tween cycle:
- t
-
integer
Ticks (milliseconds) duration of the tween from start to stop. - forever
-
boolean
Repeat the cycle forever, restarting on each cycle completion. - repeat
-
integer
Repeat the cyclen
times, then stop. - bounce
-
bool
If the cycle is repeating, on each cycle completion reverse the tween direction, bouncing the tween between its edge points.
Cycle control methods:
- start/stop/pause/resume
-
These all take an optional ideal event time in SDL ticks, see "ACCURACY" for more info. The difference between
start/stop
andpause/resume
is thatstart/stop
resets the tween, whilepause/resume
works so that the tween starts from where it left off.These can also be called on the
SDLx::Tween::Timeline
. It will broadcast the call to all tweens it has created. - get_cycle_start_time
-
Returns the tween cycle start time, in SDL ticks.
- get/set_duration
-
Get/set the tween duration, in ticks.
The tween translates ticks from the SDLx::Controller
into changes in game elements. To change these elements it uses a proxy. The proxy calls methods on your game objects, or changes array refs directly.
- method proxy
-
If the tween constructor arg
on
is an array ref of 2 elements, a string and a blessed ref, eg:# as part of the tween constructor arg hash on => [method_name => $game_object],
Then the tween will use the method proxy. The tween value will be set by calling the given method on the given object.
If the path requires a
from
arg (e.g. linear path), and none is supplied, the method defined for the proxy is used to get the initial tween value. In this case the proxy method shoud be able to do get/set.When using the method proxy you can use the optional
round
flag when creating the tween. If true, values will be rounded and made distinct by dropping repeated values. - array proxy
-
If the tween constructor arg
on
is an array ref of numbers, eg:$pos = [320, 200]; ... # as part of the tween constructor arg hash on => $pos,
Then the tween will use the array proxy. The elements of the array ref
$pos
will be changed directly by the tween. This is very fast, but you lose any semblance of encapsulation.
The deault tween changes in constant speed because it uses the linear easing function. The time used by the path to compute position of the tween value, advances in a linear rate.
By setting the ease
arg in the tween constructor you can make time advance according to a non-linear curve. For example to make the tween go slow at first, then go fast:
# as part of the tween constructor arg hash
ease => 'p2_in',
This will cause time to advance in a quadratic curve. At normalized time $t
where $t=$elapsed/$duration 0≤$t≤1
, the p2
tween will be where a linear tween would be at time $t**2
.
All easing functions except linear ease have 3 variants: _in
, _out
, and _in_out
. To get exponential
easing on the forward dir of the tween, you use exponential_in
easing. To get it on both dirs, you use exponential_in_out
.
These are the available easing functions. See eg/03-easing.pl
in the distribution for a visual explanation. See https://github.com/warrenm/AHEasing/blob/master/AHEasing/easing.c for a math explanation. The tweening functions originate from http://robertpenner.com/easing/penner_chapter7_tweening.pdf.
linear
p2
p3
p4
p5
sine
circular
exponential
elastic
back
bounce
The simplest tween follows a linear path, the only option when tweening a 1 dimensional value. You can also use the linear path for tweening values up to 4D, by providing the tween with an array ref of size 4 as the range:
# constructing a tween in 4D
range => [[0, 0, 0, 0], [320, 200, 10, 640]],
When tweening 2D values, you can customize the path the tween takes through the plane. The path is given in the path
arg of the tween constructor hash. Some paths also require a path_args
key to configure the path. Here are paths available:
- linear
-
Requires one of 2 constructor args:
range
orfrom + to
. If norange
and nofrom
are given then the value is taken from the tween target using the proxy. Thus you can tween a GOB from its current position to another without specifying the current position twice. Here are the 3 options for using the default linear path:# option 1: construct a tween only with "to", from is taken from the target to => [320, 200] # option 2: provide "from" + "to" from => [ 0, 0] to => [320, 200] # option 3: provide "range" range => [[0, 0], [320, 200]]
- sine
-
Tweens a value along a sine curve. Uses the same
from + to
setup as the linear path, but requirespath_args
with amplitude and frequency.range => [[0, 0], [320, 200]] path => 'sine', path_args => {amp => 100, freq => 2},
- circular
-
Tweens a value along a circle with a given radius and center, between 2 angles.
path => 'circle', path_args => { center => [320, 200], radius => 100, begin_angle => 0, end_angle => 2*pi, },
- spiral
-
Tweens a value along a spiral.
path => 'spiral', path_args => { center => [320, 200], begin_radius => 50, end_radius => 150, begin_angle => 0, rotations => 3, },
- polyline
-
Tweens a value along an array of segments, specified by the xy coordinates of the waypoints. The tween will start at the 1st waypoint and continue until the last following a linear path.
path => 'polyline', path_args => { points => [ [200, 200], [600, 200], [200, 400], [600, 400], [200, 200], ]},
Two special paths exists from tweening SDL colors in a linear path through the 4D space of RGBA.
- fade
-
Tween the opacity of a color between 2 values. To tween the opacity of some red color from opaque to transparent:
path => 'fade', from => 0xFF0000FF, to => 0x00,
- rgba
-
Tween a linear path between 2 points in the 4D color space. To transform red into semi-transparent green:
path => 'rgba', from => 0xFF0000FF, to => 0x00FF00AA,
If you need to spawn creeps, missiles, or whatever, you can tween the spawn method on your spawner with an integer wave number:
$spawn_tween = $timeline->tween(
duration => 10_000,
on => [spawn_creep => $gob],
range => [0, 9],
round => 1,
);
Will call spawn_creep
once a second for 10 seconds with the numbers 0 through 9 as the only arg. You can use the value as the wave number. You can also set an easing function to change the rate of spawning.
The tail is a simple behavior for making one position follow another at a given velocity. It is constructed from the timeline, like tweens, and takes 3 constructor args: speed, head, and tail. The speed is given in changes per tick. Head and tail should be given as array refs. The tail behavior will move the position in the tail array ref towards the head array ref, even if the head position changes.
For example, if you want a game object to follow the cursor, create an array ref $cursor_pos
and set its elements on mouse move. This will be the head. Create another array ref $gob_pos
for the game object position. This will be the tail, whose elements will be changed by the behavior. It is the position array ref that you read in your paint handler. Then create the behavior:
$rail = $timeline->tail(
speed => 100/1000,
head => $cursor_pos,
tail => $gob_pos,
);
You can then control the tail as you would a tween.
The tail will stop when the distance to the head is smaller than 1, or when the head passes through the tail.
There are two issues with tween memory management: how do you keep a ref to the tween in game objects, and how does the tween keep ref to the game elements it changes.
The timeline only keeps weak refs to the tweens it creates, active or inactive. This means you must keep a strong ref to the tween somewhere, usually in the game object. When the game object goes out of the scope, the tween will stop, be destroyed, and cleaned out of the timeline automatically.
The tween only keeps weak refs to the game elements (objects or array refs) it changes. Usually other game object will have strong refs to them, as part of the game scene graph. When the game object that is the target of the tween goes out of scope, you must stop the tween, and never use it again. TODO add event for this and allow changing of targets.
SDLx::Tween
takes into account rounding errors, the inaccuracy of the SDLx::Controller
move_handler
, and the inaccuracy of time/distance limits on behaviors. Used correctly, 2 tweens on the same path, one with duration 1 sec and the other 2 sec, will always meet every 2 cycles, even 100 years later.
To get this absolute accuracy with no errors growing over time, you need to set ideal start/pause/resume
times when controling tween cycles.
Here is an example of starting 2 tweens which is NOT accurate:
# dont do this!
$t1 = $timeline->tween(...);
$t1->start;
$t2 = $timeline->tween(...);
$t2->start;
$t1
and $t2
will not have the same cycle_start_time
, and this applies to all cycle control methods. One way to get accuracy, is to start the tweens through the timeline:
# do this
$t1 = $timeline->tween(...);
$t2 = $timeline->tween(...);
$timeline->start;
The timeline will make sure both tweens share the same cycle_start_time
. Another way to get accuracy is to use the optional ideal time argument of the cycle control methods:
# or this
$start_time = SDL::get_ticks;
$t1 = $timeline->tween(...);
$t1->start($start_time);
$t2 = $timeline->tween(...);
$t2->start($start_time);
When chaining tweens, the 2nd tween ideal start time should be set as the 1st tween start time + the tween duration.
When spawning tweens, compute the ideal spawn time, and make that the cycle start time.
TODO sugarize this and allow implicit passing of ideal times for sequence/parallel/spawn tweens, then delete this section.
Writing Perl SDL game move handlers is hard. Consider a missile with 3 states:
firing - some sprite animation
flying towards enemy - need to update its position until it hits enemy
exploding - another sprite animation
The move handler for this game object (GOB) is hard to write, because it needs to:
update GOB properties
you must take into account acceleration and paths in the computation of these values
you need to set limits of the values, wait for the limits
GOBs need to act differently according to their state, so you need to manage that as well
it all must be very accurate, or animations will miss each other
it has to be fast- this code is run per each GOB per each update
As a game becomes more wonderful, the GOB move handlers become more hellish. Brave souls have done it, but even they could not do it in a way us mortals can reuse or even understand.
SDLx::Tween
solves the missile requirements. Instead of writing a move handler, declare tweens on your GOBs. SDLx::Tween
will take care of the move handler for you.
Instead of writing a move handler which updates the position of $my_gob from its current position to x=100 in 1 second, you can go:
$tween = $timline->tween(on => [x => $my_gob], to =>100, t => 1_000);
SDLx::Tween
will setup the correct move handler.
According to http://en.wikipedia.org/wiki/Tweening:
"In the inbetweening workflow of traditional hand-drawn animation, the
senior or key artist would draw the keyframes ... and then would hand over
the scene to his or her assistant the inbetweener who does the rest."
Let SDLx-Tween be your inbetweener.
Perl SDL move handlers are rarely a simple linear progression. An ideal tweening library should feature:
tween any method, e.g. a Moose get/set accessor, or directly on an array
tween a property with several dimensions, e.g. xy position, some 4D color space
tween xy position not on a line, but on some curve
round the tween values, and pass only values when they change
smooth the motion with acceleration/deceleration using easing functions
make the tween bounce, repeat for N cycles or forever
pause/resume tweens
hasten/slow a tween, for example when creeps are suddenly given a speed bonus
follow a moving target, e.g. a homing missile with constant acceleration
chain tweens, paralellize tween, e.g start explode tween after reaching target
tween sprite frames, color/opacity/brightness/saturation/hue, volume/pitch, spawning, rotation, size, camera position
delay before/after tweens
rewind/ffw/reverse/seek tweens, and generaly play with elastic time for making the game faster or slower
SDLx::Tween
doesn't do everything yet. See the TODO
file in the distribution for planned features, and the docs above for supported features.
Tweening examples in the distribution dir eg/
:
01-circle.pl
-
the hello world of tweening, a growing circle
02-starfield.pl
-
demo of 6000 concurrent tweens
03-easing.pl
-
demo of all easing functions
04-paths.pl
-
demo of all paths
05-colors.pl
-
demo of color transitions
06-colors.pl
-
demo of 100 tail behaviors
Development is at https://github.com/PerlGameDev/SDLx-Tween.
Interesting implementations of the tweening idea:
Very little safety in XS code. Lose your ref to the tween target (object or array ref being set) and horrible things will happen on next tick.
eilara <ran.eilam@gmail.com>
Big thanks to:
Sam Hocevar, from 14 rue de Plaisance, 75014 Paris, France
https://raw.github.com/warrenm
For his most excellent AHEasing lib which SDLx-Tween
uses for easing functions. The license is in the tweencee/
dir. The library is at https://github.com/warrenm/AHEasing.
Check that page for some great info about easing functions.
Huge thanks to Zohar Kelrich <lumimies@gmail.com> for patient listening and advice.
Copyright (C) 2011 by Ran Eilam
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.10.1 or, at your option, any later version of Perl 5 you may have available.
Hey! The above document had some coding errors, which are explained below:
- Around line 238:
-
Non-ASCII character seen before =encoding in '0≤$t≤1>,'. Assuming UTF-8