众所周知,物理开销一直是 CPU 的一个大头,而且还很容易出问题。对于开放世界,该如何进行物理运算,以及采用什么方案计算碰撞。
本文针对这个问题做了一些细微的研究,算是对 Unity 下的解决方案有了一个大致的方向。
1、现有物理方案
目前 Unity 里可用的物理方案(现成的)有3种:
- Unity 默认的 PhysicsX:默认的物理,大家用得最熟的就是这个;
- Unity Dots Physics:在 Dots 系统中的物理;
- Unity Havok Physics:基于 Havok 的物理系统;
这里我们对三种方案都进行一个测试:
1、底部放置一个 Mesh (地形),尺寸约 2000*100*800,顶点数 1229,三角面 2171 :
2、之后在空中 100*100 区域的范围洒落 球/立方体 碰撞:
3、等方块/球落地后基本稳定、帧数不变之后,记录当前的实体数量;
4、记录帧数刚好在 60/45/30 时对应的实体数量;
注:这里的测试方案我是选择了对于物理系统最不友好的方式(大量物体都积累在同一区域相互碰撞),如果物体均匀分散,性能压力会小很多。但进行压力测试时,都是考虑的最差情况。
这里我在小米9上进行了真机测试,结果如下:
三种物理系统的差别并不算大,Havok 的性能会好一些,而 Unity.Physics 和 默认的 PhysicsX 没有太大差异。究其原因,Unity 的默认碰撞 PhysicsX,其实也是多线程。
2、开放世界物理方案
通过上文的测试,无论采用什么方案,如果数量到达了数千的地步,都是接受不了的。如果考虑到开放世界中的场景丰度:石头、树木、关卡交互等,数量可以非常庞大。考虑到 小米9 并不是支持的最低端手机,因此建议同时激活的带碰撞的物体要一直控制在 1000 以下。我后面也没有纠结物理系统了,就直接使用 Unity 的 PhysicsX 就行了(会的人多,实现起来简单),即便使用其他的方案也不会有质变。
之前看到一个方案,就是通过射线检测来替代物理碰撞(《腾讯游戏开发精粹Ⅱ》第10章 物理查询介绍及玩法应用、第11章 基于物理的角色翻越攀爬通用解决方案),也能大大降低性能消耗。我这里简单介绍下这个方案的思路:
在玩家的移动方向上打射线(不一定只能从上往下打),越靠近玩家射线的精度越高。在计算时,先在最远处检测,如果检测到有障碍,便激活中距离的检测(精度更高、性能开销也越大);如果在中距离检测到了,便激活近处的射线检测(此时检测到的数据就与玩法相关,以实现爬墙、跳跃等规则)。
这种分级射线检测的好处,在玩家处于空旷地块时,可大幅减少射线检测的频率。此外,使用射线代替碰撞,能减少大量物理开销,也能减少因为物理碰撞产生的各种飞天遁地等表现异常。
3、岛屿轮廓的碰撞
我们的地形数据是生成的高度图,岛屿形状其实并不能在 CPU 中体现,自然也无法获取到类似于网格这种的碰撞数据。需求上,有需要船只会被岛屿边缘弹开(有类似推开的物理效果)的功能。针对海岛,我设计了基于有向距离场(Signed Distance Function,以下简称 SDF)的碰撞方案。
关于 SDF,建议先参考以下文章:
Unity 手把手教你实现有向距离场(SDF)图像生成工具(1)【猴子都能学会】 - 哔哩哔哩本文将采用Dead Reckoning算法实现一个有向距离场的生成工具,并实现一个如下的简单变形效果,当然对于SDF应用远不止于此,诸如原神的脸部阴影之类的,这个网上很多就不在这里详细讲了。该算法的优势相较于遍历全局耗时更少,虽有一定的精度下降,但相较于近似方法消除了产生的棱角,毕竟对于一张1024*1024的图要遍历全局的时间都够下楼买杯奶茶再上来了,而采用该算法可以在2秒内计算完成。 首先先创建一个ComputeShader和一个脚本如何创建ComputeShader给ComputeShader命名为https://www.bilibili.com/read/cv24786763/ 在我们游戏中,其至少有以下2个功能:
-
海水波浪计算:在岛屿靠近岛屿区域时,海浪高度(强度)减弱,且海浪不能漫过岛屿。
-
行船碰撞计算:开船不能直接撞上岛屿,也不能直接生硬拦停,通过SDF图可以拟合渐变实现碰撞转向的效果。
因为这个值需要在CPU、GPU都读取,在Unity2020+中可以使用 RawBuffer 辅助两边的数据共享。这里使用实时在线生成的方式生成SDF图,由GPU进行计算。
动态生成的好处是能支持动态的阻挡物,而且全局唯一一份(只在船或相机附近生成)固定大小的图,更为节省内存。但缺点是生成的结果不能立即拿到,需要一定时间进行生成,可能在2s左右才能获得最终结果(由于海洋主题中,船只的行进都是较慢的,所以问题不大)。
以玩家(相机)为中心,生成一张 1024*1024 的贴图,数据存储为 RGBA 32 bit,也就是每个像素4个字节。其中RG表示最近障碍物的距离,BA表示法线。整张图的重构逻辑依旧是AOI的九宫拆分:
SDF图的重建逻辑:初始化时,以玩家(相机)为中心生成一张1024*1024的贴图。玩家从A点移动到C点:当玩家在B点(黄色区域时)仍使用当前数据。当玩家移动到C点(离开黄色区域)则开始按照C点为初始点构建新的图(重新构建时有4~6个格子的数据可以复用,但一般还是直接整张图重建)。
在 ComputeShader 中,每帧进行一次迭代,每个像素计算周围8个格子,类似于流场的做法。虽然理论上,最坏情况需要 52 万次才能迭代完成,但实际应用中收敛次数不会这么多,具体需要测试。预测会在2~3s内完成收敛(迭代30~60次)。
之后,CPU 和 GPU 都可以通过 SDF 图数据来计算了。
4、总结
对于开放大世界的物理碰撞,根据现有解决方案,需要将带物理的物体控制在 1k 以下,就能很好地在手机上运行并支持玩法。
对于特殊的需求(例如翻墙、攀岩)等,通过射线手段进行辅助检测,减少物理碰撞的开销。基于 GpuTerrain 实现的地形,则需要通过生成 SDF 图的方式来辅助碰撞,基本上能满足需求。
对于一些对碰撞精度要求较高的需求,也可以通过取巧的方式来实现。(例如,如需要玩家在前进过程中自动绕开一些小型灌木。这种如果不能上物理和寻路、RVO,可以将灌木的数据记录在地块信息图中,在玩家经过时播放一个向左/向右的躲避的动画,在表现上与玩家绕开障碍就一般无异了)