Day 10: Centralized Texture Manager

Using the texture class

Drawing a texture

On day 8 we created a self-drawing texture class, but we never used it. It is now time to finally give it a try (not to mention you should now have Android Studio opened). First we need a simple image we can work with. Create a directory named drawable under the res directory of your Artenus module. Download the following image into your drawable directory:

Sample Texture Image

Now add a field of type Texture to the Stage class of your Artenus module:

private Texture tex;

You can create the texture at Stage’s constructor by adding the following:

tex = new Texture(R.drawable.blast);

Loading the texture can’t be done at the constructor because the OpenGL ES context is not ready yet. It will be ready though at onSurfaceCreated. So add the following code to that function:

tex.load(getContext());

The final part is to draw the texture. Currently your onDrawFrame method should be programmed to draw a simple rectangle. We still need to draw that rectangle. The texture class uses glDrawArrays, but does not load any primitive beforehand. By design it relies on the caller to do that. So the changes that need to be made are replacing the current glDrawArrays with a call to the texture’s drawing methods and moving all transformations there. Pushing and popping the matrix are also handled by our Texture class. The final code will look like this:

public final void onDrawFrame(GL10 gl) {
	gl.glClear(GLES10.GL_COLOR_BUFFER_BIT);
	gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
	tex.prepare(gl, GL10.GL_CLAMP_TO_EDGE);
	tex.draw(gl, w / 2, h / 2, tex.getWidth(), tex.getHeight(), 0);
}

The code is self-explanatory. The only twist is that after setting the vertex buffer we leave the rest to Texture. You can review day 8 for full details on prepare and draw methods. Otherwise you can now run My Game to see the result. You should see the texture drawn at the center of the screen.

Screenshot

If you are confused with code placements, you can download the full version of the Stage class and have a look:

Problem with this approach

We did the above test to see how our texture class works. However, this kind of usage is far away from our goal. We drew textures by modifying the code in Stage. But Artenus is a game library, not a graphical prototype. Here is a list of problems we are facing here:

  • We modified library code here. A library is meant to be used, not modified for each specific application.
  • Here we had to deal with only one texture. A game has an indefinite number of textures.
  • We have to be careful when we load or unload our textures. A complete game is loading and unloading texture all the time and it can be frustrating using this approach.
  • Ugly OpenGL ES code is in the way. We had to manually render the texture through so many function calls.

Mainly, the user should not know there is OpenGL ES working behind the scene. If we achieve this, we will have high level games that can be easily ported to other platforms, provided our engine is available for those platforms. The first step is to hand texture management over to the library user (game developer).

Texture manager

We are now going to create a centralized texture bank that takes care loading, unloading, and validity check for multiple textures. Our goal in the longer term is to take the Texture class behind the curtains, so the user only works with Android resource identifiers. Texture manager will only have static methods, because we don’t need more than one manager per app.

We are going to put the following functionalities in this class:

  • Adding textures to the bank when required.
  • Removing textures when they are not needed.
  • Loading elements before they are drawn.
  • Unloading everything when the app is paused.

Let us begin! Create a class named TextureManager in Artenus (beside Texture and Stage).

State management

Before we can draw something, it should be loaded. The manager should always know when to load textures. However, it is inefficient to always check the status of all assets (which can be numerous in some cases) all the time. That’s why we do that only when we need. Our approach is to define states for our manager. It can be at one of the following states at any time:

STATE_FRESH

One or more textures are not yet loaded and a loading routine needs to be executed before it is self to use textures in the manager. I chose “fresh” because “unloaded” would have double meaning (before load/after unload). The state indicates that we have a fresh and unprocessed set of textures.

STATE_LOADING

The manager is loading textures. No attempt should be made by Stage to draw any of them in this state.

STATE_LOADED

The manager has finished loading textures. All textures are now available for drawing.

STATE_UNLOADING

An unload process is being carried out (either the app is paused or some unneeded textures are thrown away). No drawing should be attempted because the list is being altered and trying to access some elements might throw an exception.

We keep the current state as an integer. The initial TextureManager looks like this:

public final class TextureManager {
	public static final int STATE_FRESH = 0;
	public static final int STATE_LOADING = 1;
	public static final int STATE_UNLOADING = 2;
	public static final int STATE_LOADED = 3;

	/* We've already loaded everything we have (which is nothing)! */
	private static int state = STATE_LOADED;

	public static int getState() {
		return state;
	}
}

As you might have noticed, we have also added a getter for state, so Stage can query the value to regulate its functionalities.

Maintaining a list

The manager needs to keep track of the textures currently added to it, and should provide means for external classes (such as Stage) to access them. Moreover, we want texture access to be as simple as possible. The game developer shouldn’t have to worry how textures are stored. What they know about their image is only a resource identifier (such as R.drawable.blast) and they should be able to retrieve the corresponding texture by calling out for that identifier.

We introduce two containers to TextureManager:

private static ArrayList texList = new ArrayList();
private static SparseArray texMap = new SparseArray();

Our main container will be the ArrayList. SparseArray is a special kind of container in Android that maps integer values to objects. We use this for mapping resource identifiers to Texture instances to respond to queries efficiently. The following function retrieves a Texture by specifying a resource id.

public static Texture getTexture(int resourceId) {
	return texMap.get(resourceId);
}
If the texture is not added to texMap, this method will return null

Adding textures

Now that we have our container, we can start adding textures to it. The interface for that should be easy to use, with a game in mind. A game has lots of textures. Adding them one by one might not be a good practice. That’s why our method accepts bulks:

public static void add(int[] textureSet) {
	state = STATE_LOADING;

	for(int i = 0; i < textureSet.length; i++) {
		Texture tex = new Texture(textureSet[i]);
		texMap.put(textureSet[i], tex);
		texList.add(tex);
	}

	state = STATE_FRESH;
}

The method above takes an array of integers, each representing a resource identifier. The flow of the code is as follows:

  1. Set state to STATE_LOADING, so nobody uses the textures.
  2. For each texture:
    1. Add it to the map with the resource identifier as the key.
    2. Add it to the list.
  3. Set state to STATE_FRESH.

One might now ask why did we just set the state to STATE_FRESH and didn’t go on loading textures? The answer is because we have a game in mind. Loading textures is time-consuming and it would be more appropriate to show a loading screen or some sort of inline animation to the user until textures are loaded. That’s why we let Stage take care of that and call load at its own discretion. All we do is just set the signal that a load is required.

Removing textures

Removing textures is a similar process. We should remove them from the map and the list. However, there is a third thing we should do. Textures reside in OpenGL ES context and take up memory. As they are unusable in our engine after deletion, we should free up that memory by destroying them:

public static void remove(int[] textureSet) {
	final int savedState = state;
	state = STATE_UNLOADING;

	for(int i = 0; i < textureSet.length; i++) {
		Texture tex = texMap.get(textureSet[i]);
		tex.destroy();
		texMap.remove(textureSet[i]);
		texList.remove(tex);
	}

	state = savedState;
}

Here we first set state to STATE_UNLOADING, and for each texture we have an extra step: destroying it. We also have another tweak. While after adding some textures we are sure that loading is necessary, after removing we are not so sure about that. We might have just removed from a set of fully loaded textures, or from a partially loaded set where there is more work to do. That’s why in the end we reset the state to whatever it was before, and not to STATE_FRESH like we did in the add method.

Optionally we can have a clear method too, which removes everything from the list:

	public static void clear() {
		state = STATE_UNLOADING;

		for(int i = 0; i < texList.size(); i++)
			texList.get(i).destroy();

		texList.clear();
		texMap.clear();
		state = STATE_LOADED;
	}

Loading

When state is STATE_FRESH, Stage is required to load the textures before any attempt to draw. The means of loading also resides in TextureManager:

public static void loadTextures(Context context) {
	if(state != STATE_FRESH)
		return;

	state = STATE_LOADING;

	final Object[] list = texList.toArray();

	for(int i = 0; i < list.length; i++) {
		Texture tex = (Texture)list[i];

		if(!tex.isLoaded())
			tex.load(context);
	}

	checkAndSetLoaded(list);
}

If state is not “fresh”, it means that this is an invalid call. We won’t throw an exception though. We just silently exit the function. We convert the ArrayList to an array for two main reasons:

  • It is generally faster to work with arrays (at least we skip method calls when accessing elements).
  • Concurrent altering and access can cause exceptions and we want to avoid that: Loading and add/remove operations are not mutually exclusive. We don’t want to fall into the dangerous situation where the list is changed due to an addition or removal, while we are traversing it.

Finally we call another method to check if everything is loaded, and change state if so. But… wait! Didn’t we just load everything? The answer to this question is another question:

What will happen if the user presses the Home button while we are loading?

Being another question it is still clear enough. We are loading textures into OpenGL ES context and if the app is paused in the meantime, the context will be destroyed. When it resumed, we will continue loading the rest of the textures, but the first half is already lost. That’s why we should go through them again to see if anything went wrong.

private static void checkAndSetLoaded(Object[] list) {
	for (int i = 0; i < list.length; i++) {
		Texture tex = (Texture)list[i];

		if(!tex.isLoaded()) {
			state = STATE_FRESH;
			return;
		}
	}

	state = STATE_LOADED;
}

This method simply checks whether all textures are loaded properly. If anything is missing, it sets the state back to STATE_FRESH and returns. It is Stage’s responsibility to call loadTextures again at a later time, whenever possible. If everything is okay, state changes to STATE_LOADED successfully.

Unloading

When the application pauses, textures should be marked unloaded. Previously the stage did this manually (when we had only one texture). But with a centralized stage manager, Stage just needs to let that do the procedure for all textures.

public static void unload() {
	state = STATE_UNLOADING;

	final Object[] list = texList.toArray();

	for(int i = 0; i < list.length; i++) {
		Texture tex = (Texture)list[i];
		tex.destroy();
	}

	state = STATE_FRESH;
}

If you have understood the code for load, this should be simple to understand. We just go through all textures and call destroy on them. This method does not remove any textures from the list. It just unloads them in place. The Stage will see STATE_FRESH on resume and will call loadTextures.

The coding of this class is finished for today. It will be amended later on in this guide, but this is the basic functionality we need for now.

Next steps

Our next step will be implementing Sprites, which will be the main graphical elements of our engine. Textures will not be used directly, but rather through Sprites and what we designed today will federate texture operations from within the library. If what we discussed here is non-obvious and you can understand better by looking at the full code, please help yourself: