本节,我们将利用Box2d引擎在页面中实现球体飞行和撞击效果。在现实中我们向外抛出一个球时,它在重力加速度的情况下会飞出一个弧线,撞到物体后它会反弹折射,我们利用Box2D可以在页面里模拟这些特性。我们将在页面里绘制一个小球,然后设置一些障碍物,我们用鼠标控制小球向外抛出的方向,小球碰到障碍物后会像现实中一样发生反弹和折射。完成本节后,我们得到效果如下:
如上图,右下角是一个圆球,左上角是障碍物,用鼠标点击小球并向左上角拖动时,小球就会模拟受到一股像外抛出的力量。当小球与左上角障碍物相撞后,会发生反射,效果如下:
左上角红色小球就是碰撞后停留在障碍物上,具体的动画特效请参更详细的讲解和代码调试演示过程,请点击链接
首先我们用代码构造上图中的小球和由三个方块构造出的篮球架,在gamescenecomponent.vue中添加代码如下:
// change 1
createGameLevel () {
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
}
},
接下来我们确定小球的发射方式,想要弹射小球时,鼠标先在小球上面按下,然后移动鼠标到目的地,然后松开鼠标,这时小球就会弹射出去。鼠标按下是的位置,与鼠标松开时的位置构成了一个方向向量,小球会根据这个方向发射出去。
在现实世界中,我们向某个方向抛出一个物体时,会对物体沿着指定方式施加一个冲击力,学过初中物理就可以知道,一个方向的力根据平行四边形法则,可以分解成任意两个方向的作用力,在这里,我们要把作用力分解成水平方向和竖直方向的作用力,如下图:
上面三角形中,r所对应的边就是外力的方向,根据平行四边形法则,我们把r分解成两个方向的力,分别是竖直方向的y和水平方向的x,竖直方向力的大小为r*sin(θ),水平方向的力大小为r*cos(θ),由于小球受到重力的作用,重力的方向与r所产生的竖直方向的力相反,因此竖直方向上的力y不断减少,直到变成负数,也就是竖直方向的力从向上转为向下,这就是为何小球被抛出后,它先向上做曲线运动,然后再向下做曲线下落。我们需要计算x和y的大小,把它合成一个向量,调用Box2D的接口,这样才能模拟力r作用到小球上。接下来我们需要计算θ的大小。
θ值不难计算,在上图中,向量r的低点就是鼠标在小球上按下时的位置,高点其实就是鼠标松开时的位置,我们把两个位置的y坐标和x坐标相减,就能得到上图的y和x,由此我们可以计算tan(θ),然后我们调用Math.atan计算tan的反函数就可以得到θ的大小。但是我们在计算时还需考虑到方向的问题,如下图:
中间的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毫秒后,在原位置重新绘制一个新的小球。
上面代码完成后,就可以实现本文开头所描述的功能,具体效果请参看视频:
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号: