SDLx::Betweener - SDL Perl XS Tweening Animation Library
# simple linear tween
use SDLx::Betweener;
# if you are writting a Perl SDL program, you probably have an app object
$sdlx_app = SDLx::App->new(...);
# tweener is the tween factory, gets ticks from app
$tweener = SDLx::Betweener->new(app => $sdlx_app);
$xy = [0, 0]; # tween the position stored in this array ref
# animate the position $xy for 1 second from [0,0] to [640,480] linearly
$tween = $tweener->tween_path(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
$msec = $tween->get_cycle_start_time;
$msec = $tween->get_total_pause_time;
$msec = $tween->get_duration;
$bool = $tween->is_active;
$msec = $tween->is_paused;
# tween methods
$tween->set_duration($new_duration); # hasten/slow a tween
$tween->start($optional_ideal_cycle_start_time);
$tween->stop;
$tween->pause($optional_ideal_pause_time);
$tween->resume($optional_ideal_resume_time);
# tween types, each with its own constructor args, but all
# sharing a common set, see below for more info
$tween = $tweener->tween_int(...);
$tween = $tweener->tween_float(...);
$tween = $tweener->tween_path(...);
$tween = $tweener->tween_spawn(...);
$tween = $tweener->tween_rgba(...);
$tween = $tweener->tween_fade(...);
# seek behavior makes one position follow another at given speed
$tail = $timeline->tail(
speed => 50/1_000, # advance a distance of 50 pixels a sec
on => $xy, # array ref of xy to update
to => $target, # array ref of xy to follow
done => \&collide, # completer to call on collision
);
SDLx::Betweener
is a library for animating Perl SDL elements. It lets you 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.
Use an SDLx::Betweener
object to create tweens. Create with an SDLx::App
:
$tweener = SDLx::Betweener->new(app => $app);
There are some cases where you would want to create more than one tweener, see "MULTIPLE TWEENERS".
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, for a given duration.
A tween is created from the tweener using the specific constructor for the tween type required. There are many options, described below, that you can configure through the tween constructor.
Once you create the tween you can control its cycle: start/stop, pause/resume, and change cycle duration.
The simplest tween uses the method proxy to call 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 half a circle clockwise in 1 second:
$tween = $timeline->tween_float(
on => {angle => $turret},
t => 1_000,
range => [0, pi], # pi from Math::Trig
);
There are tween constructors like tween_float
for all kinds of tweens.
- tween_int
-
Tween a 1D integer value over a range.
# print once per second the numbers 1..10 $tween = $timeline->tween_int( on => sub { say shift }, t => 10_000, range => [1, 10], );
The value in your code is only updated when the integer tweened value changes, not on every tick.
If the tween arg
bounce
is true then the tweened value will be rounded into an integer, otherwise it will be floored. - tween_float
-
Tween a 1D floating point value over a range.
# Call scale() method every tick from its current value # to 2.5 over 1 second $tween = $timeline->tween_float( on => {scale => $shape}, t => 1_000, to => 2.5, );
- tween_path
-
Tween a 2D integer position along a path through the plane.
# move $enemy around in a circle # xy() will be called with array ref of 2 integers # when ticks cause position to change $tween = $timeline->tween_path( on => {xy => $enemy}, t => 2_000, path => [circular => { center => [320,240], radius => 140, from => 0, to => 2*pi, }], );
There are other paths available besides
circular
. See "PATHS" for the full list. - tween_spawn
-
Useful when you want to spawn waves of creeps, bullets from a machine gun, and generally call GOB constructors over time. Requires
waves
constructor arg, providing how many waves to spawn. Does not work withdirect proxy
, only with code calling proxies.Unlike other tweens, your code gets called with two args. Besides the tweened value as 1st parameter (in this case the wave number, starting from 1), you also get the ideal spawn time for the GOBs you are about to spawn. This is useful if they themselves are animated, see "ACCURACY" for more info.
# float 60 balloons at rate of 2 balloons/sec from the $balloons array $spawner = $tweener->tween_spawn( t => 30_000, waves => 60, on => sub { my ($balloon_num, $start_t) = @_; $balloons[ $balloon_num - 1 ]->float_to_sky($start_t); }, );
- tween_fade
-
Tween the opacity of an SDL color over a range. Provide the starting color in the
from
key (Uint32 color), and the final opacity (Uint8) in theto
key.# fade away a red curtain to reveal what is behind it $tween = $tweener->tween_fade( t => 2_000, from => 0xFF0000FF, to => 0x00, on => \&set_curtain_color, );
- tween_rgba
-
Tween a 32bit RGBA value over a range. Th color given in the
from
key, will be blended into theto
color.# slowly transform a blue circle into a semi-transparent green one $tween = $tweener->tween_rgba( t => 10_000, from => 0x0000FFFF, to => 0x00FF0099, on => {set_color => $circle}, );
- tween_seek
-
The seeker is a simple behavior for making one GOB follow another moving GOB. Useful for homing missiles following a moving target at a constant velocity. It is more limited than other tween types in that there is no cycle control or easing (see below for info on these tween features), but you gain the ability to tween towards a dynamic, rather than static, target.
# the circle will follow the cursor at a constant velocity # of 50 pixels/sec # you can change the $cursor position in a mouse move event handler, # and the circle will seek the new value until it reaches the cursor $circle = {xy => [ 0, 0]}; # starts at 0, 0, set by tween $cursor = {xy => [100,100]}; # starts at 100,100, set by mouse event handler $seeker = $tweener->tween_seek( on => $circle->{xy}, to => $cursor->{xy}, speed => 100 / 1_000, );
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
Duration of the tween from start to stop in msec - forever
-
boolean
Repeat the cycle forever, restarting on each cycle completion. - repeat
-
integer
Repeat the cyclen
times, then stop. - reverse
-
bool
Should the tween start reversed. - bounce
-
bool
If the cycle is repeating, on each cycle completion reverse the tween direction, bouncing the tween between its edge points. - done
-
CODE
orHASH
ref of method name and object. Code or method to be called when tween cycle completes.
Cycle control methods:
- start/stop/pause/resume
-
A
start()/stop()
sequence will reset the tween. Apause()/resume()
sequence will resume it from its last position before thepause()
. These all take an optional ideal event time in SDL msec since game start, see "ACCURACY" for more info. - is_active
-
true if tween is started.
- is_paused
-
true for paused tweens.
- get_cycle_start_time
-
Returns the tween cycle start time, in SDL style msec since game start.
- get/set_duration
-
Get/set the tween duration, in msec. This lets you slow/haste a tween as it is animating.
The tween computes its tweened value on each tick, and uses the proxy to deposit it inside your code. You can use a proxy to call a method, call a callback, or directly set a Perl value. The proxy is set using the on
key of the tween constructor. There are several proxy types:
- method proxy
-
Create by setting
on
to a hash with one key- method name, and one value- some Perl object.# as part of the tween constructor arg hash on => {method_name => $game_object},
The tween value will be set by calling the given method on the given object.
Some tweens require a range (e.g.
tween_path
with a linear path). If none is supplied, the method defined for the proxy is used to get the initial tween value. In this case the proxy method is expected to support get and set. This is mere syntax sugar for not specifying the initial value in the range. You can always provide afrom
key explicitly in constructor, or use therange
key for settingfrom
andto
at once. - callback proxy
-
If the
on
key points at a code ref, then it will be called with the tweened value on each tick.# as part of the tween constructor arg hash on => sub { my $v = shift; say "tweened value=$v" },
- direct proxy
-
If the tween constructor arg
on
is a scalar ref on a number, or a 2D numeric array ref (for path tweens), then the tween will use the direct proxy:$color = 0xFF0000FF; ... # as part of the tween constructor arg hash on => \$color,
The numeric color value will be updated on each tick. This is very fast, but you lose any semblance of encapsulation.
The default 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
and $t is between 0 and 1, the p2_in
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
. _out
is reverse of _in
. _in_out
suffix variant is _in
followed by _out
. E.g. exponential_in
starts slow then speeds up, exponential_out
starts fast and slows down, and exponential_in_out
starts slow, speeds up, then slows down.
These are the available easing functions. See eg/04-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
Simple tweens follow a linear path, and this is the only option when tweening a 1D value.
When building a tween of type tween_path
, you can customize the path the tween takes through the plane. The path is given in the path
arg of the tween constructor hash. It is given as an array ref of, whose 1st value is the path name, and the other array values are path args to specify the path construction.
- 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]]
Linear path, being the default and the simplest of the paths, needs no
path
key. Just make sure the tween has a way to getfrom
andto
. - circular
-
Tweens a value along a circle with a given radius and center, between 2 angles.
path => [circular => { center => [320, 200], radius => 100, from => 0, to => 2*pi, }],
- 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 between them.
path => [polyline => [ [200, 200], [600, 200], [200, 400], [600, 400], [200, 200], ]],
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 SDLx::Betweener
only keeps weak refs to active tweens. 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 tweener 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.
A tween tick can trigger user code, e.g. a done
handler could call a user method. This code could destroy tweens, e.g. a missile hitting a target. In this case the two tweens should be created using two different SDLx::Betweener
objects.
TODO fix and delete this section.
SDLx::Betweener
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 controlling tween cycles.
Here is an example of starting 2 tweens which is NOT accurate:
# dont do this!
$t1 = $tweener->tween_int(...);
$t1->start;
$t2 = $tweener->tween_int(...);
$t2->start;
$t1
and $t2
will not have the same cycle_start_time
, and this applies to all cycle control methods.
If accuracy is needed, use the optional ideal time argument of the cycle control methods:
# or this
$start_time = SDL::get_ticks;
$t1 = $tweener->tween_int(...);
$t1->start($start_time);
$t2 = $tweener->tween_int(...);
$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. Tween completion handlers (the done
arg) are given the tween ideal stop time so you don't need to compute this:
$tween = $tweener->tween_int(
...
# start time for next tween is this tween cycle complete time
done => sub { $next_tween->start(shift) },
When spawning tweens the second parameter in the spawn handler is the ideal spawn time. Use this for the animation start time on the spawned GOB.
TODO fix and 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::Betweener
solves the missile requirements. Instead of writing a move handler, declare tweens on your GOBs. SDLx::Betweener
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::Betweener
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-Betweener 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, reverse, and 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, parallelize 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 generally play with elastic time for making the game faster or slower
SDLx::Betweener
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
-
many concurrent path tweens
03-polyline.pl
-
polyline path demo
04-easing.pl
-
demo of all easing functions available
05-colors.pl
-
demo of color transitions
06-chaining.pl
-
how to chain tweens
07-seeker.pl
-
demo of many seekers following cursor
Development is at https://github.com/PerlGameDev/SDLx-Betweener.
Interesting implementations of the tweening idea:
Very little safety in XS code. Lose your ref to the tween target (object or ref being updated) and horrible things will happen on next tick. See TODO
file in distribution for more issues.
Timeline has a std::set of Tween objects. It broadcasts tick(Uint32 now) to them.
The Tween tick method normalizes the time given using now/tween_duration, so that it is between 0 and 1.
Then the time is passed through the easing function, which maps it to some eased time value, useful for non-linear speed animations.
The tween then calls tick(Uint32 now) on its ITweenForm. The tween form translates the eased time into some domain value and pushes it into the tween target using the tween proxy.
LinearTweenForm interpolates between 2 values ("from" and "to") in a linear path. The values tweened are Vector<typename T, int DIM>.
PathTweenForm is more general. It lets you tween a Vector<int,2> along a 2D path, given in construction. LinearPath makes the form behave like a LinearTweenForm. CircularPath tweens along a circle or a pie slice from it. PolylinePath tweens along a set of linear segments.
A tween form uses an IProxy for pushing the value into Perl. There are proxies for setting Perl values (scalar for 1D tween, array ref for 2D) directly, for calling a callback with the tweened value, and for calling a method.
A tween uses a CycleControl object for controlling cycle repeat and cycle reverse. A tween can be made to run forever, to reverse on cycle completion (bounce), to start reversed, and to repeat N times.
Tween start/stop registers/unregisters the tween on a timeline.
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 excellent AHEasing lib which SDLx-Betweener
uses for easing functions: 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.