安卓游戏秘籍(四)
十三、在有障碍物情况下移动角色
如果游戏,尤其是平台游戏,以人物从左到右无阻碍地奔跑为特色,它们不会长久地吸引玩家的兴趣。
平台游戏,如超级马里奥兄弟,小行星 2,和无数其他包含障碍,玩家必须导航。在你的游戏中使用障碍物是增加刺激和阻止动作的好方法。然而,在编写游戏代码时,障碍也会增加复杂性。
这一章将会介绍一些你在游戏中遇到障碍时可能会遇到的情况。第一个场景包括让一个角色在平台之间跳跃。
13.1 平台之间的跳跃
问题
该游戏不允许玩家在游戏关卡的平台之间跳跃。
解决办法
使用预定的距离,以及数学公式来调整跳跃动画。
它是如何工作的
每个人都知道一个人跳起来是什么样子。跳跃有一种特殊的运动和流畅,这在游戏中很难复制。在这个解决方案中,我们将修改早期解决方案中的一些代码,为可玩角色创建一个跳跃动作。
第一步是创建一个供用户“跳转”的控件在解决方案 5.4 中,我向你展示了如何使用SimpleOnGestureListener的onFling()方法创建一个手势。修改代码以设置一个公共变量来指示玩家想要跳转。
GestureDetector.SimpleOnGestureListener gestureListener = new
GestureDetector.SimpleOnGestureListener(){
@Override
publicbooleanonFling(MotionEvente1, MotionEvente2, float velocityX,
floatvelocityY) {
playeraction = PLAYER_JUMPING;
}
};
playeraction和PLAYER_JUMPING变量是存储在项目可访问的类中的整数。
我们要跳跃的角色是SuperBanditGuy。在本书的前面,你创建了一个SuperBanditGuy类,它创建了游戏的主角并在屏幕上移动他。修改SuperBanditGuy类,添加两个浮点数(x和y),用于跟踪角色在跳跃过程中的 x 和 y 坐标。
public class SuperBanditGuy {
public float x = .75f;
public float y = .75f;
//I like to start characters a little higher than the bottom of the screen so
//that the player can see some ground under them
...
}
现在,在游戏循环中添加以下浮动,Renderer。
private float previousJumpPos = 0;
private float posJump = 0;
同样,在之前的解决方案中,我们在游戏循环中创建了一个case语句,允许您测试playeraction并相应地移动角色。现在让我们修改case语句来测试PLAYER_JUMPING ,然后开始计算跳跃。清单 13-1 和 13-2 (分别是 OpenGL ES 1 和 OpenGL ES 2/3)会让你的角色进行一次基本的跳跃。
清单 13-1 。PLAYER_JUMPING (OpenGL 是 1)
switch(playeraction){
case PLAYER_MOVE_RIGHT:
...
break;
case PLAYER_JUMPING:
previousJumpPos = posJump;
posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump<= Math.PI)
{
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.y = .75f;
}
}
goodguy.x += PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(goodguy.x, goodguy.y, 0f);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
...
}
清单 13-2 。PLAYER_JUMPING (OpenGL 是 2/3)
switch(playeraction){
case PLAYER_MOVE_RIGHT:
...
break;
case PLAYER_JUMPING:
previousJumpPos = posJump;
posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump<= Math.PI)
{
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.y = .75f;
}
}
goodguy.x += PLAYER_RUN_SPEED;
Matrix.translateM(RotationMatrix, 0, goodguy.x, goodguy.y, 0);
break;
...
}
无论 OpenGL ES 版本如何,以令人信服的跳跃动作移动角色的关键是以下公式:
posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump<= Math.PI)
{
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.y = .75f;
}
}
注意根据你的具体游戏,这个公式中有许多值你需要调整。这些值包括跳跃的高度和时间长度。
注意这个公式只作用于角色位置的 y 轴。x 轴位置将以角色确定的运行速度继续向左或向右移动。让我们检查这个公式的每一行。
previousJumpPos = posJump;
第一行设置了previousJumpPos ,供稍后在公式中使用。
posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
这条线描绘了正弦波上跳跃的位置。这并不用于直接确定角色在屏幕上的跳转位置。相反,它用于在内存中确定角色何时到达跳跃的顶点。
.5的值是跳跃的时间长度。虽然它不代表特定的时间单位,但它可以增加或减少,以创建更长或更短的持续跳跃。这条线的Math.PI/2或半圆周率部分简单地表示了这样一个事实,即我们开始的平面已经是正弦波的一半。
当你处理正弦波时,圆周率是周期波所需要的时间。所以我们这波的时机是半π对π。当正弦波上的当前位置小于π时,我们知道角色正处于上升到跳跃顶点的过程中。一旦角色的位置大于圆周率,我们就可以开始让它回到地面。下一行的目的是测试这种情况。
if (posJump<= Math.PI)
{
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
...
}
最后,在if语句中,第一个条件在 y 轴上向上移动字符,第二个条件向下移动字符。
goodguy.y += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
请注意,该角色的 y 轴位置会因公式而增加。这将在 y 轴上向上移动角色。与此同时,角色的 x 轴位置正在增加,就好像角色在 x 轴上朝着跳跃的方向正常移动一样。
下面一行将字符下移。
goodguy.y -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
此公式减少 y 轴位置的值,以将角色向下移动到地平面。
注意两个移动语句中的值 1.5 代表跳跃的高度。同样,您可以根据需要调整该值,以获得更高或更短的跳跃。
当跳转结束时,只需测试角色的位置是否再次位于 y 轴上的 0.75 处(添加到渲染器的 float 中定义的起始位置),然后退出跳转,如下所示。
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.y = .75f;
}
此代码的最佳位置是在正在减少 y 轴位置的if语句中。如果你把这段代码放在if语句之外,你就冒着让角色在一瞬间掉到地平面以下的风险。
最后,抽出角色,如列表 13-3 和 13-4 所示。
清单 13-3 。绘制角色 (OpenGL ES 1)
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(goodguy.x, goodguy.y, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
清单 13-4 。绘制角色(OpenGL ES 2/3)
Matrix.translateM(RotationMatrix, 0, goodguy.x, goodguy.y, 0);
这段代码可以很容易地添加到您的渲染器中,以产生跳跃运动。
13.2 向上移动台阶
问题
角色需要跳上或跳下台阶,或者不平坦平面上的其他物体。
解决办法
使用跳转解决方案的修改版本来导航步骤。
它是如何工作的
在第十五章中,我将介绍碰撞检测的解决方案。虽然本章中的解决方案确实提出了冲突检测的主题,但它没有后面将要介绍的解决方案那么深入。相反,这个解决方案将专门处理导航步骤所需的来自上一个解决方案的跳转代码的修改。
如果您正在向上跳,您将从来自清单 13-1 和 13-2 的相同代码开始。
需要做的修改是在if语句中,该语句测试角色是否通过跳跃下降足够远以再次到达地面。将测试值替换为台阶的高度。
if (goodguy.y<=<height of step>){
playeraction = PLAYER_STAND;
goodguy.y =<height of step>;
}
通过测试角色是否下降到台阶的高度,可以停止角色在台阶上的移动。这种解决方案要求您知道或测试台阶的高度。
这种解决方案适用于您可以在代码中预测级别布局的级别。例如,如果你使用瓷砖来建造你的关卡,你可以测试瓷砖地图来知道台阶在哪里,从而知道停止下降的高度。如果关卡的布局可以动态改变,这种解决方案就不那么好了。
如果你的关卡中有碎片或者可能有移动的平台,而你不能使用这种方法,请参考第十五章,其中有更多关于碰撞检测的内容。
十四、发射武器
许多游戏要求玩家向障碍物或敌人开火或投掷武器。如果你曾经试图发射武器,你可能会遇到让你的投射物以可预测的方式离开你的角色,并沿着设定的路径到达目标的问题。
武器可以有多种形状、大小和功能。在许多游戏情况下,子弹是直线行进的,导弹、激光和大多数其他推进武器也是如此。投掷的武器,如石头、手榴弹,甚至在一定程度上是箭,都遵循更抛物线的轨迹。不管你为武器选择了什么样的图像或动画,从 A 点到 B 点的数学方法都是一样的。
本章将介绍触发武器的“按钮”的多种解决方案,以及在屏幕上制作武器动画的多种解决方案。很像第十三章,这一章在 OpenGL ES 中并不像过去的一些章节那么沉重。当你需要一个使用武器的角色时,需要更多的外围编码。
我们要看的第一个配方将提供一种在屏幕上连接“发射按钮”的方法。这个按钮将基于以前的解决方案,给你一个方法来控制游戏中武器的发射。
在很多游戏场景中,你可能不需要开火按钮。相反,这些武器可以自动发射,甚至可以持续发射。不断开火的武器在 top/down shooters 等游戏中相当受欢迎。如果你打算使用武器自动开火的游戏类型,请随意跳过本章的第一个配方。
14.1 为“火灾”按钮接线
问题
玩家没有办法发射武器。玩家需要一个按钮——或屏幕上的互动区域—来发射角色的武器。
解决办法
在屏幕上创建一个互动空间,玩家可以点击它来触发武器的发射。这将在两个不同的解决方案中演示。
它是如何工作的
我将从两个方面着手解决这个问题。第一种是基于配方 5.3 中的先前解决方案,其中屏幕区域被分成触摸区。我们现在将这些区域中的一个专用于射击。如果玩家触摸屏幕的这个区域,将会设置发射武器的标志。
这种方法适用于某些情况;然而,如果游戏类型要求屏幕上有多个触摸区域,这可能会导致武器被无意中发射。因此,第二个解决方案也将被探索,玩家可以双击屏幕上的任何地方来发射武器。
解决方案 1
对于第一种解决方案,覆盖游戏活动的onTouchEvent() 。请记住,这不一定是主要的活动,尤其是当你的游戏以菜单开始的时候。当此事件检测到触摸时,设置PLAYER_FIRE_WEAPONS标志,如清单 14-1 所示。
清单 14-1 。onTouchEvent()
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
int height = outMetrics.heightPixels / 4;
int playableArea = outMetrics.heightPixels - height;
if (y >playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x <outMetrics.widthPixels / 2){
playeraction = PLAYER_FIRE_WEAPONS;
}
break;
}
}
return false;
}
在本书包含的许多解决方案中,您一直在使用playeraction int。这个int是在本书的前面建立的,作为当前动作的持有者。游戏循环包含一个case语句,它将读取这个int,并在playeraction = PLAYER_FIRE_WEAPONS时执行武器开火代码。
注意本解决方案中使用的display变量设置在游戏的主活动中。这是玩家启动游戏时开始的活动。因此,display变量设置如下:
display = ((WindowManager)
getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
如果这个解决方案不是你所需要的,你可以很容易地设置一个不同的解决方案,玩家可以双击屏幕上的任何地方来触发武器的发射。接下来我们将看看这个解决方案。
解决方案 2
要检测双击,需要实现GestureDetector 。清单 14-2 中的代码将允许玩家双击屏幕并发射武器。
清单 14-2 。使用GestureDetector的活动
public class SBGGameMain extends Activity {
private GestureDetector gd;
@Override
public void onCreate(Bundle savedInstanceState) {
...
gd = new GestureDetector(this,gestureListener);
}
@Override
protected void onResume() {
super.onResume();
gameView.onResume();
}
@Override
protected void onPause() {
super.onPause();
gameView.onPause();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
int height = outMetrics.heightPixels/4;
int playableArea = outMetrics.heightPixels - height;
if (y >playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x <outMetrics.widthPixels/2){
playeraction = PLAYER_MOVE_LEFT;
}else{
playeraction = PLAYER_MOVE_RIGHT;
}
break;
case MotionEvent.ACTION_UP:
playeraction = PLAYER_STAND;
break;
}
}
else {
return gd.onTouchEvent(event);
}
return false;
}
GestureDetector.SimpleOnGestureListener gestureListener = new
GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEvent arg0) {
//TODO Auto-generated method stub
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//TODO Auto-generated method stub
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, 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;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
playeraction = PLAYER_FIRE_WEAPONS;
return false;
};
};
}
解决方案 2 的关键是在您的活动中创建一个GestureDetector。然后建立一个新的SimpleOnGestureListener()并将事件从onTouchEvent()传递给它。然后SimpleOnGestureListener()将确定该事件是否是双击的结果,并将playeraction设置为PLAYER_FIRE_WEAPONS。
14.2 制作导弹动画
问题
当玩家发射武器时,抛射体应该离开角色并沿直线行进,直到击中目标或离开屏幕。
解决办法
创建一个新的导弹类,并使用 OpenGL ES 将它从角色移动到目标。
它是如何工作的
第一步是为你的武器创建一个新的职业。这个类,像本书中其他解决方案中创建的许多类一样,将为图像的纹理绘制正方形,然后将纹理映射到正方形中。绘制武器的新类看起来应该如清单 14-3 (OpenGL ES 1)和清单 14-4 (OpenGL ES 2/3)所示。
清单 14-3 。SBGWeapon() (OpenGL 是 1)
public class SBGWeapon {
public float posY = 0f;
public float posX = 0f;
public boolean shotFired = false;
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 SFWeapon() {
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[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);
}
public void loadTexture(GL10gl,int texture, Context context) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
Matrix flip = new Matrix();
flip.postScale(-1f, -1f);
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();
}
}
清单 14-4 。SBGWeapon() (OpenGL 是 2/3)
public class SBGWeapon {
public float posY = 0f;
public float posX = 0f;
public boolean shotFired = false;
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;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, TexCoordOut);" +
"}";
private float texture[] = {
0f, 0f,
1f, 0f,
1f, 1f,
0f, 1f,
};
private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int program;
private int positionHandle;
private int matrixHandle;
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 void loadTexture(int texture, Context context) {
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){
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
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();
}
public SBGWeapon() {
ByteBuffer byteBuff = ByteBuffer.allocateDirect(
byteBuff.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuff.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
byteBuff = ByteBuffer.allocateDirect(texture.length * 4);
byteBuff.order(ByteOrder.nativeOrder());
textureBuffer = byteBuff.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
ByteBuffer indexBuffer = ByteBuffer.allocateDirect(
indexBuffer.order(ByteOrder.nativeOrder());
drawListBuffer = indexBuffer.asShortBuffer();
drawListBuffer.put(indices);
drawListBuffer.position(0);
int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
}
public void draw(float[] matrix) {
GLES20.glUseProgram(program);
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
GLES20.glEnableVertexAttribArray(positionHandle);
int vsTextureCoord = GLES20.glGetAttribLocation(program, "TexCoordIn");
GLES20.glVertexAttribPointer(positionHandle, 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, textures[0]);
intfsTexture = GLES20.glGetUniformLocation(program, "TexCoordOut");
GLES20.glUniform1i(fsTexture, 0);
matrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");
GLES20.glUniformMatrix4fv(matrixHandle, 1, false, matrix, 0);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
GLES20.glDisableVertexAttribArray(positionHandle);
}
}
除了 OpenGL ES 所要求的特性之外,SBGWeapon()类还包含三个关键特性。两个变量(x和y)用于通过游戏循环跟踪武器的轴上坐标。shotFired变量用于确定SBGWeapon的具体实例化是否已经被触发,是应该被绘制在屏幕上还是被忽略。
为什么要用一个布尔值来表示是否开枪?玩家在游戏中快速连续射击多次是很常见的。这意味着,在任何时候,你的游戏都必须在游戏循环的一次迭代中跟踪许多镜头。通过使用shotFired boolean,你可以确定内存中的哪些SBGWeapons已经被触发,哪些正在等待被抽取。
接下来的计划是在您的Renderer类中实例化一个SBGWeapon()。然后,当您检测到PLAYER_FIRE_WEAPON时,在游戏循环的每次迭代中绘制SBGWeapon()并沿直线移动它,直到SBGWeapon()击中目标或到达屏幕的末端。
在Renderer类中,实例化一个由SBGWeapon组成的数组。在清单 14-5 和清单 14-6 中,我将使用一个由四枚导弹组成的数组,这意味着一次只能有四枚导弹同时出现在屏幕上。
private SBGWeapon[] playerFire = new SBGWeapon[4];
不要忘记为你发射的任何武器的图像加载纹理。纹理被加载到Renderer的onSurfaceCreated()方法中(参见清单 14-5 和清单 14-6 )。
清单 14-5 。加载纹理(OpenGL ES 1)
for(int x = 0; x<4; x++){
playerFire[x].loadTexture(gl,R.drawable.weapon, context);
}
清单 14-6 。加载纹理(OpenGL ES 2/3)
for(int x = 0; x<4; x++){
playerFire[x].loadTexture(R.drawable.weapon, context);
}
最后,创建一个可以从游戏循环中调用的新方法。在本书的许多解决方案中,我引用了一个作用于playerAction的case语句。在这个语句中添加一个新的case来测试playerAction = PLAYER_FIRE_WEAPON。如果PLAYER_FIRE_WEAPON被检测到,调用你的新方法将武器绘制到屏幕上(见清单 14-7 和 14-8 )。
清单 14-7 。firePlayerWeapon() (OpenGL 是 1)
private void firePlayerWeapon(GL10gl){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){
int nextShot = 0;
if (playerFire[x].posY> 4.25){ //represents the top of the screen
playerFire[x].shotFired = false;
}else{
if (playerFire[x].posY> 2){
if (x == 3){//since we only have 4 should, recycle any that are no longer in use
nextShot = 0;
}else{
nextShot = x + 1;
}
if (playerFire[nextShot].shotFired == false){
playerFire[nextShot].shotFired = true;
//set the weapon x to the x of the character when it was fired
playerFire[nextShot].posX = player.x;
playerFire[nextShot].posY = 1.25f;
}
}
playerFire[x].posY += .12f; //the speed of the shot as it moves
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playerFire[x].posX, playerFire[x].posY, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
playerFire[x].draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
}
}
}
}
清单 14-8 。firePlayerWeapon() (OpenGL 是 2/3)
private void firePlayerWeapon(GL10 unused, float[] rotationMatrix, float[] matrix){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){
int nextShot = 0;
if (playerFire[x].posY> 4.25){ //represents the top of the screen
playerFire[x].shotFired = false;
}else{
if (playerFire[x].posY> 2){
if (x == 3){//since we only have 4 should, recycle any that are no longer in use
nextShot = 0;
}else{
nextShot = x + 1;
}
if (playerFire[nextShot].shotFired == false){
playerFire[nextShot].shotFired = true;
//set the weapon x to the x of the character when it was fired
playerFire[nextShot].posX = player.x;
playerFire[nextShot].posY = 1.25f;
}
}
playerFire[x].posY += .12f; //the speed of the shot as it moves
Matrix.translateM(RotationMatrix, 0, playerFire[x].posX, playerFire[x].posY, 0);
playerFire[x].draw(matrix);
Matrix.multiplyMM(matrix, 0, rotationMatrix, 0, matrix, 0);
}
}
}
}
这个方法将从角色的位置发射一个镜头,一直向上直到它碰到屏幕的顶部边缘。修改SBGWeapon()的x和y值的赋值,使镜头向不同方向移动。通过增加或减少x值,您的镜头将向右或向左移动;通过增加或减少y值,你的镜头会上下移动。
在第十五章中,你将会看到实现碰撞检测的解决方案。碰撞检测是当你的镜头击中目标时采取行动的关键,而不是简单地让你的镜头离开屏幕边缘。
在下一个解决方案中,您将修改firePlayerWeapon()方法,以抛物线运动方式移动镜头,就好像是投掷而不是直线拍摄一样。
14.3 制作投掷武器的动画
问题
武器不会像投掷武器那样沿弧线飞行。
解决办法
使用一个公式,就像跳跃时使用的公式,来确定一个弯曲的轨迹。
它是如何工作的
要像投掷一样以弧形运动移动您的镜头,您需要修改firePlayerWeapon()方法。我们将使用《??》第十三章中让角色跳跃的相同数学公式,并将其放入firePlayerWeapons()公式中。这显示在清单 14-9 和清单 14-10 中。
清单 14-9 。拱形轨迹(OpenGL ES 1)
private void firePlayerWeapon(GL10gl){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){
int nextShot = 0;
previousArcPos = arcJump;
arcJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (arcJump<= Math.PI)
{
playerFire[x].posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
playerFire[x].posY -=(Math.sin((double)posArc) - Math.sin((double)previousArcPos))* 1.5;
if (playerFire[x].posY<= .75f){
playerFire[x].shotFired = false;
playerFire[x].posY = .75f;
}else{
if (x == 3){//since we only have 4 should, recycle any that are no longer in use
nextShot = 0;
}else{
nextShot = x + 1;
}
}
if (playerFire[nextShot].shotFired == false){
playerFire[nextShot].shotFired = true;
playerFire[nextShot].posX = player.x;
playerFire[nextShot].posY = player.y;
}
}
playerFire[x].posx += .12f;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glTranslatef(playerFire[x].posX, playerFire[x].posY, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
playerFire[x].draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
}
}
}
}
清单 14-10 。拱形轨迹(OpenGL ES 2/3)
private void firePlayerWeapon(GL10 unused, float[] rotationMatrix, float[] matrix){
for(int x = 0; x < 4; x++ ){
if (playerFire[x].shotFired){
int nextShot = 0;
previousArcPos = arcJump;
arcJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (arcJump<= Math.PI)
{
playerFire[x].posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
playerFire[x].posY -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (playerFire[x].posY<= .75f){
playerFire[x].shotFired = false;
playerFire[x].posY = .75f;
}else{
if (x == 3){//since we only have 4 should, recycle any that are no longer in use
nextShot = 0;
}else{
nextShot = x + 1;
}
}
if (playerFire[nextShot].shotFired == false){
playerFire[nextShot].shotFired = true;
playerFire[nextShot].posX = player.x;
playerFire[nextShot].posY = player.y;
}
}
playerFire[x].posx += .12f;
Matrix.translateM(RotationMatrix, 0, playerFire[x].posX, playerFire[x].posY, 0);
playerFire[x].draw(matrix);
Matrix.multiplyMM(matrix, 0, rotationMatrix, 0, matrix, 0);
}
}
}
}
通过这个小小的改动,你可以让你的武器产生一个抛出的弧线,而不是已经发射的射弹的直线。
摘要
在第十三章中,你回顾了允许你在游戏中添加敌人的食谱。然而,如果玩家没有办法保护自己,在游戏中加入敌人是不公平的。本章中的食谱帮助你为玩家提供了一种发射武器的方法。
十五、碰撞检测
碰撞检测是几乎所有游戏和所有游戏类型的关键组件。在一个没有碰撞检测的游戏中,物品、障碍物、角色和武器会在屏幕上四处移动,彼此漂浮而过,不会产生任何后果。
您的游戏代码需要能够确定屏幕上的对象是否相互接触或交叉。只有在你确定两个或更多的物体接触后,你才能对它们执行动作,比如施加伤害、停止运动、启动角色或摧毁一个物体。
本章将介绍一些解决碰撞检测问题的方法。碰撞检测可能很棘手,但是本章中的解决方案应该有助于使这个过程变得简单一些。
15.1 探测障碍物
问题
游戏角色可以通过屏幕上的物体来阻止他们。
解决办法
使用基本的碰撞检测来确定角色是否接触了障碍物或屏幕边缘。
它是如何工作的
如果您正在创建一个角色面临静态障碍物(如地板和平台、屏幕边缘或台阶)的游戏,则基本碰撞检测非常有用。测试静态对象的位置时,可以使用常数值。例如,在制作角色跳跃的配方 13.1 和 13.2 中,我使用了基本的碰撞检测来确定角色何时完成跳跃并回到地面,如清单 15-1 (OpenGL ES 1)和 15-2 (OpenGL ES 2/3)所示。
清单 15-1 。基本跳跃碰撞检测(OpenGL ES 1)
previousJumpPos = posJump;
posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump <= Math.PI)
{
goodguy.posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy. posY -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.posY<= .75f){
playeraction = PLAYER_STAND;
goodguy.posY= .75f;
}
}
goodguy. posX += PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(goodguy. posX, goodguy. posY, 0);
gl.glPopMatrix();
gl.glLoadIdentity();
清单 15-2 。基本跳跃碰撞检测(OpenGL ES 2/3)
previousJumpPos = posJump;
posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump <= Math.PI)
{
goodguy. posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy. posY -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.posY= .75f;
}
}
goodguy. posX += PLAYER_RUN_SPEED;
Matrix.translateM(RotationMatrix, 0, goodguy. posX, goodguy. posY, 0);
在列表 15-1 和列表 15-2 中加粗的代码说明了在测试角色的 y 位置已经到达地面的水平时,如何使用常数. 75。因为我们知道游戏的地面总是在 y 轴的 0.75 处,所以这种简单的碰撞检测是有效的。
从屏幕边缘跑出来怎么办?如果您的游戏动作需要包含在一个屏幕中,并且 OpenGL ES 中的 x 轴已经被缩放到从 0(最左边)到 4(最右边)的范围内,您可以测试您的角色来阻止图像离开屏幕。
if(goodguy.posX<= 0 )
{
//the player has reached the left edge of the screen
goodguy. posX = 0; //correct the image's position and perform whatever action is necessary
}
如果您要测试与屏幕右边缘的碰撞,这个过程需要一个额外的步骤。OpenGL ES 中字符的 x 位置代表图像的左下角。因此,如果您正在测试字符的图像是否遇到了屏幕的右侧,则在整个图像已经离开屏幕之前,字符在左下角的 x 位置不会到达屏幕的右侧。
您可以通过将角色图像的大小添加到测试碰撞的if语句中来对此进行补偿。
if(goodguy. posX +.25f>= 4 )
{
//the player has reached the right edge of the screen
goodguy. posX = (4f - .25f); //correct the image's position and
//perform whatever action is necessary
}
碰撞检测的基本方法对于不太复杂的游戏逻辑是有效的,其中有许多静态对象,其大小和位置对于游戏循环来说是容易知道的。
如果你的游戏逻辑没那么简单怎么办?下一个解决方案帮助您检测正在移动且位置不可预测的对象之间的碰撞。
15.2 检测多个移动物体之间的碰撞
问题
该游戏需要检测两个或多个移动物体是否发生了碰撞。
解决办法
使用循环方法测试所有 OpenGL 图像边缘的碰撞。
它是如何工作的
要实现更健壮的碰撞检测,创建一个可以从游戏循环中调用的新方法。该方法将遍历屏幕上的所有活动项目,并确定是否有任何碰撞。
实现这种碰撞检测所需的关键字段是对象当前位置的 x 轴和 y 轴坐标,以及对象的状态。对象的状态是指该对象是否有资格被包括在碰撞检测中。这可以包括对象已经被破坏的标志,或者可能被测试的角色已经完成了一项成就,允许他们在特定的时间段内免于碰撞检测。
清单 15-3 和 15-4 描述了游戏中一个角色的职业。该类中添加了三个公共值:x 轴和 y 轴坐标各一个,用于跟踪角色的当前位置,还有一个布尔值,用于指示角色是否已经被破坏。
清单 15-3 。SBGEnemy() (OpenGL 是 1)
public class SBGEnemy {
public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBufferi ndexBuffer;
private float vertices[] = {
0.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 1.0, 0.0,
0.0, 1.0, 0.0,
};
private float texture[] = {
0.0, 0.0,
0.25f, 0.0,
0.25f, 0.25f,
0.0, 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[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);
}
public void loadTexture(GL10gl,int texture, Context context) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
Matrix flip = new Matrix();
flip.postScale(-1f, -1f);
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();
}
}
清单 15-4 。SBGEnemy() (OpenGL 是 2/3)
public class SBGEnemy {
public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;
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;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, TexCoordOut);" +
"}";
private float texture[] = {
0, 0,
1f, 0,
1f, 1f,
0, 1f,
};
private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int program;
private int positionHandle;
private int matrixHandle;
static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float vertices[] = { -1f, 1f, 0.0,
-1f, -1f, 0.0,
1f, -1f, 0.0,
1f, 1f, 0.0 };
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 void loadTexture(int texture, Context context) {
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){
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
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();
}
public SBGEnemy () {
ByteBuffer byteBuff = ByteBuffer.allocateDirect(
byteBuff.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuff.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
byteBuff = ByteBuffer.allocateDirect(texture.length * 4);
byteBuff.order(ByteOrder.nativeOrder());
textureBuffer = byteBuff.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
ByteBuffer indexBuffer = ByteBuffer.allocateDirect(
indexBuffer.order(ByteOrder.nativeOrder());
drawListBuffer = indexBuffer.asShortBuffer();
drawListBuffer.put(indices);
drawListBuffer.position(0);
int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
}
public void draw(float[] matrix) {
GLES20.glUseProgram(program);
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
GLES20.glEnableVertexAttribArray(positionHandle);
int vsTextureCoord = GLES20.glGetAttribLocation(program, "TexCoordIn");
GLES20.glVertexAttribPointer(positionHandle, 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, textures[0]);
int fsTexture = GLES20.glGetUniformLocation(program, "TexCoordOut");
GLES20.glUniform1i(fsTexture, 0);
matrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");
GLES20.glUniformMatrix4fv(matrixHandle, 1, false, matrix, 0);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
GLES20.glDisableVertexAttribArray(positionHandle);
}
}
现在,构建一个可以从游戏循环中调用的新类。在第十四章中,玩家可以使用武器。发射的武器排成一列,允许四发子弹同时出现在屏幕上。我们将在此基础上循环四个镜头中的每一个,检查它们是否被主动发射,然后检查它们是否与前面代码清单中的敌人角色发生碰撞。
完成碰撞测试最简单的方法是在每个活动对象周围创建一个边界框(在内存中),然后测试任何两个对象的边界框的边缘是否碰撞。为什么是边框?测试直线(如盒子边缘)比计算非常复杂形状的真实边缘更容易。此外,游戏中的物体通常会碰撞得如此之快,以至于眼睛无法察觉到碰撞发生在距离实际物体的可见边界不到一毫米的地方。
通过将大小(以坐标表示)添加到对象的当前 x 和 y 坐标位置来创建边界框。这意味着在坐标轴上缩放到 0.25 平方的对象将有一个从 x 到(x + .25)和从 y 到(y + .25)的边界框。任何进入那个空间的东西都会与那个物体相撞。在本例中,要测试碰撞,只需检查另一个对象的边界框是否包含介于(x 到(x + .25))和(y 到(y + .25))之间的点。如果是这样,那两个物体就相撞了。
在清单 15-5 中,正在发射的子弹有. 25 坐标值包围盒,敌人有 1 坐标值包围盒。
清单 15-5 。检测边界框
private void detectCollisions(){
for (int y = 1; y < 4; y ++){ //loop through the 4 potential shots in the array
if (playerFire[y].shotFired){ //only test the shots that are currently active
if(!enemy.isDestroyed){
//only test the shot against the enemy if it is not already destroyed
//test for the collision
if (((playerFire[y].posY >= enemy.posY
&& playerFire[y].posY <= enemy.posY + 1f ) ||
(playerFire[y].posY +.25f>= enemy.posY
&& playerFire[y].posY + .25f<= enemy.posY + 1f )) &&
((playerFire[y].posX>= enemy.posX
&& playerFire[y].posX<= enemy.posX + 1f) ||
(playerFire[y].posX + .25f>= enemy.posX
&& playerFire[y].posX + 25f<= enemy.posX + 1f ))){
//collision detected between enemy and a shot
}
}
}
}
}
这种方法在检测一轮射击和单个敌人之间的碰撞时效果很好。为了测试一轮射击和许多敌人之间的碰撞,你需要稍微修改方法来循环通过你的敌人阵列(见清单 15-6 )。
清单 15-6 。穿过敌人
private void detectCollisions(){
for (int y = 1; y < 4; y ++){
if (playerFire[y].shotFired){
for (int x = 1; x < 10; x++ ){ //assumes you have an array of 10 enemies
if(!enemies[x].isDestroyed){
if (((playerFire[y].posY >= enemies[x].posY
&& playerFire[y].posY <= enemies[x].posY + 1f ) ||
(playerFire[y].posY +.25f>= enemies[x].posY
&& playerFire[y].posY + .25f<= enemies[x].posY + 1f ))
&& ((playerFire[y].posX>= enemies[x].posX
&& playerFire[y].posX<= enemies[x].posX + 1f) ||
(playerFire[y].posX + .25f>= enemies[x].posX
&& playerFire[y].posX + 25f<= enemies[x].posX + 1f ))){
//collision detected between enemy and a shot
}
}
}
}
}
}
这种碰撞检测方法将帮助您测试游戏中多个对象的边界框之间的碰撞。一旦检测到冲突,您就可以在注释区域中处理该冲突。你可能想要采取的一个行动是改变一个物体的轨迹——就像一个球从墙上弹回一样。
下一个食谱将包括改变一个物体的轨迹。
15.3 改变物体轨迹
问题
一个游戏对象,比如一个球,当它撞到墙上时不会改变方向。
解决办法
当一个对象与另一个对象碰撞时,使用碰撞检测来改变其轨迹。
它是如何工作的
有些游戏类型中的物体在与其他物体碰撞时不一定会停止或爆炸。一些游戏,如突破式砖块击碎器,包含碰撞时相互弹开的物体。
本解决方案中对detectCollisions()方法的修改有助于您检测两个对象(在本例中是一个球和一块砖)之间的碰撞,并在接触时改变球的轨迹。
清单 15-7 中的代码直接来自我写的一个旧的砸砖游戏,我在代码中留下了循环砖块来帮助你。在这个示例中,ball是一个Ball()类的实例化,它与我们在本章前面看到的SBGEnemy()类相同。另外,wall是一个包含行集合的类。行则是实例化砖块的集合,砖块的类也与SBGEnemy()相同。这为玩家创造了一堵由一排排砖块组成的墙。
最后,清单 15-7 不仅检查球和砖块之间的碰撞,还检查球和屏幕的边缘。如果球碰到屏幕的边缘,它会弹开,导致轨迹改变,并保持球在比赛中。
清单 15-7 。detectCollisions()
private void detectCollisions(){
if(ball.posY<= 0){
}
for (int x = 0; x <wall.rows.length; x++)
{ //cycle through each brick and see if the ball has collided with it
for(int y = 0; y <wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
if (((ball.posY>wall.rows[x].bricks[y].posY - .25f) //the height of the brick is .25
&& (ball.posY<wall.rows[x].bricks[y].posY)
&& (ball.posX + .25f>wall.rows[x].bricks[y].posX)
&& (ball.posX<wall.rows[x].bricks[y].posX + 1.50))) //the legnthof the brick
{ //there is a collision, destroy the brick and change the trajectory
//of the ball
wall.rows[x].bricks[y].isDestroyed = true;
//change the trajectory by inverting the y axis
ballTargetY = ballTargetY * -1f;
//if the ball was originally moving to the left when it collided, move it to
//the right after the bounce - otherwise move it to the left
if(ballTargetX == -2f){
ballTargetX = 5f;
}else{
ballTargetX = -2f;
}
}
}
}
}
//Now check for collisions with the player's "paddle" and bounce the ball off accordingly
if((ball.posY - .25f<= .5f)
&& (ball.posX + .25f>player.PosX ) //the paddle has the same dimensions as a brick,
//keep it simple
&& (ball.posX<player.PosX + 1.50)){
//collision detected, change the Y trajectory of the ball, and the direction on the x axis
ballTargetY = ballTargetY * -1f;
if(ballTargetX == -2f){
ballTargetX = 5f;
}else{
ballTargetX = -2f;
}
}
//check for collision with edge of the screeen, change the x axis trajectory on impact
if(ball.posX< 0 || ball.posX + .25f>3.75f)
{
ballTargetX = ballTargetX * -1f;
}
}
15.4 碰撞时损坏物体,并清除损坏的物体
问题
游戏不会在碰撞后“损坏”物体。同样,一旦物体被损坏,它们在游戏中仍然可见。
解决办法
使用一个类来跟踪对象损坏并移除被破坏的对象。
它是如何工作的
在配方 15.2 中,设置了一个isDestroyed标志来表示该物体与另一个物体发生了碰撞,应该被摧毁,从而将其从游戏中移除。这是跟踪对象是否被破坏的一种方式。但是,如果你想创建一个系统,让一个物体在被摧毁之前可以被击中(碰撞)多次呢?
修改 objects 类。引用清单 15-1 中的SBGEnemy()类来包含一个damageCounter。
public class SBGEnemy {
public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;
public int damageCounter = 0;
...
}
现在,碰撞时伤害计数器加 1。如果计数器达到预定阈值,则设置isDestroyed标志。
private void detectCollisions(){
...
//collision detected
character.damageCounter += 1;
if(character.damageCounter == 3){
character.isDestroyed = true;
}
...
}
随着角色“被摧毁”,最后一步是从屏幕上删除它。最简单的方法就是不画它。在你的游戏循环中,在绘制之前测试一个角色或物体是否被破坏。
if(!character.isDestroyed){
character.draw(gl);
}
十六、得分
到目前为止,本书已经解决了许多与 Android 游戏开发相关的问题。从移动角色到碰撞检测,你的游戏应该进展顺利。然而,一个基本的问题还没有解决:你如何保持得分?
得分是大多数游戏不可或缺的一部分。没有分数,玩家将无法确定他们在游戏中的进展如何,也无法将他们的进展与其他玩家进行比较。从最早的电子游戏开始,分数一直是许多玩家吹嘘的中心。
在这一章中,我将介绍一些与游戏中记分相关的常见问题的解决方案。解决方案应该适用于大多数游戏类型。
16.1 给对象分配点值
问题
该游戏不会因为玩家破坏物品而给其评分。
解决办法
修改对象的类,为其分配分数。
它是如何工作的
这个解决方案包括给对象分配点值,并使用这些值作为玩家的分数。在游戏中给一个对象赋值非常容易,只需要修改对象的类。
在游戏中,物体可以是任何你想要赋值的东西。例如,敌人、易碎物品和屏幕上的目标(如关卡中的路点)都可以被分配一个分值,然后用于计算玩家的分数。
给敌人分配一个点值,修改敌人的职业增加一个名为pointValue的属性。在这个例子中,我们将修改在本书中多次使用的SBGEnemy()类,并给它赋值 3。
public class SBGEnemy {
public boolean isDead = false;
public int pointValue = 3;
...
}
同样的解决方案可以应用到你游戏中的所有职业,给每个职业分配一个分值,这个分值可以用于游戏的总分数。
这个解决方案的另一种实现方式是以毕业的形式。例如,我们可以使用一个类来创建多个对象,每个对象都有不同的点值。看一看下面的类。这个类取自我写的一个突破式游戏,其中一个砖块类被用来创建五种不同的砖块。
类是用 OpenGL ES 1 写的;然而,OpenGL ES 代码对于解决方案并不重要。如果你的游戏是在 OpenGL ES 2/3 中,不要担心,因为对这个类的修改不是特定于 OpenGL ES 版本的,可以很容易地跟随。
清单 16-1 。PBBrick()
public class PBBrick {
public float posY = 0f;
public float posX = 0f;
public float posT = 0f;
public boolean isDestroyed = false;
public int brickType = 0;
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, .25f, 0.0f,
0.0f, .25f, 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 PBBrick(int type) {
brickType = type;
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);
}
}
让我们修改这个类,以便为五种不同类型的砖块分配一个从 1 到 5 的不同分值。
清单 16-2 。PBBrick()修改
public class PBBrick {
public float posY = 0f;
public float posX = 0f;
public float posT = 0f;
public boolean isDestroyed = false;
public int brickType = 0;
public int pointValue = 0;
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, .25f, 0.0f,
0.0f, .25f, 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 PBBrick(int type) {
brickType = type;
switch(type){
case 1:
pointValue = 1;
break;
case 2:
pointValue = 2;
break;
case 3:
pointValue = 3;
break;
case 4:
pointValue = 4;
break;
case 5:
pointValue = 5;
break;
}
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);
}
...
}
在配方 16.2 中,你将获得分配给游戏对象的分值,并使用它们来创建玩家的分数。
16.2 添加并跟踪分数
问题
游戏不跟踪玩家的分数,即使每个对象都被分配了一个点值。
解决办法
使用游戏角色的职业来追踪整体分数。
它是如何工作的
在这个解决方案中,您将修改玩家角色的类来添加一个属性。该属性将用于跟踪玩家的总得分。修改玩家角色类后,您将修改碰撞检测方法,以便为新的分数属性指定正确的点值。
首先,修改玩家角色类,添加一个名为overallScore的新属性。
public class SuperBanditGuy {
public boolean isDead = false;
public int overallScore = 0;
...
}
在第十五章中,你创建了一个执行碰撞检测的方法。由于此解决方案假设奖励点数的基础是某种碰撞(例如,破坏一个对象),因此您将修改碰撞检测方法以在必要时指定点数。
清单 16-3 。detectCollisions()
private void detectCollisions(){
for (int y = 1; y <4; y ++){
if (playerFire[y].shotFired){
for (int x = 1; x <10; x++ ){ //assumes you have an array of 10 enemies
if(!enemies[x].isDestroyed){
if (((playerFire[y].posY>= enemies[x].posY
&& playerFire[y].posY<= enemies[x].posY + 1f ) ||
(playerFire[y].posY +.25f>= enemies[x].posY
&& playerFire[y].posY + .25f<= enemies[x].posY + 1f )) &&
((playerFire[y].posX>= enemies[x].posX
&& playerFire[y].posX<= enemies[x].posX + 1f) ||
(playerFire[y].posX + .25f>= enemies[x].posX
&& playerFire[y].posX + 25f<= enemies[x].posX + 1f ))){
//collision detected between enemy and a shot
goodguy.overallScore += enemies[x].pointValue;
}
}
}
}
}
}
如前所述,这个方法摘自《??》第十五章。这是一个跟踪十个不同敌人碰撞的基本方法。enemies[]数组是一个由SBGEnemy()类组成的数组。方法中的goodguy只是SuperBanditGuy()类的一个实例化。
使用这个解决方案,玩家角色的总分数将会在每次消灭一个敌人时增加。
16.3 将分数写入屏幕
问题
游戏不会在屏幕上显示玩家的分数。
解决办法
使用多个 OpenGL 形状和一个 sprite 表向用户显示分数。
它是如何工作的
若要跟踪乐谱,请在项目中添加一个新的 sprite 表单,其中包含用于显示乐谱的所有数字。在图 16-1 的中可以看到这个斜桅板。
图 16-1 。分数位数 spritesheet
接下来,创建一个名为SBGScoreTile() 的新类。这个类将被用于在 OpenGL ES 1 和 OpenGL ES 2/3 中向屏幕显示一个纵向的分数块(参见清单 16-4 和清单 16-5 )。稍后,您将使用 sprite 工作表在图块上显示特定的分数数字。
清单 16-4 。SBGScoreTile() (OpenGL 是 1)
public class SBGScoreTile {
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;
private float vertices[] = {
0.0f, 0.0f, 0.0f,
0.25f, 0.0f, 0.0f,
0.25f, 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 SBGScoreTile() {
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) {
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);
}
}
清单 16-5 。SBGScoreTile() (OpenGL 是 2/3)
public class SBGScoreTile {
public float scoreX = 0;
public float scoreY = 0;
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 scoreX;" +
"uniform float scoreY;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x +
scoreX,TexCoordOut.y + scoreY));"+
"}";
private float texture[] = {
0f, 0f,
1f, 0f,
1f, 1f,
0f, 1f,
};
private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int program;
private int positionHandle;
private int matrixHandle;
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 void loadTexture(int texture, Context context) {
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){
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
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();
}
public SBGScoreTile() {
ByteBuffer byteBuff = ByteBuffer.allocateDirect(
byteBuff.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuff.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
byteBuff = ByteBuffer.allocateDirect(texture.length * 4);
byteBuff.order(ByteOrder.nativeOrder());
textureBuffer = byteBuff.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
ByteBuffer indexBuffer = ByteBuffer.allocateDirect(
indexBuffer.order(ByteOrder.nativeOrder());
drawListBuffer = indexBuffer.asShortBuffer();
drawListBuffer.put(indices);
drawListBuffer.position(0);
int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
}
public void draw(float[] matrix) {
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, textures[0]);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsScoreX = GLES20.glGetUniformLocation(mProgram, "scoreX");
int fsScoreY = GLES20.glGetUniformLocation(mProgram, "scoreY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsScoreX, scoreX);
GLES20.glUniform1f(fsScoreY, scoreY);
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);
}
}
绘制时,每个图块应默认为 0。这是在清单 16-6 中完成的,在 OpenGL ES 1 中对纹理矩阵中的坐标 0,0,0 执行glTranslatef(),并在 OpenGL ES 2/3 中间接将片段着色器的TexCoordOut.x设置为 0(关于 sprite sheets 如何工作的更详细信息,请参见第六章“加载 Sprite Sheet”)。
清单 16-6 。绘制图块(OpenGL ES 1)
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f , 0.0f);
清单 16-7 。绘制图块(OpenGL ES 2/3)
SBGScoreTile.scoreX = 0;
SBGScoreTile.scoreY = 0;
只需将精灵表推进到分数的正确数字。首先,创建一个switch...case语句来设置每个相应数字的 x 和 y 子画面坐标位置。
清单 16-8 。平铺switch语句
switch(SuperBanditGuy){
case 0:
x = 0;
y = 0;
break;
case 1:
x = 0;
y = .25;
break;
case 2:
x = 0;
y = .50;
break;
case 3:
x = 0;
y = .75;
break;
...
}
最后,使用在switch语句中设置的 x 和 y 坐标来显示正确的图块。
清单 16-9 。显示图块(OpenGL ES 1)
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(x, y ,0.0f);
清单 16-10 。显示图块(OpenGL ES 2/3)
SBGScoreTile.scoreX = x;
SBGScoreTile.scoreY = y;