Animation and other Effects ¶
This section assumes that you are familiar with the nature of State
classes
and how to use them. It's sufficient to read the Getting Started section to understand
the following examples. This section will build upon the previous sections to create
more complex effects and arrangements of sprites.
The engine provides some simple and basic animations for sprites that can also be combined for storyline-like animations to produce scripted video sequences. An animation is regarded as a process where the properties of a sprite are changing over time. There are several ways to implement your animation or effect logic which are described in the following subsections:
- Add animations via
AnimationSuite
to a sprite - Use the
AnimationTimeline
for story-like sequences - Implement the logic directly in a sprite class
AnimationSuite¶
To implement a basic animation for a sprite the following animations are available so far:
- AlphaValueAnimation: change the alpha value of a sprite
- RotateAnimation: rotate a sprite
- AbsoluteSingleNodeLinearTranslation: : move a sprite using absolute screen coordinates. Independent from the drawing surface.
- RelativeLinearTranslation: move a sprite with along the drawing surface.
Every class extends the AnimatedSuite
class. To see some actual examples
please have a look into the section Basic Animations.
AnimationTimeline¶
The AnimationTimeline
class is used to create video-like timed sequences.
- create a
StoryLineSlot
for each sequence - at the end put every
Storylineslot
in theAnimationTimeline
and control the update/render logic in the State - multiple storylineslot will be played in a sequence
Example¶
- Animation of a sprite from the top-left corner of the screen to to bottom right corner
- this is put in the first animation slot and played first
- the second animation slot contains a text where the alpha value is changing to let the text appear on the screen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | AnimationTimeline storyLine = new AnimationTimeline(); int duration = 4000; AnimatedSprite fighter = new AnimatedSprite(); fighter.init(ResManager.ENEMY_FIGHTER, new PointF(0, 0)); AbsoluteSingleNodeLinearTranslation anim = new AbsoluteSingleNodeLinearTranslation(fighter, AbsoluteSingleNodeLinearTranslation.Direction.BOTTOMRIGHT, duration); anim.setLoop(false); fighter.addAnimation(anim); StoryLineSlot slot0 = new StoryLineSlot(duration); slot0.setOverwrite(true); slot0.addSprite(fighter); AlphaValueTransition alphaAnim = new AlphaValueTransition(0, 255, 5000); TextElement logoText = new TextElement("Hello, World", new PointF(RetroEngine.W / 2 - 50, RetroEngine.H / 2 - 50)); logoText.setFont(new GameFont(35)); logoText.init(); logoText.addAnimation(alphaAnim); StoryLineSlot slot1 = new StoryLineSlot(logoText, 5000); slot1.setOverwrite(true); storyLine.add(slot0); storyLine.add(slot1); List<AbstractSprite> spriteList = storyLine.getCurrentElement().getAnimatedSprites(); storyLine.initCurrentSlot(); //beginne storyLine.finalize(); addSprites(spriteList); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @Override public void updateLogic() { List<AbstractSprite> erg = storyLine.update(); if (erg != null) { if (storyLine.isOverwrite()) { clearSprites(); addSprites(erg); } else { addSprites(erg); } } updateSprites(); } @Override public void render(Canvas canvas, Paint paint, long currentTime) { canvas.drawColor(Color.WHITE); drawSprites(canvas, currentTime); } |
Implement logic in Sprite class¶
Though implementing the logic in the sprite class is possible it isn't a recommended approach. An example will makes things more clear.
Example
- in method
updateLogicTemplate()
and draw method itself - Example "Resize and Fade In/Out Circle":
- Extend from
CircleSprite
class and override the update-Method:
1 2 3 4 5 6 7 8 | public void updateLogic() { alpha -= 15; if (alpha <= 0) { active = false; alpha = 0; } radius += 15; } |
- the
RenderThread
will handle the max frames per second but you can control your own update rate of your logic like this:
1 2 3 4 5 6 7 8 | long starttime = 0; //FRAMES_PER_SECOND is a constant which defines after how many frames in a second the logic will be executed public void updateLogic() { if (RetroEngine.getTickCount() > starttime + FRAMES_PER_SECOND) { starttime = RetroEngine.getTickCount(); // animation logic } } |
Basic Animations ¶
In this section it is shown how to use the default animation implementations
to create some basic effects, namely, AlphaValueAnimation
, RotateAnimation
, AbsoluteSingleNodeLinearTranslation
and RelativeLinearTranslation
. These animation classes extend the abstract class AnimationSuite
. We will only explain the usage of these classes by using the animation classes AlphaValueAnimation
and RotateAnimation
.
The procedure to use one of those animations is as follows:
- Instantiate an animation class and modify the settings
- Add it to a sprite (sprite class must implement the interface
ISpriteAnimateable
) - Start the animation by calling the
beginAnimation()
method of the sprite object
For every animation, you can decide whether the final value of the animated property should be kept or reset. The default is true
for all animations, and that means that the property of the sprite will remain in this state if the animation finishes.
Multiple animations can be added to a sprite. Added animations can be started all together or by calling a specific animation to start.
The following two examples show the usage for AlphaValueAnimation
and RotateAnimation
. The same applies for all other animation classes.
AlphaValueAnimation¶
The alpha value of the sprite can be changed to make it opaque or transparent. Therefore a value from 0 to 255 is used where 0 means fully transparent.
In this example a sprite will become fully opaque in two seconds, starting from a half see-through sprite. If the animation ends the alpha value will be reset so that the sprite will be half transparent again (the initial value of the animation will be used - in this case 100).
1 2 3 4 | AlphaValueTransition alpha = new AlphaValueTransition(100, 255, 2000); alpha.setDoReset(true); // reset to the initial value of 100 sprite.addAnimation(alpha); sprite.beginAnimation(); |
RotateAnimation¶
In this example a triangle will rotate 405° starting from a 45° angle. After the animation finishes it will stop just there:
1 2 3 4 5 6 7 8 | TriangleSprite triangle = new TriangleSprite(Color.BLUE); triangle.initWithLength(200, new PointF(200, 200)); RotationAnimation alpha = new RotationAnimation(45, 405, 4000); alpha.setDoReset(false); // this is default alpha.setLoop(false); triangle.addAnimation(alpha); triangle.beginAnimation(); |
The second argument of the RotationAnimation
says how many degrees the
sprite should be rotated whereas the first argument depicts the starting angle.
Advanced Effects¶
This section shows some practical examples on how to create some more complex animations or effects. You can see the full source code in the example app in the Play Store or look into Github.
Random explosions¶
For this example we want to create animated explosion effects. That means we need some image that contains the whole animation.
First, we're going the load a horizontal sprite sheet which contains each step of the whole explosion animation. This texture comes from Explosion Generator, a tool for creating some explosion sprite sheets.
All in all there are 12 single frames, and the image has a size of 768x64 (width x height) pixels - we need both information later.
The code for loading a bitmap in Android is:
1 2 3 4 5 | BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inPreferredConfig = Bitmap.Config.ARGB_8888; opt.inScaled = false; Bitmap bitmapExplosion = BitmapFactory.decodeResource(RetroEngine.Resources, R.drawable.explode_3, opt); |
For that we use the android BitmapFactory
class. The texture has to be placed
in the resource directory of your android app. Here it was saved under the
name explode_3.png
. Have a look into the section Loading Bitmaps on how
to access the Resources
object of android.
Next, we have to create an animated sprite with the following code:
1 2 3 | PointF position = new PointF(RetroEngine.W / 2, RetroEngine.H / 2); AnimatedSprite explosion = new AnimatedSprite(); explosion.initAsAnimation(bitmapExplosion, 64, 64, 25, 12, position, true); |
Remember the values of the sprite sheet? Those are needed for the init()
method:
The first argument sets the bitmap we loaded before. The next two arguments
are for the size of one single frame in the animation. Our sprite sheet has a width of
768 pixels and we have 12 frames that makes 768 / 12 = 64 pixels. The height
is 64 pixels and so for each frame. It's important to use pixel values here.
The value of 25 (fourth argument) describes the
framerate at which the animation should be played. This value should be set in
accordance with the overall frames available to achieve a fluent and realistic animation.
In this case the explosion will take roughly a half second.
Furthermore, the sprite is positioned in the center of the state when added later on.
The last argument determines if the animation should be repeated after it ends.
Decorated sprites with text¶
This nice feature enables you to display a text onto a sprite. To decorate a sprite we need a sprite first. You can use any sprite even animated ones. Let's use the explosion sprite of the example before. To add a text on top of the explosion:
1 2 3 4 5 6 7 | TextElement textElement = new TextElement(explosion); GameFont font = new GameFont(34); font.setFontColor(Color.WHITE); textElement.setFont(font); textElement.initWithText("This is an explosion"); addSprite(textElement); |
All what we have to do was to place the previously created explosion sprite
as argument in the constructor of the TextElement
class. Additionally we
set a bigger font size and the font colour the white. Now that the explosion sprite
is decorated with a text sprite we don't need to add this sprite to the root node. Instead
just add the textElement
object with addSprite(textElement)
. Because
of the decorator pattern used here the TextElement
class will call the
appropriate parent sprite methods before its own to update the logic and the drawing.
Random colored circles falling down¶
This example utilises the animation system of the engine. We will generate random numbers to place the circles at an arbitrary position and get variable animation duration for each circle. You will learn how to add multiple animations at once, and how to start each circle animation with a delay.
The first thing to do is to create a State
class. The main logic is defined
in the updateLogic()
method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @Override public void updateLogic() { updateSprites(); if (getSpriteCount() < MAX_CIRCLES) { // choose a random colour from the array int ixColour = MathUtils.getRandomBetween(0, COLOURS.length - 1); int color = this.manager.getParentActivity().getResources().getColor(COLOURS[ixColour]); final CircleSprite circleSprite = circleBuilder.createAtRandomPosition(color, 10); circleSprite.setAlphaValue(0); //make circle transparent first addSprite(circleSprite); // do not start the animation for each circle at the same time int startDelay = MathUtils.getRandomBetween(300, 2000); getHandler().postDelayed(new Runnable() { @Override public void run() { circleSprite.beginAnimation(); } }, startDelay); } } |
To set up the circles we declared an inner class that will act as a builder for the sprites:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | /** * The builder class for all the circles */ private static class CircleBuilder { CircleSprite createAtRandomPosition(int color, float radius) { int posX = MathUtils.getRandomBetween((int) radius, RetroEngine.W); int posY = MathUtils.getRandomBetween((int) radius, RetroEngine.H); CircleSprite circleSprite = new CircleSprite(radius, color); circleSprite.init(posX, posY); addAnimation(circleSprite); return circleSprite; } void addAnimation(final CircleSprite circleSprite) { int duration = MathUtils.getRandomBetween(1000, 5000); ScaleAnimation scaleAnimation = new ScaleAnimation(1, 6, duration); PointF oldPos = circleSprite.getPosition(); PointF end = new PointF(0, RetroEngine.H - oldPos.y); RelativeLinearTranslation linearTranslation = new RelativeLinearTranslation(end, duration); linearTranslation.setDoReset(false); circleSprite.addAnimation(scaleAnimation); circleSprite.addAnimation(linearTranslation); circleSprite.addAnimation(new AlphaValueTransition(0, 255, duration)); scaleAnimation.setListener(new IAnimationSuiteListener() { @Override public void onAnimationStart(AnimationSuite animationSuite) { } @Override public void onAnimationRepeat(AnimationSuite animationSuite) { } @Override public void onAnimationEnd(AnimationSuite animationSuite) { circleSprite.setActive(false); } }); } } |
The createAtRandomPosition()
method shows how to create the circle sprites whereas the
addAnimation()
method illustrates how to add multiple animations to a sprite to realise the
described effect. This builder class takes its action in the updateLogic()
method as seen before.
Animated Tiled Background¶
This more advanced example shows you how to combine several animations and the sprite system to create a animated tiled-based background.
Lets begin with loading the following tile image tile_1.png
(64 x 64 pixels) in the init()
method
of a state:
1 2 3 4 5 6 | BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inScaled = false; Bitmap bitmapTile = BitmapFactory.decodeResource(RetroEngine.Resources, R.drawable.tile_1, opt); |
If we want to add a sprite we do this:
1 2 3 | AnimatedSprite tile = new AnimatedSprite(); tile.init(bitmapTile, new PointF(0, 0)); addSprite(tile); |
Perfect! The tile sprite will be drawn on the top-left corner of the canvas.
Now comes the algorithm. If we want fo fill the whole canvas with the sprites
we have to measure the width of the screen first to calculate how many
of those sprites can fit in one row. After that we have to calculate how many
rows of sprites can be placed onto the canvas. We can obtain the canvas size with
the RetroEngine
class. The size of the tile image is know and we can acquire
this information by the bitmapExplosion
variable.
There are some ways how to achieve this effect. I will do it in the following way:
Draw a matrix of tile sprites first to fill the complete canvas. Add an additional column
"left" from the canvas and one row "over" the top of the visible drawing surface.
Add all sprites to a new SpriteGroup
, then add the RelativeLinearTranslation
to the sprite group.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //create a new group SpriteGroup background = new SpriteGroup(); int width = bitmapTile.getWidth(); int height = bitmapTile.getHeight(); int rowCount = (int) (RetroEngine.W / width + 1f); int colCount = (int) (RetroEngine.H / height + 1f); // begin "behind" the visible drawing surface outside the screen for (int i = -1; i <= colCount; i++) { for (int j = -1; j <= rowCount; j++) { AnimatedSprite tile = new AnimatedSprite(); AnimatedSprite tmp = tile.init(bitmapTile, new PointF(j * width, i * height)); // (!) important so that the sprite doesn't get deactivated tmp.setAutoDestroy(false); background.add(tmp); } } RelativeLinearTranslation linearTranslation = new RelativeLinearTranslation(new PointF(0, 64), 1000); linearTranslation.setLoop(true); background.addAnimation(linearTranslation); background.beginAnimation(); addSprite(background); |
This will achieve the desired effect in the horizontal downward movement. Nonetheless the method may have a drawback. After the animation finishes it repeats - for that the position of every sprite is reset to its original position. This may take a while and can results in a sluggish animation if the duration of the translation is to small. Of course it's sufficient for the translation value to be a factor of the tile width and height. In this example 64 pixels. Use negative values to translate in the opposite direction.