十四、发射武器

许多游戏要求玩家向障碍物或敌人开火或投掷武器。如果你曾经试图发射武器,你可能会遇到让你的投射物以可预测的方式离开你的角色,并沿着设定的路径到达目标的问题。

武器可以有多种形状、大小和功能。在许多游戏情况下,子弹是直线行进的,导弹、激光和大多数其他推进武器也是如此。投掷的武器,如石头、手榴弹,甚至在一定程度上是箭,都遵循更抛物线的轨迹。不管你为武器选择了什么样的图像或动画,从 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-3SBGWeapon() (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-4SBGWeapon() (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()类还包含三个关键特性。两个变量(xy)用于通过游戏循环跟踪武器的轴上坐标。shotFired变量用于确定SBGWeapon的具体实例化是否已经被触发,是应该被绘制在屏幕上还是被忽略。

为什么要用一个布尔值来表示是否开枪?玩家在游戏中快速连续射击多次是很常见的。这意味着,在任何时候,你的游戏都必须在游戏循环的一次迭代中跟踪许多镜头。通过使用shotFired boolean,你可以确定内存中的哪些SBGWeapons已经被触发,哪些正在等待被抽取。

接下来的计划是在您的Renderer类中实例化一个SBGWeapon()。然后,当您检测到PLAYER_FIRE_WEAPON时,在游戏循环的每次迭代中绘制SBGWeapon()并沿直线移动它,直到SBGWeapon()击中目标或到达屏幕的末端。

Renderer类中,实例化一个由SBGWeapon组成的数组。在清单 14-5清单 14-6 中,我将使用一个由四枚导弹组成的数组,这意味着一次只能有四枚导弹同时出现在屏幕上。

private SBGWeapon[] playerFire = new SBGWeapon[4];

不要忘记为你发射的任何武器的图像加载纹理。纹理被加载到RendereronSurfaceCreated()方法中(参见清单 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);
}

最后,创建一个可以从游戏循环中调用的新方法。在本书的许多解决方案中,我引用了一个作用于playerActioncase语句。在这个语句中添加一个新的case来测试playerAction = PLAYER_FIRE_WEAPON。如果PLAYER_FIRE_WEAPON被检测到,调用你的新方法将武器绘制到屏幕上(见清单 14-714-8 )。

清单 14-7firePlayerWeapon() (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-8firePlayerWeapon() (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()xy值的赋值,使镜头向不同方向移动。通过增加或减少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 章中,你回顾了允许你在游戏中添加敌人的食谱。然而,如果玩家没有办法保护自己,在游戏中加入敌人是不公平的。本章中的食谱帮助你为玩家提供了一种发射武器的方法。