五、创造你的角色
到本书的这一点,你已经做了相当多的开发工作,并且已经了解了很多关于 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 <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.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 <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.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 速率,让游戏尽可能快地运行。
六、添加敌人
作为一名 Android 游戏开发者,你的技能范围越来越广。仅在前一章中,您就添加了您的第一个可玩角色,处理了精灵动画,并创建了一个基本的侦听器来允许玩家控制角色;对于一个基本的 2-D 射击游戏,你的游戏真的正在成形。
在这一章中,你将创建一个类来帮助你管理你的纹理。你还将创建一个敌人职业,用于在星际战士中创建三种不同类型的敌人。在下一章,你将为这些敌人创建一个基本的人工智能系统。
中局看家
记住,这本书的目的是帮助你从头到尾完成一个游戏的创作过程。游戏创作并不总是一个线性的过程。有时候,你需要回头重新评估你所做的事情,以优化你的游戏工作方式。
前两章主要教你如何加载和处理精灵和精灵表。但是,使用当前的代码,您将为每个角色加载一个单独的 sprite 表。这是学习如何使用 sprite sheet 的最简单的方法,但绝不是使用sprite sheet 的最好方法。事实上,为每个角色创建一个单独的 sprite 工作表几乎违背了 sprite 工作表的目的——也就是说,你应该将所有角色的所有图像加载到一个 sprite 工作表中。
提示:当然,如果你有太多的精灵而不适合一张图片,你仍然可以使用多个精灵表。但这应该不是这个游戏中有限的角色数量的问题。
通过将游戏中所有角色的所有图像加载到一个 sprite 表中,您将大大减少游戏消耗的内存量以及 OpenGL 渲染游戏所需的处理量。
也就是说,现在是时候对你的游戏代码进行一些小的整理了,让它使用一个通用的精灵表
创建纹理类
您将使用loadTexture()
方法创建一个通用纹理类。loadTexture()
方法将执行与SFGoodGuy()
类中的loadTexture()
方法相同的功能。不同之处在于,这个公共类将返回一个 int 数组,您可以将该数组传递给所有实例化的字符。
第一步是打开SFGoodGuy()
类并移除loadTexture()
方法(以及支持它的任何变量)。完成后,修改后的SFGoodGuy()
类应该是这样的:
`package com.proandroidgames;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
public class SFGoodGuy {
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;
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, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[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);
}
}`
**注意:**当您完成这些更改时,根据您使用的 IDE,您将开始从代码的其他区域得到一些错误。现在不用担心他们;您将在本章的后面处理这些错误。
接下来,让我们创建一个新的公共类来加载你的纹理到 OpenGL 并返回一个 int 数组。在主包中创建一个名为SFTextures()
的新类。
`package com.proandroidgames;
public class SFTextures {
}`
现在,为接受一个GL10
实例的SFTextures()
创建一个构造函数。该实例将用于初始化纹理。您还需要一个纹理变量来初始化两个元素的 int 数组。
`package com.proandroidgames;
import javax.microedition.khronos.opengles.GL10;
public class SFTextures {
**private int[] textures = new int[1];
public SFTextures(GL10 gl){
}**
}`
您需要让 OpenGL 为您正在加载的纹理生成一些名称。以前,这是在使用glGenTextures()
方法的SFGoodGuy()
类的loadTexture()
方法中完成的。但是,因为您计划多次调用这个通用的纹理类,所以每次调用 load 方法时,OpenGL 都会为纹理分配新的名称,这将使跟踪纹理变得很困难,如果不是不可能的话。
为了避免给同一个纹理分配多个名称,您将把glGenTextures()
方法调用移动到SFTextures()
构造函数:
`package com.proandroidgames;
import javax.microedition.khronos.opengles.GL10;
public class SFTextures {
**private int[] textures = new int[1];
gl.glGenTextures(1, textures, 0);
public SFTextures(GL10 gl){
}**
}`
您需要为SFTextures()
创建一个loadTexture()
方法。在SFGoodGuy()
和SFBackground()
类中,loadTexture()
方法是一个简单的方法,没有返回。为了让你更好地控制纹理的访问,特别是当你开始加载多个 sprite 的时候,创建SFTextures()
的loadTexture()
方法来返回一个 int 数组。
`package com.proandroidgames;
import javax.microedition.khronos.opengles.GL10;
public class SFTextures {
**private int[] textures = new int[1];
gl.glGenTextures(1, textures, 0);
public SFTextures(GL10 gl){
}
public int[] loadTexture(GL10 gl,int texture, Context context,int textureNumber)
{
}**
}`
注意添加了textureNumber
参数。虽然现在这个值是 1,但是在下一章中,当你开始使用这个类来加载多个 sprite 工作表时,它将被用来指示哪个工作表正在被加载。
除此之外,loadTexture()
方法的核心看起来与其在SFGoodGuy()
类中的对应部分完全相同。唯一的变化——除了对glGenTextures()
的调用被移除——是textureNumber
参数现在被用作glBindTextures()
调用中指向的数组,并且loadTextures()
现在在结束时返回纹理的 int 数组。
`package com.proandroidgames;
**import java.io.IOException;
import java.io.InputStream;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;**
public class SFTextures {
**private int[] textures = new int[1];
gl.glGenTextures(1, textures, 0);
public SFTextures(GL10 gl){
}
public int[] loadTexture(GL10 gl,int texture, Context context,int textureNumber)
{
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.glBindTexture(GL10.GL_TEXTURE_2D, textures[textureNumber - 1]);
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_CLAMP_TO_EDGE);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
return textures;
}**
}`
你的普通纹理类已经完成,可以使用了。保存SFTextures()
和SFGoodGuy()
,暂时关闭。同样,您现在应该会看到来自SFGameSFGameRenderer()
类的错误。暂时忽略这些错误;当你阅读这一章时,你将会注意到它们。
在下一节中,你将创建一个类来装载你的敌舰,并让他们为与玩家的战斗做好准备。
创造敌人阶级
不管你玩过什么游戏,它们都有一个共同点:永远不会只有一个敌人。在一个游戏中只有一个敌人会导致一个非常快速和非常无聊的游戏。
在星际战士中,你将为玩家创造 30 个敌人在屏幕上战斗。我们在第二章中勾勒出了星际战士所依据的故事。根据这个故事,提到了三种不同类型的敌人。在本章的这一节,你将创建这三种类型的敌人所基于的职业,以及 30 个实例化的敌人。
添加新的精灵工作表
您需要添加到项目中的第一件事是一个新的 sprite 表。在前一章中,你已经了解了 sprite sheets 对于 2d 游戏的重要性和用途。现在,您已经在代码中为所有角色精灵提供了一个通用的精灵表,您可以将它添加到您的项目中。图 6–1 展示了常见的精灵表。
图 6–1。 普通雪碧单
只需移除 drawable 文件夹中的good_guy
sprite 工作表,然后添加这个。
**注意:**注意玩家的角色在这个精灵表上的位置和他们在上一个精灵表上的位置一样。因此,您将不必为玩家角色更改任何纹理定位。
接下来,您需要编辑SFEngine
类来添加您将在本章中使用的常量和变量。这次有不少。你将需要 17 个常数来帮助你独自控制敌人 AI。其中一些您可能要到下一章才会用到,但是现在添加它们是个好主意:
**public static int CHARACTER_SHEET = R.drawable.character_sprite; public static int TOTAL_INTERCEPTORS = 10; public static int TOTAL_SCOUTS = 15; public static int TOTAL_WARSHIPS = 5; public static float INTERCEPTOR_SPEED = SCROLL_BACKGROUND_1 * 4f; public static float SCOUT_SPEED = SCROLL_BACKGROUND_1 * 6f; public static float WARSHIP_SPEED = SCROLL_BACKGROUND_2 * 4f; public static final int TYPE_INTERCEPTOR = 1; public static final int TYPE_SCOUT = 2; public static final int TYPE_WARSHIP = 3;** **public static final int ATTACK_RANDOM = 0; public static final int ATTACK_RIGHT = 1; public static final int ATTACK_LEFT = 2; public static final float BEZIER_X_1 = 0f; public static final float BEZIER_X_2 = 1f; public static final float BEZIER_X_3 = 2.5f; public static final float BEZIER_X_4 = 3f; public static final float BEZIER_Y_1 = 0f; public static final float BEZIER_Y_2 = 2.4f; public static final float BEZIER_Y_3 = 1.5f; public static final float BEZIER_Y_4 = 2.6f;**
因为你添加到屏幕上的敌人将作为一个职业开始,就像背景和可玩的角色一样,所以给你的主包添加一个名为SFEnemy()
的新职业。这个职业将被用来把你的敌人带入游戏。
**提示:**尽管你总共会有 30 个三种不同类型的敌人,但它们都是从同一个SFEnemy()
类中实例化出来的。
创建 SFEnemy 类
在本节中,您将创建一个类,用于在星际战士游戏中产生所有三种类型的敌人。向您的项目添加一个名为SFEnemy()
的新类:
`package com.proandroidgames;
public class SFEnemy {
}`
当你开始创建 AI 逻辑时,你的敌人需要一些属性来帮助你。您将需要一些属性来设置或获取敌人当前的 x 和 y 位置、t 因子(用于以曲线飞行敌人)以及到达目标的 x 和 y 增量。
`package com.proandroidgames;
public class SFEnemy {
public float posY = 0f; //the x position of the enemy
public float posX = 0f; //the y position of the enemy
public float posT = 0f; //the t used in calculating a Bezier curve
public float incrementXToTarget = 0f; //the x increment to reach a potential
target
public float incrementYToTarget = 0f; //the y increment to reach a potential
target
}`
你还需要一些属性,让你设置或获取敌人攻击的方向,敌人是否被消灭,以及这个实例代表什么类型的敌人。
`package com.proandroidgames;
public class SFEnemy {
**public float posY = 0f; //the x position of the enemy
public float posX = 0f; //the y position of the enemy
public float posT = 0f; //the t used in calculating a Bezier curve
public float posXToTarget = 0f; //the x increment to reach a potential target
public float posYToTarget = 0f; //the y increment to reach a potential target
public int attackDirection = 0; //the attack direction of the ship
public boolean isDestroyed = false; //has this ship been destroyed?
public int enemyType = 0; //what type of enemy is this?**
}`
接下来你的敌人职业需要的三个属性是一个指示器,让你知道它是否锁定了一个目标(这对你的 AI 逻辑至关重要)和两个坐标,代表锁定目标的位置。
`package com.proandroidgames;
public class SFEnemy {
public float posY = 0f; //the x position of the enemy
public float posX = 0f; //the y position of the enemy
public float posT = 0f; //the t used in calculating a Bezier curve
public float posXToTarget = 0f; //the x increment to reach a potential target
public float posYToTarget = 0f; //the y increment to reach a potential target
public int attackDirection = 0; //the attack direction of the ship
public boolean isDestroyed = false; //has this ship been destroyed?
public int enemyType = 0; //what type of enemy is this
public boolean isLockedOn = false; //had the enemy locked on to a target?
public float lockOnPosX = 0f; //x position of the target
public float lockOnPosY = 0f; //y position of the target
}`
接下来,给你的SFEnemy()
类一个接受两个 int 参数的构造函数。第一个参数将用来表示应该创造的敌人类型:TYPE_INTERCEPTOR
、TYPE_SCOUT
或TYPE_WARSHIP
。第二个参数将用于指示特定敌人将从屏幕上的哪个方向进攻:ATTACK_RANDOM
、ATTACK_RIGHT
或ATTACK_LEFT
。
`package com.proandroidgames;
public class SFEnemy {
**…
public SFEnemy(int type, int direction) {
}**
}`
在SFEnemy()
的构造函数中,需要根据传入构造函数的 int 类型来设置敌方类型。你也将设定方向。看到这些参数将让你在游戏循环中根据敌人的类型和运动方向做出决定。
`package com.proandroidgames;
public class SFEnemy {
**…
public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
}**
}`
星际战士(在第二章中)的故事描述了三种不同敌人的攻击特点。侦察兵以快速但可预测的模式飞行,截击机锁定并直接飞向玩家的角色,战舰以随机模式机动。每艘船都需要从屏幕上的一个特定点出发。
通常在滚动射击游戏中,敌人从屏幕外 y 轴上的一点开始,然后向下滚动到玩家。因此,在构造函数中你要做的下一件事是为敌人建立一个 y 轴起点。
Android 的随机数生成器是选择起点的好方法。Android 随机数生成器将生成一个介于 0 和 1 之间的数字。然而,你的敌人的 y 轴是从 0 到 4。将随机数生成器生成的数字乘以 4,结果将是屏幕上一个有效的 y 轴位置。在有效的 y 位置上加 4,然后将起点推出屏幕。
`package com.proandroidgames;
public class SFEnemy {
**…
private Random randomPos = new Random();
public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;
}**
}`
它负责 y 轴。现在,你需要建立一个 x 轴位置。看看您在SFEngine
中创建的常量。三个代表 x 轴上敌人可能攻击的位置:ATTACK_LEFT
、ATTACK_RANDOM
和ATTACK_RIGHT
。左侧的 x 轴值为 0。右边的 x 轴值是 3(从 4 中减去 1 个单位以说明精灵的大小)。
可以使用一个case
语句,根据传递给构造函数的攻击方向来分配 x 轴的起点。
`package com.proandroidgames;
public class SFEnemy {
**…
public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;
switch(attackDirection){
case SFEngine.ATTACK_LEFT:
posX = 0;
break;
case SFEngine.ATTACK_RANDOM:
posX = randomPos.nextFloat() * 3;
break;
case SFEngine.ATTACK_RIGHT:
posX = 3;
break;
}
}**
}`
您需要建立的最后一个变量是posT
。不要担心posT
现在做什么;在本章的后面你会发现。将posT
设置为SFEngine.SCOUT_SPEED
的值。
`package com.proandroidgames;
public class SFEnemy {
…
public SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;
switch(attackDirection){
case SFEngine.ATTACK_LEFT:
posX = 0;
break;
case SFEngine.ATTACK_RANDOM:
posX = randomPos.nextFloat() * 3;
break;
case SFEngine.ATTACK_RIGHT:
posX = 3;
**break;
}
posT = SFEngine.SCOUT_SPEED;
}**
}`
你可以创造的两种敌人类型,截击机和战舰,将会以对角线的直线行进。生成这些攻击路径的代码将在游戏循环中处理,因为沿直线引导物体相对容易。另一方面,侦察型敌人会以一种被称为贝塞尔曲线的模式移动。在下一节中,您将创建帮助敌人以曲线飞行的方法。
贝塞尔曲线
虽然你可能不知道它的名字,但你很可能以前见过贝塞尔曲线。图 6–2 展示了贝塞尔曲线的样子。
图 6–2。 一条二次贝塞尔曲线
为了让侦察兵以二次贝塞尔曲线从屏幕的顶部到底部飞行,您需要两个方法:一个是获取贝塞尔曲线上的下一个 x 轴值,另一个是获取贝塞尔曲线上的下一个 y 轴值。每次你调用这些方法,你会得到 x 和 y 轴上的下一个点,特定的敌人需要移动到这个点。
幸运的是,在贝塞尔曲线上绘制点相当简单。要构造一条二次贝塞尔曲线,你需要四个笛卡尔点:一个起点,一个终点,以及曲线环绕的两个点。这些点在星际战士游戏中永远不会改变。每个侦察兵都会沿着同一条曲线,从左边或右边。因此,在SFEngine
中创建了八个常数来表示每个轴上的四个二次贝塞尔曲线点。
绘制点的关键值是 t 因子。t 因子告诉公式您在曲线上的位置,从而允许公式计算该单个位置的 x 或 y 坐标。因为你的船将以一个预先定义的速度移动,你将使用这个值作为 t 的种子值。
**提示:**如果你不理解本节公式背后的数学,有许多关于贝塞尔曲线的伟大资源,包括下面的维基百科页面:【http://en.wikipedia.org/wiki/Bézier_curve】??。
在您的SFEnemy()
类中创建两个方法:一个获取下一个 x 轴值,另一个获取下一个 y 轴值。
`package com.proandroidgames;
public class SFEnemy {
**…
public SFEnemy(int type, int direction) {
…
}
public float getNextScoutX(){
}
public float getNextScoutY(){
}**
}`
下面是在 y 轴上的二次贝塞尔曲线上寻找一个点的公式(用x
代替y
来寻找 x 轴上的值):
(y<sub>1</sub>*(t<sup>3</sup>)) + (y<sub>2</sub> * 3 * (t<sup>2</sup>) * (1-t)) + (y<sub>3</sub> * 3 * t * (1-t)<sup>2</sup>) + (y<sub>4</sub>* (1-t)<sup>3</sup>)
在您的getNextScoutY()
方法中使用这个公式和正确的变量。
`package com.proandroidgames;
public class SFEnemy {
**…
public SFEnemy(int type, int direction) {
…
}
public float getNextScoutX(){
}
public float getNextScoutY(){**
*return (float)((SFEngine.BEZIER_Y_1(posTposTposT)) +
(SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}**
}`
对 x 轴使用相同的公式,有一个小的变化。如果敌人从屏幕的左边攻击,而不是右边,你需要颠倒公式。
`package com.proandroidgames;
public class SFEnemy {
**…
public SFEnemy(int type, int direction) {
…
}
public float getNextScoutX(){
if (attackDirection == SFEngine.ATTACK_LEFT){
return (float)((SFEngine.BEZIER_X_4*(posTposTposT)) +
(SFEngine.BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_2 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT))));
}else{
return (float)((SFEngine.BEZIER_X_1*(posTposTposT)) +
(SFEngine.BEZIER_X_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_4 * ((1-posT) * (1-posT) * (1-posT))));
}
}
public float getNextScoutY(){
return (float)((SFEngine.BEZIER_Y_1*(posTposTposT)) +
(SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}**
}`
注意,在计算 x 轴的右侧时,值为 x 1 、x 2 、x 3 和 x4—从左侧开始,点的使用顺序相反:x 4 、x 3 、x 2 和 x 1 。
考虑到使用新的通用 sprite 表所做的更改,SFEnemy
类的其余部分看起来应该和SFGoodGuy
类一样。
`package com.proandroidgames;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.Random;
import javax.microedition.khronos.opengles.GL10;
public class SFEnemy {
public float posY = 0f;
public float posX = 0f;
public float posT = 0f;
public float incrementXToTarget = 0f;
public float incrementYToTarget = 0f;
public int attackDirection = 0;
public boolean isDestroyed = false;
public int enemyType = 0;
public boolean isLockedOn = false;
public float lockOnPosX = 0f;
public float lockOnPosY = 0f;
private Random randomPos = new Random();
**private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;
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 SFEnemy(int type, int direction) {
enemyType = type;
attackDirection = direction;
posY = (randomPos.nextFloat() * 4) + 4;
switch(attackDirection){
case SFEngine.ATTACK_LEFT:
posX = 0;
break;
case SFEngine.ATTACK_RANDOM:
posX = randomPos.nextFloat() * 3;
break;
case SFEngine.ATTACK_RIGHT:
posX = 3;
break;
}
posT = SFEngine.SCOUT_SPEED;
**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 float getNextScoutX(){
if (attackDirection == SFEngine.ATTACK_LEFT){
return (float)((SFEngine.BEZIER_X_4*(posTposTposT)) +
(SFEngine.BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_2 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT))));
}else{
return (float)((SFEngine.BEZIER_X_1*(posTposTposT)) +
(SFEngine.BEZIER_X_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_4 * ((1-posT) * (1-posT) * (1-posT))));
}
}
public float getNextScoutY(){
return (float)((SFEngine.BEZIER_Y_1*(posTposTposT)) +
(SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT *
((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}
**public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[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);
}**
}`
你现在有了一个工人阶级,你可以在游戏中培养所有的敌人。保存SFEnemy()
类。在下一章,你将开始为你的敌人创造人工智能。
总结
在这一章中,你的技能又向前迈进了一大步。为你的游戏创造敌人已经做了很多工作,还有更多工作要做。以下列表描述了您在本章中学到的内容,您将在第七章中对您所学的内容进行扩展:
- 创建一个通用的纹理类来保存一个大的 sprite 表。
- 创建一个数组来容纳游戏中的所有敌人,以便于处理。
- 创建
SFEnemy()
类来繁殖三个不同的敌人。 - 创建一个用贝塞尔曲线移动敌人的方法。