Android jbox2d实现碰撞效果

2022-05-10 20:52:42 浏览数 (2)

最近有个需求需要实现弹性碰撞,需要用到物理引擎实现弹性碰撞。比较场景的物理引擎是 box2d,有一个 Java 版本的 jbox2d 则可以在 Android 上运行。

jbox2d 的地址是 https://github.com/jbox2d/jbox2d,jbox2d 内部模拟了真实的物理世界里物体的运动规则,引擎把计算出的坐标告诉使用者,使用者可以通过这些坐标去完成最终的绘制。

基本概念

开始编写我们的碰撞 demo 之前,我们先了解一下 box2d 里面常用的一些基础概念。

  • shape 形状,就是我们理解的那个形状
  • body 刚体,就是一个物体,刚体是一个力学概念。指的是一个物体内力做功之和为0,因此刚体在外力作用下发生的形变可以忽略,即刚体上任意两点的距离是保持不变的
  • fixture 固定装置,这个可以绑定一些特性给物体,例如密度,摩擦力等等
  • world 世界,这个世界代表的应该就是物理引擎里面的物理世界,相当于是各个概念的一个集合。box2d 里的各种概念构成了这个物理世界

‍‍‍实现效果

基于上面这些概念,我希望用 jbox2d 去实现一个这样的效果:底部发射小球,当小球碰撞到手机屏幕边缘的时候,小球会弹开,并且在重力的作用下小球的运动速度逐渐减弱最终会在底部停止。这里先看下最后的实现效果:

http://mpvideo.qpic.cn/0b2eluaasaaal4aap4tc3brfaxodbfoqacia.f10002.mp4?dis_k=e3d94c34a34617794b3bafc11c4dcd5a&dis_t=1652187132&vid=wxv_2365604422866501633&format_id=10002&support_redirect=0&mmversion=false

实现思路

我们把小球放在屏幕的最下面,整个弹射碰撞的过程有几个必须的要素:

  • 边界 :这里我们把屏幕四个边作为碰撞的边界,边界宽高就是屏幕宽高
  • 小球:一个运动中的刚体,主要还要依赖它自身的一些物理属性
  • 重力:世界本身是有重力的,重力的方向是设置成往下,和日常一样
  • 初始线速度:线速度是一个矢量,用小球的质点在运动时候轨迹的切线来表示,想要小球顺利的弹出去,线速度矢量横竖轴方向大约要设置为:(width / 2, width/2*(height/width))

demo实现

添加依赖

首先依赖 jbox2d 库到工程内:

代码语言:javascript复制
implementation group: 'org.jbox2d', name: 'jbox2d-serialization', version: '1.1.0'
implementation group: 'org.jbox2d', name: 'jbox2d-library', version: '2.2.1.1'
创建 jbox2d 相关的内容

我们把 Jbox2d 相关的逻辑封装在一个 JboxImple 类内,这个类主要负责几件事:

  • 初始化 World
  • 构造边界
  • 构造运动刚体
  • 开始运动,获取计算结果

首先初始化 World, 需要给 World 一个重力,我们设置成和现实一样,这里图个方便写成 10f,方向是向下的,所以是正数:

代码语言:javascript复制
class JboxImpl {
  private val world:World = World(Vec2(0f,10f))
}

接下来要确定世界的大小,我们的世界映射到 APP 内其实就是屏幕,所以世界的大小就是屏幕的宽高,但是笔者试了下,如果完全设置的一样,那么box2d计算的会比较慢,所以这里我们还需要弄个屏幕宽度和世界宽度的比例,把世界宽度设置成10,后续的计算都通过比例计算,所以还需要几个全局的变量:

代码语言:javascript复制
const val WIDTH = 1f  // 常量,小球在世界的宽
const val WIDTH_WORLD = 10f  // 常量,世界宽
  
private var width = 0f    // 宽度
private var height = 0f  // 高度
private var ratio = 1f   // 高宽比
private var ratioForBox2dToScreen = 1f // 屏幕宽和世界宽的比例

因为最后需要把 jbox2d 计算的结果反馈到 View 层,所以需要暴露一个设置宽高的地方:

代码语言:javascript复制
fun onSizeChanged(w: Int, h: Int) {
  width = w.toFloat()
  height = h.toFloat()
  ratio = height / width
  ratioForBox2dAndScreen = width/ WIDTH_WORLD
  initEdges()
}

在这里我们构造我们的边界:

代码语言:javascript复制
fun initEdges() {
  // 创建边界

  val edgeList=  listOf(
    Vec2(0f,0f),
    Vec2(WIDTH_WORLD, 0f),
    Vec2(WIDTH_WORLD, WIDTH_WORLD*ratio),
    Vec2(0f, WIDTH_WORLD*ratio),
  )

  for (i in 0..3) {
    val bodyDef = BodyDef()
    bodyDef.type = BodyType.STATIC
    val body = world.createBody(bodyDef)
    val boxShape = EdgeShape()
    boxShape.set(edgeList[i], edgeList[(i 1)%4])
    val fixtureDef = FixtureDef()
    fixtureDef.shape = boxShape
    fixtureDef.density = 1f
    fixtureDef.restitution = 1f
    body.createFixture(fixtureDef)
  }
}

这里通过 EdgeShape 围了四个边,每个边创建了静态的刚体。这里需要注意一下 restitution 这个属性,这个指的是弹性恢复系数,取值在[0,1]之间。当r是0的时候,碰撞为完全非弹性碰撞,为1的时候,为完全弹性碰撞。一般来说弹射效果都是非弹性碰撞,所以千万不要把这个值漏设或者设为接近0的,不然你会发现碰撞之后小球看起来更像是往上跑了,而不是“反弹”。

接下来我们创建运动刚体:

代码语言:javascript复制
fun createBody() {
    val bodyDef = BodyDef()
    bodyDef.type = BodyType.DYNAMIC
    bodyDef.position = Vec2((WIDTH_WORLD/2), WIDTH_WORLD*ratio)
    bodyDef.linearVelocity = Vec2(WIDTH_WORLD, -WIDTH_WORLD*ratio)
    val circleShape = CircleShape()
    circleShape.radius = WIDTH/2
    val ballFixtureDef = FixtureDef()
    ballFixtureDef.shape = circleShape
    ballFixtureDef.density = 1f
  }
  val ballBody = world.createBody(bodyDef)
  ballBody.createFixture(ballFixtureDef)
}

这里刚体创建的形状是 CircleShape,起始坐标是屏幕下方 1/2 处,即 ((WIDTH_WORLD/2), WIDTH_WORLDratio)。因为小球初始运动方向在竖轴上是往上的,所以需要设置为负数:(WIDTH_WORLD, -WIDTH_WORLDratio)。

最后我们提供一个重绘方法,内部很简单,就是调用 Worldstep方法,这里会进行每一步的计算:

代码语言:javascript复制
fun invalidate() {
  world.step(16/1000f,20,20)
}

这里 step 有 3 个参数

  • dt:每一步的时间间隔,单位是秒。demo里我就每一帧获取一次
  • velocityIterations 和 positionIterations, 速度和位置的迭代次数,大部分物理引擎都有的属性,设的越大,计算精度越高,开销也越大

这些值在实际需求里还是需要进行调整的。这里 jbox2d 相关的东西都做好了,接下来要做的就是把计算结果告诉 Android 的 View,让View去绘制。

View层绘制

创建一个自定义View来绘制我们的小球:

代码语言:javascript复制
class JboxView @JvmOverloads ocnstructor(context:Context,attrs:AttributeSet?=null)
  : View(context, attrs) {
    private var ratioForBox2dAndScreen = 1f
    
    private val paint by lazy {
      Paint().apply {
        style = Paint.Style.FILL
        color = Color.RED
      }
    }
    
    val jboxImpl by lazy { JboxImpl() }
    
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
      super.onSizeChanged(w,h,oldw,oldh)
      jboxImpl.onSizeChanged(w,h)
      ratioForBox2dAndScreen = w/ WIDTH_WORLD
    }
   
}

在ondraw里面,我们需要获取 World 的回调数据,根据 jbox2d 内的坐标和屏幕映射比例计算出实际的View坐标。WorldgetBodyList 可以获取到世界里所有的 Body,坐标则根据 BodygetPosition 获取。注意这里我们只需要获取代表运动刚体的 Body, 可以根据 type 是 Dynamic 的进行选择:

代码语言:javascript复制
override fun onDraw(canvas: Canvas?) {
  super.onDraw(canvas)
  val list = jobxImpl.world.bodyList
  while (list != null) {
    if (list.type == BodyType.DYNAMIC) {
      val position = list.position
      val x = (position.x- WIDTH/2)*ratioForBox2dAndScreen
      val y = (position.y- WIDTH/2)*ratioForBox2dAndScreen
      val radius = WIDTH/2*ratioForBox2dAndScreen
      canvas.drawCircle(x,y,radius,paint)
    }
    list = list.next
  }  
  jobxImpl.invalidate()
  invalidate()
}

最后我们通过点击触发一次碰撞运动:

代码语言:javascript复制
// in activity
jboxView.jboxImpl.startWorld()
  
// in JboxImpl
fun startWorld(){
  createBody()
  invalidate()
}

总结

这里就完成了一个碰撞效果的demo,实际需求中我们会基于这些 api 做更加复杂的效果。使用box2d非常适合完成一些复杂的碰撞动效,尤其是希望运动轨迹符合真实的物理定律的。从效果看还是很棒的,box2d里面还有其他的一些概念例如关节之类的,物理引擎在一些游戏的开发中也是非常重要的地位,感兴趣的朋友也可以进一步研究。

0 人点赞