Unity ECS 笔记

Unity ECS 提供了一种更好的游戏设计方法,使开发者专注于正在解决的实际问题:组成游戏的数据和行为。它利用 C#Job System 和 Burst Compiler 使应用程序能够充分利用当今的多核处理器。从面向对象的设计转向面向数据的设计使开发者可以更轻松地重用代码,并使其他人更容易理解和处理代码。

ECS 优点

  • 编写极高性能的代码 (Extremely performant code)
  • 更易于阅读的代码 (Easier to read)
  • 更易于代码重用 (Easier to reuse code)
  • Burst 编译器 (Burst compiler)
  • C# 作业系统 (C# Job System )

ECS 基本概念

  • Entity 是实例,作为承载组件的载体,也是框架中维护对象的实体
  • Component 只包含数据,具备这个组件便具有这个功能
  • System 作为逻辑维护,维护对应的组件执行相关操作

ESC 的使用

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using Unity.Entities;
using UnityEngine;

// Entity 实体:挂载 GameObjectEntity 组件的游戏对象。需要自定义结构体给 Entity 组装需要的 Component
struct Components
{
public Transform transform;
public Rotator rotator;
}

// Component 数据:可以在Inspector窗口中编辑的旋转速度值
[RequireComponent(typeof(GameObjectEntity))]
class Rotator : MonoBehaviour
{
public float speed;
}

// System 行为:继承自ComponentSystem来处理旋转操作
class RotatorSystem : ComponentSystem
{
override protected void OnUpdate()
{
foreach (var e in GetEntities<Components>())
{
e.transform.rotation *= Quaternion.AngleAxis(e.rotator.speed * Time.deltaTime, Vector3.up);
}
}
}

Entity Debugger

Unity ECS 系统提供的一个可视化的调试 ECS 的工具,通过工具可以看到 Entity 数量和某个 Entity 所包含的 Component,以及对应的 System 的耗时。

Job System

在 Unity 中,Unity API 必须在主线程中使用,无法在子线程调用。Job System 旨在让用户能更加容易地编写与 Unity API 交互的多线程代码。

多线程的问题

当创建的多线程数量超过 CPU 内核数量时,会导致线程相互争夺 CPU资源,从而导致频繁的上下文切换。而 CPU 执行上下文切换时,需要做大量工作来确保新线程的状态是正确的,这是相当耗费资源的,应尽可能避免。

Job System 概览

  • 数据和方法分离 (Separate data from function)
  • 多核处理 (Multi-core processing)
  • 节省多线程 (Save multi-threading)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine.Jobs;

public class CubeAccelerationParallelFor : MonoBehaviour
{
[SerializeField]
protected int m_ObjectCount = 10000;

[SerializeField]
protected float m_ObjectPlacementRadius = 100f;

protected GameObject[] m_Objects;
protected Transform[] m_Transforms;

public Vector3 m_Acceleration = new Vector3(0.0002f, 0.0001f, 0.0002f);

NativeArray<Vector3> m_Velocities;
TransformAccessArray m_TransformsAccessArray;

PositionUpdateJob m_Job;
AccelerationJob m_AccelJob;

JobHandle m_PositionJobHandle;
JobHandle m_AccelJobHandle;

void Awake()
{
m_Objects = new GameObject[m_ObjectCount];
m_Transforms = new Transform[m_ObjectCount];
}

void Start()
{
m_Velocities = new NativeArray<Vector3>(m_ObjectCount, Allocator.Persistent);

for (int i = 0; i < m_ObjectCount; i++)
{
var cube = CreateCube();
cube.transform.position = Random.insideUnitSphere * m_ObjectPlacementRadius;
m_Objects[i] = cube;
}

for (int i = 0; i < m_ObjectCount; i++)
{
var obj = m_Objects[i];
m_Transforms[i] = obj.transform;
}

m_TransformsAccessArray = new TransformAccessArray(m_Transforms);
}

struct PositionUpdateJob : IJobParallelForTransform
{
[ReadOnly]//通过将其声明为只读,允许多个作业并行访问数据
public NativeArray<Vector3> velocity; // 通过 AccelerationJob 赋值

public float deltaTime;

public void Execute(int i, TransformAccess transform)
{
transform.position += velocity[i] * deltaTime;
}
}

struct AccelerationJob : IJobParallelFor
{
public NativeArray<Vector3> velocity;

public Vector3 acceleration;

public void Execute(int i)
{
velocity[i] = i * acceleration;
}
}

public void Update()
{
m_AccelJob = new AccelerationJob()
{
velocity = m_Velocities,
acceleration = m_Acceleration,
};

m_Job = new PositionUpdateJob()
{
deltaTime = Time.deltaTime,
velocity = m_Velocities,
};

//安排并行工作。第一个参数是每次迭代执行的次数,第二个参数是批量大小
m_AccelJobHandle = m_AccelJob.Schedule(m_ObjectCount, 64);
m_PositionJobHandle = m_Job.Schedule(m_TransformsAccessArray, m_AccelJobHandle);

m_PositionJobHandle.Complete();
}

private void OnDestroy()
{
m_Velocities.Dispose();
m_TransformsAccessArray.Dispose();
}

private GameObject CreateCube()
{
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);

// 关闭阴影
var renderer = cube.GetComponent<MeshRenderer>();
renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
renderer.receiveShadows = false;

// 禁用 Collider
var collider = cube.GetComponent<Collider>();
collider.enabled = false;

return cube;
}
}

Burst

Burst 是一种新的基于 LLVM 的数学感知后端编译器。它将 C# 的 Job 编译为高度优化的机器代码,可以利用编译的平台的特定功能。Burst主要用于与Job系统高效协作。可以通过使用属性 [BurstCompile] 装饰 Job 结构,从而在代码中简单地使用 Burst 编译器 。

Burst是一个实验包,目前支持Unity 2018.3及更高版本,且目前还只是预览版。

更多介绍请参考:https://docs.unity3d.com/Packages/com.unity.burst@0.2/manual/index.html

LLVM是一个自由软件项目,它是一种编译器基础设施,以C++写成,包含一系列模块化的编译器组件和工具链,用来开发编译器前端和后端。

参考链接

  1. Unity 的官方示例库 EntityComponentSystemSamples
  2. Job System 示例库 job-system-cookbook
  3. Get Started with the Unity* Entity Component System (ECS), C# Job System, and Burst Compiler