ECS 当中的 Transform

在 ECS 当中,主要含有 5 个 ECS Component,分别是:

  • LocalToWorld:位置
  • LocalTransform:旋转
  • PostTransformMatrix:缩放
  • Parent, Child:层级关系

其中,还有两个 System 来维护这些 Component:

  • ParentSystem
  • LocalToWorldSystem

LocalToWorld

表示从本地坐标世界坐标的变换矩阵。这个矩阵是由内置的 LocalToWorldSystem 来维护的。一般只需要读取不需要改变其值。

LocalTransform

表示 entity 对其父 entity 的相对坐标,旋转和缩放,如果没有父 entity,则是相对于世界坐标。值得注意的是:其中的 Rotation 也就是旋转,是一个 float 值变量,也就是 LocalTransform 只能表示等比缩放(uniform scale)。因此需要 PostTransformMatrix,来处理非等比缩放的情况。

Parent

用来设置 entity 父子关系,一个 entity 最多只能有一个 Parent

Child

entity 可以有不限数量个 Child,Child 的值也是由内置的 ParentSystem 来维护的。

Reparenting

首先在我们的 SubScene 当中直接右键点击 RotationCube 添加一个新的 Cube ,新添加的无需挂载任何脚本。接着我们再添加如下两个脚本即可观察到:RotationCube 一直在旋转,Cube 转一会儿停一会儿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// RotationSystem.cs
namespace HelloCube.Reparenting
{
public partial struct RotationSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<Execute.Reparenting>();
}

[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;

foreach (var (transform, speed) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
{
transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
}
}
}
}

第一个脚本不解释,就是最开始例子里面的旋转方法。

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
// ReparentingSystem.cs
namespace HelloCube.Reparenting
{
public partial struct ReparentingSystem : ISystem
{
private bool attached;
private float timer;
private const float interval = 0.7f;

[BurstCompile]
public void OnCreate(ref SystemState state)
{
timer = interval;
attached = false;
state.RequireForUpdate<Execute.Reparenting>();
}

[BurstCompile]
public void OnUpdate(ref SystemState state)
{
timer -= SystemAPI.Time.DeltaTime;
if (timer > 0)
{
return;
}

timer = interval;

var rotatorEntity = SystemAPI.GetSingletonEntity<RotationSpeed>();
var ecb = new EntityCommandBuffer(Allocator.Temp);

if (attached)
{
DynamicBuffer<Child> children = SystemAPI.GetBuffer<Child>(rotatorEntity);
for (int i = 0; i < children.Length; i++)
{
ecb.RemoveComponent<Parent>(children[i].Value);
}
} else
{
foreach (var (transform, entity) in SystemAPI.Query<RefRO<LocalTransform>>()
.WithNone<RotationSpeed>()
.WithEntityAccess())
{
ecb.AddComponent(entity, new Parent() { Value = rotatorEntity });
}
}

ecb.Playback(state.EntityManager);

attached = !attached;
}
}
}

这就是我们的 Reparenting 脚本,注意此处有一点和官方不一样的是在 OnCreate 当中 attched 初始化为了 false 。此处逻辑与官方示例则相反。此处先说明一下是为什么,因为我是自己创建子对象复现的,但是在创建的时候,并不会根据 UnityEditor 的 gameobject 关系自动添加 Child, Parent 相关的组件,并且我试过在 bake 的时候也不会。所以我们最开始的状态就应当为 attched = false。我后面又看了看官方文档的场景和代码,发现其的 RotationCube 最开始了带了 Child 的组件(此处我不知道是怎么回事,因此提了个 issue,坐等其他佬回复了),因此官方示例使用 attched = true 是完全正确的。

好了,再来看一看这个代码,首先有三个成员变量:

  • attached:表明两个 gameobject 之间是否有 父子关系
  • timer:简单的计时器
  • interval:单次 旋转/暂停 时间间隔
  1. 在每次计时器时间到了的时候,都使用 SystemAPI.GetSingletonEntity<RotationSpeed>() 获取我们场景中唯一挂载了 RotationSpeedRotationCube
  2. 接着使用 new EntityCommandBuffer(Allocator.Temp) 创建了一个临时的实体命令缓冲区
  3. 如果两者具有父子关系,则首先获取 RotationCube 实体上的 Child 动态缓冲区,然后移除其中每一个 entity 的 Parent 组件。如果两者没有,对于每个不具有 RotationSpeed 组件的实体,添加一个 Parent 组件(其值就是 RotationCube 实体)
  4. 最后使用 ecb.Playback(state.EntityManager); 执行缓冲区的更改,并且反转 attached 的状态。