在前面的介绍中,我们都是通过代码来实现物体在场景中的显示,例如我们要显示一个Cube,通过创建Entity,添加RenderMesh等Component,设置相应的Material和Mesh值即可。这样就会产生一些新的问题,例如很多资源其实都是美术那边提供或者设置的,例如场景布置,那么美术给的资源我们如何转换成ECS。
下面的内容就会给大家介绍Unity为我们提供的一些方法,来将我们传统的模式转换为ECS模式。
参考文章:https://zhuanlan.zhihu.com/p/109943463
Game Object Conversion
我们先来做个试验,ECS为我们提供了一个名为ConvertToEntity的Monobehaviour组件,顾名思义,功能就是将我们的GameObject转换成Entity。我们在场景中先创建一个Cube的GameObject,然后为其添加上ConvertToEntity组件,Conversion Mode用其默认的Convert And Destroy选项。
或者我们可以直接勾选Inspector面板中的ConvertToEntity选项,编辑器会自动替我们添加ConvertToEntity组件
运行后,我们会发现在Hierarchy面板中,我们的Cube消失了,然而在场景中依旧能看见这个方块。其实这个方块已经变成了我们的Entity了,可以在Entity Debugger中找到它
可以看出GameObject上的一些Monobehaviour组件也转换成相应的Component,关联到了Entity上。
接下来我们来简单的看看Unity具体是如何实现这个转换过程的
ConversionWorld
在ConvertToEntity中的Convert()方法中有这么一段代码
using (var gameObjectWorld = settings.CreateConversionWorld())
在转换时,会创建一个特殊的World名为ConversionWorld,然后往下看有一段为
foreach (var convert in toBeConverted)
AddRecurse(gameObjectWorld.EntityManager, convert.transform, toBeDetached, toBeInjected);
这里就是遍历了所有带有ConvertToEntity的GameObject,然后调用AddRecurse方法,传递的是ConversionWorld的EntityManager。
AddRecurse方法中有如下两段代码,前者调用GameObjectEntity.AddToEntityManager方法传入我们的GameObject,后者则是递归该GameObject下的Child。
......
GameObjectEntity.AddToEntityManager(manager, transform.gameObject);
......
foreach (Transform child in transform)
AddRecurse(manager, child, toBeDetached, toBeInjected);
......
接着我们看看GameObjectEntity.AddToEntityManager方法内部干了些什么
public static Entity AddToEntityManager(EntityManager entityManager, GameObject gameObject)
{
GetComponents(gameObject, true, out var types, out var components);
EntityArchetype archetype;
try
{
archetype = entityManager.CreateArchetype(types);
}
......
var entity = CreateEntity(entityManager, archetype, components, types);
return entity;
}
static Entity CreateEntity(EntityManager entityManager, EntityArchetype archetype, IReadOnlyList<Component> components, IReadOnlyList<ComponentType> types)
{
var entity = entityManager.CreateEntity(archetype);
......
entityManager.SetComponentObject(entity, types[t], component);
......
}
很熟悉的代码,利用EntityManager创建Archetype,创建Entity。同时获取到该GameObject上的Monobehaviour Component关联到Entity上。
因此可以看出,每个带有ConvertToEntity的GameObject以及其子GameObject,都会一对一的生成一个Entity,并关联上GameObject上的Monobehaviour Component,存储在ConversionWorld当中,我们可以将其当做是一个中转站。
DestinationWorld,PrimaryEntity
接着往下看有这么一段代码,一样的,看下里面的具体实现
GameObjectConversionUtility.Convert(gameObjectWorld);
internal static void Convert(World conversionWorld)
{
using (var conversion = new Conversion(conversionWorld))
{
using (s_UpdateConversionSystems.Auto())
{
DeclareReferencedObjects(conversionWorld, conversion.MappingSystem);
conversion.MappingSystem.CreatePrimaryEntities();
conversionWorld.GetExistingSystem<GameObjectBeforeConversionGroup>().Update();
conversionWorld.GetExistingSystem<GameObjectConversionGroup>().Update();
conversionWorld.GetExistingSystem<GameObjectAfterConversionGroup>().Update();
}
......
using (s_UpdateExportSystems.Auto())
conversionWorld.GetExistingSystem<GameObjectExportGroup>()?.Update();
}
}
其中CreatePrimaryEntities方法里面(代码就不贴了,大家可以自己看看)则是根据ConversionWorld中Entity的数量,在DestinationWorld中再次生成一份,DestinationWorld中的Entity就将是我们的最终转换结果。
DestinationWorld的设置为下面代码,其中convertToWorld.Key的值就是我们的DefaultWorld(World.DefaultGameObjectInjectionWorld)。
var settings = new GameObjectConversionSettings(
convertToWorld.Key,
GameObjectConversionUtility.ConversionFlags.AssignName);
而存储在DestinationWorld的Entity,我们称之为PrimaryEntity。在GameObjectConversionSystem中(下面会提到)我们可以通过GetPrimaryEntity方法来通过GameObject或者GameObject上的Monobehaviour组件获取到对应的PrimaryEntity:
public Entity GetPrimaryEntity(UnityObject uobject) => m_MappingSystem.GetPrimaryEntity(uobject);
public Entity GetPrimaryEntity(Component component) => m_MappingSystem.GetPrimaryEntity(component != null ? component.gameObject : null);
生成好Entity后,我们可以看见通过获取特定的SystemGroup执行其Update方法,这样我们的整个的转换过程就大体的完成了。有关这些System的知识,继续往下看。
ConversionSystem
在ConversionWorld中会有一些特定的ConversionSystem,用于将我们ConversionWorld中Entity的Monobehaviour Component转换成Component(IComponentData)关联到对应的PrimaryEntity上。
被标记了如下 attribute 的 system 将在 ConversionWorld 中被调用:
[WorldSystemFilter(WorldSystemFilterFlags.GameObjectConversion)]
由于这个attribute是可继承的,因此我们可以通过继承ECS库中提供的GameObjectConversionSystem来实现自定义的ConversionSystem。
下面我们来看两个简单的例子,都是ECS库中提供的继承于GameObjectConversionSystem的System:
一个是TransformConversion,用于将我们的Transform和RectTransform转换为LocalToWorld,Translation,Rotation和NonUniformScale等Component。
另一个则是MeshRendererConversion,将MeshRenderer和MeshFilter转换成RenderMesh等Component。
我们来看一下TransformConversion的代码,MeshRendererConversion的有兴趣的可以自己去看下
[UpdateInGroup(typeof(GameObjectBeforeConversionGroup))]
[ConverterVersion("joe", 1)]
class TransformConversion : GameObjectConversionSystem
{
private void Convert(Transform transform)
{
var entity = GetPrimaryEntity(transform);
DeclareDependency(transform, transform.parent);
DstEntityManager.AddComponentData(entity, new LocalToWorld { Value = transform.localToWorldMatrix });
if (DstEntityManager.HasComponent<Static>(entity))
return;
var hasParent = HasPrimaryEntity(transform.parent);
if (hasParent)
{
DstEntityManager.AddComponentData(entity, new Translation { Value = transform.localPosition });
DstEntityManager.AddComponentData(entity, new Rotation { Value = transform.localRotation });
if (transform.localScale != Vector3.one)
DstEntityManager.AddComponentData(entity, new NonUniformScale { Value = transform.localScale });
DstEntityManager.AddComponentData(entity, new Parent { Value = GetPrimaryEntity(transform.parent) });
DstEntityManager.AddComponentData(entity, new LocalToParent());
}
else
{
DstEntityManager.AddComponentData(entity, new Translation { Value = transform.position });
DstEntityManager.AddComponentData(entity, new Rotation { Value = transform.rotation });
if (transform.lossyScale != Vector3.one)
DstEntityManager.AddComponentData(entity, new NonUniformScale { Value = transform.lossyScale });
}
}
protected override void OnUpdate()
{
Entities.ForEach((Transform transform) =>
{
Convert(transform);
});
Entities.ForEach((RectTransform transform) =>
{
Convert(transform);
});
}
}
理解起来很简单在Update中找到ConversionWorld中带有Transform和RectTransform的Entity,然后执行Convert方法,在Convert方法中,找到对应的PrimaryEntity,然后为其添加LocalToWorld,Translation等组件,并赋上对应的值。如果scale值为1,则不会为Entity添加NonUniformScale组件
这也就解释了前面我们的Cube转换成Entity后,拥有了LocalToWorld,Translation和RenderMesh等Component。
Conversion顺序
ConversionSystem同样有着相应的执行顺序,如果我们自定义一个ConversionSystem,需要获取到PrimaryEntity的Translation Component,那就必须在TransformConversion后执行。
在ConversionWorld中,ECS提供了下列这些Group(声明在GameObjectConversionSystem.cs中):
public class GameObjectDeclareReferencedObjectsGroup : ComponentSystemGroup { }
public class GameObjectBeforeConversionGroup : ComponentSystemGroup { }
public class GameObjectConversionGroup : ComponentSystemGroup { }
public class GameObjectAfterConversionGroup : ComponentSystemGroup { }
public class GameObjectExportGroup : ComponentSystemGroup { }
我们同样可以使用UpdateInGroup的attribute来给System设置Group,例如
[UpdateInGroup(typeof(GameObjectBeforeConversionGroup))]
class TransformConversion : GameObjectConversionSystem { }
TransformConversion将运行在GameObjectBeforeConversionGroup中,若没有设置的话,将默认运行在GameObjectConversionGroup。
注意:我们不能使用[UpdateBefore(typeof(TransformConversion)] 或者 [UpdateAfter(typeof(TransformConversion)],因为这些ECS库提供的ConversionSystem不是Public的。
自定义的Monobehaviour组件转换
像Transform,MeshRenderer这些组件ECS已经为我们提供好了相对应的ConversionSystem,但是往往在开发中我们会有很多的自定义的Monobehaviour组件,要想转换成相应的ECS Component的话,有下面两种方法可以实现。
假设我们有一个名为MoveMono的Monobehaviour组件,用于控制物体的移动,代码如下:
public class MoveMono : MonoBehaviour
{
public int Speed;
}
要转换到ECS的话,就需要相对应的有个ECS Component,名为MoveComponent
public class MoveComponent : IComponentData
{
public int Speed;
}
下面我们就来看看如何将GameObject+MoveMono转换为Entity+MoveComponent
自定义ConversionSystem
类似于前面的TransformConversion,我们可以通过继承GameObjectConversionSystem,来实现自定义的ConversionSystem,然后在里面查询到所有带有MoveMono的GameObject,给对应的PrimaryEntity添加上MoveComponent即可,代码如下:
[UpdateInGroup(typeof(GameObjectBeforeConversionGroup))]
public class MoveConversion : GameObjectConversionSystem
{
private void Convert(MoveMono move)
{
var entity = GetPrimaryEntity(move);
DstEntityManager.AddComponentData(entity, new MoveComponent() { Speed = move.Speed });
}
protected override void OnUpdate()
{
Entities.ForEach((MoveMono move) =>
{
Convert(move);
});
}
}
IConvertGameObjectToEntity
ECS提供了一个名为IConvertGameObjectToEntity的接口,其内部方法Convert如下:
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
同时ECS中有一个名为ConvertGameObjectToEntitySystem的ConversionSystem,它会遍历ConversionWorld中所有的GameObject。然后通过GetComponents方法获取到该GameObject下所有实现了IConvertGameObjectToEntity的组件,然后调用它们的Convert方法,因此我们的转换逻辑就可以写在Convert方法中。
来看看Convert方法中三个参数的具体含义
Entity entity | 该GameObject对应的PrimaryEntity |
EntityManager dstManager | DestinationWorld的EntityManager(注意不是ConversionWorld的) |
GameObjectConversionSystem conversionSystem | ConvertGameObjectToEntitySystem |
具体实现代码如下
public class MoveMono : MonoBehaviour, IConvertGameObjectToEntity
{
public int Speed;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
dstManager.AddComponentData(entity, new MoveComponent(){Speed = Speed});
}
}
了解了这些知识我们就可以很清晰的知道一个GameObject是如果转变成我们的Entity了。接下来进行一些拓展
Parent
前面我们将一个GameObject转换成了Entity,现在我们来试试将带有层次结构的一串GameObject转换看看,如下图:
我们创建一些空的GameObject(只带有Transform,且Scale的值都为1,防止部分添加上NonUniformScale组件,导致Archetype不同),同时在根节点的GameObject(图中的A)添加ConvertToEntity组件,运行看看结果。
根据前面的介绍,由于在AddRecurse方法中存在递归,因此挂有ConvertToEntity的GameObject其子节点也都会被转换为Entity,所以A-G都变为了Entity。
但是为什么一样的GameObject却生成了三个Chunk呢?这就和我们的层次结果有有关了,其实在前面的TransformConversion的代码中我们就可以发现,对于有Parent的Entity添加了Parent和LocalToParent。最终可以总结出如下四种情况
- 无Parent,无Child(如前面例子的Cube):无额外添加
- 无Parent,有Child(如A):添加Child
- 有Parent,有Child(如B):添加Child,添加Parent,LocalToParent,PreviousParent
- 有Parent,无Child(如C):添加Parent,LocalToParent,PreviousParent
注:LocalToWorld的计算是基于Parent的
我们简单的看下这几个组件的实现以及其作用
//Parent.cs
[Serializable]
[WriteGroup(typeof(LocalToWorld))]
public struct Parent : IComponentData
{
public Entity Value;
}
[Serializable]
public struct PreviousParent : ISystemStateComponentData
{
public Entity Value;
}
[Serializable]
[InternalBufferCapacity(8)]
[WriteGroup(typeof(ParentScaleInverse))]
public struct Child : ISystemStateBufferElementData
{
public Entity Value;
}
Parent | Component Data,纪录了父节点的Entity |
PreviousParent | System State Component Data,同样是纪录父节点的Entity,主要作用在于当新增或删除或改变Parent的时候,用做判断(例如,一开始Parent和PreviousParent的值都是Entity1,某时刻Parent的值变为了Entity2,与PreviousParent的值不同了,说明了该Entity的Parent值改变了) |
Child | System State 和 Dynamic buffer的结合Component Data,类似Array,用于纪录所有子节点的Entity |
我们来看下D中的Component数据帮助理解,D的Parent为A,Child为E和F
Convert To Entity (Stop)
ECS还为我们提供了StopConvertToEntity的Monobehaviour Component,其主要功能就是中断自身以及其子层级的Entity转换。例如上面的例子中,我们给D添加StopConvertToEntity,那么将不会生成DEF三个Entity。其实现代码其实就在我们前面提到过的AddRecurse方法中:
static void AddRecurse(EntityManager manager, Transform transform, HashSet<Transform> toBeDetached, List<Transform> toBeInjected)
{
if (transform.GetComponent<StopConvertToEntity>() != null)
{
toBeDetached.Add(transform);
return;
}
......
}
GameObject Disabled
在Hierarchy中隐藏的GameObject,同样会被转换成Entity,但是会被添加上Disabled Component。
ConvertAndInjectGameObject
在前面,我们ConvertToEntity组件的ConversionMode选择的都是默认的ConvertAndDestroy,即转换成功后删除原始GameObject。它还有另个选项ConvertAndInjectGameObject,我们将上面例子中 A 上挂载的ConvertToEntity组件选择该选项,运行起来看看结果是如何:
这次只生成了一个Entity(A),没有将A的Child也转换为Entity,同时原始的Transform组件也被关联到了Entity上。在Hierarchy中也可以看到原始的GameObject(A)也没有被删除。(具体的实现大家可以自行看看源码,就不展开了)
这个模式的作用在于,我们可以利用Entity关联着的原始组件(例如Transform或者其他挂载在GameObject上的Monobehaviour组件)追溯到原始的GameObject。
注:当Parent(如A)的ConvertToEntity选择ConvertAndInjectGameObject后,Child(如B)的ConvertToEntity无效,如图:
使用System控制GameObject
根据上面的特性,我们就可以利用System来管理GameObject,而抛弃传统的Monobehaviour。在System中我们可以利用相关的Monobehaviour 组件查询并追溯到原始的GameObject,利用System的Update等方法对他们进行逻辑处理即可。
这么做的好处在于首先使用System的查询是很方便且快捷的(利用了Job的多线程),其次当有多个System共同协作的时候,也可以利用 UpdateBefore/After 的Attribute来控制执行顺序。
举个简单例子,例如我们想要移动一类GameObject,可以创建一个空的Monobehaviour Component来当作Tag,纯粹是用于查询使用的(假设叫做Move),然后挂在那些要移动的GameObject上(当然了,根据前面提到的,这些GameObject不能相互嵌套)。然后我们利用ConvertToEntity的ConvertAndInjectGameObject,将其转换成Entity,这样我们就会得到一堆带有Move和Transform的Entity,当然可能还有其他被转换的Entity,但是其他的Entity肯定不带有Move。
接着我们写一个System,查询带有Move和Transform的Entity,并追溯到原始GameObject,添加上移动相关的代码即可。
public class MoveCubeSystem : SystemBase
{
EntityQuery query;
protected override void OnCreate()
{
base.OnCreate();
query = GetEntityQuery(ComponentType.ReadOnly<Transform>(), ComponentType.ReadOnly<Move>());
}
protected override void OnUpdate()
{
Transform[] transArray = query.ToComponentArray<Transform>();
foreach (var trans in transArray)
{
trans.position += trans.forward * Time.DeltaTime;
}
}
}
我们也可以使用Entities.ForEach方法来处理,需要注意的是,必须要使用WithoutBurst,并且使用Run来执行Job,否则报错如下:
error DC0023: Entities.ForEach uses managed IComponentData Transform&. This is only supported when using .WithoutBurst() and .Run().
同时对于Transform不能使用ref或者in关键字,否则报错如下:
error DC0024: Entities.ForEach uses managed IComponentData Transform& by ref. To get write access, receive it without the ref modifier.
所以,正确的代码如下:
Entities.ForEach((Transform trans, Move move) => {
trans.position += trans.forward * Time.DeltaTime;
}).WithoutBurst().Run();
//或者
//Entities.WithAll<Move>().ForEach((Transform trans) =>
//{
// trans.position += trans.forward * Time.DeltaTime;
//}).WithoutBurst().Run();
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)