安卓游戏秘籍(三)
九、将背景与角色运动同步
在第八章中,介绍了创建可滚动的多层背景的解决方案。然而,如果你试图同步背景的滚动和角色的移动,你可能会遇到问题。
在这一章中,你会看到两个方法——第一个是在两个方向上滚动多图像背景,第二个是同步背景的滚动和可玩角色的移动。
9.1 向两个方向滚动背景
问题
当玩家可以两个方向奔跑时,背景只向一个方向滚动 。
解决办法
修改背景类以跟踪两个方向的运动。
它是如何工作的
这个解决方案假设你的游戏,可能是一个平台风格的游戏,有一个角色可以向两个方向移动。回想一下流行的平台风格游戏(如超级马里奥兄弟),很多时候可玩角色可以向右移动以在游戏中前进。人物也可以向左移动,通常是在有限的能力下,来追溯他们的脚步。
在背景可以与角色同步之前,它需要能够在两个方向上移动。这个解决方案将获取一个三幅图像的背景,加载它,向右滚动,然后反向向左滚动。
第一步是将用作背景的三幅图像复制到res/drawable文件夹中。图 9-1 到 9-3 代表了我在这个例子中使用的三张图片。请注意,它们是单一背景的图层,已经被拉开,以便它们可以以不同的速度滚动。
图 9-1 。最远的背景层
图 9-2。中间层
图 9-3 。地面层
注意为了在本书中打印图像,图像的透明部分被涂成灰色。
一旦图像在项目中,实例化三个SBGBackground()类的实例——每个背景层一个。
private SBGBackground background1 = new SBGBackground();
private SBGBackground background2 = new SBGBackground();
private SBGBackground background3 = new SBGBackground();
下一步是创建三组变量来控制和跟踪每一层背景的速度和位置。
private float bgScroll1;
private float bgScroll2;
private float bgScroll3;
public static float SCROLL_BACKGROUND_1 = .002f;
public static float SCROLL_BACKGROUND_2 = .003f;
public static float SCROLL_BACKGROUND_3 = .004f;
在第七章(清单 7-1 和 7-2)中,给出了一个创建scrollBackground()方法的解决方案。
改变方法,允许背景向左或向右滚动,这取决于玩家移动的方向。
在本书的早期,提供了一个允许角色在屏幕上移动的解决方案(使用 spritesheet)。这个解决方案的一部分需要创建一些变量来跟踪玩家的动作。在这个解决方案中使用相同的变量。
public static int playerAction = 0;
public static final int PLAYER_MOVE_LEFT = 1;
public static final int PLAYER_MOVE_RIGHT = 2;
注意前面提到的变量应该在你收集玩家输入的时候设置。有关这方面的更多信息,请参见第五章和第六章。
使用 OpenGL ES 1 和 2/3 向接受playerAction变量的scrollBackground()方法添加一个新参数,如下所示。
对于 OpenGL ES 1:
private void scrollBackground1(GL10gl, int direction){
...
}
对于 OpenGL ES 2/3:
private void scrollBackground1(int direction){
...
}
传递给scrollBackground()方法的方向将用于决定如何滚动背景图像。在当前的scrollBackground()方法中,下面一行控制图像的滚动:
bgScroll1 += SCROLL_BACKGROUND_1;
这条线的关键部分是+=。要改变滚动方向,需要将操作者从+=切换到-=。创建一个switch...case语句,根据从玩家输入中收集的方向来改变这个操作符。
OpenGL ES 1 和 OpenGL ES 2/3 的scrollBackground()方法分别显示在清单 9-1 和清单 9-2 中。
清单 9-1 。scrollBackground() (OpenGL 是 1)
private void scrollBackground1(GL10gl, int direction){
if (bgScroll1 == Float.MAX_VALUE){
bgScroll1 = 0f;
}
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(1f, 1f, 1f);
gl.glTranslatef(0f, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(bgScroll1,0.0f, 0.0f);
background1.draw(gl);
gl.glPopMatrix();
switch(direction)
{
case PLAYER_MOVE_RIGHT:
bgScroll1 += SCROLL_BACKGROUND_1;
break;
case PLAYER_MOVE_LEFT:
bgScroll1 -= SCROLL_BACKGROUND_1;
break;
}
gl.glLoadIdentity();
}
清单 9-2 。scrollBackground() (OpenGL 是 2/3)
private void scrollBackground1(int direction){
if (bgScroll1 == Float.MAX_VALUE){
bgScroll1 = 0f;
}
background1.draw(mMVPMatrix, bgScroll1);
switch(direction)
{
case PLAYER_MOVE_RIGHT:
bgScroll1 += SCROLL_BACKGROUND_1;
break;
case PLAYER_MOVE_LEFT:
bgScroll1 -= SCROLL_BACKGROUND_1;
break;
}
}
背景现在将根据传递给scrollBackground()方法的方向向右或向左滚动。下一个解决方案将这个方法与玩家的移动联系起来。
9.2 移动背景以响应用户输入
问题
根据玩家的移动,背景不会开始或停止滚动。
解决办法
需要结合movePlayer()方法调用scrollBackground()方法来控制两个的移动。
它是如何工作的
在第六章中,清单 6-7 给出了一个解决方案,它创建了一个movePlayer()方法来促进角色的动画。这个方法需要修改,以允许背景的滚动同步到它。首先,更改它的名称以表明它的新用途。
在 OpenGL ES 1 中:
private void movePlayerAndBackground(GL10gl){
...
}
在 OpenGL ES 2/3 中:
private void movePlayerAndBackground(){
...
}
注意,在现有的movePlayer()方法中,有一个switch语句移动播放器(使用 spritesheet)。switch语句需要重写,以便当角色到达屏幕的大致中间时,它不再移动(参见清单 9-3 和 9-4 )。角色应该看起来在这一点上运行到位,背景应该滚动到近似运动。
清单 9-3 。movePlayerAndBackground() (OpenGL 是 1)
private void movePlayerAndBackground(GL10gl){
background1.draw(gl);
if(!goodguy.isDead)
{
switch(playerAction){
case PLAYER_MOVE_RIGHT:
currentStandingFrame = STANDING_RIGHT;
currentRunAniFrame += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame = .0f;
}
if(playerCurrentLocation>= 3f)
{
scrollBackground1(gl, playerAction);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.50f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();
}else{
playerCurrentLocation += PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.50f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();
}
break;
case PLAYER_MOVE_LEFT:
currentStandingFrame = STANDING_LEFT;
currentRunAniFrame += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame = .0f;
}
if(playerCurrentLocation<= 2.5f)
{
scrollBackground1(gl, playerAction);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.75f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();
}else{
playerCurrentLocation -= PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.75f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();
}
break;
case PLAYER_STAND:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentStandingFrame,.25f, 0.0f);
goodguy.draw(gl,spriteSheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
}
}
清单 9-4 。movePlayerAndBackground() (OpenGL 是 2/3)
private void movePlayerAndBackground(){
background1.draw(mMVPMatrix, bgScroll1);
if(!goodguy.isDead)
{
switch(playerAction){
case PLAYER_MOVE_RIGHT:
currentStandingFrame = STANDING_RIGHT;
currentRunAniFrame += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame = .0f;
}
if(playerCurrentLocation>= 3f)
{
scrollBackground1(playerAction);
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentRunAniFrame, .75f);
}else{
playerCurrentLocation += PLAYER_RUN_SPEED;
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentRunAniFrame, .50f);
}
break;
case PLAYER_MOVE_LEFT:
currentStandingFrame = STANDING_LEFT;
currentRunAniFrame += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame = .0f;
}
if(playerCurrentLocation<= 2.5f)
{
scrollBackground1(playerAction);
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentRunAniFrame, .75f);
}else{
playerCurrentLocation -= PLAYER_RUN_SPEED;
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentRunAniFrame, .50f);
}
break;
case PLAYER_STAND:
goodguy.draw(spriteSheets,SBG_RUNNING_PTR, currentStandingFrame, .25f);
break;
}
}
}
角色动画大约在中途停止在屏幕上前进。然后该方法调用scrollBackground()方法开始移动背景。
十、使用瓷砖建造关卡
在这一章中,你将会看到两种用瓷砖建造关卡的方法。许多 2D 游戏(特别是侧滚平台和自上而下的冒险/RTS 风格的游戏)都实现了用可重复的瓷砖构建的关卡。
如果你在用瓷砖建造关卡时有困难,这一章应该会有帮助。第一个方法是从一个 sprite 表中加载图块并创建一个关卡贴图。第二个配方将使用精灵表和级别映射,然后从瓷砖创建一个完整的级别。
10.1 从子画面表加载图块
问题
用于创建关卡的图块存储在 sprite sheet 中,无法确定在哪个位置使用哪个图块。
解决办法
使用纹理加载器将瓷砖纹理映射到一组顶点,并使用级别映射来指定将哪些瓷砖放置在哪里。
它是如何工作的
这个解决方案需要使用两个类。第一个类包含创建顶点和索引的信息,以及绘制图块的方法。第二个类保存纹理信息。
在第六章的中,提供了加载子画面的解决方案。这些解决方案将纹理加载方法从对象类中分离出来,以允许在一个地方加载和保存多个子画面。这个解决方案将在纹理类上进行扩展,以保存新平铺子画面。像往常一样,首先将你的精灵表复制到项目中。图 10-1 中的所示的这个例子的 sprite 表中有两个图块。一个瓷砖是地面瓷砖,有一些草和一点天空;第二个瓦片是天空瓦片。记住,你的可能有几百个。
图 10-1 。有两个图块的精灵表
SBGTile()类 类
将图像添加到项目中后,创建一个新类SBGTile()。SBGTile()类将设置你的顶点和索引(见清单 10-1 和 10-2 )。该类的结构应该看起来非常熟悉,因为它现在已经在其他几个解决方案中使用;但是,加粗的代码已经更改,允许加载多个 sprite 表。
清单 10-1 。SBGTile() (OpenGL 是 1)
public class SBGTile {
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,
.25f, 0f,
.25f, .25f,
0f, .25f,
};
private byte indices[] = {
0,1,2,
0,2,3,
};
public SBGTile() {
ByteBufferbyteBuf = 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(GL10gl,int[] spriteSheet,int currentSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[currentSheet - 1]);
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);
}
}
清单 10-2 。SBGTile() (OpenGL 是 2/3)
class SBGBackground{
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_Position = uMVPMatrix * vPosition;" +
" TexCoordOut = TexCoordIn;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float posX;" +
"uniform float posY;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x+
posX,TexCoordOut.y + posY));"+
"}";
private float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};
private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int mProgram;
private int mPositionHandle;
private int mMVPMatrixHandle;
static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float squareCoords[] = { -1f, 1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f, 1f, 0.0f };
private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };
private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;
public SBGBackground() {
ByteBuffer bb = ByteBuffer.allocateDirect(
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
bb = ByteBuffer.allocateDirect(texture.length * 4);
bb.order(ByteOrder.nativeOrder());
textureBuffer = bb.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
ByteBuffer dlb = ByteBuffer.allocateDirect(
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);
mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
}
public void draw(float[] mvpMatrix, float posX, float posY,
int[] spriteSheet, int currentSheet) {
GLES20.glUseProgram(mProgram);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, spriteSheet[currentSheet - 1]);
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
GLES20.glEnableVertexAttribArray(mPositionHandle);
int vsTextureCoord = GLES20.glGetAttribLocation(mProgram, "TexCoordIn");
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsPosX = GLES20.glGetUniformLocation(mProgram, "posX");
int fsPosY = GLES20.glGetUniformLocation(mProgram, "posY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsPosX, posX);
GLES20.glUniform1f(fsPosY, posY);
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
请特别注意加粗的行。这些行接受一个代表多个 sprite sheet 纹理的int数组,以及一个指示哪个 sprite sheet 用于特定绘制操作的int。
SBGTextures()类类
现在您需要一个类来处理多个 sprite 表的加载。创建一个名为SBGTextures()的新类,如清单 10-3 和清单 10-4 所示。
清单 10-3 。SBGTextures() (OpenGL 是 1)
public class SBGTextures {
private int[] textures = new int[2];
public SBGTextures(GL10gl){
gl.glGenTextures(2, textures, 0);
}
public int[] loadTexture(GL10gl,int texture, Context context,int textureNumber) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
Bitmap temp = null;
Matrix flip = new Matrix();
flip.postScale(-1f, -1f);
try {
temp = BitmapFactory.decodeStream(imagestream);
bitmap = Bitmap.createBitmap(temp, 0, 0, temp.getWidth(), temp.getHeight(), flip, true);
}catch(Exception e){
}finally {
//Always clear and close
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;
}
}
清单 10-4 。SBGTextures() (OpenGL 是 2/3)
public class SBGTextures {
private int[] textures = new int[2];
public SBGTextures(){
}
public void loadTexture(int texture, Context context, int textureNumber) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
android.graphics.Matrix flip = new android.graphics.Matrix();
flip.postScale(-1f, -1f);
try {
bitmap = BitmapFactory.decodeStream(imagestream);
}catch(Exception e){
//Handle your exceptions here
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
//Handle your exceptions here
}
}
GLES20.glGenTextures(2, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[textureNumber - 1]);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
}
}
同样,请注意每个清单中的粗体行。这里使用的int数组意味着你可以根据需要扩展你能容纳的独立 sprite 工作表的数量。
实例化所需的类,并在您的Renderer中创建一个 sprite 表数组。
private SBGTile tiles = new SBGTile();
private SBGTextures textureloader;
private int[] spriteSheets = new int[2];
然后,在Renderer的onSurfaceCreated()方法中,设置textureloader,用它来加载 tiles sprite 表。
textureloader = new SBGTextures(gl);
spriteSheets = textureloader.loadTexture(gl, R.drawable.tiles, context, 1);
现在瓷砖(作为一个纹理)可以使用了。但是游戏怎么知道瓷砖放在哪里呢?为此,您需要创建一个关卡地图。
创建关卡地图
关卡地图是游戏应该放置每一个方块的地方。该地图将是一个由int组成的二维数组
该图就像一个int值的矩阵。每个int值代表一个特定的图块。此解决方案中的示例只有两个不同的图块;因此,级别映射将仅由 0 和 1 组成。0 代表地面瓷砖,1 代表天空瓷砖。
将这些级别映射创建为二维数组是存储许多级别的体系结构的一种快速而简单的方法。以下是该解决方案的二维阵列级映射示例。
int map[][] = {
{0,0,0,0,0,0,0,0,0,0},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1},
};
在这里,我们创建了一个 10x 10 的 0 和 1 的数组来表示平铺在屏幕上的位置。在下一个解决方案中,您将编写一个 tiles 引擎来读取这个数组,并将 tiles 实际放置在屏幕上的正确位置。
10.2 从瓷砖创建一个级别
问题
您的游戏无法读取关卡地图int数组来使用瓷砖创建关卡。
解决办法
创建一个图块引擎,它读入数组并在所需位置写出图块。
它是如何工作的
该解决方案将带您构建一个 tile 引擎。平铺引擎读入级别贴图数组,一次一个维度,然后根据数组中的值绘制平铺。
在前一个解决方案中,我们创建了一个只有两个值的数组,即 0 和 1。这些值对应于 sprite 表中的两个图块。请记住,你可以很容易地有更多的瓷砖,给你一个看起来更精致的水平。
提示如果您使用更多的图块,从而在您的数组中有更多的int,您将不得不对该图块引擎进行的唯一更改是向switch...case语句添加更多的事例。
第一步是在您的Renderer中创建一个drawTiles()方法。
对于 OpenGL ES 1:
private void drawtiles(GL10 gl, int[][] map){
}
对于 OpenGL ES 2/3:
private void drawtiles(int[][] map){
}
drawTiles()方法将接受您的二维数组地图并遍历它。但是,在遍历数组之前,需要设置两个变量。
这些变量的目的是在您将图块设置到位时转换模型矩阵。这里的概念是,您读入地图数组的第一个元素,然后设置并绘制相应的图块。然后,您必须将模型矩阵平移到屏幕上的下一个位置,以便放置下一个图块。
float tileLocY = 0f;
float tileLocX = 0f;
现在,创建一个嵌套的for循环,它将迭代 map 数组的两个维度。
for(int x=0; x<10; x++){
for(int y=0; y<10; y++){
}
}
如果你使用的是 OpenGL ES 1,第一步就是缩放和转换模型矩阵,然后设置纹理矩阵。
for(int x=0; x<10; x++){
for(int y=0; y<10; y++){
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.20f, .20f, 1f);
gl.glTranslatef(tileLocY, tileLocX, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
}
}
注意在加粗的代码中,模型矩阵被之前设置的tileLocY和tileLocX值转换。随着循环的进行,这些变量将递增,以便将下一个图块放置在正确的位置。
下一步是设置一个简单的switch...case语句来读取 map 数组的当前元素。
for(int x=0; x<10; x++){
for(int y=0; y<10; y++){
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.20f, .20f, 1f);
gl.glTranslatef(tileLocY, tileLocX, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
switch(map[x][y]){
case 1:
break;
case 0:
break;
}
}
}
因为,在这一点上,矩阵模式已经被设置为纹理,你在switch...case语句中唯一要做的事情就是将 sprite 表转换为正确的 tile 图像。
switch(map[x][y]){
case 1:
gl.glTranslatef(.75f,.75f, 0f);
break;
case 0:
gl.glTranslatef(.75f,1f, 0f);
break;
}
提示关于使用精灵工作表的更多信息,参见第六章“加载精灵工作表”
瓷砖是在适当的位置,纹理设置为正确的图像。现在绘制图块并增加tileLocY变量以移动到下一个位置。
switch(map[x][y]){
case 1:
gl.glTranslatef(.75f,.75f, 0f);
break;
case 0:
gl.glTranslatef(.75f,1f, 0f);
break;
}
tiles.draw(gl, spriteSheets, SBG_TILE_PTR);
tileLocY += .50;
剩余的嵌套循环在每个新行上弹出矩阵,并根据需要推进tileLocX变量。
如果您使用的是 OpenGL ES 2/3,概念保持不变,但过程略有不同。您仍然需要遍历地图的每个值,并使用一个switch语句来处理每种情况。不同之处在于,不像在 OpenGL ES 1 中那样翻译矩阵,您可以简单地将每个图块的位置传递给drawtiles()方法。这与你使用 sprite 工作表的过程是一样的(见第六章关于 sprite 工作表的更深入的讨论)。清单 10-5 显示了完成的方法应该是什么样子。完成的 OpenGL ES 2/3 版本的drawtiles()如清单 10-6 所示。
清单 10-5 。drawtiles() (OpenGL 是 1)
private void drawtiles(GL10gl){
float tileLocY = 0f;
float tileLocX = 0f;
for(int x=0; x<10; x++){
for(int y=0; y<10; y++){
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.20f, .20f, 1f);
gl.glTranslatef(tileLocY, tileLocX, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
switch(map[x][y]){
case 1:
gl.glTranslatef(.75f,.75f, 0f);
break;
case 0:
gl.glTranslatef(.75f,1f, 0f);
break;
}
tiles.draw(gl, spriteSheets, SBG_TILE_PTR);
tileLocY += .50;
}
gl.glPopMatrix();
gl.glLoadIdentity();
tileLocY = 0f;
tileLocX += .50;
}
}
清单 10-6 。drawtiles() (OpenGL 是 2/3)
private void drawtiles(){
float tileLocY = 0f;
float tileLocX = 0f;
Matrix.translateM(mTMatrix, 0, tileLocX, tileLocT, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0) ;
for(int x=0; x<10; x++){
for(int y=0; y<10; y++){
switch(map[x][y]){
case 1:
tiles.draw(mMPVMatrix, .75f, .75, spriteSheets, SBG_TILE_PTR);
break;
case 0:
tiles.draw(mMPVMatrix, .75f, .75, spriteSheets, SBG_TILE_PTR);
break;
}
tileLocY += .50;
}
tileLocY = 0f;
tileLocX += .50;
}
}
如果你正在使用 OpenGL ES 2/3,确保在你的Renderer中设置一个新的翻译矩阵(清单 10-6 中的mTMatrix)。平移矩阵的工作是移动瓷砖的位置。它是 OpenGL ES 2/3 版的glTranslatef()。下面的代码显示了转换矩阵。
public class SBGGameRenderer implements GLSurfaceView.Renderer {
...
private final float[] mTMatrix = new float[16];
...
@Override
public void onDrawFrame(GL10 unused) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);
drawtiles();
...
}
}
由该贴图数组和子画面组合产生的级别显示在图 10-2 中。
图 10-2 。一个简单的关卡,由关卡地图和方块组成
请记住,要利用更多的图块,只需扩展您的switch...case语句的范围。
十一、移动角色
在屏幕上移动角色——无论是人、动物、机器人还是车辆——是引人注目的游戏中最重要的部分之一。如果你试图创造一个在游戏中自由移动的角色,你可能会遇到一些问题。
本章将介绍帮助你移动角色的解决方案。本章中的解决方案包括让角色奔跑,以及在角色移动时改变角色动画。
第一种方法帮助你在屏幕上向四个方向移动你的角色。其余的解决方案帮助您以不同的速度移动角色,并在角色移动时为其设置动画。
11.1 向四个方向移动角色
问题
屏幕上的角色不会移动。
解决办法
使用游戏循环来控制角色的移动。
它是如何工作的
这个解决方案要求你追踪玩家想要角色移动到哪里,然后将这个意图转换到模型矩阵的 x 或 y 轴。换句话说,一旦你捕捉到玩家想要移动的位置,你就可以使用一个switch...case语句来确定在模型矩阵中平移哪个轴,从而相应地移动屏幕上的角色。
您分三步完成此解决方案。您需要确定玩家想要移动的方向,然后创建一个保存该值的标志,最后使用该值来移动屏幕上的角色。第一步是捕捉玩家想要移动的方向。我们将通过使用SimpleOnGestureListener() 来实现这一点。
玩家将向左、向右、向上或向下滑动来指示角色应该朝哪个方向跑(想想类似寺庙跑步风格的输入系统)。在游戏的主意图中,实例化一个新的SimpleOnGestureListener(),如清单 11-1 所示。
清单 11-1 。SimpleOnGestureListener()
GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEventarg0) {
//TODO Auto-generated method stub
return false;
}
@Override
public boolean onFling(MotionEvente1, MotionEvente2, float velocityX,
float velocityY) {
float leftMotion = e1.getX() - e2.getX();
float upMotion = e1.getY() - e2.getY();
float rightMotion = e2.getX() - e1.getX();
float downMotion = e2.getY() - e1.getY();
if((leftMotion == Math.max(leftMotion, rightMotion))&&
(leftMotion>Math.max(downMotion, upMotion)) )
{
}
if((rightMotion == Math.max(leftMotion, rightMotion))&&
(rightMotion>Math.max(downMotion, upMotion) )
{
}
if((upMotion == Math.max(upMotion, downMotion))&&
(upMotion>Math.max(leftMotion, rightMotion)) )
{
}
if((downMotion == Math.max(upMotion, downMotion))&&
(downMotion>Math.max(leftMotion, rightMotion)) )
{
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//TODO Auto-generated method stub
}
@Override
public boolean onScroll(MotionEvente1, MotionEvente2, float distanceX,
float distanceY) {
//TODO Auto-generated method stub
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//TODO Auto-generated method stub
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//TODO Auto-generated method stub
return false;
}
};
注意这个实例化中的四个if语句。它们代表向左、向右、向上和向下的动作。现在创建一个可以从主意图和游戏循环中访问的int。根据SimpleOnGestureListener()检测到的方向设置int(参见清单 11-2 )。
清单 11-2 。SimpleOnGestureListener()
public static int playeraction = 0;
public static final int PLAYER_MOVE_LEFT = 1;
public static final int PLAYER_MOVE_RIGHT = 2;
public static final int PLAYER_MOVE_UP = 3;
public static final int PLAYER_MOVE_DOWN = 4;
...
if((leftMotion == Math.max(leftMotion, rightMotion)) &&
(leftMotion>Math.max(downMotion, upMotion)) )
{
playeraction = PLAYER_MOVE_LEFT;
}
if((rightMotion == Math.max(leftMotion, rightMotion)) &&
(rightMotion>Math.max(downMotion, upMotion) )
{
playeraction = PLAYER_MOVE_RIGHT;
}
if((upMotion == Math.max(upMotion, downMotion)) &&
(upMotion>Math.max(leftMotion, rightMotion)) )
{
playeraction = PLAYER_MOVE_UP;
}
if((downMotion == Math.max(upMotion, downMotion)) &&
(downMotion>Math.max(leftMotion, rightMotion)) )
{
playeraction = PLAYER_MOVE_DOWN;
}
...
最后,在Renderer中,创建一个方法来读取你刚刚设置的int的值,并相应地转换角色的模型矩阵,如清单 11-3 和清单 11-4 所示。
清单 11-3 。movePlayer() (OpenGL 是 1)
private void movePlayer(GL10gl){
switch(playeraction){
case PLAYER_MOVE_RIGHT:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(0f, .75f, 0f);
character.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PLAYER_MOVE_LEFT:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(0f, -.75f, 0f);
character.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PLAYER_MOVE_UP:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(.75f, 0f, 0f);
character.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PLAYER_MOVE_DOWN:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(-.75f, 0f, 0f);
character.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
}
清单 11-4 。movePlayer() (OpenGL 是 2/3)
private void movePlayer(GL10gl){
switch(playeraction){
case PLAYER_MOVE_RIGHT:
Matrix.translateM(mTMatrix, 0, 0, .75f, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;
case PLAYER_MOVE_LEFT:
Matrix.translateM(mTMatrix, 0, 0, -.75f, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;
case PLAYER_MOVE_UP:
Matrix.translateM(mTMatrix, 0, .75f, 0, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;
case PLAYER_MOVE_DOWN:
Matrix.translateM(mTMatrix, 0, -.75f, 0, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;
}
}
对glTranslatef()的调用在清单 11-3 (对于 OpenGL ES 代码)中用粗体突出显示,因为你应该用在你的特定游戏中效果最好的值来转换你的模型矩阵。
11.2 以不同的速度移动角色
问题
游戏角色需要以不同的速度行走和奔跑。
解决办法
使用游戏循环数来决定角色何时应该改变速度。
它是如何工作的
在这个解决方案中,您计算已经执行的游戏循环次数,并使用这个计数来确定角色的速度何时应该改变。例如,您的游戏是以这样的方式构建的,当玩家触摸屏幕的右侧时,角色将向右移动,当玩家触摸屏幕的左侧时,角色将向左移动,当玩家不触摸屏幕时,角色静止不动。如果玩家只是短时间触摸屏幕,你可以使用这种架构让角色行走,如果玩家触摸屏幕的时间更长,就让角色奔跑。
提示 第五章概述了设置基于触摸控制的游戏的解决方案。
第一步是创建两个变量,这两个变量可以从游戏中的任何类中读取。第一个变量跟踪已经执行的游戏循环次数,第二个变量跟踪角色的当前速度。
public static final float PLAYER_RUN_SPEED = .15f;
public static int totalGameLoops = 0;
接下来,在Renderer类中创建一个movePlayer()方法。到目前为止,这种方法已经在本书的多个解决方案中使用。如果你需要这个方法如何工作的基本解释,请参见第六章。
movePlayer()方法包含一个switch...case语句,该语句读取玩家的动作并相应地移动角色。修改这个方法来测试执行循环的次数,并在此基础上改变角色的速度(参见清单 11-5 和清单 11-6 )。
清单 11-5 。改变移动速度(OpenGL ES 1)
private void movePlayer(GL10gl){
if (totalGameLoops> 15)
{
PLAYER_RUN_SPEED += .5f;
}
switch(playeraction){
case PLAYER_MOVE_RIGHT:
playercurrentlocation += PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PLAYER_MOVE_LEFT:
playercurrentlocation -= PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PLAYER_STAND:
PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;
break;
}
}
清单 11-6 。改变移动速度(OpenGL ES 2/3)
private void movePlayer(GL10gl){
if (totalGameLoops> 15)
{
PLAYER_RUN_SPEED += .5f;
}
switch(playeraction){
case PLAYER_MOVE_RIGHT:
playercurrentlocation += PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0,playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;
case PLAYER_MOVE_LEFT:
playercurrentlocation -= PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0,playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix);
break;
case PLAYER_STAND:
PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;
break;
}
}
最后,在Renderer的onDrawFrame()方法中,每次执行增加totalGameLoops int(参见清单 11-7 )。
清单 11-7 。totalGameLoops
public void onDrawFrame(GL10gl) {
...
totalGameLoops +=1;
movePlayer(gl);
...
}
11.3 当角色移动时制作动画
问题
当游戏角色移动时,它看起来不像是在行走。
解决办法
使用 spritesheet 动画使角色在移动时看起来像在行走。
它是如何工作的
这个解决方案将涉及到对您已经广泛使用的movePlayer()方法进行修改。在模型矩阵被转换后,转换纹理矩阵以显示 spritesheet 中的下一帧。
注意关于使用斜板的解决方案,参见第六章。
首先创建一个在所有类中都可见的作用域变量,该变量将用于跟踪 spritesheet 动画的当前帧。
public static float currentrunaniframe = 0f;
接下来,对movePlayer()方法进行加粗的修改(参见清单 11-8 和 11-9 )。
清单 11-8 。动画角色(OpenGL ES 1)
private void movePlayer(GL10gl){
if (totalGameLoops> 15)
{
PLAYER_RUN_SPEED += .5f;
}
currentrunaniframe += .25f;
if (currentrunaniframe> .75f)
{
currentrunaniframe = .0f;
}
switch(playeraction){
case PLAYER_MOVE_RIGHT:
playercurrentlocation += PLAYER_RUN_SPEED;
scrollBackground1(gl, playeraction);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentrunaniframe,.50f, 0.0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PLAYER_MOVE_LEFT:
playercurrentlocation -= PLAYER_RUN_SPEED;
scrollBackground1(gl, playeraction);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentrunaniframe,.75f, 0.0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PLAYER_STAND:
PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;
break;
}
}
清单 11-9 。制作角色动画(OpenGL ES 2/3)
private void movePlayer(GL10gl){
if (totalGameLoops> 15)
{
PLAYER_RUN_SPEED += .5f;
}
currentrunaniframe += .25f;
if (currentrunaniframe> .75f)
{
currentrunaniframe = .0f;
}
switch(playeraction){
case PLAYER_MOVE_RIGHT:
playercurrentlocation += PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0, playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix,currentrunaniframe, .50f );
break;
case PLAYER_MOVE_LEFT:
playercurrentlocation -= PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0, playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix,currentrunaniframe, .75f );
break;
case PLAYER_STAND:
PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;
break;
}
}
清单 11-8 中加粗的if语句基于四帧动画工作。如果 sprite 工作表中有四帧角色动画,则在四次循环后,动画需要重置为第一帧。if语句测试当前帧,并在到达第四帧时重置动画。
制作角色动画的关键(如果你在 OpenGL ES 2/3 中工作)是修改你的角色类的draw()方法,以传入你想要显示的 sprite sheet 图像的 x 和 y 位置(见第六章以获得完成这个的详细解决方案)。
最后,修改PLAYER_STAND案例,将动画从变为静态的“站立”图像。请记住,根据 spritesheet 的设置,该解决方案中显示的坐标可能需要更改(参见清单 11-10 和 11-11 )。
清单 11-10 。PLAYER_STAND (OpenGL 是 1)
case PLAYER_STAND:
PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playercurrentlocation, 0f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(.25f,.25f, 0.0f);
goodguy.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
清单 11-11 。PLAYER_STAND (OpenGL 是 2/3)
case PLAYER_STAND:
PLAYER_RUN_SPEED = .15f;
totalGameLoops = 0;
playercurrentlocation -= PLAYER_RUN_SPEED;
Matrix.translateM(mTMatrix, 0, 0, playercurrentlocation, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0)
character.draw(mMVPMatrix,.25f, .25f );
break;
十二、移动敌人
在屏幕上移动角色——无论是人、动物、机器人还是车辆——是引人注目的游戏中最重要的部分之一。如果你试图创造一个在游戏中自由移动的角色,你可能会遇到一些问题。
这一章将介绍帮助你在游戏中增加敌人的方法。本章的解决方案包括将敌人装载到游戏中预定的位置,并沿着特定的路径移动敌人。
12.1 将敌人装载到预定位置
问题
游戏没有在正确的位置装载敌人。
解决办法
使用一个类来确定敌人的产卵点在哪里。
它是如何工作的
许多游戏类型都有“繁殖点”,玩家可以在那里繁殖后代。要在这些预定的位置繁殖敌人,你需要在你的敌人职业中添加一些floats,然后使用这些floats将敌人的模型矩阵转换到繁殖位置。
本章的解决方案将基于一个基本的角色职业,而这个职业又是基于第七章和第八章中的SBGBackground职业。鉴于我们现在谈论的是游戏中的敌人,让我们重新命名这个职业SBGEnemy。该类的内容应该如清单 12-1 和清单 12-2 所示。
清单 12-1 。SBGEnemy() (OpenGL 是 1)
public class SBGEnemy {
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 SBGEnemy() {
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(GL10gl, 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);
}
}
清单 12-2 。SBGEnemy() (OpenGL 是 2/3)
public class SBGEnemy {
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_Position = uMVPMatrix * vPosition;" +
" TexCoordOut = TexCoordIn;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float texX;" +
"uniform float texY;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x +
texX,TexCoordOut.y + texY));"+
"}";
private float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};
private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer indexBuffer;
private final FloatBuffer textureBuffer;
private final int mProgram;
private int mPositionHandle;
private int mMVPMatrixHandle;
static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float vertices[] = { -1f, 1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f, 1f, 0.0f };
private final short indices[] = { 0, 1, 2, 0, 2, 3 };
private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;
public SBGEnemy() {
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);
int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);
mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
}
public void draw(float[] mvpMatrix, int texX, int texY, int[] spriteSheet) {
GLES20.glUseProgram(mProgram);
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
GLES20.glEnableVertexAttribArray(mPositionHandle);
int vsTextureCoord = GLES20.glGetAttribLocation(mProgram, "TexCoordIn");
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, spriteSheet[0]);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsTexX = GLES20.glGetUniformLocation(mProgram, "texX");
int fsTexY = GLES20.glGetUniformLocation(mProgram, "texY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsTexX, texX);
GLES20.glUniform1f(fsTexY, texY);
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, indexBuffer);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
然后修改这个类,添加两个 floats。一个浮动将跟踪 x 轴的繁殖位置,另一个浮动将跟踪 y 轴的繁殖位置(见清单 12-3 )。
清单 12-3 。追踪产卵位置的浮标
public class SBGEnemy {
public float posY = 0f;
public float posX = 0f;
...
}
在SBGEnemy构造函数中将这些浮点数设置到期望的产卵位置(见清单 12-4 )。
清单 12-4 。给位置浮动赋值
public class SBGEnemy {
public float posY = 0f;
public float posX = 0f;
...
public SBGEnemy() {
posX = .25;
posY = .25;
...
}
...
}
现在你可以使用 OpenGL ES 1 的glTranslatef()方法调用中的SBGEnemy.posX和SBGEnemy.posY在你绘制敌人的模型矩阵之前将它移动到产卵位置。您可以在 OpenGL ES 2/3 的Matrix.translateM()方法中使用相同的属性。在清单 12-5 和清单 12-6 中显示的spawnEnemy()方法,可以在你的游戏中创建,以帮助你在一个位置产生敌人。
清单 12-5 。spawnEnemy() (OpenGL 是 1)
private SFEnemy enemy = new SFEnemy();
spawnEnemy(){
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(enemy.posX, enemy.posY, 0f);
}
清单 12-6 。spawnEnemy() (OpenGL 是 2/3)
private SFEnemy enemy = new SFEnemy();
spawnEnemy(){
Matrix.translateM(mTMatrix, 0, enemy.posX, enemy.posY, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mTMatrix, 0, mMVPMatrix, 0);
}
12.2 将敌人装载到随机地点
问题
游戏需要在随机的地点产生敌人。
解决办法
修改最后一个解决方案,为产卵的敌人创造“随机”的位置。
它是如何工作的
许多游戏会在随机地点产生敌人。这增加了你游戏的难度,因为它剥夺了预先设定的产卵位置的可预测性。
上一个解决方案中的代码可以很容易地修改,以生成随机的产卵位置。清单 12-7 和 12-8 显示了应该进行的修改,以适应随机的产卵位置。
清单 12-7 。SBGEnemy()对于随机位置(OpenGL ES 1)
public class SBGEnemy {
public float posY = 0f;
public float posX = 0f;
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 SBGEnemy() {
Random randomPos = new Random();
posX = randomPos.nextFloat() * 3;
posY = randomPos.nextFloat() * 3;
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(GL10gl, 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);
}
}
清单 12-8 。SBGEnemy()对于随机位置(OpenGL ES 2/3)
public class SBGEnemy {
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_Position = uMVPMatrix * vPosition;" +
" TexCoordOut = TexCoordIn;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float texX;" +
"uniform float texY;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x +
texX,TexCoordOut.y + texY));"+
"}";
private float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};
private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer indexBuffer;
private final FloatBuffer textureBuffer;
private final int mProgram;
private int mPositionHandle;
private int mMVPMatrixHandle;
static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float vertices[] = { -1f, 1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f, 1f, 0.0f };
private final short indices[] = { 0, 1, 2, 0, 2, 3 };
private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;
public SBGEnemy() {
Random randomPos = new Random();
posX = randomPos.nextFloat() * 3;
posY = randomPos.nextFloat() * 3;
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);
int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);
mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
}
public void draw(float[] mvpMatrix, int texX, int texY, int[] spriteSheet) {
GLES20.glUseProgram(mProgram);
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
GLES20.glEnableVertexAttribArray(mPositionHandle);
int vsTextureCoord = GLES20.glGetAttribLocation(mProgram, "TexCoordIn");
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, spriteSheet[0]);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsTexX = GLES20.glGetUniformLocation(mProgram, "texX");
int fsTexY = GLES20.glGetUniformLocation(mProgram, "texY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsTexX, texX);
GLES20.glUniform1f(fsTexY, texY);
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, indexBuffer);
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
这个解决方案建立在您在前一个解决方案中创建的posX和posY属性的基础上。不再用静态值填充这些属性,SBGEnemy类的构造函数现在将随机填充位置到posX和posY浮动中。结果就是现在当你从spanEnemy()方法调用SBGEnemy.posX和SBGEnemy.posY的时候,敌人会在屏幕上的随机位置被创造出来。
12.3 沿着路径移动敌人
问题
敌人不会沿着预定的路径移动。
解决办法
使用算法为角色创建自动移动的路径。
它是如何工作的
这个解决方案是为了让你的敌人沿着一条特定的路径移动,这条路径被称为贝塞尔曲线。贝塞尔曲线通常在游戏中使用,因为它们可以通过一个相当简单的算法很容易地产生。他们也可以被修改来创造变化,使游戏变得有趣和不可预测。图 12-1 展示了贝赛尔曲线的样子。
图 12-1 。二次贝塞尔曲线
为了让敌人以二次贝塞尔曲线从屏幕顶部移动到底部,你需要两种方法。您可以创建一个方法来获取贝塞尔曲线上的下一个 x 轴值,并创建一个方法来给出贝塞尔曲线上的下一个 y 轴值。每次你调用这些方法时,你会得到 x 和 y 轴上的下一个特定敌人需要移动到的位置。一旦你有了这些位置,你使用glTranslatef()移动模型矩阵到计算的位置。
幸运的是,在贝塞尔曲线上画点相当简单。要构建一条二次贝塞尔曲线,你需要四个笛卡尔点:一个起点,一个终点,以及曲线环绕的两个曲线点。现在我们来回顾一下如何做到这一点。
创建八个新的浮点数来跟踪这些点的 x 和 y 坐标,如清单 12-9 所示。
清单 12-9 。贝塞尔跟踪坐标
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;
修改SBGEnemy类,给现有的posX和posY添加一个posT浮动,如清单 12-10 所示。
清单 12-10 。posT
public class SBGEnemy {
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
...
}
绘制点的关键值称为 t 位置。t 位置告诉公式您在曲线上的位置,从而允许公式计算该单个位置的 x 或 y 坐标。
提示如果你不理解以下公式背后的数学原理,有很多很好的资源,包括一个 wiki,可以找到贝塞尔曲线。
在你的SBGEnemy()类中创建两个方法(参见清单 12-11 )。一种方法用于获取下一个 x 轴值,一种方法用于获取下一个 y 轴值。此外,将随机值添加到posX和posY浮点中,并将一个设定值添加到posT中。
清单 12-11 。播种位置值
public class SBGEnemy {
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 SBGEnemy() {
posY = (randomPos.nextFloat() * 4) + 4;
posX = randomPos.nextFloat() * 3;
posT = .012;
}
public float getNextPosX(){
}
public float getNextPosY(){
}
}
在 y 轴上的二次贝塞尔曲线上寻找一个点的公式如下:
(y1*(??)) + (y2 * 3 * (??) * (1-t)) + (y3 * 3 * t * (1-t)2) + (y4* (1-t)3)
注意要得到 x 轴点,只需将前面等式中的 y 替换为 x 即可。
在你的getNextPosY() 中使用这个公式来计算你的敌人的位置(见清单 12-12 )。
清单 12-12 。getNextPosY()
public class SBGEnemy {
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 SBGEnemy() {
posY = (randomPos.nextFloat() * 4) + 4;
posX = randomPos.nextFloat() * 3;
posT = .012;
}
public float getNextPosX(){
}
public float getNextPosY(){
return (float)((BEZIER_Y_1*(posT*posT*posT)) +
(BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) +
(BEZIER_Y_3 * 3 * posT * ((1-posT) * (1-posT))) +
(BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}
}
对 x 轴使用同样的公式,稍作修改,如清单 12-13 所示。
清单 12-13 。getNextPosX()
public class SBGEnemy {
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 SBGEnemy() {
posY = (randomPos.nextFloat() * 4) + 4;
posX = randomPos.nextFloat() * 3;
posT = sfengine.SCOUT_SPEED;
}
public float getNextPosX(){
return (float)((BEZIER_X_4*(posT*posT*posT)) +
(BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) +
(BEZIER_X_2 * 3 * posT * ((1-posT) * (1-posT))) +
(BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT))));
}
public float getNextPosY(){
return (float)((BEZIER_Y_1*(posT*posT*posT)) +
(BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) +
(BEZIER_Y_3 * 3 * posT * ((1-posT) * (1-posT))) +
(BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));
}
}
注意,当计算 x 轴的右侧时,值是 x1、x2、x3,然后是 x4;然而,从左侧开始,这些点以相反的顺序使用,x4、x3、x2,然后是 x1。
现在,在游戏循环的每次执行中,将SBGEnemy.posX设置为SBGEnemy.getNextPosX()并将SBGEnemy.posY设置为SBGEnemy.getNextPosY(),然后将模型矩阵平移到posX和posY点,就像你一直在做的那样。