Evaluation Warning: The document was created with Spire.Doc for .NET.
大家好,我是阿赵。继续学习Unity官方的DOTS例子工程。
之前介绍了Entities101工程的组成部分,接下来先看看由基础小例子构成的HelloCube:

由于HelloCube里面的例子太多,所以会分开几篇来介绍。
1、 MainThread

这是一个很简单的例子,运行之后,有一个有盒子组装在一起的模型在场景里面旋转。
通过这个小例子,我们先来认识一下Entity实体在使用上的一些最基础的东西。
1. SubScene
首先要知道的是,我们一般的GameObject,是实例化在Scene场景里面的,但如果需要使用ECS,那么Entity实体并不是存在于我们平常用的Scene里面,而是需要建立一个SubScene子场景来存放的。
打开MainThread的场景,会发现里面还有一个场景叫做HelloCube_MainThreadSubscene:

这个子场景是通过场景左上角的加号创建的:

在创建子场景的时候,会叫我们指定名字,然后就会保存成一个场景文件,并且会嵌入到主场景里面:

这里有一个需要注意的地方,我们在子场景里面可以摆放东西,然后在子场景的右边有一个可以打钩的地方:

如果把勾勾上,那么在没运行的时候,我们就可以在主场景看到子场景里面的内容:

但在运行中,如果这个勾选勾上了,那么主场景里面只会看到没运行前的子场景内容,子场景实际运行中的实体我们会看不到:

如果我们不勾选这个地方:

会看到在没运行的时候,在Hierarchy是看不到子场景的内容的。在Scene场景虽然可以选中子场景的内容,但却看不到它的属性:

会提示这个实体只会在runtime时存在。
但在运行的过程中,我们可以从Scene视图里面选中实际运行中的实体,并且查看它们的属性

2. Bake实体
由于ECS里面运行的不再是像GameObject这样的对象,而是Entity实体,那么我们需要有一个从对象转换到实体的过程。
上面介绍了子场景,但在子场景里面的还是传统的对象:

所以我们需要想办法做转换。这里需要注意一点,在低版本Unity里面,比如Unity2019之类,转换的方法和现在的版本比如Unity2022或者Unity6完全不一样了。旧版本的ConvertToEntity组件在新版本已经不存在了,Convert方法也替换成了Bake方法。
具体的转换过程是这样的:
1. 确定需要转换的组件或者数据
首先,在正常的对象上面,也有很多默认的组件:

如果你看不到,需要安装Entities Graphics:

这些组件都是可以使用的。如果觉得这些组件不够用,那么可以自定义组件数据,比如MainThread这个例子是需要方块自己旋转的,所以需要有一个旋转速度,于是需要指定了这个旋转速度的数据结构:
public struct RotationSpeed : IComponentData
{
public float RadiansPerSecond;
}
这离注意两点:
(1) 这是struct结构体,不是class
(2) 继承IComponentData,证明它是一个组件数据结构
2. 写Bake方法
所谓的Bake方法,是指把原来的MonoBehaviour类Bake成组件结构,这个例子里面就是要最终生成RotationSpeed 这个继承了IComponentData的结构。
所以我们先要有一个MonoVehaviour脚本,在这个例子里面,就是RotationSpeedAuthoring。这里有一个Unity官方建议的命名习惯,如果是用于Bake成ComponentData的MonoBehaviour脚本,都是用Authoring结尾,这样比较容易分辨。
这个RotationSpeedAuthoring脚本里面只有一个公共的参数,就是DegreesPerSecond,这代表了旋转的时候需要多少度每秒。
接下来通过一个Bake类和Bake方法来把RotationSpeedAuthoring转换成RotationSpeed结构。
class Baker : Baker<RotationSpeedAuthoring>
{
public override void Bake(RotationSpeedAuthoring authoring)
{
// The entity will be moved
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new RotationSpeed
{
RadiansPerSecond = math.radians(authoring.DegreesPerSecond)
});
}
}
从class Baker : Baker可以看到,这个Baker对应转换的类是指定了RotationSpeedAuthoring,然后下面重写Bake方法,输入的参数是RotationSpeedAuthoring authoring,然后下面是通过GetEntity(TransformUsageFlags.Dynamic)方法拿到一个实体,然后通过AddComponent方法,新建一个RotationSpeed结构体,给RadiansPerSecond 参数赋值,并且赋予给刚刚得到的实体。
到这一步为止,创建实体的Bake方法就做完了。当RotationSpeedAuthoring脚本挂在GameObject对象上面,并且运行的时候,在SubScene里面,这个GameObject对象就会变成实体,并且把RotationSpeed结构体挂到这个实体身上了。
3. System
通过上面的步骤,我们已经有了运行实体的SubScene场景,也有了可以运行的Entity实体,接下来就是需要写个System脚本去控制Entity了。

在这个例子里面,就是这个RotationSystem了。

首先看看这个脚本的特点:
public partial struct RotationSystem : ISystem
(1) 这同样是一个Struct,而不是Class
(2) 使用了partial修饰符
(3) 继承ISystem
然后看到ISystem下面有波浪线,看看提示:

这里说明了一个问题,ISystem这个接口实际上是需要实现3个方法的:

而例子里面的RotationSystem少实现了OnDestroy方法。
那么这个System脚本是需要怎样生效的呢?
如果是传统的MonoBehaviour脚本,是需要在场景里面找一个GameObject挂上去,然后就会走MonoBehaviour的声明周期来运行了。
但使用ECS,是没有挂MonoBehaviour的说法的。它控制实体的方式是针对不同的组件写System控制。
然后这个具体是怎样的情况才会执行呢?
(1) 原则上,只要这个System的脚本存在,这个System的OnCreate就会执行。
(2) 然后,System的OnUpdate原则上也是存在就会执行,不过像这个例子里面,在OnCreate里面写了Update的执行条件:
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExecuteMainThread>();
}
这里规定了需要ExecuteMainThread组件才会执行该System的Update。而ExecuteMainThread是一个什么东西呢?
看一下子场景里面的Execute对象:

上面挂载了一个ExecuteAuthoring的Monobehaviour脚本,然后勾选了MainThread,再打开这个脚本看看:

其实是一个很简单的脚本,上面有这么多个勾选,在Bake的时候,只要你勾选了某个选项,下面就使用AddComponent添加对应的组件而已。比如上面说的ExecuteMainThread

添加这个组件其实是没有功能性的,只是为了区分当前System是否需要运行的一个依据。这也是ECS比较常见的一种System开关手段。
(3) 再看看在这个System的OnUpdate方法
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
// Loop over every entity having a LocalTransform component and RotationSpeed component.
// In each iteration, transform is assigned a read-write reference to the LocalTransform,
// and speed is assigned a read-only reference to the RotationSpeed component.
foreach (var (transform, speed) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
{
// ValueRW and ValueRO both return a ref to the actual component value.
// The difference is that ValueRW does a safety check for read-write access while
// ValueRO does a safety check for read-only access.
transform.ValueRW = transform.ValueRO.RotateY(
speed.ValueRO.RadiansPerSecond * deltaTime);
}
}
值得注意的地方是:
foreach (var (transform, speed) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
这个写法是ECS使用组件的典型写法,它只针对场景上所有实体身上的某些组件,比如这里关注了2个组件,一个是自带的LocalTransform,另外一个是我们自己定义的RotationSpeed组件。
当子场景里面的实体身上有这些指定的组件时,逻辑才会执行,如果没有这些组件,就不处理了。
针对这个例子,我们看到的是,当找到一个实体身上同时有LocalTransform和RotationSpeed组建时,则通过Transforms的RotateY方法,使用RotationSpeed的RadiansPerSecond给当前的实体进行旋转操作。
所以运行的时候,会看到盒子在自转。
最后注意一下,在设置值和读取值的时候,出现了transform.ValueRW和transform.ValueRO。那么ValueRW和ValueRO有什么区别呢?
ValueRW和ValueRO都是这个组件值的引用,区别在于ValueRW是可读写的,而ValueRO是只读的。
(4) 如果在项目里面写了System,由不想它自动启动,可以通过DisableAutoCreation来禁止它自动执行,比如可以这样写:

- 运行时的实体
在保持子场景不打钩的情况下,在运行中我们是可以直接选中实体的:


留意看Inspector面板,这里的显示就和传统的GameObject的组件显示不一样,显示的都是实体运行时的组件,比如LocalTransform、LocalToWorld这种自带的组件,还能看到我们自己定义的RotationSpeed组件:

我们甚至可以直接改Inspector面板上面的值,直接改变实体的运行表现。
对于第一个HelloCube_MainThread例子,我们居然分析出了这么多东西。并不是说这个例子有多么复杂,而是因为,这是第一个接触ECS的例子,有很多基础的东西需要交代清楚,不然是完全看不懂的。回顾一下,ECS需要的要素有:
1. 让实体运行的子场景SubScene
2. 把MonoBehaviour脚本挂载的对象通过Bake方法变成实体和ComponentData
3. 给对应的ComponentData写对应的System,控制实体对应组件的功能
都是很基础的东西,搞明白之后,看接下来的内容时,就不会再重复叙述了。由于这个例子占用的篇幅有点长,所以剩下的例子再开一篇。
3614

被折叠的 条评论
为什么被折叠?



