ECS概念
传统OOP缺陷
传统OOP下的MonoBehaviour/GameObject模式, 可以非常方便的为创作游戏编写代码, 但是往往在后期会使得代码难以阅读, 维护, 优化, 游戏开销大而性能低, 这是由一系列因素导致的:
- OOP模型
- Mono编译的非最优机器吗
- GC
- 单线程
ECS模型
ESC(Entity-Component-System)是unity中DOTS(Data-Oriented Tech Stack)的核心(还有Burst Compile和Job System), 分为三个主要部分:
- Entity: the entities, or things, that populate your game or program
- Component: the data associated with your entities, but organized by the data itself rather than by entity. (This difference in organization is one of the key differences between an object-oriented and a data-oriented design.)
- System: the logic that transforms the component data from its current state to its next state— for example, a system might update the positions of all moving entities by their velocity times the time interval since the previous frame.
作为取代GameObject/Component的模式, 其模式遵循组合优于继承原则, 游戏内的每一个基本单元都是一个Entity, 每个Entity又是由一个或者多个Component构成, 每个Component仅仅包含代表其特性的数据(即Component中没有任何方法). System是来处理具有一个或多个Component组件的Entity集合的工具, 只拥有行为(即在System中没有任何数据).
Entity和Component是一对多的关系, Entity拥有怎样的能力, 完全取决于有哪些Component, 通过动态添加或者删除Component, 可以在运行时改变Entity的行为.
Rotation Example
Legacy
class Rotator : MonoBehaviour {
public float RadiansPerSecond;
public override void Update() {
transform.rotation *= Quatern.AngleAxix(Time.deltaTime * RadiansPerSecond, Vector3.up);
}
}
复制
继承自MonoBehabiour的GameObject即包含了数据(数据域), 又包含了行为(Update).
ECS
// RotationSpeed.cs
using System;
using Unity.Entities;
[Serializable]
public struct RotationSpeed : IComponentData {
public float RadiansPerSecond;
}
复制
// RotationSpeedProxy.cs
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
[RequiresEntityConversion]
public class RotationSpeedProxy : MonoBehaviour, IConvertGameObjectToEntity {
public float DegreesPerSecond;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) {
var data = new RotationSpeed {
RadiansPerSecond = math.radians(DegreesPerSecond)
};
dstManager.AddComponentData(entity, data);
}
}
复制
// RotationSpeedSystem.cs
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public class RotationSpeedSystem : JobComponentSystem {
private ComponentGroup _componentGroup;
protected override void OnCreateManager() {
_componentGroup = GetComponentGroup(typeof(Rotation), ComponentType.ReadOnly<RotationSpeed>());
}
[BurstCompile]
private struct RotationSpeedJob : IJobChunk {
public float DeltaTime;
public ArchetypeChunkComponentType<Rotation> RotationType;
[ReadOnly] public ArchetypeChunkComponentType<RotationSpeed> RotationSpeedType;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
var chunkRotations = chunk.GetNativeArray(RotationType);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
for (var i = 0; i < chunk.Count; i++) {
var rotation = chunkRotations[i];
var rotationSpeed = chunkRotationSpeeds[i];
chunkRotations[i] = new Rotation {
Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
};
}
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps) {
var rotationType = GetArchetypeChunkComponentType<Rotation>();
var rotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed>();
var job = new RotationSpeedJob {
DeltaTime = Time.deltaTime,
RotationType = rotationType,
RotationSpeedType = rotationSpeedType
};
return job.Schedule(_componentGroup, inputDeps);
}
}
复制
我们可以看到ECS的工作模式:
- ECS的行为(System)和数据(Component)分别实现
- Entity中存储多种数据(Component)
- 如果存储在Entity中的Component满足本组的数据列表, 则由System执行行为
ECS优势
- Component是sturct而不是class, 这意味着我们在存储数据是的时候不是通过new到heap中, 离散到存储, 而是在内存中连续对其存储. 值得注意的是, NativeArray将native内存直接暴露到managed code中, 从而使得managed和native之间数据共享.
- 基于Burst Compile, 可以生成优于MonoBehabiour的机器码, 以增加预编译时间为代价, 提高运行时效率.
- 基于Job System, System在调度jobs的时候会把任务放到队列中, 由worker threads多线程完成, 并通过细粒度话数据的读写权限, 加速执行, 提高CPU的利用效率.