五、创造你的角色
到本书的这一点,你已经做了相当多的开发工作,并且已经了解了很多关于 OpenGL 和 Android 的知识——如此之多,你现在应该对 OpenGL 和你过去可能使用过的任何其他 API 工具之间的细微差别相当熟悉了。
到目前为止,您还没有编写过多的代码。但是你写的东西给你的游戏开了一个好头,给你带来了很大的视觉冲击。你已经完成了两层双速滚动背景、背景音乐、闪屏和主菜单系统的开发。就可玩的游戏而言,所有这些东西都有一个共同点:它们相当无聊。
也就是说,一个游戏玩家不会为了看一个花哨的双层背景滚动而过而买你的游戏。玩家需要一些动作来控制。这就是《第五章:创造你的角色》的全部内容。
在这一章中,你将创建你的可玩角色。本章结束时,你将拥有一个玩家可以在屏幕上移动的动画角色。本章的第一部分将向你介绍 2d 游戏开发的主要部分——精灵动画。然后,使用 OpenGL ES,您将从一个完整的精灵表中加载不同的精灵来创建动画角色的幻觉。你将学习如何在动作的关键点装载不同的精灵,让你的角色看起来像是在飞行中倾斜。
动画精灵
sprite 动画是 2d 游戏开发人员领域中历史最悠久的工具之一。回想一下你最喜欢的 2d 游戏,很有可能任何角色的动画都是用精灵动画制作的。
从技术上讲,精灵是二维游戏中的任何图形元素。因此,根据定义,你的主要角色是一个精灵。精灵本身只是静止的图像,停留在屏幕上,不会改变。精灵动画是你要用来赋予你的角色一些生命的过程,即使那个角色只是一艘宇宙飞船。
注意:不要混淆动画和动作。在屏幕上移动精灵(图像、纹理、顶点或模型)与动画精灵不同;这两个概念和技能是相互排斥的。
精灵动画是使用翻书风格的效果来完成的。想想几乎所有的 2d 游戏,例如,马里奥兄弟。马里奥兄弟是结合了精灵动画的 2d 平台游戏的最好例子之一。在游戏中,你通过一个侧滚的环境向左或向右移动马里奥。马里奥沿着你移动他的方向走,有时跑。在行走的过程中,他的双腿显然是活动的。
这个行走动画实际上是由一系列静止图片组成的。每张图片描绘了行走动作中的不同点。当玩家向左或向右移动角色时,不同的图像被交换出来,给人一种马里奥在行走的错觉。
在游戏星际战士中,你将采用同样的方法为你的主角制作一些动画。星际战斗机中的主要可玩角色是一艘飞船;因此,它不需要行走动画。尽管宇宙飞船确实需要一些动画。在这一章中,你将创建玩家飞行时向右倾斜和向左倾斜的动画。在后续章节中,您将创建碰撞爆炸动画。
精灵动画的伟大之处在于你在前一章中学习了实现它所需的所有技巧。也就是说,您学习了如何将纹理加载到 OpenGL 中。更重要的是,你学会了将纹理映射到一组顶点上。精灵动画的关键是纹理如何映射到你的顶点。
用于实现精灵动画的纹理在技术上不是独立的图像。每秒 60 次加载和映射一个新纹理所需的时间和能量——如果你能做到的话——将远远超过 Android 设备的能力。相反,你将使用一种叫做 sprite sheet 的东西。
sprite sheet 是一个单独的图像,其中包含执行 sprite 动画所需的所有独立图像。图 5–1 显示了主游戏船的精灵表。
图 5–1。 主角雪碧单
注:图 5–1中的 sprite sheet 只显示了一部分。加载到 OpenGL 中的实际图像为 512 × 512。图像的下半部分是透明的,为了在书中更好地显示,已经被裁剪了。
你如何制作一个充满小图像的动画?这实际上比你想象的要容易。您将把图像作为一个纹理加载,但是您将只显示包含您想要显示给玩家的图像的纹理部分。当您想要动画显示图像时,您只需使用glTranslateF()
移动到您想要显示的图像的下一部分。
如果这个概念还不太有意义,不要担心;在本章的下一节中,你将把它付诸行动。然而,第一步是创建一个类来处理你的游戏角色的绘制和加载。
注意:你可能想知道为什么精灵图片中的船只面朝下而不是朝上;特别是因为可玩的角色将会在屏幕的底部飞向顶部。这是因为 OpenGL 渲染了从最后一行到第一行的所有位图。因此,当 OpenGL 渲染这个 sprite 工作表时,它将出现在屏幕上,如图Figure 5–2所示。
图 5-2。 精灵工作表在屏幕上的样子
是的,你可以使用正确的方法绘制精灵,然后使用 OpenGL 将纹理旋转到正确的位置。然而,使用任何图像软件都可以很容易地反转 sprite 工作表,这样,你就为 OpenGL 省去了反转的周期和麻烦。
装载你的角色
在前一章中,您创建了一个类,它为背景图像加载纹理,然后在被调用时绘制该图像。你用来创建这个类的机制和你需要加载和绘制你的主角的机制是一样的。您将做一些小的调整,以允许使用 sprite 表,但除此之外,这段代码应该看起来很熟悉。
首先在项目包中创建一个名为SFGoodGuy
的新类:
`package com.proandroidgames;
public class SFGoodGuy {
}`
在SFGoodGuy()
类中,剔除一个构造函数、draw()
方法和loadTexture()
方法。
提示:记住,您可以在 Eclipse 中使用 alt + shift + O 快捷键来暴露您可能需要的任何缺失的导入。
`package com.proandroidgames;
public class SFGoodGuy {
public SFGoodGuy() {
} public void draw(GL10 gl) {
} public void loadTexture(GL10 gl,int texture, Context context) {
} }`
接下来,建立您将在课程中使用的缓冲区。同样,这看起来应该和你在上一章处理游戏背景时所做的一样。
您还可以添加代码来创建vertices[]
数组。顶点将与背景中使用的顶点相同。
`package com.proandroidgames;
public class SFGoodGuy {
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, };
public SFGoodGuy() {
} public void draw(GL10 gl) {
} public void loadTexture(GL10 gl,int texture, Context context) {
} }`
现在,为纹理贴图创建数组。
创建纹理映射数组
纹理映射是SFGoodGuy()
类与你加载背景时使用的不同之处。您将加载到这个类中的纹理是一个大的 sprite 表,它包含主要可玩角色的五个图像。您的目标是一次只显示这些图像中的一个。
理解如何告诉 OpenGL 你想要显示的图像的位置的关键是如何在 sprite 表上配置图像。再看一下图 5–1中的精灵表。请注意,图像的布局是均匀的,第一行有四个图像,第二行有一个图像。在纹理的第一行只有四个图像,整个纹理有 1 个单位长,你可以推测你只需要显示整个纹理的四分之一就可以显示第一行四个图像中的一个。
这意味着不像对背景那样映射整个纹理(从(0,0)到(1,1),而只需要映射它的四分之一(从(0,0)到(0,. 25)。您将只使用 0.25 或四分之一的纹理来映射并显示船只的第一张图像。
像这样创建你的纹理数组:
`package com.proandroidgames;
public class SFGoodGuy {
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, 0.25f, 0.0f, 0.25f, 0.25f, 0.0f, 0.25f, };
public SFGoodGuy() {
} public void draw(GL10 gl) {
} public void loadTexture(GL10 gl,int texture, Context context) {
} }`
索引数组、draw()
方法和构造函数都与在SFBackground
类中使用的相同:
`package com.paroandroidgames;
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 SFGoodGuy {
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, 0.25f, 0.0f, 0.25f, 0.25f, 0.0f, 0.25f, };
private byte indices[] = { 0,1,2, 0,2,3, };
public SFGoodGuy() { 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) {
} }`
在完成之前,您还需要对SFGoodGuy()
类做一个修改。在背景的loadTexture()
方法的类中,将glTexParameterf
设置为GL_REPEAT
,以便在顶点上移动纹理时能够重复纹理。这对于可玩的角色来说并不是必须的;因此,您要将该参数设置为GL_CLAMP_TO_EDGE
。
用下面的loadTexture()
方法完成你的SFGoodGuy()
类:
`…
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();
} }`
现在你有了一个函数类,它将你的可玩角色纹理作为一个 sprite 表加载,显示 sprite 表中的第一个 sprite,并且当它被移动时不包装纹理。
将纹理加载到角色上
加载一个可玩角色的下一步是实例化一个SFGoodGuy()
并加载一个纹理。保存并关闭SFGoodGuy()
类;您现在不需要向它添加任何代码。
让我们给SFEngine
添加几个快速变量和常量。你将需要这些在你的游戏循环。
首先,您将添加一个名为playerFlightAction
的变量。这将用于跟踪玩家采取了什么行动,以便您可以在游戏循环中做出适当的反应。
`package com.proandroidgames;
import android.content.Context; import android.content.Intent; import android.view.Display; import android.view.View;
public class SFEngine {
…
public static int playerFlightAction = 0;
/Kill game and exit/ public boolean onExit(View v) { try { Intent bgmusic = new Intent(context, SFMusic.class); context.stopService(bgmusic); musicThread.stop(); return true; }catch(Exception e){ return false; }
} }`
接下来,添加一个常量,指向本章最后一节的 sprite 表。
`package com.proandroidgames;
import android.content.Context; import android.content.Intent; import android.view.Display; import android.view.View;
public class SFEngine {
…
public static int playerFlightAction = 0; public static final int PLAYER_SHIP = R.drawable.good_sprite;
/Kill game and exit/ public boolean onExit(View v) { try { Intent bgmusic = new Intent(context, SFMusic.class); context.stopService(bgmusic); musicThread.stop(); return true; }catch(Exception e){ return false; }
} }`
接下来的三个常量表示玩家采取了什么行动。当玩家试图移动角色时,这些将被分配给playerFlightAction
变量。
`package com.proandroidgames;
import android.content.Context; import android.content.Intent; import android.view.Display; import android.view.View;
public class SFEngine {
…
public static int playerFlightAction = 0; public static final int PLAYER_SHIP = R.drawable.good_sprite; public static final int PLAYER_BANK_LEFT_1 = 1; public static final int PLAYER_RELEASE = 3; public static final int PLAYER_BANK_RIGHT_1 = 4;
/Kill game and exit/ public boolean onExit(View v) { try { Intent bgmusic = new Intent(context, SFMusic.class); context.stopService(bgmusic); musicThread.stop(); return true; }catch(Exception e){ return false; }
} }`
根据您对刚刚添加到SFEngine
中的常数的观察程度,您可能会奇怪为什么PLAYER_BANK_LEFT_1
的值是1
,而PLAYER_RELEASE
的值是3
。这些值将代表你的精灵动画的阶段。在 sprite 工作表中,左岸动画有两个阶段,右岸动画有两个阶段。然而,在循环的代码中,您将能够推断出在PLAYER_BANK_LEFT_1
和PLAYER_RELEASE
之间有一个值为2
的PLAYER_BANK_LEFT_2
,并且这个常量不必在SFEngine
中表示。当您在本节后面看到这个概念的实际应用时,它肯定会更有意义。
您需要的下一个常量将指示多少次循环迭代将等于一帧精灵动画。请记住,可玩角色和游戏背景之间的最大区别是,当角色在屏幕上移动时,您要制作角色动画。跟踪这个动画将是一件棘手的事情。游戏循环以每秒 60 帧的速度运行。如果您为循环的每次迭代运行一个新的精灵动画帧,您的动画将在玩家有机会欣赏它之前就结束了。常量PLAYER_FRAMES_BETWEEN_ANI
会被设置为9
,表示主游戏循环每迭代九次,就会有一帧精灵动画被绘制出来。
`package com.proandroidgames;
import android.content.Context; import android.content.Intent; import android.view.Display; import android.view.View;
public class SFEngine {
…
public static int playerFlightAction = 0; public static final int PLAYER_SHIP = R.drawable.good_sprite; public static final int PLAYER_BANK_LEFT_1 = 1; public static final int PLAYER_RELEASE = 3; public static final int PLAYER_BANK_RIGHT_1 = 4; public static final int PLAYER_FRAMES_BETWEEN_ANI = 9;
/Kill game and exit/ public boolean onExit(View v) { try { Intent bgmusic = new Intent(context, SFMusic.class); context.stopService(bgmusic); musicThread.stop(); return true; }catch(Exception e){ return false; }
} }`
最后,再添加一个常量和一个变量。这些将代表玩家的船从左向右移动的速度以及玩家的船在 x 轴上的当前位置。
`package com.proandroidgames;
import android.content.Context; import android.content.Intent; import android.view.Display; import android.view.View;
public class SFEngine {
…
public static int playerFlightAction = 0; public static final int PLAYER_SHIP = R.drawable.good_sprite; public static final int PLAYER_BANK_LEFT_1 = 1; public static final int PLAYER_RELEASE = 3; public static final int PLAYER_BANK_RIGHT_1 = 4; public static final int PLAYER_FRAMES_BETWEEN_ANI = 9; public static final float PLAYER_BANK_SPEED = .1f; public static float playerBankPosX = 1.75f;
/Kill game and exit/ public boolean onExit(View v) { try { Intent bgmusic = new Intent(context, SFMusic.class); context.stopService(bgmusic); musicThread.stop(); return true; }catch(Exception e){ return false; }
} }`
SFEngine
现在有了帮助你实现你的可玩角色所需的所有代码。保存并关闭文件。
打开SFGameRenderer.java
文件。这个文件是你的游戏循环的家。在前一章中,您创建了游戏循环,并添加了两种方法来绘制和滚动背景的不同层。现在,你要添加代码到你的循环中来绘制和移动可玩的角色。
设置游戏循环
第一步是实例化一个名为player1
的新SFGoodGuy()
:
`public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private SFGoodGuy player1 = new SFGoodGuy();
private float bgScroll1; private float bgScroll2; … }`
对象player1
将会以与background
和background2
相同的方式被使用。你将从player1
调用loadTexture()
和draw()
方法来将你的角色加载到游戏中。
您需要创建一个变量来跟踪游戏循环经过了多少次迭代,这样您就可以知道何时翻转 sprite 动画中的帧。
`public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private SFGoodGuy player1 = new SFGoodGuy(); private int goodGuyBankFrames = 0;
private float bgScroll1; private float bgScroll2; … }`
接下来,定位SFGameRenderer
渲染器的onSurfaceCreated()
方法。这个方法处理游戏纹理的加载。在上一章中,你在这个方法中调用了background
和background2
的加载方法。现在,您需要添加一个对player1
的loadTexture()
方法的调用。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{ private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private SFGoodGuy player1 = new SFGoodGuy(); private int goodGuyBankFrames = 0;
…
@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);
background.loadTexture(gl,SFEngine.BACKGROUND_LAYER_ONE, SFEngine.context); background2.loadTexture(gl,SFEngine.BACKGROUND_LAYER_TWO, SFEngine.context);
player1.loadTexture(gl, SFEngine.PLAYER_SHIP, SFEngine.context); }
}`
到目前为止,这段代码都很基本:创建纹理,加载纹理。现在,该是这一章的真正内容了。是时候写一个方法来控制你的玩家角色的移动了。
移动人物
本节将帮助您创建在屏幕上移动玩家角色所需的代码。为此,您将创建一个新的方法,将服务器作为您的核心游戏循环。最后,您将从这个循环中调用方法来执行移动角色的任务。在SFGameRenderer SFGameRenderer
中创建一个接受GL10
的新方法。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){
}
…
}`
在movePlayer1()
方法中,您将在本章前面添加到SFEngine
的playerFlightAction
int 上运行一条switch
语句。以防你从未使用过,switch
语句将检查输入对象(playerFlightAction
)并根据输入的值执行特定的代码。此switch
语句的情况有PLAYER_BANK_LEFT_1
、PLAYER_RELEASE
、PLAYER_BANK_RIGHT_1
和default
。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1:
break; case SFEngine.PLAYER_BANK_RIGHT_1:
break; case SFEngine.PLAYER_RELEASE:
break; default:
break; }
}
…
}`
让我们从默认情况开始。当玩家没有对角色采取任何行动时,默认情况下将被调用。
绘制角色的默认状态
现在,顶点和屏幕一样大。因此,如果你现在把这个可玩的角色画到屏幕上,它会填满整个屏幕。你需要将游戏角色缩放大约 75 %,这样它在游戏中看起来会更好。
为此,您将使用glScalef()
。将比例乘以 0 . 25 会使船缩小到原来的四分之一。这有一个非常重要的后果,你需要了解。
在上一章中,您简要地发现了缩放或平移顶点需要在模型矩阵模式下工作。您在任何矩阵模式下进行的任何操作都会影响该矩阵模式下的所有项目。因此,当您将玩家船的顶点缩放 0.25 倍时,您也缩放了它所占据的 x 轴和 y 轴。换句话说,当比例默认为 0(全屏)时,x 轴和 y 轴从 0 开始,到 1 结束,而当比例乘以. 25 时,x 轴和 y 轴将从 0 到 4 运行。
这对你很重要,因为当你试图跟踪玩家的位置时,你需要意识到背景可能会从 0 滚动到 1,但玩家可以从 0 滚动到 4。
加载模型矩阵视图,并在 x 和 y 轴上将播放器缩放 0.25。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1:
break; case SFEngine.PLAYER_BANK_RIGHT_1:
break; case SFEngine.PLAYER_RELEASE:
break; default: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f);
break; }
} …
}`
接下来,通过变量playerBankPosX
中的值平移 x 轴上的模型矩阵。变量playerBankPosX
将保存玩家在 x 轴上的当前位置。因此,当玩家没有采取任何行动时,角色将会回到上次离开的地方。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ …
default: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
break; }
}
…
}`
当播放器静止时,不需要采取其他动作,所以加载纹理矩阵,并确保它在默认位置,这是 sprite 表中的第一个 sprite。请记住,纹理矩阵模式将是您用来移动精灵片纹理的位置以翻转动画的模式。如果玩家没有移动角色,应该没有动画——因此,纹理矩阵应该默认为第一个位置。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ …
default: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity();
break; }
}
…
}`
在switch
语句中,您编写的下一个案例是针对PLAYER_RELEASE
的。当玩家移动角色后释放控制时会调用PLAYER_RELEASE
动作。当你还没有为游戏的实际控制编码时,玩家将触摸一个控制来告诉角色移动。当玩家释放这个控制键,从而告诉角色停止移动时,就会调用PLAYER_RELEASE
动作。
编码播放器 _ 释放动作
现在,PLAYER_RELEASE
的情况将执行与default
情况相同的动作。也就是说,角色将停留在它在屏幕上留下的地方,无论 sprite 表中显示的是什么纹理,它都将返回到表中的第一个纹理。将default
中的整个代码块复制粘贴到PLAYER_RELEASE
的案例中。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1:
break; case SFEngine.PLAYER_BANK_RIGHT_1:
break; case SFEngine.PLAYER_RELEASE: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity();
break;
…
}
}
…
}`
在完成PLAYER_RELEASE
案例之前,您需要再添加一行代码。在本章早些时候,您已经了解到不能以与游戏循环相同的速率(每秒 60 帧)翻转精灵动画,因为精灵动画中只有两帧,在玩家意识到它发生之前就会结束。因此,您需要一个变量来保存游戏循环次数。通过了解游戏循环次数,您可以将该次数与PLAYER_FRAMES_BETWEEN_ANI
常量进行比较,以确定何时需要翻转精灵动画帧。你在本章前面创建的goodGuyBankFrames
变量将用于跟踪已经执行的游戏循环次数。
在PLAYER_RELEASE
的例子中,添加下面几行代码,每次执行一个循环,就将goodGuyBankFrames
加 1。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1:
break; case SFEngine.PLAYER_BANK_RIGHT_1:
break; case SFEngine.PLAYER_RELEASE: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity(); goodGuyBankFrames += 1;
break;
… }
}
…
}`
在你的movePlayer1()
方法的四种情况中,PLAYER_RELEASE
和default
是最容易的。现在,您需要编写当调用PLAYER_BANK_LEFT_1
动作时会发生什么。
当玩家使用控件将角色船向左倾斜时,就会调用PLAYER_BANK_LEFT_1
动作。这意味着你不仅需要沿着 x 轴向左移动角色,还需要使用 sprite sheet 上的两个 sprite 来设置角色的动画,这两个 sprite 表示左边的一排。
向左移动字符
就 OpenGL 而言,沿 x 轴移动角色和改变 sprite 页位置的操作使用了两种不同的矩阵模式。您将需要使用模型矩阵模式来沿着 x 轴移动角色,并且您将需要使用纹理矩阵模式来移动精灵表纹理-创建银行动画。让我们首先处理模型矩阵模式操作。
第一步是加载模型矩阵模式,并将 x 轴和 y 轴的比例设置为 0.25。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f);
**break;
… }
}**
…
}`
接下来,您将使用glTranslatef()
沿着 x 轴移动顶点。您从当前 x 轴位置减去PLAYER_BANK_SPEED
,该位置存储在playerBankPosX
中。(你在做减法以得到你需要移动到的位置,因为你试图沿着 x 轴向左移动字符。如果你想向右移动,你会增加。)然后,使用glTranslatef()
将顶点移动到playerBankPosX
中的位置。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f);
break;
…
}
}
…
}`
现在,您正在沿着 x 轴移动角色,您需要翻转到动画的下一帧。
加载正确的精灵
再一次看一下图 5–1中的精灵表。请注意,与向左倾斜运动相对应的两个动画帧是第一行上的第四帧和第二行上的第一帧(请记住,如果纸张向后看,它会反转,因此看起来向右倾斜的帧在渲染时会向左倾斜)。
加载纹理矩阵模式,并平移纹理以在第一行显示第四个图像。因为纹理是用百分比来表示的,所以你需要做一点数学计算。再说一次,一行只有四张图片,数学很简单。
sprite 工作表的 x 轴从 0 到 1。如果除以 4,工作表中的每个精灵占据 x 轴的 0.25。因此,要将 sprite 工作表移动到该行的第四个 sprite,需要将其平移 0.75。(第一个子画面占用 x 值 0 到. 24,第二个子画面占用. 25 到. 49,第三个子画面占用. 50 到. 74,第四个子画面占用. 75 到 1。)
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ **switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.75f,0.0f, 0.0f);
break;**
… }
}
…
}`
绘制船之前的最后一步是增加goodGuyBankFrames
,这样你就可以开始跟踪何时翻到脚本表中的第二帧。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ **switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.75f,0.0f, 0.0f); goodGuyBankFrames += 1;
break;**
…
}
}
…
}`
这段代码有一个主要问题。玩家现在可以沿着 x 轴向左移动角色,船的精灵会变成左岸动画的第一个精灵。问题是,由于代码是现在写的,精灵会向左移动到无穷远处。您需要将移动字符的代码块包装在一个if. . . else
语句中,该语句测试字符是否到达 x 轴上的 0。如果角色在 0 位置,表示他们在屏幕的左边缘,停止移动角色并将动画返回到默认的精灵。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); if (SFEngine.playerBankPosX > 0){ SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.75f,0.0f, 0.0f); goodGuyBankFrames += 1; }else{ gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); }
break;
…
}
}
…
}`
现在,通过调用draw()
方法来绘制字符,并将矩阵弹出堆栈。过程中的这一步应该与两个背景层相同。事实上,这一步在游戏中几乎所有的 OpenGL 操作中都是通用的。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); if (SFEngine.playerBankPosX > 0){ SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.75f,0.0f, 0.0f); goodGuyBankFrames += 1; }else{ gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); } player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity();
break;
…
}
}
…
}`
现在你有一个例子,如果玩家向左移动角色,顶点沿着 x 轴向左移动,直到它们到达零。此外,纹理从默认(自上而下的视图)精灵开始,当玩家向左移动时,精灵将更改为左侧银行动画的第一帧。
加载第二帧动画
如果玩家向左移动足够远,你需要将动画翻转到左岸动画的第二帧。查看Figure 5–1中的 sprite 表,左岸动画的第二帧是第二行的第一帧。使用glTranslatef()
很容易导航到这个页面。问题是,你怎么知道什么时候翻转雪碧?
在本章的前面,您在SFEngine
中创建了一个名为PLAYER_FRAMES_BETWEEN_ANI
的常量,并将其设置为9
。该常量表示您希望每九帧游戏动画(即游戏循环)翻转一次玩家的角色动画。您还创建了一个名为goodGuyBankFrames
的变量,每当玩家的角色被绘制时,该变量就会增加 1。
你需要比较goodGuyBankFrames
和PLAYER_FRAMES_BETWEEN_ANI
的当前值。如果goodGuyBankFrames
少,画第一帧动画。如果goodGuyBankFrames
更大,画第二帧动画。下面是你的if . . . then
声明应该是什么样子。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ **switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); if (goodGuyBankFrames 0){ SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.75f,0.0f, 0.0f); goodGuyBankFrames += 1; }else if (goodGuyBankFrames > = SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX > 0){ SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED;
}else{ gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); } player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity();
break;
…
}
}**
…
}`
在if . . . else if
条件下,你测试goodGuyBankFrames
的值是否大于PLAYER_FRAMES_BETWEEN_ANI
,这表示你应该翻到左倾斜动画的下一帧。让我们来编写翻转动画的代码块。
在图 5-1 中,左侧堤岸动画的第二帧在第二行第一个位置。这意味着该 sprite 的左上角位于 x 轴上的 0 位置(最左边),然后是 y 轴上的 1/4 处(. 25)。简单地使用glTranslatef()
方法将纹理移动到这个位置。
注意:在你移动纹理之前,你需要加载纹理矩阵模式。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ **switch (SFEngine.playerFlightAction){ case SFEngine.PLAYER_BANK_LEFT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); if (goodGuyBankFrames 0){ SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.75f,0.0f, 0.0f); goodGuyBankFrames += 1; }else if (goodGuyBankFrames >= SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&SFEngine.playerBankPosX > 0){ SFEngine.playerBankPosX -= SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.25f, 0.0f); }else{ gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); } player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity();
break;
…
}
}**
…
}`
您的将角色向左移动并实现两帧精灵动画的switch
语句已经完成。
向右移动字符
在完成movePlayer1()
方法之前,您需要完成的最后一个 case 语句是针对PLAYER_BANK_RIGHT_1
的。当玩家想要将角色移动到屏幕的右侧,x 轴的正方向时,就会调用这种情况。
案例的布局看起来是一样的,但是你需要从 sprite 表中加载不同的帧。首先,布置你的模型矩阵,缩放角色顶点,并像在PLAYER_BANK_LEFT_1
案例中一样设置if . . . else if
语句。
这个if . . . else if
语句与PLAYER_BANK_LEFT_1
情况下的语句有一个不同之处。在PLAYER_BANK_LEFT_1
的例子中,您测试了顶点在 x 轴上的当前位置是否大于 0,这表明角色没有离开屏幕的左侧。对于PLAYER_BANK_RIGHT_1
的情况,你需要测试角色是否到达了屏幕最右边。
默认情况下,x 轴从 0 开始,到 1 结束。然而,为了使游戏角色在屏幕上看起来更小,你已经将 x 轴缩放到 0.25。这意味着 x 轴现在从 0 到 4。你需要测试可玩的角色没有向右滚动超过 4 个单位。正确吗?
不,不完全是。
OpenGL 追踪顶点的左上角。因此,如果您在遇到 4 时测试该情况,该字符将已经离开屏幕。你需要考虑角色顶点的宽度。角色顶点的宽度为 1 个单位。测试角色没有超过 x 轴值 3 将使它保持在玩家可以看到的屏幕上。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ **switch (SFEngine.playerFlightAction){
…
case SFEngine.PLAYER_BANK_RIGHT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); if (goodGuyBankFrames < SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX < 3){
}else if (goodGuyBankFrames >= SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX < 3){
}else{ gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); } player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity();
break;
… }
}**
…
}`
PLAYER_BANK_RIGHT_1
案例中的初始代码块与PLAYER_BANK_LEFT_1
中的几乎相同。您正在调整模型矩阵,测试角色在 x 轴上的位置,并测试已经运行的游戏循环帧数,以判断需要显示哪一帧精灵动画。
现在,您可以在适当的位置显示右岸动画的第一帧和第二帧。
加载右岸动画
玩家向右倾斜时应显示的第一帧动画在第一行第二个位置(参见Figure 5–1中的 sprite sheet)。因此,您需要将纹理矩阵在 x 轴上平移 0.25,在 y 轴上平移 0,以显示此帧。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ **switch (SFEngine.playerFlightAction){ …
case SFEngine.PLAYER_BANK_RIGHT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); if (goodGuyBankFrames <SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX < 3){ SFEngine.playerBankPosX += SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.25f,0.0f, 0.0f); goodGuyBankFrames += 1; }else if (goodGuyBankFrames >= SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&SFEngine.playerBankPosX < 3){
}else{ gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); } player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity();
break;
… }**
}
…
}`
注意是这个代码块将PLAYER_BANK_SPEED
的值加到玩家的当前位置,而不是从中减去。这是在 x 轴上向右移动顶点的关键,而不是向左。
重复这段代码,您需要在 x 轴上将纹理转换为. 50,以显示右侧银行的 sprite 动画的第二帧。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
private void movePlayer1(GL10 gl){ **switch (SFEngine.playerFlightAction){
…
case SFEngine.PLAYER_BANK_RIGHT_1: gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glPushMatrix(); gl.glScalef(.25f, .25f, 1f); if (goodGuyBankFrames < SFEngine.PLAYER_FRAMES_BETWEEN_ANI && SFEngine.playerBankPosX < 3){ SFEngine.playerBankPosX += SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.25f,0.0f, 0.0f); goodGuyBankFrames += 1; }else if (goodGuyBankFrames >= SFEngine.PLAYER_FRAMES_BETWEEN_ANI &&SFEngine.playerBankPosX < 3){ SFEngine.playerBankPosX += SFEngine.PLAYER_BANK_SPEED; gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.50f,0.0f, 0.0f); }else{ gl.glTranslatef(SFEngine.playerBankPosX, 0f, 0f); gl.glMatrixMode(GL10.GL_TEXTURE); gl.glLoadIdentity(); gl.glTranslatef(0.0f,0.0f, 0.0f); } player1.draw(gl); gl.glPopMatrix(); gl.glLoadIdentity();
break;
…
}
}**
…
}`
你的movePlayer1()
方法现在完成了。当正确的动作被应用时,你的可玩角色将成功地向左和向右移动。您现在所要做的就是从游戏循环中调用movePlayer1()
方法,并创建一个允许玩家实际移动角色的进程。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
…
@Override public void onDrawFrame(GL10 gl) { try { Thread.sleep(SFEngine.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);
scrollBackground1(gl); scrollBackground2(gl);
movePlayer1(gl);
//All other game drawing will be called here
gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA);
} …
}`
保存并关闭SFGameRenderer
。
在本章的下一节,你将学习如何在 Android 设备的屏幕上收听TouchEvent
。然后,您将使用那个TouchEvent
来设置玩家动作,从而将屏幕上的角色向左或向右移动。
使用触摸事件移动您的角色
您已经创建了必要的方法和调用来在屏幕上移动您的可玩角色。然而,到目前为止,玩家没有办法与游戏互动,并告诉游戏循环进行移动角色的调用。
在本节中,您将编写一个简单的触摸监听器,它将检测玩家是否触摸了屏幕的左侧或右侧。玩家将通过触摸屏幕的那一侧来向左或向右移动角色。监听器将进入托管您的游戏循环的活动,在本例中为SFGame.java
。
打开SFGame.java
,为onTouchEvent()
方法添加一个覆盖。
`package com.proandroidgames;
import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent;
public class SFGame extends Activity {
…
**@Override public boolean onTouchEvent(MotionEvent event) {
return false; }** }`
onTouchEvent()
是一个标准的 Android 事件监听器,它将监听活动中发生的任何触摸事件。因为您的游戏是从SFGame
活动运行的,所以这是您必须监听的触摸事件的活动。
提示:不要把游戏的活跃度和游戏的循环混淆。游戏循环就是SFGameRenderer
;发射它的Activity
是SFGame
。
只有当设备的屏幕被触摸、滑动、拖动或释放时,监听器才会触发。对于这个游戏,你只关心触摸或释放,以及它发生在屏幕的哪一侧。为了帮助你确定这一点,Android 向onTouchEvent()
监听器发送一个MotionEvent
视图;它将提供您所需要的一切,以确定哪种触摸事件触发了监听器,以及触摸发生在屏幕上的什么位置。
解析运动事件
在onTouchEvent()
监听器中,您首先关心的是获取触摸的 x 和 y 坐标,这样您就可以确定触摸是发生在设备屏幕的左侧还是右侧。传递给onTouchEvent()
监听器的MotionEvent
有getX()
和getY()
方法,可以用来确定触摸事件的 x 和 y 坐标。
注意:你在onTouchEvent()
监听器中处理的 x 和 y 坐标是屏幕坐标,不是 OpenGL 坐标。
`package com.proandroidgames;
import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent;
public class SFGame extends Activity {
…
**@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY();
return false; }**
}`
接下来,您将在屏幕上设置一个可玩区域。也就是说,您不希望对屏幕上任何地方的触摸事件作出反应,所以您将在屏幕底部设置一个您将作出反应的区域。屏幕上的可触摸区域很低,因此玩家可以在手持设备时用拇指触摸。
由于可玩的角色大约占据了设备屏幕的下四分之一,你将把那个区域设置为你将做出反应的区域。
`package com.proandroidgames;
import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent;
public class SFGame extends Activity {
…
**@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); int height = SFEngine.display.getHeight() / 4; int playableArea = SFEngine.display.getHeight() - height;
return false; }**
}`
现在,您已经有了触摸事件的位置和想要对触摸事件做出反应的区域。使用一个简单的if
语句来决定你是否应该对此事件做出反应。
`package com.proandroidgames;
import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent;
public class SFGame extends Activity {
…
**@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); int height = SFEngine.display.getHeight() / 4; int playableArea = SFEngine.display.getHeight() - height; if (y > playableArea){
} return false; }**
}`
MotionEvent
有一个非常有用的方法叫做getAction()
,它返回你在屏幕上检测到的动作类型。在这个游戏中,你关心的是ACTION_UP
和ACTION_DOWN
的动作。这些动作表示玩家的手指最初接触屏幕(ACTION_DOWN
)然后又离开屏幕(ACTION_UP
)的时刻。
陷印动作 _ 向上和动作 _ 向下
建立一个简单的switch
语句来执行ACTION_UP
和ACTION_DOWN
动作。一定要省去default
案例,因为你只想对这两个具体案例做出反应。
`package com.proandroidgames;
import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent;
public class SFGame extends Activity {
…
**@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); int height = SFEngine.display.getHeight() / 4; int playableArea = SFEngine.display.getHeight() - height; if (y > playableArea){ switch (event.getAction()){ case MotionEvent.ACTION_DOWN:
break; case MotionEvent.ACTION_UP:
break; } } return false; }**
}`
在本章的前面,您编写了在屏幕上移动角色的代码。这段代码对您创建的三个动作常量做出反应:PLAYER_BANK_LEFT_1
、PLAYER_BANK_RIGHT_1
和PLAYER_RELEASE
。这些动作将在onTechEvent()
中的适当情况下设置。
让我们从PLAYER_RELEASE
开始。这种情况将在玩家将手指抬离屏幕时设置,从而触发一个ACTION_UP
事件。
`package com.proandroidgames;
import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent;
public class SFGame extends Activity {
…
**@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); int height = SFEngine.display.getHeight() / 4; int playableArea = SFEngine.display.getHeight() - height; if (y > playableArea){ switch (event.getAction()){ case MotionEvent.ACTION_DOWN:
break; case MotionEvent.ACTION_UP: SFEngine.playerFlightAction = SFEngine.PLAYER_RELEASE; break; } } return false; }**
}`
最后,设置PLAYER_BANK_LEFT_1
和PLAYER_BANK_RIGHT_1
动作。为此,您仍然需要确定玩家是触摸了屏幕的左侧还是右侧。这可以通过比较MotionEvent
的getX()
值和 x 轴的中点很容易地确定。如果getX()
小于中点,则动作在左边;如果getX()
值大于中点,则事件发生在右侧。
`package com.proandroidgames;
import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent;
public class SFGame extends Activity {
…
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); int height = SFEngine.display.getHeight() / 4; int playableArea = SFEngine.display.getHeight() - height; if (y > playableArea){ switch (event.getAction()){ case MotionEvent.ACTION_DOWN: if(x < SFEngine.display.getWidth() / 2){ SFEngine.playerFlightAction = SFEngine.PLAYER_BANK_LEFT_1; }else{ SFEngine.playerFlightAction = SFEngine.PLAYER_BANK_RIGHT_1; } break; case MotionEvent.ACTION_UP: SFEngine.playerFlightAction = SFEngine.PLAYER_RELEASE; break; } } return false; }
}`
保存并关闭您的SFGame.java
类。您已经完成了这个游戏的用户界面(UI)。玩家现在可以触摸屏幕的右侧或左侧来向左或向右移动角色。
在本章的最后一节,我们将重温游戏线程和每秒帧数的计算。
调整 FPS 延迟
在前一章中,您创建了一个延迟来减慢游戏循环,并强制它以每秒 60 帧(FPS)的速度运行。这个速度是开发者的游戏运行起来最希望的速度。然而,你可能已经开始意识到这个速度并不总是可以达到的。
你在游戏循环中执行的功能越多,循环完成的时间就越长,游戏运行的速度就越慢。这意味着你创造的延迟需要调整或完全关闭,这取决于游戏运行的速度。
只是为了比较,在当前状态下运行游戏,有两个背景和一个可玩的角色,我在 Windows 模拟器上实现了大约每秒 10 帧,在 Droid X 上大约每秒 35 帧,在摩托罗拉 Xoom 上大约每秒 43 帧。
其中一个问题是,你在不分青红皂白地延迟线程。您需要调整游戏循环的线程延迟,以考虑运行循环所需的时间。以下代码将确定循环运行所需的时间,然后从延迟中减去该时间。如果循环运行的时间比延迟的时间长,则延迟被关闭。
`package com.proandroidgames;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class SFGameRenderer implements Renderer{
private SFBackground background = new SFBackground(); private SFBackground background2 = new SFBackground(); private SFGoodGuy player1 = new SFGoodGuy();
private int goodGuyBankFrames = 0; private long loopStart = 0; private long loopEnd = 0; private long loopRunTime = 0 ;
private float bgScroll1; private float bgScroll2;
@Override public void onDrawFrame(GL10 gl) { loopStart = System.currentTimeMillis(); try { if (loopRunTime <SFEngine.GAME_THREAD_FPS_SLEEP){ Thread.sleep(SFEngine.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);
scrollBackground1(gl); scrollBackground2(gl);
movePlayer1(gl);
//All other game drawing will be called here
gl.glEnable(GL10.GL_BLEND); gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA); loopEnd = System.currentTimeMillis(); loopRunTime = ((loopEnd - loopStart));
} …`
编译并运行你的游戏。尝试在屏幕上移动角色,观察动画的变化。
总结
在这一章中,你在星际战士游戏中又前进了一大步。现在,您可以将以下技能添加到您的技能列表中:
- 创建一个可玩的角色。
- 使用精灵表中的纹理制作角色动画。
- 检测设备屏幕上的触摸输入。
- 基于玩家的触摸事件移动角色并制作动画。
- 调整了 FPS 速率,让游戏尽可能快地运行。
版权属于:月萌API www.moonapi.com,转载请注明出处