Retro Graphics Engine

This part of the documentation will give some insights of the implementation of the various aspects of the framework. It will help you to extend the framework in order to implement new functions etc.

Lifecycle

The sequence flow of the engine architecture is depicted in the following figure. It shows, in terms of the life cycle, which components are started and when:


Engine Lifecycle

The procedure can be described as follows:

  1. Activity#onCreate of activity is called
    • find the custom DrawView component of the activity
    • instantiate the RenderThread class and pass the DrawView as argument (optional)
      • needs the SurfaceHolder from the DrawView component
  2. DrawView is created within the layout
    • RetroEngine.init(context) is called
    • StateManager instance is initialized
    • attach touch and key listeners (all kinds of key and touch events)
    • canvas dimension is assigned to the RetroEngine class
    • RenderThread will be created if not manually passed before to the component
    • DrawView knows about the RenderThread to control it
  3. RenderThread is started (or will be instantiated if not available)
    • knows about the SurfaceHolder from the DrawView component
    • initializes the next State to update and draw
  4. the loop in RenderThread is calling the update logic and the render method for the currently active State
    • the currently active State class will be retrieved from the StateManager class within the RenderThread
    • the state delegates this to its sprite objects

Render loop

The implemented render loop is framerate independent. Zhe rendering and the update part of the sprites is separated. Therefore it consists of a update and a render cycle.

In short, the render loop is a mixture of the implementation from JGO - Game loops! and dewitters-gameloop and can be considered as a loop with fixed timestep so that a constant speed with maximum FPS can be achieved.

The distinct parts of this loop are described in short below.

Update cycle

  • RenderThread will call the update method of the currently active State class
  • State class is in charge to call the update method updateLogic of every sprite

Implementation Hint

It`s convenient to use the State#updateSprites() method if you add your sprites to the root group with State#addSprite()

The update is limited to get a framerate independent application. The application runs at a constant speed with a maximum FPS. Its based on a fixed fps and should practically never be over the set fps of TICKS_PER_SECOND = 50 in this default configuration. The updates will be done at steady 50 times per second.

You can experiment with those variables defined in the class RetroEngine and change them to your personal needs.

Render cycle

The canvas of the DrawView component is locked to call the render method of the currently active State which delegates the canvas object as argument to the render function of the sprites defined in this State. The sprite objects using the canvas to draw bitmap graphics on it.

Rendering is done as many times as the CPU of your smartphone is capable. There is no limitation.

Points to remember

  • Canvas is used for displaying graphics
  • the special implementation DrawView (a custom Android view) provides the canvas
  • The RenderThread will get the current state from the StateManager and will pass the canvas from the DrawView component to the State class' render method

Controlling the loop

You can call those method of RetroEngine anywhere to control the render loop:

  • Pause: RetroEngine.pauseRenderThread();
    • don't process the update and render loop
  • Resume: RetroEngine.resumeRenderThread();
    • resume processing update and render loop
  • Exit the loop: RetroEngine.changeRunningState(false);

Engine Initialization

Each DrawView component in the layout will have its own render thread. This allows multiple independent graphics rendering.

You have two options to get the engine initialized. This architecture allows you to define your own render thread or use the one provided by the DrawView component. In both approaches you can use the default implementation RenderThread provided by Retro Graphics Engine.

Create the RenderThread

Create the RenderThread class by yourself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//inside an activity

    public RenderThread renderThread;
    public DrawView drawView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_layout);

        drawView = findViewById(R.id.graphics);
        // use the default render thread from the engine
        renderThread = new RenderThread(drawView);
    }

We're using the default RenderThread class provided by Retro Graphics Engine. You can extend this class and provide your own implementation with that approach.

With that approach both components get to know each other automatically.

Acquire the RenderThread

Let the DrawView component create the default RenderThread and get it back when the surface is created:

 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
public class MyActivity extends AppCompatActivity implements DrawView.DrawViewInteraction {

    public static RenderThread renderThread;
    private DrawView drawView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_touch_interaction);

        drawView = findViewById(R.id.graphics);
        // subscribe this activity to listened when the DrawView is
        // creating the surface
        drawView.addListener(this);
    }

    @Override
    public void onCreated(DrawView drawView) {
        // get the RenderThread created by the DrawView
        renderThread = drawView.getRenderThread();

        // now add your custom states here
        renderThread.addState(new MyBasicState());
    }
}

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 in the same way like this:

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

Sprite types

Different types of sprite classes exist in the framework:

  • Sprites (default sprites, class AbstractSprite implements ISprite)
  • Animated sprites (class AnimatedSprite extends from AbstractSprite)
  • Background layer (interface BackgroundLayer implements ISprite)
  • Sprite groups (interface ISpriteGroup implements ISprite)

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

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

The class AbstractSprite implements the general logic and behaviour of animated sprites and simple shapes.

As mentioned before, the render method draw() in the AbstractSprite class is displaying the bitmap and is using linear transformation (a subset of linear algebra, especially used in computer graphics) to scale, rotate and translate the sprite. This approach unifies all operations no matter what kind of sprite is handled on the canvas.

You can use the prototyping pattern approach to create multiple instances of one sprite with ease. Just use the init() function of a sprite class. See the example section for more details.

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 main canvas. This complies with the generic render function of AbstractSprite because some straightforward linear algebra is done to transform (translate, rotate) the graphic object (bitmap).

Circle

A circle can be created. 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);

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

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

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.

AnimatedSprites

Sprites can also be animated, in the sense that the texture of the sprite changes over time. 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.

To animate a sprite in respect of its properties see section 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.

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

The viewpoint origin

This point represents the origin of the canvas, the drawing surface. Normally it has the value (0,0) and depicts the top-left corner of the canvas. This may change in case of the background layer you use. Because when the canvas is translated then the coordinates of every sprite on it will change.

This important point can then be used to set the absolute position of sprites on the canvas or to check for collision between sprites. In those cases you need the absolute position.

You can call State#getViewportOrigin() in a State class at every time. Each sprite also has this method available. If the sprite has a parent then the parents value will be used.

States

  • a state defines a scene where sprites can be arranged and the logic for a scene can be implemented
  • scene objects are nodes, respectively sprites
  • every State has a so called root node which is a SpriteGroup. It`s convenient to use for automatically drawing every other child sprite

Base methods for sprite handling:

Method Description
setInitAsync() / isInitAsync() should state initialised asynchronously by DrawView? A standard progess dialog is shown
addSprite / addSprites() add one or more sprites to the rootGroup of the state
updateLogic() all the update logic as to be implemented here. call updateSprites() to delegate all updates to the sprites added to rootGroup
init() initialisation routines, for instance create all sprites, load resources etc.
render() place your render sprite routines here. use drawSprites() to delegate all render stuff to the sprites added to rootGroup

Methods for background handling:

TODO

StateManager

  • a class which holds of every State
  • capability to switch between State classes or to start an Activity
  • Interact with Activity: within a State you can call the StateManager and get access to the Activity
1
manager.getParentActivity()

Keep in mind Each activity and class has access to the same states that were added before - the StateManager is a singleton. Throughout the app every class has access to the same objects of this class.

In contrast, each RenderThread has its own individual added states. The StateManager acts as a global resource holder of all states. Each DrawView with its associated render thread can have different states. If the render thread exits all added states will be cleared automatically. But they share all the same instance of each state.

See more examples in the section Handle different flows.

Change to another State

To change the state within a State to another one, the state has to be added to the RenderThread before. Then you can simply call:

1
StateManager.getInstance().changeGameState(MyNewState.class);

The class of the state to change to has to be passed as argument to the method.

Animation

The engine provides some simple and basic animations for sprites that also can be combined for storyline-like animations to produce scripted video sequences.

There are several ways to implement your animation or effect logic which are described in the following subsections:

  1. logic directly implemented in a sprite class
  2. add animations via AnimationSuite to a sprite
  3. AnimationTimeline for story-like sequences

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
    }
}

AnimationSuite

To implement a basic animation for a sprite the following animations are available so far:

  1. AlphaValueAnimation
  2. RotateAnimation
  3. AbsoluteSingleNodeLinearTranslation
  4. RelativeLinearTranslation
  5. LinearTranslation

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 the AnimationTimeline 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);
    }

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.

Helper classes

  • MathUtils
  • ColorTools
  • InterpolationHelper
  • BitmapHelper