Android基于box2d开发弹弓类游戏[四]-------------创建发射器
发表于2016-07-08
前一章中,我们已经把游戏世界创建完成了,但是,出了一个不能显示的地面之外,还没有添加其他的物体。不用着急,接下来,我们就要去体验box2d物理引擎的强大功能了。
七、创建发射器
1、世界中加入发射器
世界和大地已经创建完成,我们模拟的世界已经比较完善了,接下来在世界中加入一些动态的物体,首先创建发射器。这个发射器将会使用到旋转关节和鼠标关节。
由于接下来创建的动态物体,例如:发射器,子弹,砖块等等都是一个单独的对象。随意我们需要为每一个类别的物体创建一个类。分析可以发现这些类需要实现的方法基本上类似,所以我们创建一个BodyInterface接口,所有的动态物体都继承这个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public interface BodyInterface { float x= 0 ; //x轴 float y= 0 ; //y轴 float width= 0 ; //宽度 float height= 0 ; //高度 float angle= 0 ; //角度 Bitmap bitmap = null ; //显示图片 void draw(Canvas canvas,Paint paint, float w); //画图 public void setX( float x); //设置x轴 public void setY( float y); //设置y轴 public void setAngle( float angle); //设置角度 public float getX(); //获取x轴 public float getY(); //获取y轴 public float getWidth(); //获取宽度 public float getHeight(); //获取高度 } |
接下来创建发射器类CatapultArm.java。实现BodyInterface接口。实现构造函数用于初始化发射器对象。
1 2 3 4 5 6 7 8 | public CatapultArm(Bitmap bitmap, float x, float y, float width, float height, float angle) { this .bmp = bitmap; this .x = x; this .y = y; this .width = width; this .height = height; this .angle = angle; } |
实现画图方法
1 2 3 4 5 6 | public void draw(Canvas canvas, Paint paint, float position_X) { canvas.save(); canvas.rotate( this .angle, x - position_X + width / 2 , y + height / 2 ); canvas.drawBitmap( this .bmp, this .x - position_X, this .y, paint); canvas.restore(); } |
这里对canvas.save()与canvas.restore()进行分析。
这里canvas.save();和canvas.restore();是两个相互匹配出现的,作用是用来保存画布的状态和取出保存的状态的。这里稍微解释一下, 当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,比如图片,一个矩形等,但是当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,那么之后在画布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,当操作之后取出之前保存过的状态,这样就不会对其他的元素进行影响
首先创建发射器物体对象,以及发射臂图片对象
1 2 | private Body catapultArmBody; private Bitmap catapultArmBitmap; |
在MainView构造方法中为发射臂图片赋值
1 | catapultArmBitmap = BitmapFactory.decodeResource(res, R.drawable.catapult_arm); |
接下来创建获取发射臂物体的方法CreateCatapultBody().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public Body CreateCatapultBody(Bitmap bitmap, float x, float y, float width, float height) { PolygonShape ps = new PolygonShape(); ps.setAsBox(width/ 2 /RATE, (height/ 2 - 40 )/RATE); FixtureDef fd = new FixtureDef(); fd.shape = ps; fd.density = 1 ; fd.friction = 0 .8f; fd.restitution = 0 .3f; BodyDef bd = new BodyDef(); bd.position.set((x+width/ 2 )/RATE, (y+height/ 2 )/RATE); bd.type = BodyType.DYNAMIC; Body body = world.createBody(bd); body.m_userData = new CatapultArm(bitmap, x, y, width,height, 0 ); body.createFixture(fd); return body; } |
这里需要注意的就是body.m_userData方法。
Body的m_userData属性,是一个object类型,其主要用途就是保存一个object实例,这样就可以讲自定义的类型保存到body的这个m_userData里面,通过便利body时取出其实例并进行操作。将会在下面的程序中使用m_userData属性。
在surfaceCreated方法中,创建发射器物体。
1 | catapultArmBody = CreateCatapultBody(catapultArmBitmap, 290 , ScreenH-FLOOR_HEIGHT-catapultArmBitmap.getHeight()-catapult_base_2.getHeight()/ 2 , catapultArmBitmap.getWidth(), catapultArmBitmap.getHeight()); |
由于物体在模拟世界中每时每刻都会变化,所以需要创建一个方法来改变物体的参数,以便画出物体,在draw方法之前调用这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public void logic() { //唤醒世界 world.step(timeStep, iterations, 6 ); //物体位置参数 Vec2 positionBuller; //世界中的所有物体的集合 Body body = world.getBodyList(); //遍历所有的物体 for ( int i = 0 ; i < world.getBodyCount(); i++) { //获取当前物体的位置 positionBuller = body.getPosition(); //判断当前物体是否为发射器 if ((body.m_userData) instanceof CatapultArm) { //获取发射器对象 CatapultArm catapultArm = (CatapultArm) body.m_userData; //设置发射器的x轴 catapultArm.setX(positionBuller.x * RATE - catapultArm.width/ 2 ); //设置发射器的y轴 catapultArm.setY(positionBuller.y * RATE - catapultArm.height/ 2 ); //设置发射器的角度 catapultArm.setAngle(( float ) (body.getAngle() * 180 / Math.PI)); } //遍历下一个对象 body = body.m_next; } } |
这个方法计算每个物体在模拟世界中变化状态。然后再draw方法中画出发射器。
1 | ((CatapultArm)catapultArmBody.m_userData).draw(canvas, paint, move_X); |
此时可以运行程序。已经有了真实世界中的效果(虽然不是我们想要的结果),发射器自由落体到地面上。

虽然已经把发射器放到了世界中。但是与想象的有些差别,游戏运行,发射器会自由落体到地面,而没有和发射器底座进行固定。接下来我们就要引入旋转关节,时发射器固定在发射器底座上,并且可以旋转移动。
2、发射器旋转
现在运行游戏可以发现,发射器是模拟世界中一个孤立的物体。我们需要某种约束来限制发射器的转动在一定角度内。借助“旋转关节”可以完美的解决这个问题。想象一个钉子将2个物体钉在一个特殊的点,但仍然允许他们转动。
回到MainView.java主文件中加入旋转关节
1 | RevoluteJointDef rjd; //旋转关节 |
然后编写创建选装关节的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 | public RevoluteJoint createRevoluteJoint() { //创建一个旋转关节的数据实例 rjd = new RevoluteJointDef(); //初始化旋转关节数据 rjd.initialize(catapultArmBody,floorBody , new Vec2(catapultArmBody.getWorldCenter().x, catapultArmBody.getWorldCenter().y+catapultArmBitmap.getHeight()/ 2 /RATE) ); rjd.maxMotorTorque = 100 ; // 马达的预期最大扭矩 rjd.motorSpeed = 1000 ; //马达最终扭矩 rjd.enableMotor = true ; // 启动马达 //利用world创建一个旋转关节 RevoluteJoint rj = (RevoluteJoint)world.createJoint(rjd); return rj; } |
在上面代码中,初始化选装关节数据的函数为initialize(Body body1,Body body2,Vec2 anchor),它的参数含义说明如下:
第一个参数:做旋转运动的Body实例。
第二个参数:旋转关节中的第二个Body实例
第三个参数:旋转锚点。
当我们创建关节时你不得不修改2个物体和连接点。你可能会想:“我们不应该把发射器连接到发射器底部吗?”。在现实世界中,没错。但是在Box2D中这可不是必要的。你可以这样做但你不得不再为发射器底部创建另一个物体并增加了模拟的复杂性。由于发射器底部在何时都是静态的并且在Box2d中枢纽物体(hinge body)不必要在其他的任何物体中,我们可以只使用我们已拥有的大地物体(floorBody)。
这里我们使用发射器底部为旋转关节的锚点。catapultArmBody.getWorldCenter().x属性是得到发射器在世界中的x轴位置。catapultArmBody.getWorldCenter().y属性石获取发射器在世界中的y轴位置。为了是锚点在发射器的底部我们需要为y轴加上发射器图片一半的高度。
当旋转关节的数据初始化之后,旋转Body是不会运动的,因为没有力的作用,所以需要一个“马达”来进行驱动Body,让Body开始做旋转运动。
“马达”也是旋转关节中的数据,所以利用RevoluteJointDef实例来进行设置,启动一个“马达”驱动旋转Body进行旋转运动,至少需要设置下面三个属性:
maxMotorTorque:马达的预期最大扭矩。
motorSpeed:马达最终扭矩。
enablemotor:启动马达。
马达预期最大扭矩指的是Body在进行旋转运动开始的一个扭矩值,可以简单理解为旋转速度。马达最终 指的是马达最终会以一个固定的转速来运动,而这个转速就是取决最终扭矩。当两者属性设置完毕,最后启动马达即可。
方法创建完成之后,在surfaceCreated方法中,调用,创建旋转关节。
1 | createRevoluteJoint(); |
此时运行游戏程序,可以查看一下效果。

此时运行游戏,可以看到发射器已经开始旋转了。但是,确实无休止的旋转。这是我们需要对旋转关节进行一些改进。加入旋转关节的起始角度和终止角度。对角度进行限制,可以使我们的发射器更加像真实世界中的发射器。
然后我们还需要通过设置”enableLimit“,”lowerAngle“,”upperAngle“激活关节。这让关节活动范围角度10°到70°。这如我们所想的那样模拟投射器运动。
因为box2d中默认的度量单位都是弧度,所有这里应用到了弧度与角度的转换。
1°=π/180°≈0.01745
1弧度=(180/π)º≈57.30º=57º18‘
所以
10°=10*π/180弧度
70°=70*π/180弧度
下面在createRevoluteJoint()方法中添加对旋转角度的限制。
1 2 3 | rjd.enableLimit = true ; rjd.lowerAngle = ( float ) (10f*Math.PI/ 180 ); rjd.upperAngle = ( float ) (70f*Math.PI/ 180 ); |
接下来运行游戏。
游戏开始发射器有了10度的弧度。

发射器最大的角度为75度。

3、推动发射器
这样看起来与真实场景有些相似了。但是发射器现在是自动旋转,这个与需要的效果有些不同。希望的效果应该是,游戏开始发射器处于静止状态,用手指去推动发射器,当松开手指的时候,发射器自动复原,这样才像一个真实的发射器。为了实现这个效果,接下来使用另一个关节。---鼠标关节。
鼠标关节:指利用鼠标提供力的作用,拖拽物体,物体朝向鼠标点击的位置进行移动,其效果如同在物体与鼠标之间绑定一个橡皮筋。
创建鼠标关节之前需要分析一下整个创建的过程。何时创建鼠标关节,何时移动鼠标关节,何时销毁鼠标关节。
创建鼠标关节:当我们用手指触摸屏幕区域,当触摸的区域内部的物体只有是发射器时,创建鼠标关节。这里需要我们通过回调函数获取触摸的对象。
移动鼠标关节:当按下触摸屏时,并且获取了发射器对象,此时再在屏幕上移动。
销毁鼠标关节:当创建了鼠标关节,并且手指离开触摸屏,销毁鼠标关节。
首先我们需要更改旋转关节的最大扭曲力与最终扭曲力。是发射器处于静止状态。
1 2 | rjd.maxMotorTorque = 400 ; // 马达的预期最大扭矩 rjd.motorSpeed =- 100 ; //马达最终扭矩 |
然后为了能触摸屏幕时获取物体对象,需要新建一个回调类。
新建一个类TouchCallBack.java 并且实现QueryCallback接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class TouchCallback implements QueryCallback{ public final Vec2 point; public Fixture fixture; public TouchCallback() { point = new Vec2(); fixture = null ; } public boolean reportFixture(Fixture argFixture) { Body body = argFixture.getBody(); //获取动态的物体 if (body.getType() == BodyType.DYNAMIC) { //当前触摸点是否有物体 boolean inside = argFixture.testPoint(point); if (inside) { fixture = argFixture; return false ; } } return true ; } } |
回到MainView.java主类中,创建一个回调类对象。
1 | private final TouchCallback callback = new TouchCallback(); |
接下来开始完成鼠标关节的创建。
下面创建鼠标关节所需要的诸多变量;
1 2 3 4 5 | MouseJoint mj; //首先创建鼠标关节对象 private Vec2 touchPoint; //手指触摸屏幕点的位置 private final AABB queryAABB = new AABB(); //物理模拟世界的范围 private Body curBody; //手指触摸的对象 private boolean withMouse = false ; //判断是否创建了鼠标关节 |
在手指触摸屏幕的时候去检测是否碰触到发射器,如果碰触到了就创建鼠标关节。
首先计算当前触摸点的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | touchPoint = new Vec2((event.getX()+move_X)/RATE, event.getY()/RATE); if (event.getAction() == MotionEvent.ACTION_DOWN) { position_X =move_X+ event.getX(); if (mj != null ) { //如果已经创建了鼠标关节,就直接返回 return true ; } //得到当前模拟世界中的最小范围 queryAABB.lowerBound.set(touchPoint.x - .001f, touchPoint.y - .001f); //得到当前模拟世界中的最大范围 queryAABB.upperBound.set(touchPoint.x + .001f, touchPoint.y + .001f); callback.point.set(touchPoint); callback.fixture = null ; world.queryAABB(callback, queryAABB); if (callback.fixture != null ){ curBody = callback.fixture.getBody(); if (curBody.m_userData instanceof CatapultArm){ withMouse = true ; MouseJointDef def = new MouseJointDef(); def.bodyA = floorBody; def.bodyB = curBody; def.target.set(touchPoint); def.maxForce = 3000 ; //鼠标关节的马力 mj = (MouseJoint) world.createJoint(def); curBody.setAwake( true ); } } } |
当创建了鼠标关节之后,手指离开了触摸屏,这是需要销毁鼠标关节。
1 2 3 4 5 6 7 8 9 10 11 12 | else if (event.getAction()==MotionEvent.ACTION_UP) { if (withMouse ) { withMouse = false ; //同时删除鼠标关节 if (mouseJoint != null ) { world.destroyJoint(mouseJoint); mouseJoint = null ; } } } |
手指移动的时候,发射器会跟随手指移动的位置去移动。这样就是想了推动发射器的效果。
1 2 3 4 5 6 7 8 9 10 11 12 | else if (event.getAction() == MotionEvent.ACTION_UP) { if (withMouse) { withMouse = false ; //同时删除鼠标关节 if (mj != null ) { world.destroyJoint(mj); //在世界中销毁鼠标关节 mj = null ; } } } |
当手指移动的时候,需要随时对鼠标关节的触摸点进行更新。
1 2 3 4 5 6 7 8 | else if (event.getAction() == MotionEvent.ACTION_MOVE) { if (mj != null ) { mj.setTarget(touchPoint); //设置鼠标关节的变化位置 } move_X = position_X-event.getX(); move_X = move_X< 0 ? 0 :(move_X>gameWidth-ScreenW?gameWidth-ScreenW:move_X); } |
到此为止,鼠标关节已经创建完成了。可以运行一下游戏,进行测试。
这里需要重点说明一下鼠标关节的马力与旋转关节的最大扭曲力有关。如果你发现创建的鼠标关节不能拖动发射器。那么增大鼠标关节的马力试一试,应该就会得到你想要的结果。
现在的游戏的结果既让我们兴奋,也让我们失望。
兴奋是因为,我们可以拖动发射器了。
失望是因为,在我们拖动发射器的同时,这个场景跟随者手指进行了移动,使得我们刚刚拖拽发射器,游戏场景却已经改变了。
我们希望得到的效果是,当手触摸到发射器的时候,场景不要进行改变。当手触摸到发射器以外的场景时,才进行场景的切换。
下面进行优化
其实很简单,只需要加上一个判断,如果鼠标关节存在,就不要进行场景的移动。在触摸屏移动的时间中,加入以下判断。
1 2 3 4 5 | if (!withMouse) { move_X = position_X-event.getX(); move_X = move_X< 0 ? 0 :(move_X>gameWidth-ScreenW?gameWidth-ScreenW:move_X); } |
然后再次运行游戏。大功告成了。发射器已经完成了。并且当我们拖拽发射器之后,发射器能自动复原。

现在我们的游戏已经初具规模了。因为我们的发射器已经完成了。在下一章节中,我们将会加入子弹,并且发射子弹!敬请关注哦!
腾讯GAD游戏程序交流群:484290331