Oxygine's "tween" sprite animation system: A clever use of templates!

Jan 16, 2022 · 6 mins read
Oxygine's "tween" sprite animation system: A clever use of templates!

Oxygine’s “tween” sprite animation system: A clever use of templates!

Oxygine is a free and open source 2D game engine/framework, written in C++. I used it once to create a small prototype of a game, and in the short time that I used it there was especially one system that left a strong impression on me: The Tween system! Today we’ll study how it was implemented.

But first, let’s quickly go through Oxygine’s scene graph.

Scene graph

The Actor class is the base class for all scene objects. Like in most other game engines and frameworks, an Actor is a part of the scene hierarchy, and can have one parent and one more children. These can be added using the addChild function.

The Actor class has a child class called VStyleActor, which is simply an Actor with somme visual properties (such as colour, blend mode). Rather than using this class directly, you will typically use one of its derived classes instead, such as the Sprite class.

The Sprite class is, as you might expect, used for displaying images and animations. In a game, you might create a sprite for each player and enemy/NPC. Animating sprites is done though Tweens, which will be the main topic of this blog post.

Tweens

Tweens are used for gradually interpolating between two images. This can be:

  • Moving a sprite from A to B.
  • Rotating a sprite over time.
  • Playing an animation.

It’s a simple but powerful system that allows you to define a visual change and add it to a Sprite. I think it’s a very clean way of doing this, rather than relying on the user manually updating the sprite inside some update function. If you want a Sprite to move to position (10, 20) you can simply add a TweenPosition to the sprite like this:

spTween tween = sprite->addTween(Actor::TweenPosition(10, 20), duration);

The sprite will then move from its current position towards (10, 20) over the specified duration. When it’s done, the tween will automatically be removed. If you want to trigger an event when the tween is done, you can add a callback like this:

tween->setDoneCallback(CLOSURE(this, &MyClass::onTweenDone));

Your callback function should look like this:

void MyClass::onTweenDone(Event *event)
{
    // code here
}

Lambdas are also supported by the way!

Implementation

Let’s study how this is implemented. We will use as an example the Enemy::explode function in the example game’s Enemy.cpp class. This function will fade out an enemy sprite when it dies. It is done like this:

_view->addTween(Actor::TweenAlpha(0), 300)->detachWhenDone();

This will gradually interpolate the alpha (opacity) towards zero, over a period of 300ms. So how is this all implemented?

TweenAlpha is defined in Actor.h like this:

typedef Property<unsigned char, unsigned char, Actor, &Actor::getAlpha, &Actor::setAlpha> TweenAlpha;

Property is a class template that encapsulates a value and an accessor function (for getting the value) and a modifier function (for setting the value). The value it encapsulates is the value that the Tween is responsible for (such as position, rotation, animation frame, etc.). It looks like this:

template <typename Value, typename valueRef, typename C, valueRef (C::*GetF)() const, void (C::*SetF)(valueRef)>
class Property : public Property0<Value, valueRef, valueRef, C, GetF, SetF>

It takes the following parameters:

  • Value type
  • Value reference type (the type of the value passed to the modifier and returned by the accessor function)
  • Class type (the type of the class containing the value)
  • Accessor function pointer (member function of the class type)
  • Modifier function pointer ()

The value and valueRef types are usually the same.

The Property class template inherits from another class template called Property0. This is the lowest level of the Tween system, and this is where the value that the Tween is responsible for (sprite position, animation frame, etc.) will be updated. It contains functions for initialising the value, setting the source/destination value and for updating the value. The value is updated like this:

void update(type &t, float p, const UpdateState &us)
{
    OX_ASSERT(_initialized);
    value v = lerp(_src, _dest, p); // LERP from initialised source value towards destination value
    set(t, v); // Set value, using the modifier function pointer that was passed to it (see above)
}

So where is this function called from then? Let’s go all the way back to the addTween function, which looks like this:

template<class Prop>
spTween addTween(const Prop& prop, timeMS duration, int loops = 1, bool twoSides = false, timeMS delay = 0, Tween::EASE ease = Tween::ease_linear)
{return addTween(createTween(prop, duration, loops, twoSides, delay, ease));}

This function takes a Property template class instance as a parameter. As we saw above, this class is used for accessing and updating a specified value of an object of some type. However this class does not have a common base class (Property0 is also a class template - not a class!). Our end goal is to create an instance of the Tween class (or more specifically: of a subclass of Tween). The addTween function template will create this instance for us. It forwards this Property to the createTween function, and returns a pointer to a new instance of type TweenT<GS>*, which is a class template that inherits from Tween. We can safely up-cast this to Tween and return it to the user.

Let’s take a closer look at the Tween class and the TweenT class template. The Tween class has an update function. This function gets called from the Actor each frame, and will end up calling another internal _update function, which is virtual. TweenT overrides this _update function, and implements it like this:

void _update(Actor& actor, const UpdateState& us)
{
    type& t = *safeCast<type*>(&actor);
    _gs.update(t, _percent, us);
}

_gs is an instance of the Property class template (which inherits from Property0) we studied above, and the update function is the LERP-function we studied earlier.

So the pieces finally fit together! It can be challenging to study template-heavy code like this, but the result is a powerful and user-friendly system for adding various behaviour to our sprites. Remember, this is all you have to do to create a new Tween type:

typedef Property<unsigned char, unsigned char, Actor, &Actor::getAlpha, &Actor::setAlpha> TweenAlpha;

Sharing is caring!


Comments

You can use your Mastodon account to reply to this post.

Reply