天天看点

ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion

在前面的介绍中,我们都是通过代码来实现物体在场景中的显示,例如我们要显示一个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组件

ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion

运行后,我们会发现在Hierarchy面板中,我们的Cube消失了,然而在场景中依旧能看见这个方块。其实这个方块已经变成了我们的Entity了,可以在Entity Debugger中找到它

ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion

可以看出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转换看看,如下图:

ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion

我们创建一些空的GameObject(只带有Transform,且Scale的值都为1,防止部分添加上NonUniformScale组件,导致Archetype不同),同时在根节点的GameObject(图中的A)添加ConvertToEntity组件,运行看看结果。

ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion
ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion
ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion

根据前面的介绍,由于在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

ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion
ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion

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组件选择该选项,运行起来看看结果是如何:

ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion

这次只生成了一个Entity(A),没有将A的Child也转换为Entity,同时原始的Transform组件也被关联到了Entity上。在Hierarchy中也可以看到原始的GameObject(A)也没有被删除。(具体的实现大家可以自行看看源码,就不展开了)

这个模式的作用在于,我们可以利用Entity关联着的原始组件(例如Transform或者其他挂载在GameObject上的Monobehaviour组件)追溯到原始的GameObject。

注:当Parent(如A)的ConvertToEntity选择ConvertAndInjectGameObject后,Child(如B)的ConvertToEntity无效,如图:

ECS的简单入门(六):传统GameObject模式转换到ECS模式Game Object Conversion

使用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();