概述

DOTS(Data Oriented Technology Stack)是面向数据的开发框架。本文会简略介绍DOTS的相关概念,然后讲述如何实现一个简单的在DOTS框架下运行的2d动画系统。

内存管理

ECS架构负责内存管理。分为三个结构(Entity实体,Component组件,System系统)

Entity就是一个单纯的由两个字段(index,version)组成的一个id结构。

其中version的意义在于支持实体的池化和重用,每次销毁Entity的时候,该Entity的version +1

Component就是单纯的数据容器,任何一个实现了IComponentData接口的struct类型都是Component。最好不要在Component下做任何涉及到其他对象的逻辑实现。

System就负责实现逻辑,在Class上实现SystemBase或者在struct上实现ISystem。实现SystemBase的系统属于托管系统,效率较低,但是可以直接存储托管对象,ISystem还支持BurstCompile,但是不能存储托管对象,必须要用BlobAsset或者NativeArray曲线救国。在实际开发中基本上都使用ISystem。

ECS的内存结构和运行方式

首先需要讲述Archetype(原型)这个概念。原型就是在生命周期内,由一组唯一组件类型构成的集合。比如有3个实体,他们的组件分别有(A,B,C)(A)(A,C,D),会生成3个不同的原型,即使它们之间有重叠的组件,但只要集合不完全相同,ECS 就会生成各自独立的原型。

ECS的优势就在于内存的高效利用,ECS的内存是按照Chunk排列的,Chunk的大小为16kb,其存储的实体数量取决于Chunk所属的原型。也就是说Chunk里面的实体全是相同的原型。例如原型(A,B,C),在对应的chunk里面就是这样排列:(A,A,A...B,B,B...C,C,C)

当System发起一次查询的时候,首先在所有的原型里面查询,查看有哪些原型是符合当前查询条件的,然后对于所有的符合条件的原型所管理的Chunk,找到具体的实体和组件,依次执行对应的逻辑。

多线程

JobSystem负责多线程任务。使用JobSystem,可以更好利用其他的CPU核心,提高游戏性能。

在ECS中,主要使用的接口就是IJobEntity,需要一个partial struct类型来实现这个接口,而这个接口本质上是会在源生成阶段被转化为IJobChunk

在后续的内容中,会讲述一些JobSystem结合ECS使用的时候会踩的坑。

BurstCompile

BurstCompile可以把C#代码编译成可以利用SIMD指令集的机器码,会找到可以并行的计算模式,然后将其重写成SIMD指令。一般情况下,只需要在System的OnUpdate上面和Job上面添加一个[BurstCompile]attribute即可。


下面开始正式内容


简单的规范

父物体上挂控制器组件,子物体挂动画组件,同时子物体还要有MeshFilter和MeshRenderer。这应该是老生常谈的规范了

踩坑:记得在父物体上挂上linked entity group authoring这个脚本,不然Entity不知道自己的子Entity是谁,如果你手动在编辑器里面创建的物体没有挂上这个脚本,在销毁父物体的时候,不会同步销毁子物体,不过动态生成的反而不会有这个问题。

因为我的动画系统是序列帧的,原理就是通过传入进来的_FrameIndex属性和_FlipX属性来决定uv的位移和x翻转,因此mesh renderer挂载的material必须暴露出这两个可供修改的属性。

踩坑:我是用shader graph做的一个带翻页节点的材质,但是实际运行后发现没法修改帧索引,拷打了ai半天然后自己查资料才发现,必须在属性的设置界面勾选上Override Property Declaration这个复选框,同时材质本身也要勾选上GPU Instancing

构建Animation相关数据结构

Component

ActiveAnimation组件存储动画相关的信息。DOTSAnimator挂载在父物体上,是用来管理动画状态和存储相关引用的工具组件。

ActiveAnimation

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
// ActiveAnimationAuthoring.cs
using Unity.Entities;
using Unity.Rendering;
using UnityEngine;

public class ActiveAnimationAuthoring : MonoBehaviour
{
[SerializeField] ActiveAnimation activeAnimation;
[SerializeField] GameObject animatorGameObject;
public class Baker : Baker<ActiveAnimationAuthoring>
{
public override void Bake(ActiveAnimationAuthoring authoring)
{
Entity entity = GetEntity(TransformUsageFlags.Dynamic);
var activeAnimation = authoring.activeAnimation;
activeAnimation.animatorEntity = GetEntity(authoring.animatorGameObject, TransformUsageFlags.Dynamic);
AddComponent(entity, activeAnimation);
AddComponent(entity, new AnimationFrameProperty
{
value = activeAnimation.currentIndex
});
AddComponent(entity, new AnimationFlipProperty {
value = activeAnimation.flipX
});
}
}
}
[System.Serializable]
public struct ActiveAnimation : IComponentData
{
public AnimationType animationType;
public int iniIndex;
public int frameCount;
public int currentIndex;
public float animationTimer;
public float frameInterval;
// DOTS事件flag
public bool isAnimationFinished;
public bool replay;
public float flipX;
public bool canFlip;
public Entity animatorEntity;
}

// 添加了这个attribute后,修改这里面的值,会把这个值同步到material property上。
[MaterialProperty("_FrameIndex")]
public struct AnimationFrameProperty : IComponentData
{
public float value;
}

[MaterialProperty("_FlipX")]
public struct AnimationFlipProperty : IComponentData
{
public float value;
}

public enum AnimationType
{
Idle,
Move,
Attack,
Death,
}

DOTSAnimator

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
// DOTSAnimatiorAuthoring.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

public class DOTSAnimatiorAuthoring : MonoBehaviour
{
[SerializeField] private GameObject activeAnimationGameObject;
public class Baker : Baker<DOTSAnimatiorAuthoring>
{
public override void Bake(DOTSAnimatiorAuthoring authoring)
{
Entity entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new DOTSAnimator
{
activeAnimationEntity = GetEntity(authoring.activeAnimationGameObject, TransformUsageFlags.Dynamic)
});

}
}
}

public struct DOTSAnimator : IComponentData
{
public Entity activeAnimationEntity;
public AnimationType currentAnimationType;
public AnimationType nextAnimationType;
}

System

ActiveAnimationSystem负责更新动画状态,传递材质属性参数。

DOTSAnimatorSystem负责与其他内容交互,最终给出下一帧的动画状态

除此之外还有一些其他系统,例如DOTS的事件触发系统DOTSEventSystem和事件flag重置系统ResetEventSystem。但和动画系统关系不大,就不贴代码了,只在这里简单介绍。

DOTSEventSystem和ResetEventSystem

二者为了保证在所有其他EventFlag更新之后再更新,都要添加上[UpdateInGroup(typeof(LateSimulationSystemGroup))]这个attribute,而ResetEventSystem的更新还要在DOTSEventSystem的后面,因此要添加一个额外参数:[UpdateInGroup(typeof(LateSimulationSystemGroup), OrderLast = true)]

DOTSEventSystem手动实现所有带有EventFlag组件的查询,如果触发了flag,则执行相应逻辑。而ResetEventSystem负责把所有这些EventFlag置为false,防止触发过事件之后在下一帧继续触发。

ActiveAnimationSystem

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
// ActiveAnimationSystem.cs
using Unity.Burst;
using Unity.Entities;
[UpdateAfter(typeof(DOTSAnimatorSystem))]
partial struct ActiveAnimationSystem : ISystem
{

[BurstCompile]
public void OnUpdate(ref SystemState state)
{
ActiveAnimationJob activeAnimationJob = new ActiveAnimationJob
{
deltaTime = SystemAPI.Time.DeltaTime
};

state.Dependency = activeAnimationJob.ScheduleParallel(state.Dependency);

}

[BurstCompile]
partial struct ActiveAnimationJob : IJobEntity
{
public float deltaTime;
public void Execute(ref ActiveAnimation activeAnimation, ref AnimationFrameProperty animationFrameProperty, ref AnimationFlipProperty animationFlipProperty)
{
activeAnimation.animationTimer += deltaTime;
if(activeAnimation.animationTimer >= activeAnimation.frameInterval)
{
activeAnimation.animationTimer -= activeAnimation.frameInterval;
activeAnimation.currentIndex++;
if(activeAnimation.currentIndex >= activeAnimation.iniIndex + activeAnimation.frameCount)
{
if (activeAnimation.replay)
{
// 重播
activeAnimation.currentIndex = activeAnimation.iniIndex;
}
else
{
// 不重播
activeAnimation.currentIndex = activeAnimation.iniIndex + activeAnimation.frameCount - 1;
}
activeAnimation.isAnimationFinished = true;
}
}

animationFrameProperty.value = activeAnimation.currentIndex;
animationFlipProperty.value = activeAnimation.flipX;
}
}

}

DOTSAnimatorSystem

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// DOTSAnimatorSystem.cs
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
// 保证时序正确
[UpdateAfter(typeof(MoverSystem))]
public partial struct DOTSAnimatorSystem : ISystem
{
// 一些其他内容的查询
ComponentLookup<Combat> combatLookup;
ComponentLookup<ProjectileSpawner> projectileSpawnerLookup;
ComponentLookup<Mover> moverLookup;
ComponentLookup<ActiveAnimation> activeAnimationLookup;
BufferLookup<CombatTargetElement> targetBufferLookup;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
// AnimationReference是我用来存储所有已定义动画数据的单例,你可以用自己的方法来获取动画数据
state.RequireForUpdate<AnimationReference>();
combatLookup = SystemAPI.GetComponentLookup<Combat>(true);
projectileSpawnerLookup = SystemAPI.GetComponentLookup<ProjectileSpawner>(true);
moverLookup = SystemAPI.GetComponentLookup<Mover>(false);
activeAnimationLookup = SystemAPI.GetComponentLookup<ActiveAnimation>(false);
targetBufferLookup = SystemAPI.GetBufferLookup<CombatTargetElement>(true);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var animReference = SystemAPI.GetSingleton<AnimationReference>();
combatLookup.Update(ref state);
projectileSpawnerLookup.Update(ref state);
moverLookup.Update(ref state);
activeAnimationLookup.Update(ref state);
targetBufferLookup.Update(ref state);
// 根据环境决定是否翻转x
UpdateAnimationFlipXJob updateAnimationFlipXJob = new UpdateAnimationFlipXJob
{
activeAnimationLookup = activeAnimationLookup,
};
state.Dependency = updateAnimationFlipXJob.ScheduleParallel(state.Dependency);
// 确定下一帧的动画类型
MonsterAnimatorJob monsterAnimatorJob = new MonsterAnimatorJob
{
combatLookup = combatLookup,
projectileSpawnerLookup = projectileSpawnerLookup,
moverLookup = moverLookup,
targetBufferLookup = targetBufferLookup
};
state.Dependency = monsterAnimatorJob.ScheduleParallel(state.Dependency);

// 把新的动画数据写给子物体的ActiveAnimation
UpdateMonsterAnimationJob updateMonsterAnimationJob = new UpdateMonsterAnimationJob
{
animationReference = animReference,
activeAnimationLookup = activeAnimationLookup
};
state.Dependency = updateMonsterAnimationJob.ScheduleParallel(state.Dependency);
}

[BurstCompile]
[WithOptions(EntityQueryOptions.IgnoreComponentEnabledState)]
[WithAny(typeof(Combat), typeof(ProjectileSpawner))]
partial struct MonsterAnimatorJob : IJobEntity
{
[ReadOnly] public ComponentLookup<Combat> combatLookup;
[ReadOnly] public ComponentLookup<ProjectileSpawner> projectileSpawnerLookup;
[NativeDisableParallelForRestriction] public ComponentLookup<Mover> moverLookup;
[ReadOnly] public BufferLookup<CombatTargetElement> targetBufferLookup;
public void Execute(ref Monster monster,ref DOTSAnimator DOTSAnimator,in Health health,Entity entity)
{
DOTSAnimator.nextAnimationType = AnimationType.Move;
moverLookup.SetComponentEnabled(entity, true);
if (health.value <= 0f)
{
moverLookup.SetComponentEnabled(entity, false);
DOTSAnimator.nextAnimationType = AnimationType.Death;
return;
}

bool isAttacking = false;
if (targetBufferLookup.HasBuffer(entity) && !targetBufferLookup[entity].IsEmpty) isAttacking = true;
if (projectileSpawnerLookup.HasComponent(entity) && projectileSpawnerLookup.IsComponentEnabled(entity)) isAttacking = true;

if (isAttacking)
{
if (moverLookup.IsComponentEnabled(entity)) moverLookup.SetComponentEnabled(entity, false);
DOTSAnimator.nextAnimationType = AnimationType.Attack;
}
}
}

partial struct UpdateMonsterAnimationJob : IJobEntity
{
[ReadOnly] public AnimationReference animationReference;
[NativeDisableParallelForRestriction] public ComponentLookup<ActiveAnimation> activeAnimationLookup;
public void Execute(ref DOTSAnimator DOTSAnimator,ref Monster monster,Entity entity)
{

if(DOTSAnimator.currentAnimationType == DOTSAnimator.nextAnimationType)
{
return;
}
else
{
var activeAnimation = GetActiveAnimationByType(animationReference, monster.type, DOTSAnimator.nextAnimationType);
float previousFlipX = activeAnimationLookup[DOTSAnimator.activeAnimationEntity].flipX;
activeAnimation.flipX = previousFlipX;
activeAnimation.currentIndex = activeAnimation.iniIndex;
activeAnimation.animationTimer = 0f;
activeAnimation.animatorEntity = entity;
activeAnimationLookup[DOTSAnimator.activeAnimationEntity] = activeAnimation;
DOTSAnimator.currentAnimationType = DOTSAnimator.nextAnimationType;
}
}
}

partial struct UpdateAnimationFlipXJob : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<ActiveAnimation> activeAnimationLookup;
public void Execute(in DOTSAnimator DOTSAnimator, in Mover mover, Entity entity)
{
ActiveAnimation newActiveAnimation = activeAnimationLookup[DOTSAnimator.activeAnimationEntity];
if (!newActiveAnimation.canFlip) return;

if (mover.moveDir.x < -0.01f)
{
newActiveAnimation.flipX = 1f;
}
else if (mover.moveDir.x > 0.01f)
{
newActiveAnimation.flipX = 0f;
}
activeAnimationLookup[DOTSAnimator.activeAnimationEntity] = newActiveAnimation;
}

}

public static ActiveAnimation GetActiveAnimationByType(AnimationReference animationReference, MonsterType monsterType, AnimationType animationType)
{
if (monsterType == MonsterType.Slime)
{
switch (animationType)
{
default: return animationReference.slimeMove;
case AnimationType.Move: return animationReference.slimeMove;
case AnimationType.Attack: return animationReference.slimeAttack;
case AnimationType.Death: return animationReference.slimeDeath;
}
}

if(monsterType == MonsterType.Robot)
{
switch (animationType)
{
default: return animationReference.robotMove;
case AnimationType.Move: return animationReference.robotMove;
case AnimationType.Attack: return animationReference.robotAttack;
case AnimationType.Death: return animationReference.robotDeath;
}
}
return animationReference.slimeMove;
}
}