Box2DSharp使用手册#3

  1.7 C#, 3.2 游戏引擎技术
  • #3部分为整个Box2D系统结构的解释,以及其运行的原理和相应步概述。不清楚有没有#4,如果有#4则会对每一个物理求解过程进行推导阐述。
  • 上一章链接:传送门
  • 需要前置知识:高等数学,大学物理

1、世界

1.1 基础信息

  • 世界-World为整个物理系统的管理运行系统,其结构如下
  • 其中:FP、FVector2、FVector3为定点数。将所有浮点数改为定点数后,可以制作确定性的物理引擎。
Box2D World 1024x476 - Box2DSharp使用手册#3
  • 其中世界主要包含三个主要内容:Body、Contact、Joint。其中Body为每一个物体的主体,类似于RigidBody的概念,主要存储每个物体的物理属性;Contact-接触点存储所有物体的接触信息;Joint-关节存储所有的关节。
  • 除此之外World有一些自身的世界属性,可以通过以下变量对物理世界运作内容有粗略概念:
FP _invDt0;				//时间步倍率,所有的冲量都会乘以该倍率。该数值为时间间隔的倒数
bool _stepComplete;		//时间步完成
bool HasNewContacts;	//存在新接触点
IDestructionListener DestructionListener;//析构监听器
IDrawer Drawer;			//调试绘制
bool ContinuousPhysics;	//是否启用连续碰撞
FVector2 Gravity;		//重力常数
bool IsAutoClearForces	//清除受力
bool IsLocked;			//是否锁定世界
bool AllowSleep;		//世界是否允许休眠
bool SubStepping;		//子步进,调试物理系统时可以开启,物理系统会按每帧执行
bool WarmStarting;		//热启动,启动以减少约束求解的迭代次数
ContactManager ContactManager;	//接触点管理器
LinkedList<Body> BodyList;		//物体链表
LinkedList<Joint> JointList;	//关节链表

1.2 结构详述

  • 物理世界存储的主要内容为Body、Joint、Contac以及一个接触点管理系统ContactManager他们共同构成了物理世界的存储结构。
  • 物理世界的运算求解结构主要为iland以及ContactManager中的broadPhase,物理系统通过粗检测-岛屿划分-近似迭代求解以得到每一个时间步(timestep)的近似计算结果。
  • 对于Body来说:概念和proxy、rigidbody或者entity相似,为一个物体的实体,其中包含阻尼、质心点、质量、位置、受力等等物理系数参数以及存储一个Body上所有夹具的FixtureList、所有链接到的接触点LinkedList-ContactEdges、所有连接到的关节LinkedLis-JointEdges。其中接触点和关节的Edges概念和图相似,Contact为一条边,每条边链接两个节点(即Body)。 对于Fixture来说,夹具存储了图层的概念,是否为传感器类型,摩擦力,恢复系数,形状,夹具代理。具体的物理求解(AABB、RayCast)是根据Fixture中的形状(Shape)来进行求解
  • 对于Joint来说,关节会链接两个物体,以及会有多种关节类型。
  • 对于Contact来说,当两个物体碰撞时才会产生接触点,并被记录在ContactManager中。每个Conatct记录的不是Body而是Fixture,即两个夹具形状间的接触。

1.3 物理世界原理-概览

  • 原理:Box2d会采用BroadPhase进行粗检测(采用AABB检测),然后根据设定的迭代次数进行迭代来得到近似值得求解。
  • 运行:
public void Step(FP timeStep, int velocityIterations, int positionIterations)
  • 每一个world都会在其Step函数中进行物理运算求解,其需要三个变量。计算timeStep时间后的物理世界,velocityIterations速度求解次数修正,positionIterations位置求解次数。 每一个timeStep成为一个时间步,可以同等理解为Update中的一个detatime
  • 首先,如果增加了新的夹具(if (HasNewContacts)),则会在ContactManager中去寻找新的接触点。然后世界会进入锁定状态(IsLocked = true;)该锁定状态会阻止任何夹具,物体,关节等的创建和销毁以防止物理系统受其影响。
  • 第二、更新所有已有的接触点ContactManager.Collide();
  • 第三、对速度进行积分,求解速度约束,整合位置
  • 第四、处理碰撞时间
  • 第五、启用受力清理,解锁世界

1.4 物理世界原理-详述

  • 详细原理可以参照知乎系列文章:https://zhuanlan.zhihu.com/p/109532468
  • 在第一步骤中,会直接调用World.ContacManager.BroadPhase的BroadPhase.UpdatePairs去更新新的接触点。在BroadPhase中会存储一颗动态树DynamicTree,该树结构为平衡二叉树用以处理AABB检测以及射线检测。BroadPhase中的pairs存储相交记录,在UpdatePairs中会去查询树中的AABB然后对pair进行回调。
  • 关于BroadPhase:物理系统会在碰撞处理之前进行碰撞检测,而如果进行完全的碰撞检测需要对所有物体两两之间进行遍历,效率为N^2。因此提出了BroadPhase的概念,在这一步中利用算法进行粗略的AABB检测,以快速筛选出那些物体有可能会发生碰撞。之后再进入到NarrowPhase阶段对这些可能碰撞的物体按照划分区域,每个区域进行N^2遍历检测。 Box2D中的BroadPhase通过DynamicTree实现检测原理,并通过移动缓冲区(m_moveBuffer)和pair缓冲区(m_pariBuffer)来维护碰撞关系。
  • 在第二步中,主要实现碰撞处理。分为计算接触点其中ContacManager会使用一个List来维护存储所有的Contact。 计算接触点:1、遍历所有的接触点并分别进行判断(1)如果接触点所属的两个Body不会发生碰撞个,或者接触点所属的两个夹具不会发生碰撞,或者两个夹具的代理在经过BroadPhase的AABB重叠检测后没有发生重叠,则删除该碰撞点 (2)如果接触点发生了碰撞,则更新改点的监听。在监听中会进行碰撞前的预处理工作。
  • 形状间实现碰撞,必须两个碰撞形状中至少有一个形状要有体积,而链形状每条边都被看作一个边缘形状,此时我们只要实现圆形、多边形、边缘三个具体形状间的碰撞,因为边缘形状没有体积,故不存在边缘与边缘之间的碰撞。剩下还有边缘和圆,边缘和多边形,圆和圆,圆和多边形,多边形和多边形等这5种,我们将这5中分成如下三类: 具体实现原理可以参照博客:https://blog.csdn.net/cg0206/article/details/8441463?spm=1001.2014.3001.55021、 边缘形状有关的碰撞。即边缘与圆,边缘与多边形2、 圆形形状有关的碰撞。即圆和圆,圆和多边形3、 多边形形状有关的碰撞。即多边形和多边形
  • 在第三步中,会重构所有岛屿,对速度进行积分,求解速度约束,整合位置。具体步骤分为:1、清除所有物体、接触点、关节的岛屿标志,并初始化岛屿。2、将BodyList中的第一个Body push到stack中并对于其约束图进行DFS搜索,并对其所有body、contact、contactedge进行岛屿标记island.Add。3、进行岛屿碰撞求解 4、进行下一个body
  • 在第四步中,会处理碰撞时间。其形式也为创建一个Island然后进行过求解。其另一主要工作是防止隧穿效应,如果想要尽可能的防止该类事件发生则要把需要的物体设置为bullet。
  • 关于两个物体是否碰撞的判断:1、通过两物体的aabb,判断是否重叠。2、通过GJK算法算出两物体间的距离,根据距离判断是否碰撞 3、通过SAT分离轴算法看是否能找出两物体间的分离轴,如果找得出就没有碰撞,找不出则碰撞。
  • 最后,第五步。清除在当前步骤中所有施加的力,然后解锁世界。

2. 物理快照

  • 对Box2D进行物理快照主要目的是为了进行帧同步的预测回滚操作。
  • 主要思路:备份整个物理世界。
  • 需要注意的是,物理世界中所有的类之间都存在相互引用关系,如果要备份所有对象无法使用序列化等方法。在其他物理系统的快照中,主要方法也是以存储所有物体的字典为主。
  • 做快照时先捋清楚整个世界的物体关系:Box2d为Body,ContactManager。根据1.1 基础信息中的图示:其主要关键点在于,对于所有引用类型的对象来说,那些需要new,那些需要根据原世界对应关系去在快照世界中查表重现这些关系。 1、new world 2、拷贝world所有值类型变量 3、构建Body列表并一一对应new body,并构建body并拷贝所有的值类型变量 4、构建所有Body中的Fixture,并拷贝所有值类型变量,并把其中的body指针一一指对。并记录下所有的Fixture 5、拷贝Body中的所有Fixture中的FixtureProxy 6、构建ContactManager 7、构建ContactManager中的所有Contact 8、构建Contact中的ContactEdge的Contact和Node 9、构建BroadPhase 10、构建DynamicTree
  • 回滚时需要注意的点:对于很多数据类型中都有Object UserData,这些是存储用户数据内容。需要明白每一个Object 具体存了对于那些内容的引用。一般来说这部分只需要进行引用拷贝即可,然后恢复其引用地址存储对象的对应关系。譬如
//以下为伪代码
world oldWorld;
world newWorld;
//此时oldWorld中有一个Body的Object UserData存储的是游戏中一个物体的RigidBody,该RigidBody也存储了对于该世界中改Body的引用指针
oldWolrd.body.userData = RigidBody;
//RigidBody结构
public class RigidBody
{
    public Body bd;
    bd = setBody();//此时bd = oldWolrd.body
}
//那么进行备份时需要
newWorld.body.userData = oldWolrd.body.userData;
//进行回滚时需要
if(newWorld.body.userData.getType() == new RigidBody.getType())
{
    var dt = newWorld.userData as RigidBody;
    dt.bd = newWorld.body;
}

3、物理系统优化

3.1 时间上的优化

  • 可以考虑对于AABB诸如此类,大批量重复计算的函数,或者遇到的for循环进行SIMD指令集优化,或者采用jobsystem + burst加速。
  • Box2DSharp版本也采用了LInkedList以及ArryPool进行了创建销毁、查找的优化。

3.2 空间上的优化

  • 对于快照来说-可以减少其备份的数据。仅备份关于物体的变化的遍历,譬如对于很多bool或者类似于一个世界的重力,根据项目的需求可以酌情备份。 如果不考虑完美回溯的话,可以仅备份Body列表即可,然后通过ContactManager进行Contact的重新生成。

LEAVE A COMMENT