在前面的介绍中,我们都是通过代码来实现物体在场景中的显示,例如我们要显示一个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的值就是我们的DefaultWorldWorld.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,用于将我们的TransformRectTransform转换为LocalToWorldTranslationRotationNonUniformScale等Component。

另一个则是MeshRendererConversion,将MeshRendererMeshFilter转换成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中带有TransformRectTransform的Entity,然后执行Convert方法,在Convert方法中,找到对应的PrimaryEntity,然后为其添加LocalToWorldTranslation等组件,并赋上对应的值。如果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添加了ParentLocalToParent。最终可以总结出如下四种情况

  • 无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();

ECS的简单入门(六):传统GameObject模式转换到ECS模式相关推荐

  1. OGG 抓取进程模式转换(集成模式→经典模式)(integrated→classic)

    说明:本文为Oracle GoldenGate进程模式转换指定手册 模式:抓取进程集成模式转经典模式(integrated转classic) 温馨提示:如果您发现本文哪里写的有问题或者有更好的写法请留 ...

  2. ECS的简单入门(一):概念

    官方文档:https://docs.unity3d.com/Packages/com.unity.entities@0.9/manual/ecs_core.html 视频资料:UUG Online直播 ...

  3. ECS的简单入门(三):Component

    Component概念 ECS中的Component和我们以往MonoBehaviour的Component不同,ECS的Component中,只用于存放数据,并不会有行为.操纵数据的行为我们交给了S ...

  4. ECS的简单入门(四):System

    概念 在System中,我们就可以对Component中的数据进行处理了,例如我们每帧改变Translation Component的值,就可以使Entity移动起来. ECS为我们提供了以下几种Sy ...

  5. Python如何把一张RGB模式转换成黑白模式

    1.首先先导入pillow库 from PIL import Image import matplotlib.pyplot as plt 如果没有安装pillow库或者matpotlib库 请在com ...

  6. 自定义分割数据集中的png格式图片的模式转换(P -> L)以及其他问题解决

    0. 问题背景 使用labelme对自己的工业数据集进行了标注,得到原图和标注的json文件,如下图所示: 因为自己前期用的是图片+mask的数据集格式进行训练,所以也想接着用这种格式,那么接下来就要 ...

  7. 阿里云平台创建 centos系统ECS 实例快速入门 (创建及释放ECS)

    文章目录 Linux系统实例快速入门 准备工作 步骤一:创建ECS实例 步骤二:添加安全组规则 步骤三:连接ECS实例 步骤四:配置Apache服务 步骤五:(可选)解析网站域名 步骤六:(可选)释放 ...

  8. 定时任务-Quartz、Mycat简单入门、Linux下安装MySQL、Linux下安装MyCAT、Mycat的数据库分片、Mycat读写分离

    表现层:页面 后台管理系统.商城门户.搜索系统.订单系统.商品详情系统.购物车系统 中间件:dubbo 系统之间的通信,服务的统计,rpc协议远程过程调用 同步通信 服务层:实现具体的业务逻辑 商品服 ...

  9. 【系列索引】结合项目实例 回顾传统设计模式 打造属于自己的模式类系列

    网上设计模式的文章很多 虫子就不再和大家扯一些没有营养的理论 开此系列博文 一方面因为自己颓废了大半年 趁此机会回顾一下 另一方面希望能够帮助新人走出设计模式的误区, 如何做好设计模式 1.在发掘新的 ...

最新文章

  1. XML文件转换成字符串互相转换操作
  2. 深度学习(19)神经网络与全连接层二: 测试(张量)实战
  3. 6、java中的排序算法
  4. 树状数组萌新讲解+基础习题【一点一滴】
  5. mysql如何进行数据透视,mysql-如何优化数据透视表的条件检查?
  6. Android应用程序消息处理机制(Looper、Handler)分析(3)
  7. 求不规则立方体表面积java_不规则立方体体积计算
  8. 嵌入式状态机编程-QP状态机框架与常见状态机方法
  9. mysql身份证号性别_mysql中身份证号判断男女人数
  10. 分享Win10虚拟机VMware安装黑苹果MacOS Sierra图文教程
  11. java毕业设计学生学习评价系统Mybatis+系统+数据库+调试部署
  12. “窗体”工具栏控件和“控件工具箱”控件基础
  13. 约瑟夫问题(小小算法,真不可笑)
  14. 2022-6-13 全O(1)的数据结构,两数相加,无重复字符的最长子串,寻找两个正序数组的中位数,盛最多水的容器,......
  15. 构建优秀产品信息架构的三要素
  16. Java工程师学习指南(2019最新版)
  17. linux 批量更换文件名,Linux下批量修改文件名的方法
  18. Leetcode 414
  19. 第一次机房收费系统验收
  20. JavaScript的精华与糟粕

热门文章

  1. 跳频扩频通信系统Matlab仿真
  2. adb修改android设备mac地址
  3. 美颜滤镜:早已不是简单的磨皮美白
  4. 用Python进行批量文件整理
  5. VS2019使用教程(使用VS2019编写C语言程序)
  6. Retinex 图像去模糊(含MATLAB代码)
  7. 高考平行报志愿计算机录取规则,2020年高考平行志愿录取规则大全
  8. MySQL记录删除后竟能按中间被删除的主键加回去,磁盘空间被重用!
  9. pomelo学习记录
  10. YII Framework学习教程-YII的日志