Box2D(C#版)
选择从Step
说起是因为Step
是Box2D对一个逻辑帧的实现。了解了Step
,你就了解了物理引擎的逻辑结构。
public void Step(float timeStep, int velocityIterations, int positionIterations)
timeStep
: 逻辑帧的时间长度.velocityIterations
:对于两个速度不同的物体碰撞时, 根据物体的质量速度大小和方向重新分配, 也需要进行矫正.positionIterations
:对于刚体碰撞之后重叠的矫正处理, 设置为1则重叠无法被即使矫正, 通常设置为10, 越高所需资源消耗也会越大.
什么是AABB
平行于坐标轴的包围盒子(Axis Aligned BoundingBox,AABB ).
Fixture
类的GetAABB()
返回的是能包围当前形状的最小矩形.
Fixture
类的CombineAABB()
能将一个刚体里的2个Fixture
合并.(有多个fixture
的话遍历这个刚体依次调用CombineAABB()
)
举个游戏上的例子:
AABB碰撞树
(1)AABB碰撞树是二叉平衡树。
(2)每一个叶子节点都是一个刚体的AABB。
(3)非叶子节点也是一个AABB,但并不是刚体的AABB。
(4)父节点的AABB总是包涵子节点的AABB。
(5)新的叶子节点插入是第一优先级是保持树的平衡。第二是插入到而两个子节点中(由于两个节点并没有排序,所以请允许我使用第二个来形容它的位置)。
(6)详细的树处理操作在DynamicTree.cs
中。
二叉树
二叉树binary tree是指每个节点最多含有两个子树的树结构。
特点:
- 所有节点最多拥有两个子节点,即度不大于2
- 左子树的键值小于根的键值,右子树的键值大于根的键值。
因为二叉树只是定义了简单的结构,所以存在多种深度可能,导致二叉树的效率低,所以引入了平衡二叉树。
平衡二叉树
平衡二叉树,基于avl算法,即是avl树(avl tree)
特点:
- 符合二叉树的条件下
- 任何节点的两个子树的高度最大差为1
如果在avl 树,中进行插入和删除节点操作,可能导致avl树失去平衡,那么可以通过旋转重新达到平衡。因此我们说的二叉树也称自平衡二叉树。
红黑树
红黑树和avl树类似,都是在进行插入和删除操作时通过特定的操作保持二叉树的平衡,从而获得较高的查找性能。
在java中TreeSet,TreeMap的底层就是用的这个方法。
特点:
- 节点是红色或黑色
- 根节点是黑色
- 叶子节点(nil,空节点)是黑色
- 每个红色节点的两个子节点都是黑色
使用AABB碰撞树进行初步检测
使用AABB碰撞树无疑是为了进行初步筛选,在AABB都无法碰撞的情况下刚体肯定无法碰撞。而且矩形碰撞检测极其简单(只需要检测矩形A的右上角是否在B的左下角的左边或下边和与B的右上角是否在A的左下角的左边或下边)。而在查询的时候只需要使用刚体的AABB在树中进行遍历(剪掉节点AABB与刚体AABB不碰撞的节点及其子节点)就能初步获取所有可能产生碰撞的刚体。并组成碰撞元素(Contact)。
如果一个刚体被标记为传感器(sensor)的话,他是不会触发碰撞初始化处理的。需要注意的是,他只会出发开始碰撞和结束碰撞,并不会持续发送碰撞信号。如果一个刚体没有被标记为传感器,在发送碰撞时它还会回调PreSolve
.这个方法本意是允许用户进行自定义个物理模拟。当然了,你也可以拿来作别的事情。
传感器
传感器是一种会发送碰撞信号但不进行系统物理碰撞模拟的刚体(你也可以自己模拟碰撞效果)。
传感器是可以穿过边界的,他真不会进行任何系统的物理碰撞。所以如果一个传感器在高速运动。你需要在有需要的时候销毁它。
当我们要模拟一些技能效果的时候,就可以使用传感器。
island是什么
Box2D在进行物理模拟(也包括下面的连续物理模拟)时使用到了island减小计算规模。Box2D将大量需要进行模拟的元素分为一个一个的island,然后再进行物理碰撞模拟。
island的定义:
- island 由刚体,碰撞元素,连接器(joint)组成。
- island 与island 之间互不影响(这是island存在的理由,也使得Box2D对island可以生成一个处理一个)。
大量的刚体,碰撞事件和连接器被分为多个island后可以有效的降低物理模拟是消耗的计算资源。原因是碰撞的算法复杂度为O(n2)。这也是为什么一个复杂机器我们会将其分为几个小的模块进行模拟,而不是使用单个动力驱动并将其组为一个整体。
连续物理模拟
物理引擎中的时间确实粒子化的,无论几秒中计算多少次都无法模拟连续的时间(更何况由于cpu或gpu算力有限,时间1秒进行的计算次数并不多)。这就产生了一个问题,如果一个物体移动速度够快,在两次物理模拟计算中间穿越了另一个物体,引擎是无法发现的(一般情况下)。而连续物理模拟就是为了缓解这一问题。
- 连续碰撞只会针对标记为bullet的刚体进行处理。
刚体Body
创建刚体需要创建多个对象,
BodyDef
要创建的刚体信息定义FixtureDef
(辅助类)要创建的刚体信息定义- 固定设施(夹具): 用来定义刚体所固有的一些属性,通常是物体材料特性相关的一些属性.
Body
最终创建刚体通过此类- 此类构造函数基本是只在Box2D内部调用
创建刚体通过 world.CreateBody(bodydef);
FixtureDef
只是定义和暂存了刚体的属性, 就像BodyDef
和Body
关系一样, 还存在Fixture
类, Fixture
对象不能单独存在, 必须绑定到刚体Body
中才能起作用.
一个刚体可能包含不止一个形状,也就是会对应多个
Fixture
对象, 创建完刚体后,可以再次为其添加Fixture
对象, 实现多个形状的组合.
CreateFixture()
方法将FixtureDef
对象中包含的属性和刚体绑定再一起.
刚体形状
- 标准圆:
CircleShape
- radius: 半径 ,单位是(米)
- 多边形类:
PolygonShape
SetAsBox()
构造一个矩形, 传入半宽,半高- 需要进行米和像素的转化: 例如创建一个100px-200px的矩形,传入的参数50/30, 100/30
Set()
传入顶点列表(List<Vector2>
)
刚体类型
- BodyType.StaticBody 静态刚体
- BodyType.KinematicBody 可动刚体
- BodyType.DynamicBody 动态刚体
刚体的创建流程总结
创建刚体的过程的4个必备要素:
- BodyDef
- Body
- FixtureDef
- Shape
/// <summary>
/// 创造一个动态刚体
/// </summary>
public static Body CreateDynamicBody()
{
return Game.Scene.GetComponent<B2S_WorldComponent>().GetWorld().CreateBody(new BodyDef() { BodyType = BodyType.DynamicBody });
}
/// <summary>
/// 为刚体挂载一个矩形碰撞体
/// 传的UserData为B2S_HeroColliderData
/// </summary>
/// <param name="self"></param>
/// <param name="hx">半宽</param>
/// <param name="hy">半高</param>
/// <param name="offset">偏移量</param>
/// <param name="angle">角度</param>
/// <param name="isSensor">是否为触发器</param>
/// <param name="userData">用户自定义信息</param>
public static void CreateBoxFixture(this Body self, float hx, float hy, Vector2 offset, float angle, bool isSensor, object userData)
{
// 创建一个多边形对象
PolygonShape m_BoxShape = new PolygonShape();
// 设置多边形的形状, 宽高,坐标,角度
m_BoxShape.SetAsBox(hx, hy, offset, angle);
// 创建夹具对象
FixtureDef fixtureDef = new FixtureDef();
// 设置夹具属性: 是否是传感器
fixtureDef.IsSensor = isSensor;
// 应用形状
fixtureDef.Shape = m_BoxShape;
// 设置UserData,传的UserData为B2S_HeroColliderData
fixtureDef.UserData = userData;
// 绑定属性到刚体上
self.CreateFixture(fixtureDef);
}
碰撞
潜在碰撞: 最小包围盒AABB指的也是扩充后的FattenAABB. 包围盒发生重叠.
五个碰撞阶段:
- 潜在碰撞
- FattenAABB包围盒发生重叠
- 常规接触碰撞
- 刚体的形状发生了接触
- 碰撞重叠处理
- 对刚体重叠处的部分进行修复
- 停止常规接触
- 发生碰撞的形状之间开始分离,不再有接触
- 潜在风险消除
- FattenAABB包围盒不再有重叠
BOX2D将碰撞信息存在Contact对象
里, 包括了碰撞的位置,角度等信息. 获取方式:
GetContactList()
方法获取Contact列表
.- 是对Box2d世界里的所有Contact遍历.
- 通过
ContactListener类
的碰撞事件处理函数来获取当前碰撞的Contact对象
.
碰撞的不同阶段处理
PreSolve()
潜在碰撞处理- 从FattenAABB接触到FattenAABB分离整个过程都每次Step都持续运行.
BeginContact()
开始碰撞的处理- 只在初次接触时运行一次
EndContact()
结束碰撞的处理- 只在初次离开时运行一次
PreSolve(Contact contact, in Manifold oldManifold)
PostSolve()
- 在碰撞修复阶段消除形状重叠阶段一直运行, 直到两个形状彻底分离
PostSolve(Contact contact, in ContactImpulse impulse)
Contact对象
Box2d会根据碰撞检测的情况自动创建. 在潜在碰撞发生时被创建, 直到碰撞双方的FattenAABB不再重叠,才会被销毁.
Manifold
在碰撞发生时, 保存碰撞点坐标的对象.
ContactImpulse
记录了碰撞时产生的冲量.
normallmpulses
属性: 垂直于碰撞面的冲量.tangentlmpulses
属性: 平行于碰撞面的冲量.
Box2D的碰撞体可视化Debugger组件
public LineRenderer lineRenderer;
https://connect.unity.com/doc/Manual/class-LineRenderer
LineRenderer线渲染器主要是用于在3D中渲染线段,虽然我们也可以使用GL图像库来渲染线段,但是使用LineRenderer我们可以对线段进行更多的操作,例如:设置颜色,宽度等。在这里要注意LineRenderer渲染出的线段的两个端点是3D世界中的点,即他是属于世界坐标(World Point)中的。
lineRenderer = this.transform.GetComponent<LineRenderer>();
//this.lineRenderer.SetWidth(0.05f, 0.05f);
// 设定开始处和结束处线段的宽度
this.lineRenderer.startWidth = 0.05f;
this.lineRenderer.endWidth = 0.05f;
this.lineRenderer.material = new Material(this.m_shader);
// this.lineRenderer.SetColors(Color.red, Color.red);
// 设定开始处和结束处线段的颜色
this.lineRenderer.startColor = Color.red;
this.lineRenderer.endColor = Color.red;