Graphics & Sprites

This engine uses the notion of sprites to represent graphical images on the drawing surface:

In computer graphics, a sprite is a two-dimensional bitmap that is integrated into a larger scene. Wikipedia

However, in RetroGraphicsEngine sprites have extended functionality. They also carry animation information and can have action logic, which can be plugged in by means of the IActionEventCallback interface (see section Action for Sprites).

Before a sprite can be created and added to a scene, a bitmap resource must be loaded first. This is described in section Loading Bitmaps.

Sprites

Sprites represent the graphics displayed on the screen. They are usually android.graphics.Bitmaps or generated from Drawables. Drawing on canvas for all sprites is done internally in the same way as this:

1
canvas.drawBitmap(bitmap, transformationMatrix, paint);

Creating an instance of one sprite is performed by using the many different init(...) functions of a concrete sprite class implementation. The following sections and the Animation and other Effects section describe this in more detail.

Sprite Types

All sprites implement the interface ISprite. Different types of sprite classes exist in the framework:

  • Static and Animated sprites: kind of default sprites, abstract class AbstractSprite implements ISprite, and AnimatedSprite is the concrete implementation of a sprite (and implements the ISpriteAnimateable interface).
  • can be static, in other words, they are not animated. This doesn't mean that they cannot have an "animated" texture.
  • can be animated, speaking of transformations
  • Background layer: interface BackgroundLayer implements ISprite
  • Sprite groups: interface ISpriteGroup implements ISprite
  • offering the symbolical (e.g., as lists) and spatial collection of sprites into a group

Every of these sprite types implements the interface ISprite. This interface has two methods:

  • void draw(Canvas canvas, long time)
  • void updateLogic()

The abstract class AbstractSprite, implementing the interface ISprite, contains common properties (e.g., the texture), implements general logic and behavior of animated sprites and simple shapes.

With respect to this, the render method draw() in the class AbstractSprite is preparing the bitmap to be drawn onto the canvas by using linear transformation (a subset of linear algebra, especially used in computer graphics) to scale, rotate and translate the sprite. Thus, this method unifies all operations no matter what kind of sprite is processed for the canvas. Subclasses can override the method and add further behavior or call the base class' method. Time can be the current time or the delta between two time steps and depends on the employed render thread implementation. As of now in the current implementation, the current time is passed to this method.

The update method updateLogic() updates the animation frame of the sprite if the texture is a sprite-stripe, calls the hook method updateLogicTemplate() for user-defined behavior of subclasses, and cycles through the configured animations of a sprite.

Basic sprites

A basic sprite extends from AnimatedSprite and implements the Colorable interface. Following shapes are implemented:

  • Circle
  • Rectangle
  • Triangle

The shape itself is first drawn into a temporary bitmap and the entire bitmap is rendered to the main canvas later. This complies with the generic render function of AbstractSprite because some straightforward linear algebra is done to transform (i.e., translate, rotate, etc.) the graphic object (bitmap).

Circle

A circle can be easily created. Therefore, the following new init() methods are available:

  • init(float x, float y): create a circle with default radius 0 at position (x,y) on the canvas
  • initWithRadius(float rds, float x, float y): create a circle with default radius rds at position (x,y) on the canvas

Create a circle with radius of 20 pixels in the middle of the drawing surface.

1
2
CircleSprite circleSprite = new CircleSprite(Color.BLUE);
circleSprite.initWithRadius(20, RetroEngine.W/2, RetroEngine.H/2);

Notice, how the core class RetroEngine is used to get the size of the drawing surface at any time.

Rectangle

This creates a black rectangle with a width of 100 pixels and a height of 200 pixels at (0,0) on the drawing surface:

1
2
3
4
5
RectangleSprite rectangleSprite = new RectangleSprite(Color.BLACK);
rectangleSprite.init(new PointF(0, 0), 100, 200);

RectangleSprite rectangleSprite = new RectangleSprite();
rectangleSprite.init(new PointF(0, 0), 100, 200, Color.BLACK);

Triangle

A equilateral green triangle with a side length of 200 pixels will be place at the right hand side and top edge of the drawing surface:

1
2
3
4
5
TriangleSprite triangleSprite = new TriangleSprite(Color.GREEN);
triangleSprite.initWithLength(200, new PointF(RetroEngine.W - 200, 0));

TriangleSprite triangleSprite = new TriangleSprite();
triangleSprite.initWithLength(200, new PointF(RetroEngine.W - 200, 0), Color.GREEN);

Decorating Sprites

Sprites can be visually and functionally decorated by other sprites. The simplest decorator to understand is the text sprite. It can be overlayed onto an existing sprite to display a caption under an image, for example. Decorators can be used by themselves without the need of an existing sprite. Then, an empty sprite is created automatically.

Text

Text are represented by theTextElement class and are decorator elements for sprites The class is a decorator and direct subclass of the Decorator class. It can also be instantiated without an existing sprite and used as it where a normal sprite. Internally an empty sprite (as placeholder) will be created where the text is drawn onto. Formatting text elements is done by the GameFont class to set colour, text size, etc.

Basic Constructor

This creates a text sprite at position (0,0) on the canvas. The default font size is 12 pixels, sans serif typeface.

1
2
TextElement text = new TextElement("I'm Bob!");
text.init(new PointF(0, 0));
Place text on top of sprite

A animated sprite is created and annotated with a label:

1
2
3
4
5
6
7
8
AnimatedSprite debris = new AnimatedSprite();
debris.initAsAnimation(ResManager.DEBRIS, 64, 61, 6, 6, new PointF(300, 200), true);

GameFont font = new GameFont(34);

TextElement text = new TextElement(debris1);
text.setFont(font);
text.initWithText("Asteroid");

TextElement as such don't need a position if they serve as a decorator. The global position of the text on the canvas is always the underlying sprite. If the position of the text is changed then actually the position of the sprite is changed. The text elements delegates all methods to the underlying sprite.

Animated Sprites

Sprites can also be animated, in the sense that the texture of the sprite changes over time. This feature is already implemented in the AbstractSprite class and the concrete implementation is available in AnimatedSprite class. Concerning animations that actually transform a sprite, see the section Animation

Sprites can include sprite stripes to create animated sprites. The framerate has to be set beforehand and should be set in relation to the number of images in the film stripe to create a fluid animation.

Example

More examples can be seen in the Animation section on how to create animates sprites.

To create a animated sprite you'll need a sprite stripe first. For this example you can take the megaman sprite sheet from the book JavaScript Application Design.

AnimatedSprite can only process a single horizontal stripe. The extracted sheet should look like this (the concrete sprite sheet was extracted with Gimp):

Megaman Jump Sprite

Megaman Jump Sprite Raster

To make a jumping animation you have to create the animated sprite object in the following way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// load the bitmap - the final size will differ depending on the target density of the
// canvas (which should be equal to the screen density)
Bitmap megamanJumpTexture = BitmapHelper.decodeSampledBitmapFromResource(RetroEngine.Resources,
        R.drawable.megaman_jump, // resource id
        150, // the full width
        90 // the full height
);

// we need to convert the units
megaman.initAsAnimation(megamanJumpTexture, // the texture - sprite sheet
    (int) MathUtils.convertDpToPixel(50), // height of a frame
    (int) MathUtils.convertDpToPixel(27), // width of a frame
    8, // the frames per second
    7, // number of frames
    new PointF(RetroEngine.W / 2, 0), // the position of the sprite
    true // repeat the animation ?
);

With the help of the util class BitmapHelper we can load the sprite sheet from the drawable resource folder. We pass the actual width and height of the texture and in return we get the sampled bitmap which has the same density as the screen. The size may be different now than the original size.

The sprite sheet contains of 7 frames. Each frame has a size of ~27x50 pixels. We need that information to initialize the sprite. Internally all calculations are based on pixel values. Because of that reason, we have to convert the values before we pass them as arguments (with the help of the MathUtils class). Additionally, we have to set the framerate to control the speed for the animation. If the rendering runs with 60 fps (see RenderThread) and we have 7 frames we can roughly set the fps for the sprite to 8 (60 / 7).

Note on Loading Bitmaps

Its important to take into consideration how the bitmap is loaded. When loading bitmaps with BitmapHelper#decodeSampledBitmapFromResource(Resources, int, int, int, boolean) and inScaled set to false, the bitmap will be loaded with the original density returning the original size of the texture. In that case you can pass also pixel values to the init-method of the animated sprite.

SpriteGroup

A group is used for grouping sprites. It's possible to manage different layers of sprites. A collection of sprites can be created to apply the same animations to all sprites within this collection.

Example

For an example look at the Tiled Background Example what you can do when grouping sprites.

The advantages to organize sprites in a group:

  • less iterations in a loop when checking specific sprite types, e.g. for collision checks or touch events
  • control all sprites in one group, apply animations to all entities at once or same properties

Sprite groups can effectively safe a lot of time and lines of code. Moreover, they provide a logical grouping for sprites of the same kind. For example, a sprite group containing all environmental objects can be stored separately from sprites that represent enemies.

There are two implementations of sprite groups available which differ in their implementation on how to keep the sprites in a data structure. Sprites can be organized spatially or in ordinary iterable collections. The interfaces are SpatialPartitionGroup and IterableSpriteGroup, respectively. All sprite groups implement the ISpriteGroup interface and extend from AbstractSprite. The Composite pattern is implemented here. A sprite can have children or not.

SpriteListGroup uses a List to manage the sprites where on the other hand a quadtree in SpriteQuadtreeGroup is used to save the sprites (the quadtree implementation from pvto is used). As mentioned before, both classes implement the interface ISpriteGroup and extend from AbstractSprite.

For the latter sprite group its necessary to define a query range where to find sprites in the 2-dimensional space. Because for the drawing and update part only those children are used that are within this range. Therefore you have to call the SpatialPartitionGroup#setQueryRange(RectF queryRange) method.

The State class offers for spatially organized groups an easy access method to set the query range (the search rectangle where to find sprites in the data structure). This can be used if the root node of the State is for instance a SpriteQuadtreeGroup. Normally it need to be set only once if the canvas isn't translated. Otherwise you have to call the State#setQueryRange(RectF rect) method in State#updateLogic().

You can see an example in the demo app in this GitHub repository on how to use this sprite group as root node in a state.

Background Layer

Backgrounds are special types of sprites. Currently available are:

  • Static backgrounds with StaticBackgroundLayer
  • Scrollable background with FixedScrollableLayer
  • Parallax background with ParallaxLayer

The class BackgroundNode exists to hold and manage all types of background layers and is used in the State class.

Don't call the drawBackground() method within the States render() method if no background layer was added. If you just want to set a background colour then you can do the following as well and go without the BackgroundLayer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    // State class

    @Override
    public void render(Canvas canvas, Paint paint, long currentTime) {
        // Two variants for a white background
        // Variant A:
        paint.setColor(Color.WHITE);
        canvas.drawRect(0, 0, RetroEngine.W, RetroEngine.H, paint);
        //Variant B:
        canvas.drawColor(Color.WHITE);

        // Draw all the sprites
        drawSprites(canvas, currentTime);
    }

Parallax Background

A parallax background can be used within a State only. Follow the steps below to implement a parallax background for your State.

Create the specific background, and in the init() method of your state type:

1
2
ParallaxLayer bgLayer = new ParallaxLayer(ResManager.BACKGROUND_STAR_2, 1f);
ParallaxLayer bgLayerTwo = new ParallaxLayer(ResManager.PARALAYER_STAR_1, 0.6f);

The second argument of the constructor takes a factor that defines how fast the background is moving in relation to a reference sprite or 2D point in space. With a value of 1.0 you say that it's moving as fast as the reference sprite. The lesser the faster the background is translated. Later on you will see why this reference sprite is needed.

The following code example creates a static background that remains still on the screen and doesn't move, and on top of it a parallax layer is added, which moves relative to the reference point:

1
2
StaticBackgroundLayer bgLayer = new StaticBackgroundLayer(ResManager.BACKGROUND_STAR_2); <br/>
ParallaxLayer bgLayerTwo = new ParallaxLayer(ResManager.PARALAYER_STAR_1, 0.6f);

Once created we have to add it to the state (right after the above code):

1
2
addBackgroundLayer(bgLayer);
addBackgroundLayer(backgroundLayer);

The order does matter. Internally the state has a BackgroundNode class that will manage all added background layers.

Next, we need a reference sprite. For the sake of convenience we create a circle with a translation animation:

1
2
3
4
5
6
7
8
CircleSprite player = new CircleSprite(20, Color.BLUE);
player.init(100, 100);
LinearTranslation translation = new LinearTranslation(100, 400, 5000);
player.addAnimation(translation);
player.beginAnimation();

//Important
setReferenceSprite(player);

The animation helps us to see the moving background in action. Therefore we needed to move the actor of the state.

Within the render() method type in the following:

1
2
3
4
5
    @Override
    public void render(Canvas canvas, Paint paint, long currentTime) {
        drawBackground(canvas);
        drawSprites(canvas, currentTime);
    }

For the method updateLogc():

1
2
3
4
5
    @Override
    public void updateLogic() {
        setReferenceSprite(player); //important when using a parallax background
        updateSprites();
    }

Because the actor (circle) gets translated the background needs the new reference point for its canvas translation. Therefore we used the setter setReferenceSprite in the updateLogic method within the state.

The reference sprite can also be an empty sprite not visible but active in the state. If you want to animate the parallax background or indirectly change the position of this reference sprite to animate the background.

Action for Sprites

You can apply actions for sprite elements to define some logic to execute when an event happens. For example this can occur when a collision is detected, a touch event is registered etc.

An AbstractSprite has an onAction() method that is empty. All subclasses can now override this method to implement their own behaviour.

The class AnimatedSprite (direct subclass of AbstractSprite) implements its own approach to use the onAction() method.

Therefore it overrides this onAction() method and in order to call the onAction() method of IActionEventCallback. For that it has a instance member of type IActionEventCallback. By using this approach (Strategy pattern) switching actions at runtime is therefore possible.

Every AnimatedSprite class has a default action implemented: the EmptyAction which does exactly nothing. The logic and behaviour of an event is now outsourced to another class which can be reused many times even by other classes.

Loading Bitmaps

A vital part of displaying graphics is the ability to load images. At one point you have to access the Resources object of android. There are many possible ways:

  • RetroEngine
  • global Resources.getSystem()
  • via the current activity

You can access the Resources object at any time via the RetroEngine class once the surface of the DrawView component was created or the core class was explicitly initialised with RetroEngine.init(ACIVITY_INSTANCE) where ACIVITY_INSTANCE is the current active activity.

If you use the second approach keep the following in mind (extracted from the docs):

Warning

Return a global shared Resources object that provides access to only system resources (no application resources), and is not configured for the current screen (can not use dimension units, does not change based on orientation, etc).

However, this should be sufficient to access the drawable resources of your app.

The latter approach may be more common to you. In an activity just call getResources(). You can apply this also within a State class by getting the StateManager instance and calling manager.getParentActivity().getResources().

Loading Resources with BitmapFactory

Before we can render some sprites, we need some graphic resources. In this case we want to use bitmap resources. For bitmap resources it's important to define inScaled = true when loading them:

1
2
3
4
5
6
7
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inPreferredConfig = Config.ARGB_8888;
opt.inScaled = true;
// ... some other options

// load the bitmap
Bitmap myBitmap = BitmapFactory.decodeResource(res, R.drawable.my_drawable, opt);

Loading Large Bitmaps in Android

This sections refers to the android developer article at https://developer.android.com/topic/performance/graphics/load-bitmap.html.

It's important to be aware of the memory limits of a mobile device and load bitmaps efficiently. To load a scaled down version of the bitmap you can use the following methods:

 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
public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

Now any bitmap of arbitrarily large size can be loaded like this:

1
Bitmap background = decodeSampledBitmapFromResource(getResources(), R.drawable.bigBackground, 800, 600);

The methods are also available in the utility class BitmapHelper for your convenience.