Day 11: Abstract Sprite

Introduction

Each game we are going to make with our engine comprises of several scenes, and scenes will be made up of sprites. So what we are going to make today is a fundamental part of our graphics framework. What Sprites usually bring to mind are pictures or small pieces of animation that are integrated into the scene. Our definition of sprite is a bit wider and encapsulates everything that you can see on the scene, even pieces of text. We will originally have a few typed of sprites as depicted in the figure below. You can think of other sprite types to add to the framework.

Sprite Types

There are properties shared among all of these types:

  • Position
  • Scaling factor
  • Rotational angle
  • Transparency (alpha value)

Moreover, rendering and animation handling, while different for each type, should be performed in a unified way. I other words we should abstract these functionalities from the type of the sprite we are dealing with (adding a new sprite type should not affect how stage or scenes work). That said, we are going to make a sprite that has these basic functionalities.

Point structure

Every sprite has a position property, which is described in the form of a 2-dimensional point. Android API has a class called PointF which provides 2-dimensional floating-point point specification. But this is only available on Android and not on other platforms. Should we need to port our engine to other platforms later on, we must separate our framework structure from native APIs. That’s why we will have our own data types for things like points.

Create a package named types under your main library package (com.annahid.libs.artenus). We will put our custom data types in there. Now add a class named Point2D in the said package. Point is simple. It just needs two numbers: x coordinate and y coordinate:

public final class Point2D {
	public float x;
	public float y;

	public Point2D(float px, float py) {
		x = px;
		y = py;
	}
}

This would be enough for our purpose. We can also add other methods to make specific tasks easy. Here are two examples:

public void setZero() {
	x = y = 0;
}

public Point2D multiply(float scalar) {
	return new Point2D(x * scalar, y * scalar);
}

You might think of other methods you can add to this structure to make it more sophisticated.

Implementing Sprite

Sprite is an abstract class and its implementation involves plugging its basic properties. Create a package named sprites under your library package. This package will contain Sprite, its subclasses and other related classes. Create a class named Sprite in the newly created package. We make Sprite an abstract class, so it cannot be directly instantiated.

public abstract class Sprite {
}

Position

The most fundamental information about a sprite is its position. Now that we have our Point2D, we can define Sprite with it. We add a private field to Sprite:

protected final Point2D pos;

We don’t define this as public because we don’t want this object to be re-assigned. Create a default constructor for Sprite and initialize pos there:

protected Sprite() {
	pos = new Point2D(0.0f, 0.0f);
}

The last thing is to add position getter and setters to the class:

public final float getRotation() {
	return rotation;
}

public final void setPosition(Point2D position) {
	if(position != null)
		setPosition(position.x, position.y);
}

public void setPosition(float x, float y) {
	pos.x = x;
	pos.y = y;
}

As you can see, we have defined two setters for the position. We never instantiate a new Point2D, but rather update the fields of the current pos. Even in the case of setting to a Point2D, we simply copy those dimensions into pos.

Scaling

Moving is not the only thing that can be done to a sprite. A sprite might be scaled up or down sometimes. Scaling can happen in any of the two dimensions. It is commonly specified as a floating point number. Scale of 1.0 indicates the original dimension. Anything below this value indicates a down-scale, and anything above that is an up-scale with the same proportion. The figure below shows how scaling factor works.

Scaling Factors

As scaling needs a two-dimensional factor, we can re-use our Point2D for scale, too. So, we add the field to our Sprite class:

protected final Point2D scale;

and initialize it in the constructor, as we did for position. But remember, here we should set both dimensions to one, to indicate that the sprite is not scaled.

scale = new Point2D(1.0f, 1.0f);

And for the getters and setters:

public final Point2D getScale() {
	return scale;
}

public final void setScale(float scaleX, float scaleY) {
	scale.x = scaleX;
	scale.y = scaleY;
}

public final void setScale(Point2D sc) {
	if(sc != null)
		setScale(sc.x, sc.y);
}

public final void setScale(float scaleValue) {
	scale.x = scale.y = scaleValue;
}

There is a third setter added here, which takes only a floating-point argument. The purpose of this setter is to shorten code whenever uniform scaling is intended.

Rotation and transparency

We mentioned two other common properties for sprites:

  • Rotational angle: changing this value will cause the sprite to rotate around its center. The value will be specified in terms of degrees.
  • Alpha value: determines how transparent the sprite looks. A sprite with alpha equal to 0 is completely invisible, and with 1 it is completely opaque. Anything in between causes a transparent-looking sprite.

While practically different, these two values are the same in terms of definition. They are both floating-point numbers:

protected float rotation = 0.0f;
protected float alpha = 1.0f;

Generic getters and setters would be enough for these fields. So just right-click each field in Android Studio and choose “Generate > Getter and Setter” (figure below):

Scaling Factors

Additional transform methods

Sometimes calling setters and getters can be annoying. For example when we need to rotate an object a certain number of degrees, first we need to get the current angle, then add the desired number to it, and then set it to the new value. To make this easier for the user of our framework, we add some additional helping methods:

public final void rotate(float angle) {
	rotation += (angle % 360);
}

public final void move(float amountX, float amountY) {
	pos.x += amountX;
	pos.y += amountY;
}

The rotate method rotates the sprite by a given number of degrees, and the move method offsets the position using the values specified.

Rendering and animation

What sprites need to do in the end of the day is to render their contents. How the sprite is rendered depends on its type. A text sprite will use the font and the characters to draw a piece of text, and an image sprite will draw a piece of a texture on the screen. But one thing is for sure; they all need the OpenGL ES context to draw. So the render method should take this as an argument:

public abstract void render(GL10 gl);

This method should obviously be abstract, because a generic sprite in our framework does not have a unique way to draw. Subclasses will implement this method according to their nature.

Next steps

Now that we have our abstract Sprite class, we can go on and create different kinds of sprites. Even framework users can make their own sprites, but we will implement the most important ones. Next day we will most probably implement image sprites. You can download today’s code below: