Unity3D ECS和作业系统

有了Unity3D,在2018年版本中,就可以使用本机(适用于Unity)ECS系统,该系统以Job System的形式添加了多线程功能。 互联网上的资料很少(Unity Technologies本身的几个项目和YouTube上的一些培训视频)。 我试图实现ECS的规模和便利性,使它成为一个没有立方体和按钮的小型项目。 在此之前,我没有设计ECS的经验,因此花了两天的时间研究材料并使用OOP重建思维,花了一天的时间来欣赏这种方法,又花了一两天的时间来开发项目,与Unity战斗,提取头发和烟雾样本。 本文包含一些理论知识和一个小的示例项目。


ECS的含义很简单-一个实体( Entity )及其组件( Component ),这些组件由系统( System )处理。

精华液


实体没有逻辑,仅存储组件(非常类似于旧的CPC方法中的GameObject)。 在Unity ECS中,为此存在Entity类。

组成部分


组件仅存储数据,有时根本不包含任何内容,并且是系统处理的简单标记。 但是他们没有任何逻辑。 继承自ComponentDataWrapper。 可以由另一个线程处理(但有细微差别)。

系统


系统负责处理组件。 在输入时,他们从Unity接收给定类型的已处理组件列表,并且在重载方法(Update,Start,OnDestroy的模拟)中,出现了游戏机制的魔力。 继承自ComponentSystem或JobComponentSystem。

工作系统


允许并行处理组件的系统机制。 在OnUpdate系统中,将创建一个Job结构并将其添加到处理中。 在无聊和空闲资源的时刻,Unity将处理结果并将其应用于组件。

多线程和Unity 2018


所有Job System工作都在其他线程中进行,并且标准组件(Transform,Rigidbody等)不能在除主线程之外的任何线程中更改。 因此,在标准包装中,有兼容的“替换”组件-位置组件,旋转组件,网格实例渲染器组件。

这同样适用于标准结构,例如Vector3或四元数。 用于并行化的组件仅使用添加到Unity.Mathematics命名空间中的最简单的数据类型(float3,float4,就是这样,图形程序员会满意的),还有一个用于处理它们的数学类。 没有字符串,没有引用类型,只有铁杆。

“告诉我代码”


所以,该移动一些东西了!

创建一个存储速度值的组件,该组件也是移动对象的系统的标记之一。 Serializable属性允许您设置和跟踪检查器中的值。

速度组件
[Serializable] public struct SpeedData : IComponentData { public int Value; } public class SpeedComponent : ComponentDataWrapper<SpeedData> {} 


使用Inject属性,系统获得的结构包含存在所有三个组件的那些实体的组件。 因此,如果某些实体具有PositionComponent和SpeedComponent组件,但没有RotationComponent,则该实体将不会添加到进入系统的结构中。 因此,可以通过组件的存在来过滤实体。

运动系统
 public class MovementSystem : ComponentSystem { public struct ShipsPositions { public int Length; public ComponentDataArray<Position> Positions; public ComponentDataArray<Rotation> Rotations; public ComponentDataArray<SpeedData> Speeds; } [Inject] ShipsPositions _shipsMovementData; protected override void OnUpdate() { for(int i = 0; i < _shipsMovementData.Length; i++) { _shipsMovementData.Positions[i] = new Position(_shipsMovementData.Positions[i].Value + math.forward(_shipsMovementData.Rotations[i].Value) * Time.deltaTime * _shipsMovementData.Speeds[i].Value); } } } 


现在,包含这三个成分的所有对象将以给定速度向前移动。

Wiiiii


很简单 尽管花了一天的时间来考虑ECS。

别说了 这里的工作系统在哪里?

事实是没有什么足以使用多线程的。 休息时间!

我从样本中提取了产生预制件的系统。 从有趣的地方开始-这是一段代码:

扳手
 EntityManager.Instantiate(prefab, entities); for (int i = 0; i < count; i++) { var position = new Position { Value = spawnPositions[i] }; EntityManager.SetComponentData(entities[i], position); EntityManager.SetComponentData(entities[i], new SpeedData { Value = Random.Range(15, 25) }); } 


因此,让我们放置1000个对象。 仍然无法在GPU上实例化网格。 5000-也大约 我将展示50,000个对象的情况。

实体调试器已出现在Unity中,显示每个系统花费多少毫秒。 可以在运行时直接打开/关闭系统,以查看它们处理的对象,通常是不可替代的。

得到这样的太空飞船球


该工具以15 fps的速度进行记录,因此整个要点位于系统列表中的数字中。 我们的MovementSystem尝试移动每帧中的所有50,000个对象,并且平均在60毫秒内执行一次。 因此,现在游戏已经足够优化。
我们将JobSystem固定在运动系统上。

改进的运动系统
 public class MovementSystem : JobComponentSystem { [ComputeJobOptimization] struct MoveShipJob : IJobProcessComponentData<Position, Rotation, SpeedData> { public float dt; public void Execute(ref Position position, ref Rotation rotation, ref SpeedData speed) { position.Value += math.forward(rotation.Value) * dt * speed.Value; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new MoveShipJob { dt = Time.deltaTime }; return job.Schedule(this, 1, inputDeps); } } 


现在,系统从JobComponentSystem继承,并在每个框架中创建一个特殊的处理程序,Unity将相同的3个组件和deltaTime传输到该处理程序。

再次发射飞船


0.15毫秒(峰值为0.4,是)与50-70! 5万个物件! 我在计算器中输入了这些数字,以回应他露出一张快乐的脸。

管理学


您可以无休止地看着飞球,也可以在飞船之间飞翔。
需要滑行系统。

旋转组件已经在预制件上,创建一个用于存储控件的组件。

控制组件
 [Serializable] public struct RotationControlData : IComponentData { public float roll; public float pitch; public float yaw; } public class ControlComponent : ComponentDataWrapper<RotationControlData>{} 


我们还需要一个播放器组件(尽管一次操纵所有5万艘飞船不是问题)

PlayerComponent
 public struct PlayerData : IComponentData { } public class PlayerComponent : ComponentDataWrapper<PlayerData> { } 


马上,用户输入阅读器。

用户控制系统
 public class UserControlSystem : ComponentSystem { public struct InputPlayerData { public int Length; [ReadOnly] public ComponentDataArray<PlayerData> Data; public ComponentDataArray<RotationControlData> Controls; } [Inject] InputPlayerData _playerData; protected override void OnUpdate() { for (int i = 0; i < _playerData.Length; i++) { _playerData.Controls[i] = new RotationControlData { roll = Input.GetAxis("Horizontal"), pitch = Input.GetAxis("Vertical"), yaw = Input.GetKey(KeyCode.Q) ? -1 : Input.GetKey(KeyCode.E) ? 1 : 0 }; } } } 


除了标准的Input,还可以有任何喜欢的自行车或AI。

最后,处理控件和转弯本身。 我面临着尚未实现math.euler的事实,因此对Wikipedia的快速突袭使我免于从Euler的角向四元数的转换。

ProcessRotationInputSystem
 public class ProcessRotationInputSystem : JobComponentSystem { struct LocalRotationSpeedGroup { public ComponentDataArray<Rotation> rotations; [ReadOnly] public ComponentDataArray<RotationSpeedData> rotationSpeeds; [ReadOnly] public ComponentDataArray<RotationControlData> controlData; public int Length; } [Inject] private LocalRotationSpeedGroup _rotationGroup; [ComputeJobOptimization] struct RotateJob : IJobParallelFor { public ComponentDataArray<Rotation> rotations; [ReadOnly] public ComponentDataArray<RotationSpeedData> rotationSpeeds; [ReadOnly] public ComponentDataArray<RotationControlData> controlData; public float dt; public void Execute(int i) { var speed = rotationSpeeds[i].Value; if (speed > 0.0f) { quaternion nRotation = math.normalize(rotations[i].Value); float yaw = controlData[i].yaw * speed * dt; float pitch = controlData[i].pitch * speed * dt; float roll = -controlData[i].roll * speed * dt; quaternion result = math.mul(nRotation, Euler(pitch, roll, yaw)); rotations[i] = new Rotation { Value = result }; } } quaternion Euler(float roll, float yaw, float pitch) { float cy = math.cos(yaw * 0.5f); float sy = math.sin(yaw * 0.5f); float cr = math.cos(roll * 0.5f); float sr = math.sin(roll * 0.5f); float cp = math.cos(pitch * 0.5f); float sp = math.sin(pitch * 0.5f); float qw = cy * cr * cp + sy * sr * sp; float qx = cy * sr * cp - sy * cr * sp; float qy = cy * cr * sp + sy * sr * cp; float qz = sy * cr * cp - cy * sr * sp; return new quaternion(qx, qy, qz, qw); } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new RotateJob { rotations = _rotationGroup.rotations, rotationSpeeds = _rotationGroup.rotationSpeeds, controlData = _rotationGroup.controlData, dt = Time.deltaTime }; return job.Schedule(_rotationGroup.Length, 64, inputDeps); } } 


您可能会问,为什么不能像MoveSystem中那样一次将3个组件传递给Job? 因为。 我为此苦苦挣扎了很长时间,但是我不知道为什么它不能那样工作。 在示例中,转弯是通过ComponentDataArray实现的,但我们不会退缩。

我们将预制件扔到舞台上,挂上组件,绑上相机,设置无聊的墙纸,然后出发!



结论


Unity Technologies的专家们朝着正确的多线程方向发展。 Job System本身仍然很潮湿(毕竟Alpha版是该版本),但是它非常有用,并且正在加速发展。 不幸的是,标准组件与作业系统不兼容(但与ECS单独不兼容!),因此您必须雕刻拐杖来解决此问题。 例如,来自Unity论坛的一个人为GPU实现了他的物理系统,并且取得了进步。
曾经使用带有Unity的ECS,例如,有一些繁荣的类似产品, 其中概述了最著名的产品。 它还描述了这种架构方法的利弊。

从我自己身上,我可以添加代码纯净度等优点。 我首先尝试在一个系统中实现运动。 依赖组件的数量迅速增长,我不得不将代码拆分为小型便捷的系统。 而且它们可以轻松地在另一个项目中重复使用。

项目代码在这里: GitHub

Source: https://habr.com/ru/post/zh-CN413897/


All Articles