Drawing the Background
In the last chapter, you created and finalized the main menu to Prison Break. You should have compiled and run your code on either the Android emulator or an Android-based phone in debug mode, and seen a functioning main menu screen. The Exit button of the main menu is wired to kill the game process. As of right now, however, the Start button is not wired to any code.
In this chapter, you will write the code for the Start button and create the background for Prison Break. To draw the game’s background to the screen, you will use calls to OpenGL ES. In the previous chapters, you used Android SDK methods to display graphics like the menu screen and the buttons. Moving forward, you will work in the realm of OpenGL ES.
Let’s start by writing the code that is activated by the Start button on the main menu.
Starting the Game
The Start button, located on the main menu, is used by the player to start the game. When starting the game, a new Android Activity that controls all of the games functions is launched. Why is the game launched as yet another new Activity?
The game is launched as another Activity so that you, the game developer, have more flexibility in controlling the way your game is executed. If you want to add to your main menu other functions that are not tied directly to your game—for example, a configurator or tally board—this is a good way to keep your game from getting weighed down with superfluous code.
Listing 4-1 shows the PBMainMenu code that you started writing in Chapter 3. The bolded code has been added to launch the PBGame Activity. Add this code to your PBMainMenu. You will create the PBGame Activity next.
Listing 4-1 . PBMainMenu.java
package com.jfdimarzio;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
public class PBMainMenu extends Activity {
/** Called when the activity is first created. */
final PBGameVars engine = new PBGameVars();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
PBGameVars.context = getApplicationContext();
/** Set menu button options */
ImageButton start = (ImageButton)findViewById(R.id.btnStart);
ImageButton exit = (ImageButton)findViewById(R.id.btnExit);
start.getBackground().setAlpha(PBGameVars.MENU_BUTTON_ALPHA);
start.setHapticFeedbackEnabled(PBGameVars.HAPTIC_BUTTON_FEEDBACK);
exit.getBackground().setAlpha(PBGameVars.MENU_BUTTON_ALPHA);
exit.setHapticFeedbackEnabled(PBGameVars.HAPTIC_BUTTON_FEEDBACK);
start.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
/** Start Game!!!! */
Intent game = new Intent(getApplicationContext(),PBGame.class);
PBMainMenu.this.startActivity(game);
}
});
exit.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
int pid= android.os.Process.myPid();
android.os.Process.killProcess(pid);
}
});
}
}
Notice that when you click the Start button now, the code is telling PBMainMenu to launch the PBGame Activity. You do not have a PBGame yet. Let’s create one.
Create a new Activity named PBGame in your Prison Break project. The PBGame is fairly simple as far as code is concerned. PBGame is going to set the content view of the Activity to the game renderer, and control the onPause and onResume events.
Note Keep in mind that when I talk about onPause and onResume, these are not game functions; rather they are Android methods that are called when Android pauses or resumes your Activity.
The code in your new PBGame Activity should look like that in Listing 4-2.
Listing 4-2 . PBGame.java
package com.jfdimarzio;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
public class PBGame extends Activity {
final PBGameVars gameEngine = new PBGameVars();
private PBGameView gameView;
@Override
public void onCreate(Bundle savedInstanceState) {
superonCreate(savedInstanceState);
gameView = new PBGameView(this);
setContentView(gameView);
}
@Override
protected void onResume() {
superonResume();
gameView.onResume();
}
@Override
protected void onPause() {
superonPause();
gameView.onPause();
}
}
Notice that the onCreate() method sets the content view of the Activity to a new instance of PBGameView. PBGameView is a new class that extends GLSurfaceView. The next section of this chapter introduces you to the GLSurfaceView as you create the PBGameView.
Creating the SurfaceView and Renderer
In this section, you will create the SurfaceView and Renderer for your game. PBGameView is a simple Android class that extends the OpenGL GLSurfaceView.
If you have never developed in OpenGL ES before, think of the GLSurfaceView as the canvas on which OpenGL draws your game. The GLSurfaceView is what Android displays to the screen. It cannot act alone, however. The GLSurfaceView needs a corresponding GLSurfaceView Renderer to render the game onto the surface.
Starting with the GLSurfaceView, create a new class named PBGameView, and extend GLSurfaceView, as shown in Listing 4-3.
Listing 4-3 . PBGameView.java
package com.jfdimarzio;
import android.content.Context;
import android.opengl.GLSurfaceView;
public class PBGameView extends GLSurfaceView {
private PBGameRenderer renderer;
public PBGameView(Context context) {
super(context);
renderer = new PBGameRenderer();
this.setRenderer(renderer);
}
}
This is a rather small class. You can see that the purpose of the only constructor in the class is to create an instance of a renderer (PBGameRenderer). There is nothing fancy here, so let’s move on to creating the renderer.
Note Before creating the PBGameRenderer, add the following to your PBGameVars: public static final int GAME_THREAD_FPS_SLEEP = (1000/60) ;
Create a new class called PBGameRenderer in your Prison Break project. This class needs to extend GLSurfaceView.Renderer, as shown in Listing 4-4.
Listing 4-4 . PBGameRenderer.java
package com.jfdimarzio;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class PBGameRenderer implements Renderer{
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;
@Override
public void onDrawFrame(GL10 gl) {
// TODO Auto-generated method stub
loopStart = System.currentTimeMillis();
// TODO Auto-generated method stub
try {
if (loopRunTime < PBGameVars.GAME_THREAD_FPS_SLEEP){
Thread.sleep(PBGameVars.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// TODO Auto-generated method stub
gl.glViewport(0, 0, width,height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
// TODO Auto-generated method stub
gl.glEnable(GL10.GL_TEXTURE_2D);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glDepthFunc(GL10.GL_LEQUAL);
}
}
The renderer has three methods that you need to override: onSurfaceCreated(), onSurfaceChanged(), and onDrawFrame(). As a developer, you will not call any of these methods in your code. The GLSurfaceView is responsible for calling all of these methods at the correct times.
In short, the onSurfaceCreated() acts as the constructor for the renderer and is called when the renderer is created. If everything is running smoothly, this should only be called once; so all of your setup code goes into this method. Right now, the only code that you are calling in your setup routine is OpenGL functions, to set up the texture and depth buffers.
The onSurfaceChanged() method is called whenever the surface is changed. Do not confuse this with onDrawFrame(), however. Drawing a frame does not constitute a surface change. A surface change is more on the lines of a change in screen orientation or a similar destructive event. The onSurfaceChanged() method is also called the first time the renderer is called, after the setup.
In onSurfaceChanged(), you want to set up your game’s view port and call OpenGL’s rendering pipeline to draw your objects. The game’s view port is the area of the game world that is drawn on the screen. Think of the view port as the display on a camera. When you point the camera in a specific direction, you only see a small portion of the entire world. So too, when you create a game using OpenGL: you may create more “objects” in your world than you can see at one time. The view port tells OpenGL what you expect to see rendered to your display.
Caution Be careful when you are using the width and height variables that are passed into onSurfaceChanged(). When GLSurfaceView calls onSurfaceChanged(), the width and height that are passed in are not necessarily the true width and height of the screen. To get the full width and height of the screen, use the context.display.width and context.display.height.
You want to put all of the code that is called on every frame in the onDrawFrame(). This includes frame-rate calculators, the code for drawing all of the objects in the game, collision detection, and cleanup. The only code that is running on each frame in Listing 4-4 is the thread marshal for the frame rate, as well as an OpenGL method for clearing the buffers.
In the next section, you will create the class that draws the background; and then, you will call that class from the onDrawFrame() and draw the background to the screen.
Creating the Background Class
In Prison Break, you are going to create a class that handles the setup of the indexes, vertices, and textures used to draw the background for your game. Each new element that we add to this game follows the format of this class. The background image that you use should be copied to the res/drawable-nodpi folder in your project and named bg1.png. Add the following variable to your PBGameVars file to help you reference the image later.
public static final int BACKGROUND = R.drawable.bg1;
The image that I am using is shown in Figure 4-1.
Figure 4-1. bg1.png, the background image for Prison Break
Now it is time to set up the class to create your background. Create a new class in your Prison Break project called PBBackground.java. You need three methods in this class: a constructor, a draw() method, and a loadTexture() method.
The constructor loads up the vertex, index, and texture arrays into buffers. Since this is the most important step in determining what your background looks like when it is rendered to the screen, let’s take a little time now to discuss what these arrays are and how they are used.
The vertex array is used to define the corners of the polygon that your background image is mapped to. The corners are defined using their x-, y-, and z-axes on the Cartesian coordinate system. Therefore, in creating a square, you would supply the x-, y-, and z-coordinates of the lower-left corner, upper-left corner, upper-right corner, and lower-right corner, respectively.
Now, I need to clear up some potential confusion in the last paragraph. While in the end, the polygon you draw is a square (or rectangle), OpenGL actually draws in right-angled triangles. Two triangles placed next to each other create a square. The purpose of the index buffer is to tell OpenGL the index order of the triangle’s edges, thus telling OpenGL which order the corners in the vertex buffer are drawn. In other words, if the index buffer is 0, 1, 2, 0, 2, 3—like the triangles in Figure 4-2, then the corners in the vertex buffer are the lower-left corner, upper-left corner, upper-right corner, and lower-right corner.
Figure 4-2. Index triangles
Finally, the texture array tells OpenGL which corners of your texture (or image) map to the particular corners of your vertices. Because there is no depth in texture mapping, the texture array only has x- and y-coordinates.
Tip If you are working with a 3D polygon that has vertices using z-coordinates, and you want to map a texture to the vertices, you still only need to provide the corners of the texture using the x- and y-coordinates. In fact, your texture array will most likely be repeated for every vertex you have. While the vertices will change, if you are working with rectilinear polygons, your texture array will probably stay the same.
The draw() method of the class is called on every frame. This method uses the matrix information that is modified in the renderer to draw the background. It also contains settings for culling the faces of the polygons that are not rendered.
The last method, loadTexture(), contains the calls for taking an image that you pass in, and loading that image into OpenGL as a texture. The class uses this texture in the draw() method. Listing 4-5 shows the code for the PBBackground class.
Listing 4-5 . The PBBackground class
package com.jfdimarzio;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
public class PBBackground {
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;
private int[] textures = new int[1];
private float vertices[] = {
0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
};
private float texture[] = {
0.0f, 0.0f,
1.0f, 0f,
1f, 1.0f,
0f, 1f,
};
private byte indices[] = {
0,1,2,
0,2,3,
};
public PBBackground() {
ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
indexBuffer = ByteBuffer.allocateDirect(indices.length);
indexBuffer.put(indices);
indexBuffer.position(0);
}
public void draw(GL10 gl) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
gl.glFrontFace(GL10.GL_CCW);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glCullFace(GL10.GL_BACK);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);
gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisable(GL10.GL_CULL_FACE);
}
public void loadTexture(GL10 gl,int texture, Context context) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(imagestream);
}catch(Exception e){
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}
gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
}
}
In the final section of this chapter, you take the PBBackground class and call it from the PBGameRenderer, thus drawing your background to the screen using OpenGL.
Drawing the Background
In this section, you create a new instance of your background and call it from the PBGameRenderer. The steps to draw your background are as follows:
Listing 4-6 shows the code for PBGameRenderer; the new code for drawing the background is in bold.
Listing 4-6 . PBGameRenderer with Calls to Draw Background
package com.jfdimarzio;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class PBGameRenderer implements Renderer{
private PBBackground background = new PBBackground();
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;
@Override
public void onDrawFrame(GL10 gl) {
// TODO Auto-generated method stub
loopStart = System.currentTimeMillis();
// TODO Auto-generated method stub
try {
if (loopRunTime < PBGameVars.GAME_THREAD_FPS_SLEEP){
Thread.sleep(PBGameVars.GAME_THREAD_FPS_SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
drawBackground1(gl);
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// TODO Auto-generated method stub
gl.glViewport(0, 0, width,height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
// TODO Auto-generated method stub
gl.glEnable(GL10.GL_TEXTURE_2D);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glDepthFunc(GL10.GL_LEQUAL);
background.loadTexture(gl,PBGameVars.BACKGROUND, PBGameVars.context);
}
private void drawBackground1(GL10 gl){
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(1f, 1f, 1f);
background.draw(gl);
gl.glPopMatrix();
}
}
Compile and run your project. At the main menu, click the Start button. You should see the background, as shown in Figure 4-3.
Figure 4-3. The rendered background
Before closing this chapter, let’s have a quick word about OpenGL matrix modes. Having seen the call in the PBGameRenderer, you may be confused by what these do. OpenGL has three modes in which you can modify a different matrix in the rendering pipeline. The three modes (and matrices) are ModelView, Texture, and Projection. Working in these modes requires some abstract thinking, but it is not too hard to grasp.
Placing OpenGL into ModelView mode loads the ModelView matrix. The ModelView matrix controls every set of polygons in your OpenGL world.
The Texture mode, on the other hand, loads up the matrix for all of the textures in your world. Keep in mind, while you associate a texture with a set of vertices, in the OpenGL world, they are still contained within two different matrices. The wording here is important. If there are 50 objects on your screen, each with a texture, placing OpenGL in Texture mode gives you access to all 50 textures—not just the one you think you want to work on.
The Project mode loads the matrix that controls OpenGL’s camera.
Note OpenGL does not really have or understand the concept of a “camera” as such. Most people understand that a camera is used to create a view and renderer, so it is easy to equate what happens in the Project matrix to the common graphics concept of a camera.
Within each matrix mode, there are specific commands that you can use to work with the objects in those matrices.
The command glLoadIdentity() tells OpenGL to load an unaltered copy of the matrix in question. Let’s say, for example, you are in Texture mode and you have a red texture that you mapped to a square. While in Texture mode, you swap the texture for a green one. Calling glLoadIdentity() loads the texture matrix with the red texture.
The command glPushMatrix() performs a similar function. This command gives you a copy of the current matrix, in its current state. Therefore, in our last example, if you were to call glPushMatrix() rather than glLoadIdentity(), you would get a copy of the matrix with a green texture. If you call glPushMatrix() after you call glLoadIdentity(), however, then you would get a copy of the matrix with a red texture.
Once you are done working with the copy of the matrix that you created using glPushMatrix(), use glPopMatrix() to write that copy back to the OpenGL pipeline. This is useful if you have multiple transformations that you want to make on a matrix and you do not want to cause any inadvertent problems to the main matrix.
Finally, there are three commands that you can use to transform your matrices: glScale, glTranslate, and glRotate. As their names imply, glScale and glRotate scale and rotate a matrix, respectively. Again, the effect of these commands depends greatly on the matrix mode you are in. The glTranslate command moves the matrix by the given set of coordinates. These commands are explored in more detail in upcoming chapters.
Summary
In this chapter, you learned how to use OpenGL to create and draw a background to the screen. You also worked with OpenGL Renderers and SurfaceViews. Finally, you created classes that handled the support of OpenGL vertices, textures, and indices.
In the next chapter, you begin to add the blocks and the player’s paddle.
98.82.120.188