十二、在三维环境中导航

你已经进入了学习 Android 游戏开发冒险的最后一章。在这本书里,你从零开始,创造了一个二维滚动射击游戏。从创建那个游戏中学到的技能,你能够创建一个 3d 游戏的环境。虽然这本书没有涵盖使用你已经获得的所有技能或一步一步地创建一个完整的 3-D 游戏,你会学到足够的基础知识,希望使用这种逻辑来完成游戏。在本章中,您将了解当您试图创建一个控制系统来导航三维走廊时,等待您的是什么样的不同。

当你为 2d星际战士游戏创建一个控制系统时,动作很简单。玩家只能向左或向右移动。在 Blob Hunter 中,玩家应该有在 z 平面上 360 度移动的自由。让我们来看看这会给你带来什么样的挑战。

在这一章的最后,我提供了一个 3D 项目的关键文件列表。选择这些文件是因为它们的复杂性、更改的数量或者在编译项目时容易引起问题。如果您在本章末尾运行 3D 项目时遇到问题,请对照摘要后列出的文件检查您的文件。

创建控制界面

在本节中,您将创建控制界面,即玩家与游戏交互的方式。

星际战斗机中,控制界面是简单的左右运动。然而,在 3d 游戏中,玩家希望能够向左、向右、向前、向后移动,还可能向上或向下看。尽管需要跟踪更多的控制,你为星际战斗机学习的基本概念仍然适用。

让我们借用一些星际战士的代码,并快速改编它,让玩家在走廊中前进。

目前,您的BlobhunterActivity应该如下所示:

`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(); }

}`

你将修改你在星际战士中创建的onTouchEvent()方法来处理向前运动。

注意:在本章中,您将只添加前进运动控制。但是,您可以轻松地调整该控件来处理向后运动。

在添加您的onTouchEvent()方法之前,您需要向BHEngine添加一些常量。

编辑 BHEngine

这里的目标是帮助你追踪玩家正在试图做什么,以及玩家在环境中的位置。为此,将以下几行添加到您的BHEngine.java文件中:

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;

PLAYER_FORWARDPLAYER_RIGHTPLAYER_LEFT常量将用于跟踪玩家触摸了什么控件,指示玩家想要在环境中移动到哪里。PLAYER_ROTATE_SPEEDPLAYER_WALK_SPEED常量分别表示玩家的视角在 y 轴上旋转的速度和玩家在环境中行走的速度。最后,playerMovementAction跟踪哪个动作(PLAYER_FORWARDPLAYER_RIGHTPLAYER_LEFT)是当前动作。

现在您的常量已经就位,您可以在BlobhunterActivity.java中创建控制界面。

编辑博客互动

您需要添加到BlobhunterActivity的第一个代码是对BHEngine.display方法的调用。你需要初始化display变量,这样控制界面就可以调用它来确定玩家触摸了屏幕上的什么地方。

`...

@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; }

...`

初始化display后,向BlobhunterActivity类添加一个onTouchEvent()方法:

`...

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

...`

如果你还有星际战士项目,可以直接从它的控制界面复制粘贴以下代码到 Blob Hunter 的新onTouchEvent()方法中。如果你不再有 Star Fighter 项目的代码,可以从 Apress 网站下载完整的项目。

注意:如果你要从星际战士项目中复制粘贴,一定要重命名适当的常量和变量,使之与斑点猎人项目中的相应。

`...

@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; }

...`

接下来,让我们添加检测向前运动的控件。

让你的球员向前移动

现在,onTouchEvent()使用y >playableArea条件来检测玩家是否触摸了屏幕的下部。添加一个else语句来检测对屏幕上部的触摸。您将使用这个触摸屏幕的上部来确定用户想要向前移动。

`...

@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; }

...`

在这段新代码中,您所做的就是检测玩家是否触摸了屏幕的上部,如果是,您就将playerMovementAction设置为PLAYER_FORWARD

请记住,当你创建一个完整的游戏时,你会想稍微调整一下,也考虑到向后触摸控制,可能还有一些向上或向下平移的控制。在下一节中,您将对BHGameRenderer类中的这些控件做出反应,并相应地在走廊中移动玩家。

穿过走廊

穿过走廊有点棘手,但通过一些练习,你可以创建一个平稳的控制系统。诚然,如果你对 OpenGL 足够熟练,能够创建自己的矩阵并执行自己的矩阵乘法,你将能够优化一个伟大的相机系统。然而,从本书开始,目标就一直是让你使用 OpenGL 的内置工具,作为手动过程的学习曲线的替代品。

打开BHGameRenderer.java,这是你的游戏循环代码存放的地方。你需要做的第一件事是添加几个变量来帮助追踪玩家的位置。

`...

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 ;

...`

corridorZPosition变量最初设置为-5。这表示玩家在走廊中的初始位置。值-5 应该将播放器设置在走廊的末端,因为走廊,正如您在BDCorridor类中设置的那样,向 z 轴上的 4 个单位延伸。因此,从-5(或向玩家/屏幕方向 5 个单位)开始播放会给人一种玩家正站在走廊入口处的感觉。

接下来,找到您在上一章中创建的drawCorridor()方法,并删除它的所有内容,除了对走廊的draw()方法的调用,如下所示:

`private void drawCorridor(GL10 gl){

corridor.draw(gl);

}`

使用switch…case语句,类似于星际战士中的语句,你将探测到玩家试图采取的动作。然而,如果向前的动作是玩家想要做的,你该如何应对呢?

星际战士项目中,你只需向左或向右移动玩家。这两种运动都是通过 x 轴上的正值或负值来完成的。然而,在一个三维环境中,在 x 轴上加减将会导致一个侧向或扫射的运动,这不是你在这里要做的。你想让玩家向前移动,让他们把头转向左边或右边。这些动作与你在星球大战中使用的动作完全不同。

要向前移动播放器,您需要向 z 轴添加值。回想一下,您正在沿着 z 轴查看走廊,走廊的 z 轴的 0 值位于远处的墙上。因此,你从-5 开始(见corridorZPosition变量)并移动到 0。

为了模拟转动玩家的头部,你需要沿着 y 轴旋转,而不是平移:你实际上并不想沿着 y 轴或 x 轴移动;而是,就像现实生活中转头一样,想绕轴旋转。

添加一条switch . . . case语句,相应地调整corridorZPositonplayerRotate值。这和星际战斗机用的工艺一样,所以就不详细讨论了。如果它看起来不熟悉,通过第 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);

}`

在下一节中,您将调整玩家在走廊中移动时的位置或视角。

调整玩家的视角

如前所述,OpenGL 不像一些 3d 系统那样有摄像机的概念。更确切地说,可以说,你是在通过欺骗的方式让环境看起来对玩家来说是特定的。

你在 Star Fighter 中用来移动场景中 2-D 模型的相同的平移和旋转也将被用来旋转和平移走廊,以便玩家相信他或她正在穿过它。

drawCorridor()方法添加一个 translate,它将沿着 z 轴移动模型,并添加一个 rotate,它将根据玩家正在看的地方旋转模型。

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

}`

编译并运行您的代码;你现在应该有一个基本的导航系统向前移动,并向左转和向右转。使用你已经学过的技能做一点工作,你可以很容易地添加一些碰撞检测来防止玩家穿墙。自己尝试这些例子:

  • 添加一个导航控件,允许玩家在走廊中倒车。这里有一个这样做的提示:即使在屏幕上创建一个触摸,当触摸时,将从当前 z 轴位置减去一个给定的整数值。
  • 创建碰撞检测系统,防止玩家穿墙而过。给你一个提示:追踪玩家当前的轴线位置,并对照走廊墙壁的已知位置进行测试。请记住,走廊墙壁不会移动。类似这样的东西可能会对你有所帮助:

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

  • 创建一个导航系统,让玩家在环境中上下查看。作为对这项任务的一个提示,考虑它听起来比实际困难。只需添加一个触摸事件,该事件将在 x 轴上的新旋转中增加或减少值。这将使玩家的视野向上或向下旋转。

你拥有创建一个全功能 3d 游戏所需的技能,而且令人惊讶的是,这些技能与你创建一个全功能 2d 游戏所用的技能是一样的;你刚刚增加了更多的细节。

总结

我希望你喜欢这本介绍创建一些有趣的休闲游戏所需的基本技能的入门书,并希望你继续练习和扩展这些技能。关于 OpenGL ES 和 Android 冰激凌三明治的内容远不止这本书所涵盖的,但是你现在已经有了一个很好的知识基础,这将帮助你在 Android 游戏开发的世界中规划你的课程。

查看关键的三维代码

下面的清单包含了在 Blob Hunter 无法正常运行时仔细检查您的工作所需的所有代码。我选择了 BHEngine.java、BHCorridor.java 和 BHGameRenderer.java。这些文件要么接触最多的代码——像 BHEngine,包含复杂的概念——像 BHCorridor,要么执行最多的功能——像 BHGameRenderer。

您可以检查的第一个文件是 BHEngine.java,如清单 12–1所示。BHEngine 是关键设置文件,它包含整个项目中使用的设置。因为这个文件在 Blob Hunter 项目中被广泛使用,所以它最有可能在编译时引起问题。

清单 12–1。BHEngine.javaT2

`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; }`

清单 12–2 显示了 BHCorridor.java 文件。这个文件可能会给您带来问题,因为它包含了一个代码概念,这个概念不仅是抽象的,而且在本书的第 1 部分中也没有涉及到。[顶点]和纹理的结构?数组是整个项目功能的关键。如果数组设置不正确,项目将无法按预期运行。检查该文件时,请密切注意数组和数组定义。

清单 12–2。BHCorridor.javaT2

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(); } }`

Blob Hunter 中的最后一个密钥文件是 BHGameRenderer.java。这个文件包含了 Blob 猎人游戏的游戏循环。就像 Star Fighter 一样,游戏循环是最有可能出现代码问题的地方,因为它拥有项目中所有文件中最多的代码。清单 12–3提供了 BHGameRenderer.java 的源代码。

清单 12–3。BHGameRenderer.javaT2

`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 blocke.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);

}

}`