Skip to content

eilara/SDLx-Betweener

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NAME

SDLx::Betweener - SDL Perl XS Tweening Animation Library

SYNOPSIS

# 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
);

DESCRIPTION

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.

DECLARING TWEENING BEHAVIORS

THE TWEEN FACTORY

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".

THE TWEEN

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 TYPES

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 with direct 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 the to 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 the to 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,
);

CYCLE CONTROL

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 cycle n 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 or HASH 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. A pause()/resume() sequence will resume it from its last position before the pause(). 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.

PROXIES

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 a from key explicitly in constructor, or use the range key for setting from and to 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.

EASING

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

PATHS

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 or from + to. If no range and no from 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 get from and to.

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],
]],

MEMORY MANAGEMENT

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.

MULTIPLE TWEENERS

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.

ACCURACY

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.

WHY

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.

FEATURES

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.

EXAMPLES

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

SEE ALSO

Development is at https://github.com/PerlGameDev/SDLx-Betweener.

Interesting implementations of the tweening idea:

BUGS

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.

DESIGN

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.

AUTHOR

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 AND LICENSE

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.