In the previous chapter, we implemented the AndroidGame
class, which ties together all the submodules for audio, file I/O, graphics, and user input handling. We want to reuse most of this for our upcoming 2D OpenGL ES game, so let's implement a new class called GLGame
that implements the Game
interface we defined earlier.
The first thing you will notice is that you can't possibly implement the Graphics
interface with your current knowledge of OpenGL ES. Here's a surprise: you won't implement it. OpenGL does not lend itself well to the programming model of your Graphics
interface; instead, we'll implement a new class, GLGraphics
, which will keep track of the GL10
instance we get from the GLSurfaceView
. Listing 7–2 shows the code.
package com.badlogic.androidgames.framework.impl;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView;
public class GLGraphics {
GLSurfaceView glView;
GL10 gl;
GLGraphics(GLSurfaceView glView) {
this.glView = glView;
}
public GL10 getGL() {
return gl;
}
void setGL(GL10 gl) {
this.gl = gl;
}
public int getWidth() {
return glView.getWidth();
}
public int getHeight() {
return glView.getHeight();
}
}
This class has just a few getters and setters. Note that we will use this class in the rendering thread set up by the GLSurfaceView
. As such, it might be problematic to call methods of a View
, which lives mostly on the UI thread. In this case, it's OK, as we only query for the GLSurfaceView
's width and height, so we get away with it.
The GLGame
class is a bit more involved. It borrows most of its code from the AndroidGame
class. The synchronization between the rendering and UI threads is a little bit more complex. Let's have a look at it in Listing 7–3.
package com.badlogic.androidgames.framework.impl;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.app.Activity;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.view.Window;
import android.view.WindowManager;
import com.badlogic.androidgames.framework.Audio;
import com.badlogic.androidgames.framework.FileIO;
import com.badlogic.androidgames.framework.Game;
import com.badlogic.androidgames.framework.Graphics;
import com.badlogic.androidgames.framework.Input;
import com.badlogic.androidgames.framework.Screen;
public abstract class GLGame extends Activity implements Game, Renderer {
enum GLGameState {
Initialized,
Running,
Paused,
Finished,
Idle
}
GLSurfaceView glView;
GLGraphics glGraphics;
Audio audio;
Input input;
FileIO fileIO;
Screen screen;
GLGameState state = GLGameState.Initialized;
Object stateChanged = new Object();
long startTime = System.nanoTime();
WakeLock wakeLock;
The class extends the Activity
class and implements the Game
and GLSurfaceView.Renderer
interface. It has an enum called GLGameState
that keeps track of the state that the GLGame
instance is in. You'll see how those are used in a bit.
The members of the class consist of a GLSurfaceView
and GLGraphics
instance. The class also has Audio
, Input
, FileIO
, and Screen
instances, which we need for writing our game, just as we did for the AndroidGame
class. The state
member keeps track of the state via one of the GLGameState
enums. The stateChanged
member is an object we'll use to synchronize the UI and rendering threads. Finally, we have a member to keep track of the delta time and a WakeLock
that we'll use to keep the screen from dimming.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
glView = new GLSurfaceView(this);
glView.setRenderer(this);
setContentView(glView);
glGraphics = new GLGraphics(glView);
fileIO = new AndroidFileIO(getAssets());
audio = new AndroidAudio(this);
input = new AndroidInput(this, glView, 1, 1);
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "GLGame");
}
In the onCreate()
method, we perform the usual setup routine. We make the Activity
go full-screen and instantiate the GLSurfaceView
, setting it as the content View
. We also instantiate all the other classes that implement framework interfaces, such as the AndroidFileIO
or AndroidInput
classes. Note that we reuse the classes we used in the AndroidGame
class, except for AndroidGraphics
. Another important point is that we no longer let the AndroidInput
class scale the touch coordinates to a target resolution, as in AndroidGame
. The scale values are both 1, so we will get the real touch coordinates. It will become clear later on why we do that. The last thing we do is create the WakeLock
instance.
public void onResume() {
super.onResume();
glView.onResume();
wakeLock.acquire();
}
In the onResume()
method, we let the GLSurfaceView
start the rendering thread with a call to its onResume()
method. We also acquire the WakeLock
.
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
glGraphics.setGL(gl);
synchronized(stateChanged) {
if(state == GLGameState.Initialized)
screen = getStartScreen();
state = GLGameState.Running;
screen.resume();
startTime = System.nanoTime();
}
}
The onSurfaceCreate()
method will be called next, which is, of course, invoked on the rendering thread. Here, you can see how the state enums are used. If the application is started for the first time, the state will be GLGameState.Initialized
. In this case, we call the getStartScreen()
method to return the starting screen of the game. If the game is not in an initialized state but was already been running, we know that we have just resumed from a paused state. In any case, we set the state to GLGameState.Running
and call the current Screen
's resume()
method. We also keep track of the current time, so we can calculate the delta time later on.
The synchronization is necessary, since the members we manipulate within the synchronized block could be manipulated in the onPause()
method on the UI thread. That's something we have to prevent, so we use an object as a lock. We could have also used the GLGame
instance itself, or a proper lock.
The onSurfaceChanged()
method is basically just a stub. There's nothing for us to do here.
@Override
public void onDrawFrame(GL10 gl) {
GLGameState state = null;
synchronized(stateChanged) {
state = this.state;
}
if(state == GLGameState.Running) {
float deltaTime = (System.nanoTime()-startTime) / 1000000000.0f;
startTime = System.nanoTime();
screen.update(deltaTime);
screen.present(deltaTime);
}
if(state == GLGameState.Paused) {
screen.pause();
synchronized(stateChanged) {
this.state = GLGameState.Idle;
stateChanged.notifyAll();
}
}
if(state == GLGameState.Finished) {
screen.pause();
screen.dispose();
synchronized(stateChanged) {
this.state = GLGameState.Idle;
stateChanged.notifyAll();
}
}
}
The onDrawFrame()
method is where the bulk of all the work is performed. It is called by the rendering thread as often as possible. Here, we check the state our game is in and react accordingly. As the state can be set on the onPause()
method on the UI thread, we have to synchronize the access to it.
If the game is running, we calculate the delta time and tell the current Screen
to update and present itself.
If the game is paused, we tell the current Screen
to pause itself as well. We then change the state to GLGameState.Idle
, indicating that we have received the pause request from the UI thread. Since we wait for this to happen in the onPause()
method in the UI thread, we notify the UI thread that it can now truly pause the application. This notification is necessary, as we have to make sure that the rendering thread is paused/shut down properly in case our Activity
is paused or closed on the UI thread.
If the Activity
is being closed (and not paused), we react to GLGameState.Finished
. In this case, we tell the current Screen
to pause and dispose of itself, and then send another notification to the UI thread, which waits for the rendering thread to shut things down properly.
@Override
public void onPause() {
synchronized(stateChanged) {
if(isFinishing())
state = GLGameState.Finished;
else
state = GLGameState.Paused;
while(true) {
try {
stateChanged.wait();
break;
} catch(InterruptedException e) {
}
}
}
wakeLock.release();
glView.onPause();
super.onPause();
}
The onPause()
method is our usual Activity
notification method that's called on the UI thread when the Activity
is paused. Depending on whether the application is closed or paused, we set the state accordingly and wait for the rendering thread to process the new state. This is achieved with the standard Java wait/notify mechanism.
Finally, we release the WakeLock
and tell the GLSurfaceView
and the Activity
to pause themselves, effectively shutting down the rendering thread and destroying the OpenGL ES surface, which triggers the dreaded OpenGL ES context loss mentioned earlier.
public GLGraphics getGLGraphics() {
return glGraphics;
}
The getGLGraphics()
method is a new method that is only accessible via the GLGame
class. It returns the instance of GLGraphics
we store so that we can get access to the GL10
interface in our Screen
implementations later on.
@Override
public Input getInput() {
return input;
}
@Override
public FileIO getFileIO() {
return fileIO;
}
@Override
public Graphics getGraphics() {
throw new IllegalStateException("We are using OpenGL!");
}
@Override
public Audio getAudio() {
return audio;
}
@Override
public void setScreen(Screen screen) {
if (screen == null)
throw new IllegalArgumentException("Screen must not be null");
this.screen.pause();
this.screen.dispose();
screen.resume();
screen.update(0);
this.screen = screen;
}
@Override
public Screen getCurrentScreen() {
return screen;
}
}
The rest of the class works as before. In case we accidentally try to access the standard Graphics
instance, we throw an exception, as it is not supported by GLGame
. Instead we'll work with the GLGraphics
method we get via the GLGame.getGLGraphics()
method.
Why did we go through all the pain of synchronizing with the rendering thread? Well, it will make our Screen
implementations live entirely on the rendering thread. All the methods of Screen
will be executed there, which is necessary if we want to access OpenGL ES functionality. Remember, we can only access OpenGL ES on the rendering thread.
Let's round this out with an example. Listing 7–4 shows how our first example in this chapter looks when using GLGame
and Screen
.
package com.badlogic.androidgames.glbasics;
import java.util.Random;
import javax.microedition.khronos.opengles.GL10;
import com.badlogic.androidgames.framework.Game;
import com.badlogic.androidgames.framework.Screen;
import com.badlogic.androidgames.framework.impl.GLGame;
import com.badlogic.androidgames.framework.impl.GLGraphics;
public class GLGameTest extends GLGame {
@Override
public Screen getStartScreen() {
return new TestScreen(this);
}
class TestScreen extends Screen {
GLGraphics glGraphics;
Random rand = new Random();
public TestScreen(Game game) {
super(game);
glGraphics = ((GLGame) game).getGLGraphics();
}
@Override
public void present(float deltaTime) {
GL10 gl = glGraphics.getGL();
gl.glClearColor(rand.nextFloat(), rand.nextFloat(),
rand.nextFloat(), 1);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
}
@Override
public void update(float deltaTime) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
}
}
}
This is the same program as our last example, except that we now derive from GLGame
instead of Activity
, and we provide a Screen
implementation instead of a GLSurfaceView.Renderer
implementation.
In the following examples, we'll only have a look at the relevant parts of each example's Screen
implementation. The overall structure of our examples will stay the same. Of course, we have to add the example GLGame
implementations to our starter Activity
, as well as to the manifest file.
With that out of our way, let's render our first triangle.