Chapter 12

Navigating the 3-D Environment

You have reached the final chapter of your adventures in learning Android game development. In this book, you started from nothing and created a 2-D scrolling shooter. From the skills learned in creating that game, you were able to create the environment for a 3-D game. Although this book does not cover using all of the skills you have acquired or step you through creating an entire 3-D game, you will learn enough of the basics that hopefully to use that logic to finish the game. In this chapter, you will learn what differences await you when you are trying to create a control system to navigate the 3-D corridor.

When you created a control system for the 2-D Star Fighter game, the movement was simple. The player could only move left or right. In Blob Hunter, the player should have the freedom to move 360 degrees on the z plane. Let's take a look at what kind of challenges that will pose for you.

At the end of this chapter, I have provided a listing of the key files for the 3D project. These files were chosen because of their complexity, number of changes, or proclivity for causing a problem when you compile your project. If you are having trouble running your 3D project at the end of this chapter, please check you files against those listed after the Summary.

Creating the Control Interface

In this section, you are going to create the control interface, the means by which your player interacts with your game.

In Star Fighter, the control interface was a simple left and right motion. However, in a 3-D game, the player will expect to be able to move left, right, forward, backward, and possibly look up or down. While these are many more controls to keep track of, the basic concepts that you learned for Star Fighter still apply.

Let's borrow some code for Star Fighter and quickly adapt it to move the player forward through the corridor.

Currently, your BlobhunterActivity should appear as follows:

package com.proandroidgames;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;

public class BlobhunterActivity extends Activity {
        private BHGameView gameView;

        @Override
        public void onCreate(Bundle savedInstanceState) {

                super.onCreate(savedInstanceState);
                gameView = new BHGameView(this);
                setContentView(gameView);
                BHEngine.context = this;
        }
        @Override
        protected void onResume() {
                super.onResume();
                gameView.onResume();
        }

        @Override
        protected void onPause() {
                super.onPause();
                gameView.onPause();
        }

}

You are going to adapt the onTouchEvent() method that you created in Star Fighterto handle a forward motion as well.

NOTE: In this chapter, you will only add the forward motion control. However, you can easily adapt the control to also handle backward motion.

Before you add in your onTouchEvent() method, you need to add a few constants to BHEngine.

Editing BHEngine

The goal here is to help you track what the player is trying to do and where the player is in the environment. To do so, add the following lines to your BHEngine.java file:

public static final int PLAYER_FORWARD = 1;
public static final int PLAYER_RIGHT = 2;
public static final int PLAYER_LEFT = 3;

public static final float PLAYER_ROTATE_SPEED = 1f;
public static final float PLAYER_WALK_SPEED = 0.1f;
public static int playerMovementAction = 0;

The PLAYER_FORWARD, PLAYER_RIGHT, and PLAYER_LEFT constants will be used to track what control the player touched, indicating where the player wants to move in the environment. The PLAYER_ROTATE_SPEED and PLAYER_WALK_SPEED constants indicate how quickly the view of the player rotates on the y axis and how quickly the player walks in the environment, respectively. Finally, playerMovementAction tracks which action (PLAYER_FORWARD, PLAYER_RIGHT, or PLAYER_LEFT) is the current one.

Now that your constants are in place, you can create the control interface in BlobhunterActivity.java.

Editing BlobhunterActivity

The first code you need to add to BlobhunterActivity is a call to the BHEngine.display method. You need to initialize the display variable so that the control interface can call it to determine where on the screen the player has touched.

...

@Override
public void onCreate(Bundle savedInstanceState) {

BHEngine.display = ((WindowManager)
getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

        super.onCreate(savedInstanceState);
        gameView = new BHGameView(this);
        setContentView(gameView);
        BHEngine.context = this;
}

...

With the display initialized, add an onTouchEvent() method to the BlobhunterActivity class:

...

@Override
public boolean onTouchEvent(MotionEvent event) {
        return false;
}

...

If you still have the Star Fighter project, you can copy and paste the following code directly from its control interface into the new onTouchEvent() method of Blob Hunter. In the event that you no longer have the code for the Star Fighter project, feel free to download the completed project from the Apress web site.

CAUTION: If you are going to copy and paste from the Star Fighter project, be sure to rename the proper constants and variables to correspond to those in the Blob Hunter project.

...

@Override
public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();

        int height = BHEngine.display.getHeight() / 4;
        int playableArea = BHEngine.display.getHeight() - height;

        if (y > playableArea){
                switch (event.getAction()){
                        case MotionEvent.ACTION_DOWN:
                                if(x < BHEngine.display.getWidth() / 2){
                                        BHEngine.playerMovementAction = BHEngine.PLAYER_LEFT;
                                }else{
                                        BHEngine.playerMovementAction = BHEngine.PLAYER_RIGHT;
                                }
                                break;
                        case MotionEvent.ACTION_UP:
                                BHEngine.playerMovementAction = 0;
                                break;
                }
        }

        return false;
}

...

Next, let's add the control for detecting forward motion.

Letting Your Player Move Forward

Right now, the onTouchEvent() uses the y >playableArea condition to detect if the player has touched the lower portion of the screen. Add an else statement to detect a touch to the upper portion of the screen. You will use this touch to the upper portion of the screen to determine that the user wants to move forward.

...

@Override
public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();


        int height = BHEngine.display.getHeight() / 4;
        int playableArea = BHEngine.display.getHeight() - height;

        if (y > playableArea){

                switch (event.getAction()){
                        case MotionEvent.ACTION_DOWN:
                                if(x < BHEngine.display.getWidth() / 2){
                                        BHEngine.playerMovementAction = BHEngine.PLAYER_LEFT;
                                }else{
                                        BHEngine.playerMovementAction = BHEngine.PLAYER_RIGHT;
                                }
                                break;
                        case MotionEvent.ACTION_UP:
                                BHEngine.playerMovementAction = 0;
                                break;
                }    
        }else{
                switch (event.getAction()){
                        case MotionEvent.ACTION_DOWN:
                                BHEngine.playerMovementAction = BHEngine.PLAYER_FORWARD;
                                break;
                        case MotionEvent.ACTION_UP:
                                BHEngine.playerMovementAction = 0;
                                break;
                }
        }

        return false;
}

...

All you are doing in this new code is detecting if the player has touched the upper portion of the screen, and if so, you set the playerMovementAction to PLAYER_FORWARD.

Keep in mind, when you are creating a full game, that you will want to tweak this slightly to also account for a backward touch control, and possibly some panning up or down controls. In the next section, you are going to react to these controls in the BHGameRenderer class and move the player accordingly through the corridor.

Moving Through the Corridor

Moving through the corridor is a little tricky, but with some practice, you can create a control system that is smooth and stable. Admittedly, you would be able to optimize a great camera system if you were adept enough at OpenGL to create your own matrices and perform your own matrix multiplication. However, from the beginning of this book, the goal has been to let you use OpenGL's built-in tools as a substitute for the learning curve of manual processes.

Open BHGameRenderer.java, which is where your game loop code is stored. The first thing you will need to do is add a couple of variables to help track the player's location.

...

public class BHGameRenderer implements Renderer{
private BHCorridor corridor = new BHCorridor();
private float corridorZPosition = -5f;

        private float playerRotate = 0f;

        private long loopStart = 0;
        private long loopEnd = 0;
        private long loopRunTime = 0 ;

...

The corridorZPosition variable is initially set to -5. This represents the initial location of the player in the corridor. The value of -5 should set the player at the end of the corridor, because the corridor, as you have set it in the BDCorridor class, extends toward the 4 units on the z axis. Therefore, starting the play at -5 (or 5 units toward the player/screen) will give the appearance that the player is standing at the entrance of the corridor.

Next, locate the drawCorridor() method that you created in the previous chapter, and erase all of its contents except for the call to the corridor's draw() method, as follows:

private void drawCorridor(GL10 gl){

corridor.draw(gl);

}

Using a switch…case statement, similar to the one in Star Fighter, you will detect which action the player is trying to take. However, how do you react to a forward motion if that is what the player wants to do?

In the Star Fighter project, you had to move the player only left or right. Both movements were accomplished by a positive or negative value on the x axis. However, in a 3-D environment adding or subtracting on the x axis would result is a sideways, or strafing, motion, and that is not what you are going for here. You want to move the player forward and let them turn their head  to the left or right. These are vastly different motions from those you used in Star Fighter.

To move the player forward, you are going to add values to the z axis. Recall that you are looking at the corridor along the z axis, and the 0 value for the z axis of the corridor is at the far wall. Therefore, you are starting at -5 (see the corridorZPosition variable) and moving to 0.

To simulate turning the player's head, you will need to rotate, not translate, along the y axis: you do not actually want to move along the y or x axis; rather, just like turning your head in real life, you want to rotate on the axis.

Add a switch . . . case statement to adjust the corridorZPositon and playerRotate values accordingly. This is the same process used in Star Fighter, so it will not be discussed in detail. If it does not look familiar, check back trough the Star Fighter code in Chapter 5.

private void drawCorridor(GL10 gl){

        switch(BHEngine.playerMovementAction){
                case BHEngine.PLAYER_FORWARD:
                        corridorZPosition += BHEngine.PLAYER_WALK_SPEED;
                        break;
                case BHEngine.PLAYER_LEFT:
                        playerRotate -= BHEngine.PLAYER_ROTATE_SPEED;
                        break;
                case BHEngine.PLAYER_RIGHT:
                        playerRotate += BHEngine.PLAYER_ROTATE_SPEED;
                        break;
                default:
                        break;
        }


        corridor.draw(gl);

}

In the next section, you will adjust the player's position, or view, while moving down the corridor.

Adjusting the View of the Player

As discussed earlier, OpenGL has no concept of a camera like some 3-D systems do. Rather, you are tricking your way through making the environment look a certain way to the player, so to speak.

The same translations and rotations that you used in Star Fighter to move 2-D models in the scene will also be used to rotate and translate the corridor so that the player will believe he or she is walking through it.

Add a translate to the drawCorridor() method that will move the model along the z axis, and add a rotate that will turn the model corresponding to where the player is looking.

private void drawCorridor(GL10 gl){

        switch(BHEngine.playerMovementAction){
                case BHEngine.PLAYER_FORWARD:
                        corridorZPosition += BHEngine.PLAYER_WALK_SPEED;
                        break;
                case BHEngine.PLAYER_LEFT:
                        playerRotate -= BHEngine.PLAYER_ROTATE_SPEED;
                        break;
                case BHEngine.PLAYER_RIGHT:
                        playerRotate += BHEngine.PLAYER_ROTATE_SPEED;
                        break;
                default:

                        break;
        }

        GLU.gluLookAt(gl, 0f, 0f, 0.5f, 0f, 0f, 0f, 0f, 1f, 0f);
        gl.glTranslatef(-0.5f, -0.5f, corridorZPosition);
        gl.glRotatef( playerRotate, 0.0f,1.0f, 0.0f);


        corridor.draw(gl);

}

Compile and run your code; you should now have a rudimentary navigation system to move forward and turn left and right. With a little work using the skills you have already learned, you can easily add some collision detection to keep the player from walking through the walls. Try these examples on your own:

  • Add a navigation control to allow the player to backup through the corridor. Here's a hint for doing this: create a touch even on the screen that will subtract a given integer value from the current z axis position when touched.
  • Create collision detection system to keep the player from walking through the walls. Here's your hint on this one: track the player's current axis positions and test them against the known positions of the corridor walls. Remember that the corridor walls will not move. Something like this might help you:
if corridorZPosition <= -5f){
        corridorZPosition = -5f;
}
if corridorZPosition >= 0f){
        corridorZPosition = 0f;
}
  • Create a navigation system to let the player look up and down in the environment. As a hint on this task, consider that it sounds more difficult than it is. Simply add a touch event that will either add or subtract values from a new rotation on the x axis. This will pivot the player's field of view up or down.

You have the skills needed to create a fully functional 3-D game, and surprisingly enough, they were the same skills you used to create a fully functional 2-D game; you've just added more details.

Summary

I hope you enjoyed this primer into the basic skills required to create some enjoyable casual games and that you continue to practice and expand on those skills. There is so much more to OpenGL ES and Android Ice Cream Sandwich than what was covered in this book, but you now have a great base of knowledge that will help you plot your course further into the world of Android game development.

Reviewing the Key 3-D Code

The listings that follow contain all of the code needed to double check your work in the event that you are having a problem getting Blob Hunter to run correctly. I have selected the BHEngine.java, BHCorridor.java, and BHGameRenderer.java. These files either touch the most code-like the BHEngine, contain complicated concepts-like the BHCorridor, or perform the most functionality-like the BHGameRenderer.

The first file that you can check is the BHEngine.java, shown in Listing 12–1. BHEngine is the key settings file and it contains the settings used throughout the project. Because this file is used so extensively in the Blob Hunter project, it has the greatest likelihood to cause problems when you compile.

Listing 12–1. BHEngine.java

package com.proandroidgames;

import android.content.Context;
import android.view.Display;


public class BHEngine {
        /*Constants that will be used in the game*/
        public static final int GAME_THREAD_DELAY = 4000;
        public static final int GAME_THREAD_FPS_SLEEP = (1000/60);
        public static final int BACK_WALL = R.drawable.walltexture256;
        public static final int PLAYER_FORWARD = 1;
        public static final int PLAYER_RIGHT = 2;
        public static final int PLAYER_LEFT = 3;
        public static final float PLAYER_ROTATE_SPEED = 1f;
        public static final float PLAYER_WALK_SPEED = 0.1f;
        /*Game Variables*/
        public static int playerMovementAction = 0;
        public static Context context;
        public static Display display;
}

Listing 12–2 shows the BHCorridor.java file. This file could cause you problem because it contains a code concept that is not only abstract, but was not covered previously in Part 1 of this book. The structure of the vertices[] and texture?array is key to the functionality of the entire project. If the arrays are not setup correctly, the project will not run as expected, if at all. When checking this file, pay close attention to the arrays and the array definitions.

Listing 12–2. BHCorridor.java

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 BHCorridor {

           private FloatBuffer vertexBuffer;
           private FloatBuffer textureBuffer;

           private int[] textures = new int[1];

           private float vertices[] = {
                           -2.0f, 0.0f, 0.0f,
                           1.0f, 0.0f, 0.0f,
                           -2.0f, 1.0f, 0.0f,  
                           1.0f, 1.0f, 0.0f,

                           1.0f, 0.0f, 0.0f,
                           1.0f, 0.0f, 5.0f,
                           1.0f, 1.0f, 0.0f,
                           1.0f, 1.0f, 5.0f,

                           0.0f, 0.0f, 1.0f,
                           0.0f, 0.0f, 5.0f,
                           0.0f, 1.0f, 1.0f,
                           0.0f, 1.0f, 5.0f,

                           -2.0f, 0.0f, 1.0f,
                           0.0f, 0.0f, 1.0f,
                           -2.0f, 1.0f, 1.0f,  
                           0.0f, 1.0f, 1.0f,
                           };

            private float texture[] = {          
                -1.0f, 0.0f,
                1.0f, 0f,
                -1f, 1f,
                1f, 1.0f,

                -1.0f, 0.0f,
                1.0f, 0f,
                -1f, 1f,
                1f, 1.0f,

                -1.0f, 0.0f,
                1.0f, 0f,
                -1f, 1f,
                1f, 1.0f,

                -1.0f, 0.0f,
                1.0f, 0f,
                -1f, 1f,
                1f, 1.0f,



                };

           public BHCorridor() {
              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);
           }

            public void draw(GL10 gl) {

             gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
             gl.glFrontFace(GL10.GL_CCW);

             gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
             gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
              gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

              gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0,4);

              gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 4,4);

              gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 8,4);

              gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 12,4);

              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);

                      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_REPEAT);
                      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_REPEAT);

                      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

                      bitmap.recycle();
                   }
                }

The final key file in the Blob Hunter is the BHGameRenderer.java. This file contains the game loop for the Blob Hunter game. Just as with Star Fighter, the game loop is the most likely place for a code problem because it has the most code of any file in the project. Listing 12–3 provides the source for the BHGameRenderer.java.

Listing 12–3. BHGameRenderer.java

package com.proandroidgames;


import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLU;

public class BHGameRenderer implements Renderer{
        private BHCorridor corridor = new BHCorridor();
        private float corridorZPosition = -5f;
        private float playerRotate = 0f;

        private long loopStart = 0;
        private long loopEnd = 0;
        private long loopRunTime = 0 ;

        @Override
        public void onDrawFrame(GL10 gl) {
                loopStart = System.currentTimeMillis();
                // TODO Auto-generated method stub
                try {
                        if (loopRunTime < BHEngine.GAME_THREAD_FPS_SLEEP){
                                Thread.sleep(BHEngine.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);
                gl.glLoadIdentity();

                drawCorridor(gl);

                loopEnd = System.currentTimeMillis();
            loopRunTime = ((loopEnd - loopStart));

        }


        private void drawCorridor(GL10 gl){

                if (corridorZPosition <= -5f){
                        corridorZPosition = -5f;
                }
                if (corridorZPosition >= 0f){
                        corridorZPosition = 0f;
                }

                switch(BHEngine.playerMovementAction){
                case BHEngine.PLAYER_FORWARD:
                        corridorZPosition += BHEngine.PLAYER_WALK_SPEED;
                        break;
                case BHEngine.PLAYER_LEFT:
                        playerRotate -= BHEngine.PLAYER_ROTATE_SPEED;
                        break;
                case BHEngine.PLAYER_RIGHT:
                        playerRotate += BHEngine.PLAYER_ROTATE_SPEED;
                        break;
                default:
                                break;
                }

                GLU.gluLookAt(gl, 0f, 0f, 0.5f, 0f, 0f, 0f, 0f, 1f, 0f);
                gl.glTranslatef(-0.5f, -0.5f, corridorZPosition);
                gl.glRotatef( playerRotate, 0.0f,1.0f, 0.0f);

                corridor.draw(gl);

        }

        @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();

                GLU.gluPerspective(gl, 45.0f, (float) width / height, .1f, 100.f);
                gl.glMatrixMode(GL10.GL_MODELVIEW);
            gl.glLoadIdentity();



        }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
                // 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);
                gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
                gl.glDisable(GL10.GL_DITHER);

                corridor.loadTexture(gl, BHEngine.BACK_WALL, BHEngine.context);

        }

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

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