本节,我们将利用Box2d引擎在页面中实现球体飞行和撞击效果。在现实中我们向外抛出一个球时,它在重力加速度的情况下会飞出一个弧线,撞到物体后它会反弹折射,我们利用Box2D可以在页面里模拟这些特性。我们将在页面里绘制一个小球,然后设置一些障碍物,我们用鼠标控制小球向外抛出的方向,小球碰到障碍物后会像现实中一样发生反弹和折射。完成本节后,我们得到效果如下:
屏幕快照 2018-07-07 上午11.26.04.png
如上图,右下角是一个圆球,左上角是障碍物,用鼠标点击小球并向左上角拖动时,小球就会模拟受到一股像外抛出的力量。当小球与左上角障碍物相撞后,会发生反射,效果如下:
屏幕快照 2018-07-07 上午11.26.27.png
左上角红色小球就是碰撞后停留在障碍物上,具体的动画特效请参更详细的讲解和代码调试演示过程,请点击链接
首先我们用代码构造上图中的小球和由三个方块构造出的篮球架,在gamescenecomponent.vue中添加代码如下:
// change 1createGameLevel () { this.createHoop() // 生成一个小球 this.spawnBall() },
createGameLevel用于选择游戏的难度和关卡,在这里,我们先直接用来绘制篮球架和小球,其中createHoop用于生成篮球架,spawnBall用于生成小球。我们先看看小球绘制的实现:
// change 2 spawnBall () { var positionX = 300 var positionY = 200 var radius = 13 // 构造球体的形状和表面积 var bodyDef = new this.B2BodyDef() var fixDef = new this.B2FixtureDef() fixDef.density = 0.6 fixDef.friction = 0.8 fixDef.restitution = 0.1 bodyDef.type = this.B2Body.b2_staticBody bodyDef.position.x = positionX / this.pxPerMeter bodyDef.position.y = positionY / this.pxPerMeter fixDef.shape = new this.B2CircleShape(radius / this.pxPerMeter) this.ball = this.world.CreateBody(bodyDef) this.ball.CreateFixture(fixDef) },
物体的生成需要定义两个属性变量,一个是body, 一个是fixture,body的设置决定物体的形状,fixuture决定物体的表皮属性,在代码中我们通过density设置物体密度,fricition设置物体的摩擦力,restitution设置物体碰撞后的恢复力,在设置body时,我们把小球指定为静态物体,然后通过B2CircleShape构造一个圆形体型,当我们调用world.CreateBody后,我们就在物理引擎的虚拟世界里制造了一个小球。
接下来我们看看篮球架的绘制,代码如下:
// change 6 createHoop () { var hoopX = 50 var hoopY = 100 var bodyDef = new this.B2BodyDef() var fixDef = new this.B2FixtureDef() fixDef.density = 1.0 fixDef.friction = 0.5 fixDef.restitution = 0.2 bodyDef.type = this.B2Body.b2_staticBody bodyDef.position.x = hoopX / this.pxPerMeter bodyDef.position.y = hoopY / this.pxPerMeter bodyDef.angle = 0 fixDef.shape = new this.B2PolygonShape() fixDef.shape.SetAsBox(5 / this.pxPerMeter, 5 / this.pxPerMeter) var body = this.world.CreateBody(bodyDef) body.CreateFixture(fixDef) bodyDef.type = this.B2Body.b2_staticBody bodyDef.position.x = (hoopX + 45) / this.pxPerMeter bodyDef.position.y = hoopY / this.pxPerMeter bodyDef.angle = 0 fixDef.shape = new this.B2PolygonShape() fixDef.shape.SetAsBox(5 / this.pxPerMeter, 5 / this.pxPerMeter) body = this.world.CreateBody(bodyDef) body.CreateFixture(fixDef) // 构建篮板 bodyDef.type = this.B2Body.b2_staticBody bodyDef.position.x = (hoopX - 5) / this.pxPerMeter bodyDef.position.y = (hoopY - 40) / this.pxPerMeter bodyDef.angle = 0 fixDef.shape = new this.B2PolygonShape() fixDef.shape.SetAsBox(5 / this.pxPerMeter, 40 / this.pxPerMeter) fixDef.restitution = 0.05 var board = this.world.CreateBody(bodyDef) board.CreateFixture(fixDef) }
篮球架由两个正方体和一个长方体组成,代码先绘制两个正方体,然后在绘制竖直的长方体,他们合在一起就形成了篮板。接着我们实现小球的弹射功能,这是本节的重点和难点。我们先实现一个获取小球所在位置的函数:
// change 3 ballPosition () { var pos = this.ball.GetPosition() return { x: pos.x * this.pxPerMeter, y: pos.y * this.pxPerMeter } },
接下来我们确定小球的发射方式,想要弹射小球时,鼠标先在小球上面按下,然后移动鼠标到目的地,然后松开鼠标,这时小球就会弹射出去。鼠标按下是的位置,与鼠标松开时的位置构成了一个方向向量,小球会根据这个方向发射出去。
在现实世界中,我们向某个方向抛出一个物体时,会对物体沿着指定方式施加一个冲击力,学过初中物理就可以知道,一个方向的力根据平行四边形法则,可以分解成任意两个方向的作用力,在这里,我们要把作用力分解成水平方向和竖直方向的作用力,如下图:
屏幕快照 2018-07-10 下午3.56.28.png
上面三角形中,r所对应的边就是外力的方向,根据平行四边形法则,我们把r分解成两个方向的力,分别是竖直方向的y和水平方向的x,竖直方向力的大小为r*sin(θ),水平方向的力大小为r*cos(θ),由于小球受到重力的作用,重力的方向与r所产生的竖直方向的力相反,因此竖直方向上的力y不断减少,直到变成负数,也就是竖直方向的力从向上转为向下,这就是为何小球被抛出后,它先向上做曲线运动,然后再向下做曲线下落。我们需要计算x和y的大小,把它合成一个向量,调用Box2D的接口,这样才能模拟力r作用到小球上。接下来我们需要计算θ的大小。
θ值不难计算,在上图中,向量r的低点就是鼠标在小球上按下时的位置,高点其实就是鼠标松开时的位置,我们把两个位置的y坐标和x坐标相减,就能得到上图的y和x,由此我们可以计算tan(θ),然后我们调用Math.atan计算tan的反函数就可以得到θ的大小。但是我们在计算时还需考虑到方向的问题,如下图:
屏幕快照 2018-07-10 下午4.06.30.png
中间的ball position其实就是鼠标按下时的位置,cursor就是鼠标松开时的位置,我们计算出θ值后,还得根据cursor所在的象限对θ值做一个变化,当鼠标在第一象限松开时,θ值不变,在第二,三象限松开时,θ需要加上π,在第四象限时,需要加上2*π。因此角度的计算代码如下:
// change 4 launchAngle (stageX, stageY) { // 根据鼠标方向设置小球发射方向 var ballPos = this.ballPosition() var diffX = stageX - ballPos.x var diffY = stageY - ballPos.y var degreeAddition = 0 //Q1 if (diffX < 0 && diffY > 0) { console.log('launchAngle: Q2') degreeAddition = Math.PI } else if (diffX < 0 && diffY < 0) { degreeAddition = Math.PI console.log('launchAngle: Q3') } else if (diffX > 0 && diffY < 0) { console.log('launchAngle: Q4') degreeAddition = Math.PI * 2 } var theta = Math.atan(diffY / diffX) + degreeAddition return theta },
函数中传入的stageX,stageY就是鼠标松开时所在的页面坐标,我们计算出x,y,得到tan(θ)的值,然后判断鼠标松开时在哪个象限,根据所在象限确定θ是否需要加上π,还是2*π,或者是不加,有了角度之后,我们就需要确定r的大小,然后将r分解成两个方向上的力量。
弹射力r的大小如何计算呢?我们根据鼠标按下到松开的时间间隔来计算,这就像弹弹弓,当你把弹弓拉的越久,松手后弹射力就越强,我们看看代码的实现:
shootBall (stageX, stageY, ticksDiff) { this.ball.SetType(this.B2Body.b2_dynamicBody) var theta = this.launchAngle(stageX, stageY) var r = Math.log(ticksDiff) * 50 var resultX = r * Math.cos(theta) var resultY = r * Math.sin(theta) // 让球产生自转 this.ball.ApplyTorque(30) var vec = new this.B2Vec2(resultX / this.pxPerMeter, resultY / this.pxPerMeter) // 给球体添加指定方向的冲击力从而让球发射出去 this.ball.ApplyImpulse(vec, this.ball.GetWorldCenter()) this.ball = undefined },
函数传入参数stageX,stageY表示鼠标松开时的页面坐标,ticksDiff记录鼠标按下到松开的时长,代码先调用SetType把小球有静止物体转变为运动物体,然后调用launchAngle计算出力分解的夹角,在这里需要注意的是,弹射力r的确定,这里使用的是log(ticksDiff)*50,也就是将鼠标按下到松开的时间取对数后再乘以50.着意味着鼠标按着的时间越久,弹射力就越大,然而力量的大小很难直接从鼠标按下的时间来决定,力量的大小不好根据时间来线性增加,我们这里默认力量的大小与时间成一个对数关系,当然你也可以用另外一种数学关系来确定弹射力r与鼠标按下时间的连续,上面只是一种经验性的做法。
有了弹射力r,以及分解角度,我们就可以计算水平方向和竖直方向的作用力,然后将两个力组合成向量B2Vec2,当我们把这个力的向量作为参数,调用ApplyImpulse函数后,引擎就会模拟弹射力r作用到小球身上,在现实世界中,当球抛出去后,它自己会有一个自旋,为了实现这个效果,我们调用ApplyTorque(30),这样的话,在页面绘制时,小球就会有一个自旋效果。
接下来我们再完成一些相关代码:
createMyWorld () { // 设置重力加速度 var gravity = new this.B2Vec2(0, 9.8) this.world = new this.B2World(gravity, true) // change 8 this.createGameLevel() },
在调用createMyWorld构建虚拟世界时,我们就调用createGameLevel来构造小球和篮板。
data () { return { canvas: null, debugCanvas: null, createWorld: null, // change 9 isPlaying: true, tickWhenDown: 0, tickWhenUp: 0 } }, ... methods: { init () { ... // change 10 this.stage.on('stagemousedown', this.stageMouseDown) this.stage.on('stagemouseup', this.stageMouseUp) ... }, // change 11 stageMouseDown (e) { console.log('mouse down') if (!this.isPlaying) { console.log('mouse down return') return } this.tickWhenDown = this.cjs.Ticker.getTicks() console.log('mousedown', this.tickWhenDown) }, stageMouseUp (e) { if (!this.isPlaying) { return } this.tickWhenUp = this.cjs.Ticker.getTicks() var ticksDiff = this.tickWhenUp - this.tickWhenDown this.shootBall(e.stageX, e.stageY, ticksDiff) // 发射后500毫秒再生成一个小球 setTimeout(this.spawnBall, 500) }, }
我们监听两个鼠标事件,分别是按下事件和松开事件,当鼠标按下时,我们开始记录按下时间,当鼠标松开时,计算鼠标按下了多久,同时得到此时鼠标所在的坐标,然后调用shootBall引发小球受到作用力r后的弹射特效,同时在500毫秒后,在原位置重新绘制一个新的小球。
作者:望月从良
链接:https://www.jianshu.com/p/700c7e969bc8
共同学习,写下你的评论
评论加载中...
作者其他优质文章