Jobs System
Jobs是Unity自己的多线程框架,在这里就对ECS提供了支持。
public class RotationSpeedSystem : JobComponentSystem
{
[ComputeJobOptimization]
struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
{
public float dt;
public void Execute(ref Rotation rotation, [ReadOnly]ref RotationSpeed speed)
{
rotation.Value = math.mul(math.normalize(rotation.Value), math.axisAngle(math.up(), speed.Value * dt));
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new RotationSpeedRotation() { dt = Time.deltaTime };
return job.Schedule(this, 64, inputDeps);
//64指的是每个JOBS至少分配64个循环,循环可以由此拆分到多个线程同时执行。
}
}
整套东西使用起来还是很简单的,用实现IJobProcessComponentData<T1, T2...>的类标记要读写的ComponentData,Execute是逻辑,然后用Schedule推入Jobs系统。按这个模板书写就行了。
[ReadOnly][WriteOnly]元标签可以指定Component的读写模式,将数据进一步分离。
剩下就是归系统内部调配了,会在不产生线程冲突的情况下分配逻辑线程的时间片,实现高效率的并发。
上面这个IJobProcessComponentData是ECS框架实现的一个比较简便的用法(不需要写循环,只写其中一个循环节就可以了),本身是IJob的一个扩展。
ISharedComponentData
与普通的IComponentData不同,虽然都是struct,如果你先初始化它的值再给每个Entity用AddSharedComponent附加上它的话,并不会产生复制,只会占用一块内存(估计传过去的是地址)
只是读取很正常,但当你修改某个Entity上它的副本的时候,它会把自己立即复制一份,然后应用这个修改。
Entitiy实际上是一个int,ComponentData也就是普通的struct,System则是各种系统事件的接受者,类似MonoBehaviour。他们都只要实现对应的接口就能实现功能。在用EnitityManager创建Entity,添加Component之后(和原来的GameObject一样),System逻辑就会自动生效。
System的逻辑实现,是在特定的事件上(如OnUpdate)用自定义的代码拉取一些Component数组来执行自己的逻辑,主要用的就是GetEntities<Group>方法,这会按Group的内容,拉取指定的Component数组出来,然后遍历就可以了。
(注意拉取的是整个场景上同类型的所有Component,而不是一个)
拉取的时候会进行筛选,没有添加指定Component的Entitiy并不会被拉取(除此之外,还可以用ComponentGroup设置筛选条件)
总之就是每个System手动遍历一遍指定的Component执行逻辑了——但也可以不遍历,或者遍历多次,两两交叉遍历都是可以的,因为全都是自定义代码,就算是JobComponentSystem也一样。
System其实就是MonoBehaviour(删除数据后)的集合体,MonoBehaviour执行一个Entity的逻辑,而System执行全部的。
class RotatorSystem : ComponentSystem
{
struct Group
{
RotateComponentData rotation;
SpeedComponentData speed;
}
override protected OnUpdate()
{
float deltaTime = Time.deltaTime;
foreach (var e in GetEntities<Group>())
{
e.rotation *= Quaternion.AxisAngle(e.speed * deltaTime, Vector3.up);
}
}
}
执行顺序
系统给方法确定顺序,只要在System上加元标签就行了:
[UpdateBefore(typeof(RotationSpeedSystem))]
觉得可以乱序执行的部分不加就是了,在使用Jobs的时候不限制顺序才能增加性能。
对GameObject系统的支持
ECS和原有的GameObject+MonoBehaviour功能其实并没有完全重合。至少在现在,Transfrom,Renderer和物理组件依然必须挂接在GameObject上,并且不挂在GameObject上也无法在场景里编辑。
Entity本来是用EntityManager.CreateEntity()来创建,然后这样进行复制的(非必须)
EntityManager.Instantiate(entity, instances);
但是如果你把上面参数里的entity换成一个Prefab对象的话,它也能正常执行,并和GameObject.Instantiate一样生成一个可显示的GameObject对象,并且照常生成对应的entity。这些生成的entity就是和GameObject绑定的存在了,也就把GameObject引入了ECS系统。
对于引入ECS的GameObject,不管是GameObject还是上面的组件都可以和ComponentData一样管理。
虽然Renderer之类必须得是Behaviour组件,但是一些挂载在GameObject用来填写数据的脚本,我们显然还是希望它们进入系统用的是ComponentData,而不是旧版的MonoBehaviour。
另外,我们的ComponentData也是需要能够在编辑器环境显示以及编辑的,而ComponentData并不能挂在GameObject上。
这个框架提供了一个桥接类来处理这个问题:
[Serializable]
public struct Radius : IComponentData
{
public float radius;
}
public class RadiusComponent : ComponentDataWrapper<Radius> { }
下面那个RadiusComponent 是可以正常挂到脚本上的。而在ECS系统内,它则会被处理隐式地处理成Radius。
依赖注入
使用[Inject]标签,可以根据字段类型自动注入一些数据。
public struct Group
{
//获得某个类型的ComponentData
public ComponentDataArray<Position> Position;
//获得某个类型的Component(这里的Component指的是原本的Unity组件)
public ComponentArray<Rigidbody> Rigidbodies;
//获得Entity列表
public EntityArray Entities;
//获得GameObject列表
public GameObjectArray GameObjects;
//标识排除所有包含MeshCollider的对象
public SubtractiveComponent<MeshCollider> MeshColliders;
//数据数量
public int Length;
}
[Inject]Group m_Group;
和之前GetEntities<Group>不同,这次的Group的不会返回迭代器,所以Group里的字段本身就必须是数组。但依然会进行筛选,并且按Entity顺序排列。
[Inject]OtherSystem m_SomeOtherSystem;
用这个方法可以直接和另一个System通信。这个框架并没有提供任何和解耦相关的东西,所以System间通信都是直接调用。
[Inject]ComponentDataFromEntity<LocalPosition> m_LocalPositions;
Entity myEntity = ...;
var position = m_LocalPositions[myEntity];
这个东西看上去和GetEntities<Group>差不多(除了只能获得一个Component外),但其实功能完全不同。GetEntities会做筛选,而它不会。