上一篇我们迅速地编写了一个超厉害的HelloWorld,然后大家什么都没学到。
对,我要的就是这个效果…
因为…这样很酷(旁白:GUN!)
这次,我们还是用上一篇的代码,然后给大家介绍最简单的概念。
1.组件(Component)
在介绍实体之前,我想先介绍组件。
我们对组件肯定不陌生,Transform就是我们最常见的Unity组件。
而ECS的组件和我们所理解的组件不一样,ECS的组件是纯组件,仅包含数据结构,不包含任何其他功能。
我们再来回忆一下组件的代码:
RotationSpeed_ForEach继承了IComponentData接口,这是ECS的组件接口。
通过代码可以发现,这个类,实际上是一个结构体。它只包含属性,没有任何其他函数。这是和传统组件的区别,传统组件,如Transform,它有SetParent等函数,这是不太"纯"的组件。
ECS的组件就是单纯地只包含1个或多个字段(唔,0个也行)的结构体。
2.实体(Entity)
传统的Unity开发,实体的概念不太明确,大致可以理解为,一个GameObject就是一个实体,实体上面挂载了一堆MonoBehaviour的子类,比如Transform。这个就不多说了,大家都很熟悉。
传统Unity开发,我们通常是把游戏逻辑都写在实体里。
而ECS的实体和我们传统的实体不太一样,它是一个纯实体,它没有任何逻辑,我们可以把ECS的实体理解为:由若干个组件组成的大组件。(非官方定义,只是帮助理解)
实际上,在用ECS这种模式的开发过程中,我们很少会直接对实体进行操作,我们都是在操作实体的组件。
甚至,我不得不告诉大家一个事实——ECS里没有实体,只有实体ID,不同的组件引用了不同的实体ID。所以,在ECS里,实体是一个比较虚无的东西,但它很重要。
3.系统(System)
ECS的System是用于处理逻辑的,并且只处理逻辑,它不应该包含组件或其他东西,它是纯的,整个游戏中,可能会存在很多很多的System。
没错,大家已经发现了,ECS里的所有东西,都是纯的。
而大家以前用的那些东西,都是不纯的(不怀好意)。
我们再来回忆一下System的代码:
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
// This system updates all entities in the scene with both a RotationSpeed_ForEach and Rotation component.
public class RotationSpeedSystem_ForEach : ComponentSystem
{
protected override void OnUpdate ()
{
Entities.ForEach((ref RotationSpeed_ForEach rotationSpeed, ref Rotation rotation) =>
{
var deltaTime = Time.DeltaTime;
/* 旋转Cube,代码逻辑不用管,这里可以是其他任何逻辑 */
rotation.Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
});
}
}
继承了ComponentSystem的类,就是系统类,看起来有点复杂(官方原本的Demo更复杂,我简化了),先别慌,慢慢来。
System可以重写OnUpdate函数,大家可以把它看成是MonoBehaviour的Update函数。这就是之前提到的,ECS把实体的逻辑专门放到System里处理了。OnUpdate函数也是会每帧调用一次的。
很奇怪是不是?System是怎么单独处理逻辑的?它怎么知道自己要处理的是哪个实体的逻辑?
这时候,组件的作用就体现出来了。
代码中的Entities可以理解为是所有实体的缓存(实际上可能不是,先这么理解),对Entities调用ForEach函数,相当于是在所有实体中搜索符合条件的实体。
那么,什么是符合条件的实体?
比如,上面的代码,ForEach的参数是一个Action,我们这个Action有两个参数:RotationSpeed_ForEach rotationSpeed和Rotation rotation。
那么,ForEach就会把所有同时包含RotationSpeed_ForEach组件和Rotation组件的实体查出来,遍历一次。(Rotation是系统自带的组件,我们的Cube在转换为实体时,也会自动附加Rotation组件)
然后,我们就可以在Action里写我们的逻辑,比如这里的逻辑是修改实体的旋转值。
4.实体在哪?
ForEach里传递的参数是一个Action,Action可以带有1到6个参数,都是组件类型(用于筛选实体)。
很奇怪是吗?我刚刚明明说的是修改实体的旋转值,但是,代码里并没有什么实体对象,只有RotationSpeed_ForEach和Rotation两个组件。
没错,我们再来理解一下。
问:Entities的ForEach是干什么用的?
答:用来筛选实体的。
问:通过什么筛选?
答:通过组件筛选——把包含指定组件的实体筛选出来。
等等,筛选出来的实体在哪?
这就是关键的地方,既然我们要的是包含某些组件的实体,我们要操作的也肯定是实体的这些组件对象。
所以,干脆直接把实体的组件返回给我们,我们直接对实体的组件进行操作就可以了。
因此,我们会看到,System里,其实只对组件进行了操作。我们修改了组件的值,那么,包含这个组件的实体就会发生变化。
比如我们的代码,是修改了组件旋转值,那么,实体就会发生旋转。
7.结束
ForEach这个Demo的主要作用是,向大家展示最简单的ECS程序,同时,让大家知道,可以通过Entities.ForEach来遍历查找实体(的组件)。
这就意味着,还有其他的方式查找定位实体(的组件),这个在后续的Demo里会说到。
接下来的几篇都会给大家介绍ECS中获取/筛选/修改实体组件数据的几种方式。
是的,ECS需要做决策的地方有点多,筛选实体的方式有好几种、创建实体的方式也有好几种,对于刚入门ECS的新手来说,真的是非常懵的(比如我)。 好了,本篇到此结束。
注意,本系列教程基于DOTS相关预览版的Package包,是预览版,不代表正式版的时候也适用。
转发本系列文章的朋友请注意,带上原文链接和原文日期,避免误导未来使用正式版的开发者。