tl;drGraphics.RenderMeshInstanced快速绘制大量相同的东西。NativeArray可以减少GC的开销,使用它们如果可以,特别是临时缓冲区。Burst和Jobs大大加快了并行计算。您可以并行化射线检测,如果需要大量射线检测。CoreCLR = 更快(一般)。

我在最新的Unity 6000.7.0a2上进行了测试,使用CoreCLR构建,获得了相比于Mono构建的1.5倍速度提高。

(Reddit只允许上传一个视频,所以展示的是CoreCLR构建视频)

Projectiles/Second CoreCLR FPS Mono FPS Editor FPS
200k 27.627 23.493 21.236
20k 272.342 214.916 101.990
2k 606.566 379.503 162.061
200 719.585 448.791 186.622
20 768.078 485.566 192.665
2 795.456 494.170 190.303

射击半百万的子弹

我创建了一个使用分离的GameObject和articulation bodies连接在一起的炮塔。用于瞄准炮塔的脚本通过将目标方向向量投影到每个关节的旋转平面(旋转轴的法线)上,然后找到旋转角度到零向量(旋转角度为零时的前向向量),最后将关节的目标设置为该角度。炮塔的回旋效果也使用了一个prismatic articulation body。炮塔跟踪了一个红色的立方体,该立方体以2秒的循环动画进行了动画。

子弹本身是未管理的结构(仅包含值类型,无引用类型)。它们具有质量、阻力、面积、位置和速度(使用float3代替Vector3以获得性能提升)。它们都存储在一个大NativeList<Projectile>中。

模拟半百万的子弹

模拟子弹的过程是使用Burst编译的批处理并行作业完成的,它同时处理了下一帧的RaycastCommand设置和子弹的力、速度和位置更新。然后,射线检测结果被并行处理,使用raycastJob.ScheduleParallelByRef。射线检测结果被进一步处理在另一个批处理并行作业中,主要任务是过滤出与物体发生碰撞的子弹,并将它们的索引发送回主线程进行处理,使用NativeQueue<int>.ParallelWriter

在主线程上,过滤后的结果被迭代,应用力到刚体(和articulation bodies)上,产生冲击效果。 (冲击粒子使用一个普通的ParticleSystem,但另一个脚本直接通过C#接口发射粒子,而不是创建ParticleSystem预设的实例。)要删除子弹,首先需要排序索引,从高到低,然后使用一个交换和弹出方法(子弹最后一个索引和要删除的子弹索引交换,然后最后一个索引减少缓冲区长度)来删除子弹。这个方法是可能的,因为子弹的顺序并不重要。

绘制半百万的子弹

每个子弹不是一个GameObject(使用半百万个GameObjectMeshRenderer会让Unity崩溃),而是使用Graphics.RenderMeshInstanced进行绘制。矩阵被在另一个批处理并行作业中生成。一个大NativeArray<Matrix4x4>被分配到所有子弹的大小。然而,以便处理子弹的独特外观,每个材质/网格对都被分配一个渲染ID,该ID索引一个包含实际网格和材质的字典。子弹本身只持有该整数(子弹必须是未管理的,因此不能包含直接引用)。

子弹的每个渲染ID的数量被跟踪,当子弹被创建时。然后,当渲染时,一个NativeArray<int>被创建,其长度等于唯一的渲染ID的数量,并将每个渲染ID的起始偏移填入其中。为了确保线程安全,使用了Interlocked.Increment()来推进偏移量。然而,由于NativeArray没有直接提供引用,因此必须使用一些不安全的代码来获取一个指向NativeArray的指针,然后将其传递给Interlocked.Increment()

矩阵本身被生成,以便网格的z轴与子弹的速度方向一致,并沿着局部z轴扩展(按速度缩放)。

一旦矩阵被填充,仅仅需要调用Graphics.RenderMeshInstanced,传递正确的起始偏移量和长度即可渲染每个材质和网格组合。

备注

在模拟和渲染子弹时,内存使用保持在相对稳定的状态。 (从0到400k子弹,内存使用从90MB到135MB,相应于每个子弹的约112字节)。在模拟和渲染子弹时,基本上没有GC,因为缓冲区使用了未管理的NativeArray,并且被显式释放。 (我相信Unity也对TempJobTemp NativeArray的分配进行了内部优化。另外,停止Unity从零开始初始化,避免了创建大Matrix4x4数组时浪费几毫秒的时间,因为这些数组将被覆盖。)

在400k子弹时,主线程开始变得卡顿,主要是因为执行射线检测作业和矩阵计算作业。然后是主线程上的射线检测结果处理,主要是因为应用力到刚体上和生成冲击粒子。

CoreCLR也显著提高了性能,我之前并没有认为这是可能的,因为大部分时间都花费在并行Burst编译的代码上。然而,这也被展示了出来,低子弹数量时FPS提高了约60%,而高子弹数量时FPS仅仅提高了17%,这表明CoreCLR构建加速了其他与性能代码相关的内容。 (我将尝试将Profiler附加到CoreCLR构建上。) CoreCLR最有可能对传统的面向对象C#代码有更大的影响,而不是Burst编译的热代码。

现在我应该开始考虑使用ECS,因为它允许物理处理被并行化。然而,我做的实际上是一个数据驱动系统。

如果您有任何问题或我有任何错误,请告诉我。