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
implementsISprite
, andAnimatedSprite
is the concrete implementation of a sprite (and implements theISpriteAnimateable
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
implementsISprite
- Sprite groups: interface
ISpriteGroup
implementsISprite
- 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 canvasinitWithRadius(float rds, float x, float y)
: create a circle with default radiusrds
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):
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 State
s 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.