GLGame: Implementing the Game Interface

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.

Listing 7–2. GLGraphics.java; Keeping Track of the GLSurfaceView and the GL10 Instance

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.

Listing 7–3. GLGame.java, the Mighty OpenGL ES Game Implementation

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.

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {        
    }

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.

Listing 7–4. GLGameTest.java; More Screen Clearing, Now with 100 Percent More GLGame

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset