前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android jbox2d实现碰撞效果

Android jbox2d实现碰撞效果

作者头像
烧麦程
发布2022-05-10 20:52:42
1.4K1
发布2022-05-10 20:52:42
举报
文章被收录于专栏:半行代码

最近有个需求需要实现弹性碰撞,需要用到物理引擎实现弹性碰撞。比较场景的物理引擎是 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里面还有其他的一些概念例如关节之类的,物理引擎在一些游戏的开发中也是非常重要的地位,感兴趣的朋友也可以进一步研究。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 半行代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本概念
  • ‍‍‍实现效果
    • 实现思路
      • 添加依赖
      • 创建 jbox2d 相关的内容
      • View层绘制
  • demo实现
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档