十四、发射武器
许多游戏要求玩家向障碍物或敌人开火或投掷武器。如果你曾经试图发射武器,你可能会遇到让你的投射物以可预测的方式离开你的角色,并沿着设定的路径到达目标的问题。
武器可以有多种形状、大小和功能。在许多游戏情况下,子弹是直线行进的,导弹、激光和大多数其他推进武器也是如此。投掷的武器,如石头、手榴弹,甚至在一定程度上是箭,都遵循更抛物线的轨迹。不管你为武器选择了什么样的图像或动画,从 A 点到 B 点的数学方法都是一样的。
本章将介绍触发武器的“按钮”的多种解决方案,以及在屏幕上制作武器动画的多种解决方案。很像第 13 章,这一章在 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
值,你的镜头会上下移动。
在第 15 章中,你将会看到实现碰撞检测的解决方案。碰撞检测是当你的镜头击中目标时采取行动的关键,而不是简单地让你的镜头离开屏幕边缘。
在下一个解决方案中,您将修改firePlayerWeapon()
方法,以抛物线运动方式移动镜头,就好像是投掷而不是直线拍摄一样。
14.3 制作投掷武器的动画
问题
武器不会像投掷武器那样沿弧线飞行。
解决办法
使用一个公式,就像跳跃时使用的公式,来确定一个弯曲的轨迹。
它是如何工作的
要像投掷一样以弧形运动移动您的镜头,您需要修改firePlayerWeapon()
方法。我们将使用《T2》第 13 章中让角色跳跃的相同数学公式,并将其放入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);
}
}
}
}
通过这个小小的改动,你可以让你的武器产生一个抛出的弧线,而不是已经发射的射弹的直线。
摘要
在第 13 章中,你回顾了允许你在游戏中添加敌人的食谱。然而,如果玩家没有办法保护自己,在游戏中加入敌人是不公平的。本章中的食谱帮助你为玩家提供了一种发射武器的方法。
版权属于:月萌API www.moonapi.com,转载请注明出处