六、添加敌人

作为一名 Android 游戏开发者,你的技能范围越来越广。仅在前一章中,您就添加了您的第一个可玩角色,处理了精灵动画,并创建了一个基本的侦听器来允许玩家控制角色;对于一个基本的 2-D 射击游戏,你的游戏真的正在成形。

在这一章中,你将创建一个类来帮助你管理你的纹理。你还将创建一个敌人职业,用于在星际战士中创建三种不同类型的敌人。在下一章,你将为这些敌人创建一个基本的人工智能系统。

中局看家

记住,这本书的目的是帮助你从头到尾完成一个游戏的创作过程。游戏创作并不总是一个线性的过程。有时候,你需要回头重新评估你所做的事情,以优化你的游戏工作方式。

前两章主要教你如何加载和处理精灵和精灵表。但是,使用当前的代码,您将为每个角色加载一个单独的 sprite 表。这是学习如何使用 sprite sheet 的最简单的方法,但绝不是使用sprite sheet 的最好方法。事实上,为每个角色创建一个单独的 sprite 工作表几乎违背了 sprite 工作表的目的——也就是说,你应该将所有角色的所有图像加载到一个 sprite 工作表中。

提示:当然,如果你有太多的精灵而不适合一张图片,你仍然可以使用多个精灵表。但这应该不是这个游戏中有限的角色数量的问题。

通过将游戏中所有角色的所有图像加载到一个 sprite 表中,您将大大减少游戏消耗的内存量以及 OpenGL 渲染游戏所需的处理量。

也就是说,现在是时候对你的游戏代码进行一些小的整理了,让它使用一个通用的精灵表

创建纹理类

您将使用loadTexture()方法创建一个通用纹理类。loadTexture()方法将执行与SFGoodGuy()类中的loadTexture()方法相同的功能。不同之处在于,这个公共类将返回一个 int 数组,您可以将该数组传递给所有实例化的字符。

第一步是打开SFGoodGuy()类并移除loadTexture()方法(以及支持它的任何变量)。完成后,修改后的SFGoodGuy()类应该是这样的:

`package com.proandroidgames;

import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

public class SFGoodGuy {

private FloatBuffer vertexBuffer; private FloatBuffer textureBuffer; private ByteBuffer indexBuffer;

private float vertices[] = { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, };

private float texture[] = { 0.0f, 0.0f, 0.25f, 0.0f, 0.25f, 0.25f, 0.0f, 0.25f, };

private byte indices[] = { 0,1,2, 0,2,3, };

public SFGoodGuy() { ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4); byteBuf.order(ByteOrder.nativeOrder()); vertexBuffer = byteBuf.asFloatBuffer(); vertexBuffer.put(vertices); vertexBuffer.position(0); byteBuf = ByteBuffer.allocateDirect(texture.length * 4); byteBuf.order(ByteOrder.nativeOrder()); textureBuffer = byteBuf.asFloatBuffer(); textureBuffer.put(texture); textureBuffer.position(0);

indexBuffer = ByteBuffer.allocateDirect(indices.length); indexBuffer.put(indices); indexBuffer.position(0); }

public void draw(GL10 gl, int[] spriteSheet) { gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[0]);

gl.glFrontFace(GL10.GL_CCW); gl.glEnable(GL10.GL_CULL_FACE); gl.glCullFace(GL10.GL_BACK);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisable(GL10.GL_CULL_FACE); }

}`

注意:当您完成这些更改时,根据您使用的 IDE,您将开始从代码的其他区域得到一些错误。现在不用担心他们;您将在本章的后面处理这些错误。

接下来,让我们创建一个新的公共类来加载你的纹理到 OpenGL 并返回一个 int 数组。在主包中创建一个名为SFTextures()的新类。

`package com.proandroidgames;

public class SFTextures {

}`

现在,为接受一个GL10实例的SFTextures()创建一个构造函数。该实例将用于初始化纹理。您还需要一个纹理变量来初始化两个元素的 int 数组。

`package com.proandroidgames;

import javax.microedition.khronos.opengles.GL10;

public class SFTextures {

**private int[] textures = new int[1];

public SFTextures(GL10 gl){

}**

}`

您需要让 OpenGL 为您正在加载的纹理生成一些名称。以前,这是在使用glGenTextures()方法的SFGoodGuy()类的loadTexture()方法中完成的。但是,因为您计划多次调用这个通用的纹理类,所以每次调用 load 方法时,OpenGL 都会为纹理分配新的名称,这将使跟踪纹理变得很困难,如果不是不可能的话。

为了避免给同一个纹理分配多个名称,您将把glGenTextures()方法调用移动到SFTextures()构造函数:

`package com.proandroidgames;

import javax.microedition.khronos.opengles.GL10;

public class SFTextures {

**private int[] textures = new int[1];

gl.glGenTextures(1, textures, 0);

public SFTextures(GL10 gl){

}**

}`

您需要为SFTextures()创建一个loadTexture()方法。在SFGoodGuy()SFBackground()类中,loadTexture()方法是一个简单的方法,没有返回。为了让你更好地控制纹理的访问,特别是当你开始加载多个 sprite 的时候,创建SFTextures()loadTexture()方法来返回一个 int 数组。

`package com.proandroidgames;

import javax.microedition.khronos.opengles.GL10;

public class SFTextures {

**private int[] textures = new int[1];

gl.glGenTextures(1, textures, 0);

public SFTextures(GL10 gl){

}

public int[] loadTexture(GL10 gl,int texture, Context context,int textureNumber) {

}**

}`

注意添加了textureNumber参数。虽然现在这个值是 1,但是在下一章中,当你开始使用这个类来加载多个 sprite 工作表时,它将被用来指示哪个工作表正在被加载。

除此之外,loadTexture()方法的核心看起来与其在SFGoodGuy()类中的对应部分完全相同。唯一的变化——除了对glGenTextures()的调用被移除——是textureNumber参数现在被用作glBindTextures()调用中指向的数组,并且loadTextures()现在在结束时返回纹理的 int 数组。

`package com.proandroidgames;

**import java.io.IOException; import java.io.InputStream;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLUtils;**

public class SFTextures {

**private int[] textures = new int[1];

gl.glGenTextures(1, textures, 0);

public SFTextures(GL10 gl){

}

public int[] loadTexture(GL10 gl,int texture, Context context,int textureNumber) { InputStream imagestream = context.getResources().openRawResource(texture); Bitmap bitmap = null; try {

bitmap = BitmapFactory.decodeStream(imagestream); }catch(Exception e){

}finally {

try { imagestream.close(); imagestream = null; } catch (IOException e) { }

}

gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[textureNumber - 1]);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle();

return textures;

}**

}`

你的普通纹理类已经完成,可以使用了。保存SFTextures()SFGoodGuy(),暂时关闭。同样,您现在应该会看到来自SFGameSFGameRenderer()类的错误。暂时忽略这些错误;当你阅读这一章时,你将会注意到它们。

在下一节中,你将创建一个类来装载你的敌舰,并让他们为与玩家的战斗做好准备。

创造敌人阶级

不管你玩过什么游戏,它们都有一个共同点:永远不会只有一个敌人。在一个游戏中只有一个敌人会导致一个非常快速和非常无聊的游戏。

星际战士中,你将为玩家创造 30 个敌人在屏幕上战斗。我们在第二章中勾勒出了星际战士所依据的故事。根据这个故事,提到了三种不同类型的敌人。在本章的这一节,你将创建这三种类型的敌人所基于的职业,以及 30 个实例化的敌人。

添加新的精灵工作表

您需要添加到项目中的第一件事是一个新的 sprite 表。在前一章中,你已经了解了 sprite sheets 对于 2d 游戏的重要性和用途。现在,您已经在代码中为所有角色精灵提供了一个通用的精灵表,您可以将它添加到您的项目中。图 6–1展示了常见的精灵表。

images

图 6–1。 普通雪碧单

只需移除 drawable 文件夹中的good_guy sprite 工作表,然后添加这个。

注意:注意玩家的角色在这个精灵表上的位置和他们在上一个精灵表上的位置一样。因此,您将不必为玩家角色更改任何纹理定位。

接下来,您需要编辑SFEngine类来添加您将在本章中使用的常量和变量。这次有不少。你将需要 17 个常数来帮助你独自控制敌人 AI。其中一些您可能要到下一章才会用到,但是现在添加它们是个好主意:

**public static int CHARACTER_SHEET = R.drawable.character_sprite; public static int TOTAL_INTERCEPTORS = 10; public static int TOTAL_SCOUTS = 15; public static int TOTAL_WARSHIPS = 5; public static float INTERCEPTOR_SPEED = SCROLL_BACKGROUND_1 * 4f; public static float SCOUT_SPEED = SCROLL_BACKGROUND_1 * 6f; public static float WARSHIP_SPEED = SCROLL_BACKGROUND_2 * 4f; public static final int TYPE_INTERCEPTOR = 1; public static final int TYPE_SCOUT = 2; public static final int TYPE_WARSHIP = 3;** **public static final int ATTACK_RANDOM = 0; public static final int ATTACK_RIGHT = 1; public static final int ATTACK_LEFT = 2; public static final float BEZIER_X_1 = 0f; public static final float BEZIER_X_2 = 1f; public static final float BEZIER_X_3 = 2.5f; public static final float BEZIER_X_4 = 3f; public static final float BEZIER_Y_1 = 0f; public static final float BEZIER_Y_2 = 2.4f; public static final float BEZIER_Y_3 = 1.5f; public static final float BEZIER_Y_4 = 2.6f;**

因为你添加到屏幕上的敌人将作为一个职业开始,就像背景和可玩的角色一样,所以给你的主包添加一个名为SFEnemy()的新职业。这个职业将被用来把你的敌人带入游戏。

提示:尽管你总共会有 30 个三种不同类型的敌人,但它们都是从同一个SFEnemy()类中实例化出来的。

创建 SFEnemy 类

在本节中,您将创建一个类,用于在星际战士游戏中产生所有三种类型的敌人。向您的项目添加一个名为SFEnemy()的新类:

`package com.proandroidgames;

public class SFEnemy {

}`

当你开始创建 AI 逻辑时,你的敌人需要一些属性来帮助你。您将需要一些属性来设置或获取敌人当前的 x 和 y 位置、t 因子(用于以曲线飞行敌人)以及到达目标的 x 和 y 增量。

`package com.proandroidgames;

public class SFEnemy { public float posY = 0f; //the x position of the enemy public float posX = 0f; //the y position of the enemy public float posT = 0f; //the t used in calculating a Bezier curve public float incrementXToTarget = 0f; //the x increment to reach a potential target public float incrementYToTarget = 0f; //the y increment to reach a potential target

}`

你还需要一些属性,让你设置或获取敌人攻击的方向,敌人是否被消灭,以及这个实例代表什么类型的敌人。

`package com.proandroidgames;

public class SFEnemy { **public float posY = 0f; //the x position of the enemy public float posX = 0f; //the y position of the enemy public float posT = 0f; //the t used in calculating a Bezier curve public float posXToTarget = 0f; //the x increment to reach a potential target public float posYToTarget = 0f; //the y increment to reach a potential target

public int attackDirection = 0; //the attack direction of the ship public boolean isDestroyed = false; //has this ship been destroyed? public int enemyType = 0; //what type of enemy is this?**

}`

接下来你的敌人职业需要的三个属性是一个指示器,让你知道它是否锁定了一个目标(这对你的 AI 逻辑至关重要)和两个坐标,代表锁定目标的位置。

`package com.proandroidgames;

public class SFEnemy { public float posY = 0f; //the x position of the enemy public float posX = 0f; //the y position of the enemy public float posT = 0f; //the t used in calculating a Bezier curve public float posXToTarget = 0f; //the x increment to reach a potential target public float posYToTarget = 0f; //the y increment to reach a potential target public int attackDirection = 0; //the attack direction of the ship public boolean isDestroyed = false; //has this ship been destroyed? public int enemyType = 0; //what type of enemy is this public boolean isLockedOn = false; //had the enemy locked on to a target? public float lockOnPosX = 0f; //x position of the target public float lockOnPosY = 0f; //y position of the target

}`

接下来,给你的SFEnemy()类一个接受两个 int 参数的构造函数。第一个参数将用来表示应该创造的敌人类型:TYPE_INTERCEPTORTYPE_SCOUTTYPE_WARSHIP。第二个参数将用于指示特定敌人将从屏幕上的哪个方向进攻:ATTACK_RANDOMATTACK_RIGHTATTACK_LEFT

`package com.proandroidgames;

public class SFEnemy {

**…

public SFEnemy(int type, int direction) {

}**

}`

SFEnemy()的构造函数中,需要根据传入构造函数的 int 类型来设置敌方类型。你也将设定方向。看到这些参数将让你在游戏循环中根据敌人的类型和运动方向做出决定。

`package com.proandroidgames;

public class SFEnemy {

**… public SFEnemy(int type, int direction) { enemyType = type; attackDirection = direction;

}**

}`

星际战士(在第二章中)的故事描述了三种不同敌人的攻击特点。侦察兵以快速但可预测的模式飞行,截击机锁定并直接飞向玩家的角色,战舰以随机模式机动。每艘船都需要从屏幕上的一个特定点出发。

通常在滚动射击游戏中,敌人从屏幕外 y 轴上的一点开始,然后向下滚动到玩家。因此,在构造函数中你要做的下一件事是为敌人建立一个 y 轴起点。

Android 的随机数生成器是选择起点的好方法。Android 随机数生成器将生成一个介于 0 和 1 之间的数字。然而,你的敌人的 y 轴是从 0 到 4。将随机数生成器生成的数字乘以 4,结果将是屏幕上一个有效的 y 轴位置。在有效的 y 位置上加 4,然后将起点推出屏幕。

`package com.proandroidgames;

public class SFEnemy {

**…

private Random randomPos = new Random(); public SFEnemy(int type, int direction) { enemyType = type; attackDirection = direction; posY = (randomPos.nextFloat() * 4) + 4;

}**

}`

它负责 y 轴。现在,你需要建立一个 x 轴位置。看看您在SFEngine中创建的常量。三个代表 x 轴上敌人可能攻击的位置:ATTACK_LEFTATTACK_RANDOMATTACK_RIGHT。左侧的 x 轴值为 0。右边的 x 轴值是 3(从 4 中减去 1 个单位以说明精灵的大小)。

可以使用一个case语句,根据传递给构造函数的攻击方向来分配 x 轴的起点。

`package com.proandroidgames;

public class SFEnemy {

**…

public SFEnemy(int type, int direction) { enemyType = type; attackDirection = direction; posY = (randomPos.nextFloat() * 4) + 4; switch(attackDirection){ case SFEngine.ATTACK_LEFT: posX = 0; break; case SFEngine.ATTACK_RANDOM: posX = randomPos.nextFloat() * 3; break; case SFEngine.ATTACK_RIGHT: posX = 3; break; }

}**

}`

您需要建立的最后一个变量是posT。不要担心posT现在做什么;在本章的后面你会发现。将posT设置为SFEngine.SCOUT_SPEED的值。

`package com.proandroidgames;

public class SFEnemy {

… public SFEnemy(int type, int direction) { enemyType = type; attackDirection = direction; posY = (randomPos.nextFloat() * 4) + 4; switch(attackDirection){ case SFEngine.ATTACK_LEFT: posX = 0; break; case SFEngine.ATTACK_RANDOM: posX = randomPos.nextFloat() * 3; break; case SFEngine.ATTACK_RIGHT: posX = 3; **break; } posT = SFEngine.SCOUT_SPEED;

}**

}`

你可以创造的两种敌人类型,截击机和战舰,将会以对角线的直线行进。生成这些攻击路径的代码将在游戏循环中处理,因为沿直线引导物体相对容易。另一方面,侦察型敌人会以一种被称为贝塞尔曲线的模式移动。在下一节中,您将创建帮助敌人以曲线飞行的方法。

贝塞尔曲线

虽然你可能不知道它的名字,但你很可能以前见过贝塞尔曲线。图 6–2 展示了贝塞尔曲线的样子。

images

图 6–2。 一条二次贝塞尔曲线

为了让侦察兵以二次贝塞尔曲线从屏幕的顶部到底部飞行,您需要两个方法:一个是获取贝塞尔曲线上的下一个 x 轴值,另一个是获取贝塞尔曲线上的下一个 y 轴值。每次你调用这些方法,你会得到 x 和 y 轴上的下一个点,特定的敌人需要移动到这个点。

幸运的是,在贝塞尔曲线上绘制点相当简单。要构造一条二次贝塞尔曲线,你需要四个笛卡尔点:一个起点,一个终点,以及曲线环绕的两个点。这些点在星际战士游戏中永远不会改变。每个侦察兵都会沿着同一条曲线,从左边或右边。因此,在SFEngine中创建了八个常数来表示每个轴上的四个二次贝塞尔曲线点。

绘制点的关键值是 t 因子。t 因子告诉公式您在曲线上的位置,从而允许公式计算该单个位置的 x 或 y 坐标。因为你的船将以一个预先定义的速度移动,你将使用这个值作为 t 的种子值。

提示:如果你不理解本节公式背后的数学,有许多关于贝塞尔曲线的伟大资源,包括下面的维基百科页面:【http://en.wikipedia.org/wiki/Bézier_curve】T2。

在您的SFEnemy()类中创建两个方法:一个获取下一个 x 轴值,另一个获取下一个 y 轴值。

`package com.proandroidgames;

public class SFEnemy {

**… public SFEnemy(int type, int direction) {

… }

public float getNextScoutX(){

} public float getNextScoutY(){

}**

}`

下面是在 y 轴上的二次贝塞尔曲线上寻找一个点的公式(用x代替y来寻找 x 轴上的值):

(y<sub>1</sub>*(t<sup>3</sup>)) + (y<sub>2</sub> * 3 * (t<sup>2</sup>) * (1-t)) + (y<sub>3</sub> * 3 * t * (1-t)<sup>2</sup>) + (y<sub>4</sub>* (1-t)<sup>3</sup>)

在您的getNextScoutY()方法中使用这个公式和正确的变量。

`package com.proandroidgames;

public class SFEnemy {

**…

public SFEnemy(int type, int direction) {

… }

public float getNextScoutX(){

}

public float getNextScoutY(){ return (float)((SFEngine.BEZIER_Y_1(posTposT*posT)) + (SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT * ((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT))));

}**

}`

对 x 轴使用相同的公式,有一个小的变化。如果敌人从屏幕的左边攻击,而不是右边,你需要颠倒公式。

`package com.proandroidgames;

public class SFEnemy {

**… public SFEnemy(int type, int direction) {

… }

public float getNextScoutX(){ if (attackDirection == SFEngine.ATTACK_LEFT){ return (float)((SFEngine.BEZIER_X_4(posTposTposT)) + (SFEngine.BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_2 * 3 * posT * ((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT)))); }else{ return (float)((SFEngine.BEZIER_X_1(posTposTposT)) + (SFEngine.BEZIER_X_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_3 * 3 * posT * ((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_4 * ((1-posT) * (1-posT) * (1-posT)))); }

}

public float getNextScoutY(){ return (float)((SFEngine.BEZIER_Y_1(posTposTposT)) + (SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT * ((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT)))); }*

}`

注意,在计算 x 轴的右侧时,值为 x 1 、x 2 、x 3 和 x4—从左侧开始,点的使用顺序相反:x 4 、x 3 、x 2 和 x 1

考虑到使用新的通用 sprite 表所做的更改,SFEnemy类的其余部分看起来应该和SFGoodGuy类一样。

`package com.proandroidgames;

import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.util.Random; import javax.microedition.khronos.opengles.GL10;

public class SFEnemy {

public float posY = 0f; public float posX = 0f; public float posT = 0f; public float incrementXToTarget = 0f; public float incrementYToTarget = 0f; public int attackDirection = 0; public boolean isDestroyed = false;

public int enemyType = 0;

public boolean isLockedOn = false; public float lockOnPosX = 0f; public float lockOnPosY = 0f;

private Random randomPos = new Random();

**private FloatBuffer vertexBuffer; private FloatBuffer textureBuffer; private ByteBuffer indexBuffer;

private float vertices[] = { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, };

private float texture[] = { 0.0f, 0.0f, 0.25f, 0.0f, 0.25f, 0.25f, 0.0f, 0.25f, };

private byte indices[] = { 0,1,2, 0,2,3, };**

public SFEnemy(int type, int direction) { enemyType = type; attackDirection = direction; posY = (randomPos.nextFloat() * 4) + 4; switch(attackDirection){ case SFEngine.ATTACK_LEFT: posX = 0; break; case SFEngine.ATTACK_RANDOM: posX = randomPos.nextFloat() * 3; break; case SFEngine.ATTACK_RIGHT: posX = 3; break; } posT = SFEngine.SCOUT_SPEED;

**ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4); byteBuf.order(ByteOrder.nativeOrder()); vertexBuffer = byteBuf.asFloatBuffer(); vertexBuffer.put(vertices); vertexBuffer.position(0);

byteBuf = ByteBuffer.allocateDirect(texture.length * 4); byteBuf.order(ByteOrder.nativeOrder()); textureBuffer = byteBuf.asFloatBuffer(); textureBuffer.put(texture); textureBuffer.position(0);

indexBuffer = ByteBuffer.allocateDirect(indices.length); indexBuffer.put(indices); indexBuffer.position(0);** }

public float getNextScoutX(){ if (attackDirection == SFEngine.ATTACK_LEFT){ return (float)((SFEngine.BEZIER_X_4(posTposTposT)) + (SFEngine.BEZIER_X_3 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_2 * 3 * posT * ((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_1 * ((1-posT) * (1-posT) * (1-posT)))); }else{ return (float)((SFEngine.BEZIER_X_1(posTposTposT)) + (SFEngine.BEZIER_X_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_X_3 * 3 * posT * ((1-posT) * (1-posT))) + (SFEngine.BEZIER_X_4 * ((1-posT) * (1-posT) * (1-posT)))); }

}

public float getNextScoutY(){ return (float)((SFEngine.BEZIER_Y_1(posTposT*posT)) + (SFEngine.BEZIER_Y_2 * 3 * (posT * posT) * (1-posT)) + (SFEngine.BEZIER_Y_3 * 3 * posT * ((1-posT) * (1-posT))) + (SFEngine.BEZIER_Y_4 * ((1-posT) * (1-posT) * (1-posT)))); }

**public void draw(GL10 gl, int[] spriteSheet) { gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[0]);

gl.glFrontFace(GL10.GL_CCW); gl.glEnable(GL10.GL_CULL_FACE); gl.glCullFace(GL10.GL_BACK);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer); gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisable(GL10.GL_CULL_FACE); }**

}`

你现在有了一个工人阶级,你可以在游戏中培养所有的敌人。保存SFEnemy()类。在下一章,你将开始为你的敌人创造人工智能。

总结

在这一章中,你的技能又向前迈进了一大步。为你的游戏创造敌人已经做了很多工作,还有更多工作要做。以下列表描述了您在本章中学到的内容,您将在第 7 章中对您所学的内容进行扩展:

  • 创建一个通用的纹理类来保存一个大的 sprite 表。
  • 创建一个数组来容纳游戏中的所有敌人,以便于处理。
  • 创建SFEnemy()类来繁殖三个不同的敌人。
  • 创建一个用贝塞尔曲线移动敌人的方法。