Comments
You can use your Mastodon account to reply to this post.
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.
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 Tween
s, which will be the main topic of this blog post.
Tweens are used for gradually interpolating between two images. This can be:
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!
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:
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!