赞
踩
游戏只是关于玩家输入改变数据,然后看到不同呈现的事物,因此玩得开心。这些数据是使用 Unity 的 WYSIWYG 工具在scene设计的。但是那些基于 GameObject 的数据与 ECS 不兼容。如果像以前一样轻松编写数据,而是在运行时获取 ECS 数据,以便我们可以有效地处理它们,那就太好了。
本文基于 Entities 包版本 0.5.1
让我们想想为什么我们会看到像这样每一帧都正确绘制的立方体?在经典的 GameObject 中,引擎代码会编译整个层次树,并以正确的顺序排序和调用绘图方法(“draw call”)。在每次调用之间,它还需要设置材质和网格来绘制(“设置传递调用”),并且如果它可以使用与前一次绘制相同的东西,则不设置。此外,Unity 可能会尝试组合网格以减少绘制调用(动态批处理和静态批处理),这些调用在动态情况下每帧都会消耗一些 CPU 周期。如果是静态的,它将在构建时预先组合准备绘制。
换句话说,我们使用 Unity 创作保存在 YAML 场景文件中的游戏。虽然游戏不是在 YAML 上运行的,但在加载一个 Scene 并跟随 Cube.prefab 文件并加载它之后,引擎会准备一些 Transform 的 C++ 内存,以便绘图代码可以根据它们进行绘制。 (在哪里画什么画)
这是每帧性能的一部分。这取决于数据、每个立方体上的transform以及告诉我们要绘制什么的 MeshFilter。不幸的是,这种排序,关心绘制调用的顺序会得到正确的结果,所有这些都是某种形式的数据迭代。 (在本机 C++ 中,但如果它们排列不当,C++ 并不等于快。)而且它不是那么快,因为这些数据没有以最佳形状排列。您知道 ECS 擅长迭代。 Burst plus ECS 有助于使数据可线程化。所以这就是我们想在 ECS 中制作游戏的原因。
另一部分是逻辑。在开始绘制之前,立方体可能有弹跳组件,不断改变其transform等,以便让您的游戏更加精彩。尽管如此,ECS 还是会在很大程度上帮助引导要被draw的数据。
游戏基本上就是这样。您有一些经常更改的数据,并且可能会根据玩家在游戏中输入的内容而更改,这会影响帧的绘制方式。
两者结合起来,信不信由你,玩家们可能已经玩得很开心了! 所以其实大家只是在改变数据,看到图纸的更新,可以放松一下。从数据的角度来看待游戏是很重要的,否则你无法接受如何在DOTS中制作游戏,因为它将会感觉非常奇怪。
如果数据在 ECS 数据库而不是 GameObject 中,想象一下,不仅改变导致绘制的数据的逻辑很快,而且绘制本身也会很快,因为我们可以更快地迭代,我们可以对Job进行排序并使用 SIMD 指令。
现在让我们跳到最后。我已经使用过 ECS,现在我有 3 个要绘制的变换矩阵(在 IComponentData 中,因此它的迭代是线性且快速的。)加上一个网格和材质。我现在想看这3件事。
我们可以在没有任何低效 GameObject 数据容器的情况下绘制东西!请参阅 Unity 的图形 API (https://docs.unity3d.com/ScriptReference/Graphics.html) 以及这个全新的 BatchRendererGroup (https://docs.unity3d.com/2019.3/Documentation/ScriptReference/Rendering.BatchRendererGroup.html)支持 NativeArray。您现在可以从数据中进行绘制。只需使用 ECS 的 EntityQuery 等获取数据并绘制即可。你做了一个游戏!
请参阅这篇日本博客文章 https://virtualcast.jp/blog/2019/10/batchrenderergroup/,展示了 BatchRendererGroup 的强大功能。尽管显示的“批次”很大,“通过批处理节省的”绝对为零,但性能非常出色。请记住,批处理实际上是组合网格,它需要工作。 Draw 只是在没有任何变化的情况下重复调用方法,如果您能像每一帧那样在 BatchRendererGroup 中保留 NativeArray 内存,那么这 1000 多个“批次”数字根本不可怕。 (“Set pass call”小数说明draws之间没有太多的 material 切换。)
Unity 制作了混合渲染器包 (https://docs.unity3d.com/Packages/com.unity.rendering.hybrid@latest) 来做到这一点。如果您有一个带有 LocalToWorld(这是矩阵)的实体,并且该块具有关联的 RenderMesh(网格和材质以及其他东西)的 ISharedComponentData,那么它将使用 BatchRendererGroup 为您绘制东西。
并且整个块根据您拥有的 LocalToWorld 实体的数量多次绘制相同的网格和相同的材料。这已经像 CPU 上的 mini-GPU 实例化,它只是一次又一次的绘制调用,现在速度很快,而且这种愚蠢的绘制甚至可以胜过动态批处理,在这种情况下,您支付 CPU 以在每一帧以不同的方式组合网格。 100% 不需要在同一块中执行 set pass 调用,因为块共享相同的网格和材质。如果您的材质启用了 GPU instancing,BatchRendererGroup 真的可以做到这一点,而不是快速绘制。
它是“混合的”,因为 BatchRendererGroup 不是 ECS API,而是常规的 Unity API。但这并不意味着它不以数据为导向。事实上,BatchRendererGroup 的工作方式是极其面向数据的。 MaterialPropertyBlock 的数据,矩阵的数据。在 NativeArray 甚至!所以不要害怕使用它,除非您知道如何使用 Graphics API 获胜或完全靠自己使用 BatchRendererGroup。 (也许是为了减少不必要的步骤,比如剔除。)
知道游戏就是关于改变数据和查看事物的,只需使用 Hybrid Renderer 包,我们就可以制作游戏。假设这是一个关于看到弹跳立方体的游戏。当你按住空格键时,它会弹得更疯狂。这对孩子们来说甚至是一种乐趣。
第一个问题,我如何在代码中获得网格和材质。所以我想我将使用 Resources.Load 加载一个虚拟预制件,其中包含此组件的资产引用:
using UnityEngine;
public class AssetHolder : MonoBehaviour
{
public Mesh myMesh;
public Material myMaterial;
}
有了这个系统,没有任何工作,只是为混合渲染器创建所需的数据。
using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Rendering; using Unity.Transforms; using UnityEngine; [UpdateInGroup(typeof(SimulationSystemGroup))] public class CubeGameSystem : JobComponentSystem { protected override void OnCreate() { base.OnCreate(); var myCube = EntityManager.CreateEntity( ComponentType.ReadOnly<LocalToWorld>(), ComponentType.ReadOnly<RenderMesh>() ); EntityManager.SetComponentData(myCube, new LocalToWorld { Value = new float4x4(rotation: quaternion.identity, translation:new float3(1,2,3)) }); var ah = Resources.Load<GameObject>("AssetHolder").GetComponent<AssetHolder>(); EntityManager.SetSharedComponentData(myCube, new RenderMesh { mesh = ah.myMesh, material = ah.myMaterial }); } protected override JobHandle OnUpdate(JobHandle inputDeps) { return default; } }
看哪!我在 (1,2,3) 处的面向数据的多维数据集!
层次结构中没有任何内容。它只是出现… 无论你如何点击它都无法选择。它只是计算机的图纸。您之前可以选择事物的事实是因为 GameObject 数据容器将您看到的每个事物与您创作的每个事物联系起来。
如果您现在查看实体调试器,您会注意到混合渲染器添加的更多内容。它也会在提交到 BatchRendererGroup 之前剔除你看不到的东西。
顺便说一下,这张图片是这样写的:“你有 1 个块,这个块有 128 个实体的容量(一个块大小为 16kB,因此 1 个实体需要 125 个字节)并且你已经使用了这个块中的 1/128。你可以有还有 127 个在内存中连续的立方体。混合渲染器将有 128 个 LocalToWorld 彼此相邻,以便将它们放入 BatchRendererGroup。
现在你有了一个数据,它变成了一个drawing,让我们来做一些游戏。
我将通过让它反弹来制作游戏玩法,但使用 LocalToWorld 是一种痛苦。想象一下,必须修改 4x4 矩阵中的正确位置。所以 Unity 给我们带来了一些好处。有大量的系统等待使用。
通过更易于使用的平移、旋转、缩放/非均匀缩放,这些系统将为您将它们转换为矩阵形式 LocalToWorld。您甚至可以使用 Parent 并说哪个具有 TRS LTW 内容的实体是其父级,然后您获得的 LocalToWorld 结果将基于父级。例如转换为零,但分配了父级,生成的 LocalToWorld 不再为零,而是完全在父级的 LTW。
我只将Translation添加到cube。游戏系统现在可以修改它而不是 LocalToWorld。 Transform 包中的某些系统会使用它来制作 LTW。我添加了 Cube 标签组件,所以当我查询时感觉更像是一个立方体,而不仅仅是任何实体。
var myCube = EntityManager.CreateEntity(
ComponentType.ReadOnly<Translation>(),
ComponentType.ReadOnly<Cube>(), //Tag
ComponentType.ReadOnly<LocalToWorld>(),
ComponentType.ReadOnly<RenderMesh>()
);
现在,弹跳。我知道余弦函数会给出一个从 0 到 1 并返回到 0 的弹跳值,如果我给它一个递增的值。因此,让我们为此使用 Time.ElapsedTime。
ECS 的美妙之处在于您可以继续“横向”向游戏添加系统以构建游戏玩法。模式是从中央ECS强大而快速的数据库中查询数据,做一些操作,并存储回去。每次添加更多系统时,您可能会对此查询和存储返回仪式感到不安,但它使事情具有可扩展性。并且 Unity ECS lib 确保重复查询都很快,因此您不会气馁将逻辑切片和切块到大量系统中。
该系统在主线程上检查是否按下了空格键,然后声明一个变量和时间变量。将它们捕获到 lambda 作业中,这些作业获取所有带有Translation 的Cube,然后在多线程作业上更改它们的Translation ,每个线程在 1 个cube和Translation块上工作。时间是由 ECS 库维护的一个属性,它从每帧的经典 Unity 时间中获取值。
using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateBefore(typeof(TransformSystemGroup))] public class CubeBouncingSystem : JobComponentSystem { protected override JobHandle OnUpdate(JobHandle inputDeps) { int multiplier = Input.GetKey(KeyCode.Space) ? 100 : 20; var elapsedTime = Time.ElapsedTime; JobHandle jh = Entities.WithAll<Cube>().ForEach((ref Translation t) => { float3 value = t.Value; value.y = math.cos((float) elapsedTime * multiplier); t.Value = value; }).Schedule(inputDeps); return jh; } }
![在这里插入图片描述](https://img-blog.csdnimg.cn/b13597028e1a4d9785f09cfddf287f94.
工作线程用于此任务。当你在系统中制作游戏时,最终几乎所有的事情都会轮流使用工作线程,这样它们就不会太闲了。
你可以看到一些问题。将游戏问题映射到面向数据已经有点困难了。 (任何其他与弹跳立方体无关的游戏)现在您甚至不能再使用所见即所得的工具来制作游戏了。为整个游戏获得这样的 LTW + Mesh + Material 将是一场噩梦。用数据来制作游戏是可怕的,尽管它会非常有效。想象一下,你怎么能只在代码编辑器中创作需要艺术感和迭代的东西。这就像回到 Cocos2D。你想在编辑器中触摸东西来计划。
但请记住,这个立方体的绘图速度非常快。如果您有更多的立方体,绘制代码会将它们一起提供到 BatchRendererGroup 中,因为它们已经在同一个块中,LocalToWorld 以内存方式排列。复制立方体不再是复制游戏对象,这甚至可能代价高昂,它只是将更多具有 LocalToWorld 的 Entity 与已经分配了网格和材料的同一个块制作在一起。
此外,不断更新回 GameObject(“代理”或 GameObjectEntity)的实体也不好。我们希望在运行时使用纯 ECS,而不是必须桥接到 MonoBehaviour 的 ECS。
您想要 ECS 性能,但您不想制作这样的游戏。
因此,Unity 团队采用的一种方法是使用常规 GameObject 仅用于创作。在运行时,没有游戏对象,只有从这些游戏对象转换而来的 ECS 数据。
我之所以必须在之前为您准备渲染器和转换,因为所有转换所做的都是让您从易于创作的 GameObject 中准备好快速转换和渲染实体。
在继续之前,我假设您了解组件对象。您可以附加到实体的不仅仅是 IComponentData。你甚至可以使用 MonoBehaviour 类型。
只是您不能在Job中使用它,并且许多带有 的 API 具有 IComponentData 约束,因此您无法使用它们。但它肯定与实体有关,并且有一些 API 需要不受 IComponentData 约束的类型 T。以下是支持组件对象的 API 示例。
内部转换模式是通过创建一个特殊用途的 World(因此您将获得一个新的 EntityManager、实体的新数据库),其中包含一些特殊的转换系统。这是转换的世界。转换世界还知道另一个世界,即存储转换结果的目的地世界。
因为转换世界是一个 ECS 世界,所以我们必须从一些 Entity 开始执行工作,而不是真正从 Hierarchy 中的 GameObject 开始。由于我之前提到的组件对象,从经典游戏对象场景的导入过程很容易。有可能获得一堆具有完全复制经典层次结构的组件对象(class类型组件)的实体。也就是说,每个实体都有 Transform ECS 组件。具有 RectTransform ECS 组件。具有 MeshFilter ECS 组件。具有 LineRenderer ECS 组件。等等。通过只有 Transform,您还可以保留完整的层次结构信息。
这些实体还不够好,我们想要一个没有组件对象的真实实体。我想要 Translation + LocalToWorld 而不是class类型 Transform。仅需要优化的 IComponentData。
通过更新一次转换世界,因此更新该世界中的所有特殊系统一次,它将处理简单导入的组件对象实体,并在目标世界中产生转换结果。至少,它会为在转换世界中找到的每个 Transform 组件对象创建一个空实体。
目标世界中生成的实体 A 称为“层次结构上游戏对象 A 的主要实体”。这种措辞而不是“结果实体”之类的措辞的原因是因为转换不一定是一对一的!它可以被构造成图像中的 A 在目标世界中产生 A1 和 A2。在这种情况下,其中之一是primary entity。
在转换世界中工作时的一个特殊功能是您可以从 GameObject 中“获取主要实体”。它也可以来自任何 MonoBehaviour 组件,在这种情况下,它只会使用 .gameObject 并使用它。例如来自转换世界中 A 实体的转换。它将在更高级的转换中有用。
转换世界以及带有组件对象的低效实体随后被销毁。
在转换世界中使用的特殊系统可以标记为:
[WorldSystemFilter(WorldSystemFilterFlags.GameObjectConversion)]
然后它们将在转换过程中被全部扫描和添加。但是这个属性是可继承的,推荐的方法是从也具有该属性的 GameObjectConversionSystem 子类化。
例如 TransformConversion 系统。这将在转换世界中寻找 Transform 组件对象,然后在目标世界的主要实体上获取 LocalToWorld 和 Translate / Rotation / NonUniformScale。如果 Transform 的比例为 1,1,1,它甚至知道不要在结果中添加 NonUniformScale!
另一个值得寻找的内置系统是 MeshRendererConversion。这就是 MeshFilter 和 MeshRenderer 从原始组件迁移到带有 Mesh 和 Material 的 RenderMesh 中。
由于转换系统都被扫描并聚集在一起,因此没有定义那么多的顺序。排序可能很重要,假设您想在转换时“获得主要实体”并期望在那里找到 LocalToWorld,那么您必须确保您的转换系统在 Unity 的 TransformConversion 之后出现。
使用旧的 UpdateInGroup 属性是可能的。但是在转换世界中,有一组新的组。
public class GameObjectDeclareReferencedObjectsGroup : ComponentSystemGroup { }
public class GameObjectBeforeConversionGroup : ComponentSystemGroup { }
public class GameObjectConversionGroup : ComponentSystemGroup { }
public class GameObjectAfterConversionGroup : ComponentSystemGroup { }
public class GameObjectExportGroup : ComponentSystemGroup { }
如果你什么都不说,那么它会转到中间的一个 GameObjectConversionGroup。因此,如果您想在 Unity 的内置转换系统之外进一步处理主要实体的转换,则必须确保您来了。请注意,您不能 [UpdateAfter(typeof(TransformConversion))] 因为该类型不是公开的。但是在屏幕截图中,您会看到它说它转到组之前。因此,您在中间组中的所有转换系统都可以期望看到 LocalToWorld 和朋友在等待primary entity。另一方面,MeshRendererConversion 位于中间组。所以如果你想在 RenderMesh 上工作 ISharedComponentData 产生,你必须把你的系统放在after group。
declare 和 export组比较特殊,因为它在某些特定程序之前和之后出现。虽然所有 3 个中间组都只是为了组织目的,并且真的一个接一个地运行。因此,要证明将其他组放入非中间 3 组的理由,您必须知道这些是什么程序。 (以后你会知道)
???
GameObjectDeclareReferencedObjectsGroup.Update()
???
GameObjectBeforeConversionGroup.Update()
GameObjectConversionGroup.Update()
GameObjectAfterConversionGroup.Update()
???
GameObjectExportGroup.Update()
除了所有可用的 GameObjectConversionSystem 之外,转换世界还会自动获得一个非常特殊的系统,称为 GameObjectConversionMappingSystem。 (从现在开始我只会说“映射系统”)这个系统是任何转换世界的老大,可以创造奇迹。
它是一个纯粹的“工具系统”,意味着它有空的 OnUpdate 并且完全期望其他系统能够获取它并作为其他系统的 OnUpdate 的一部分手动执行操作。 (你可以将这个设计理念应用到你自己的游戏中。)
您可以在此处执行 GetPrimaryEntity 以与目标世界通信,或创建更多(次要等)实体。当您要执行转换时,您必须明确指定目标世界。那个世界被用作映射系统的构造器,供转换时使用。
从你的转换系统,你可以 GetOrCreateSystem 来获取你自己的映射系统,但是如果你继承 GameObjectConversionSystem 已经为你获取了映射系统,以及有用的受保护的方法来使用它。
我们知道足以执行转换。现在让我们使用 Unity 的内置转换系统。即 TransformConversion 和 MeshRendererConversion,因此请确保输入具有 Transform、MeshFilter 和 MeshRenderer,以便您看到一些结果。
要选择场景中的一些游戏对象导入到转换世界,我们可以使用 ConvertToEntity 组件。在 Awake 上,它将使用一些标准来选择要导入的内容。因此,如果我像这样将一个游戏对象附加到一个游戏对象上,那么在转换世界中,我将拥有一个带有这些 ECS 组件对象的实体:Transform,Hello。
一旦您进入播放模式,在 Awake 时您将获得:
这绝对没有准备好绘制,因为它还没有 RenderMesh。但是您会看到 TransformConversion 内置系统如何在破坏转换世界之前看到 Transform 组件对象并在主要实体中生成 LTW / TR。
如果我添加一些Scale:
由于 TransformConversion 转换系统代码看到 Transform 具有非同一性尺度,因此决定向primary entity添加附加组件。
通过在转换目标上使用 MeshFilter 和 MeshRenderer,让我们另外看看 MeshRendererConversion 的作用。
我在我的纯数据弹跳立方体旁边得到了一个正确转换的立方体!转换世界也可以为你分配实体名称,因为转换世界知道Transform,很容易在转换世界销毁之前回到.gameObject.name。
“并销毁”原始游戏对象行为不是转换世界流程之一。这只是 ConvertToEntity 额外执行的一件事。因为如果转换给出等效结果,它认为您不希望场景中有重复的东西。如果不是因为它被破坏,我会在同一位置有 2 个立方体,一个来自 MeshFilter 和 MeshRenderer,另一个来自 RenderMesh 和 LocalToWorld 的混合渲染器渲染。
如果您不喜欢这样,您可以使用 GameObjectConversionUtility.ConvertGameObjectHierarchy 来“转换”。但那是为了以后学习。
ConvertToEntity 实际上是将所有子 GameObject 一起提交到转换世界,除非用 ConvertToEntity (Stop) 停止。
转换世界得到:A B E F G H。每一个都有模仿原始对象的组件对象。 Transform 组件对象是这里的关键,因为它有 .parent,转换世界将能够找出整个层次树。
将 ConvertToEntity 添加到 D 将产生警告,表明它没有做任何事情。将 ConvertToEntity 添加到 B 不会产生任何警告,但会给出等效的结果,因为它已由 ConvertToEntity 在 A 导入到转换世界。
ECS 有一个名为 Parent 的组件,其中有一个 Entity 字段来表示转换层次结构。计算的 LocalToWorld 将始终尊重 Parent。如果 Translation 为 0,则情况不再是 LocalToWorld 在第 1 2 3 行第 4 列处全为 0,就好像它有一个父级,而是在父级。
而负责填充 Parent 的转换系统正是 TransformConversion 系统,它为我们提供了 LocalToWorld 和相关components…让我们看看结果:
在 A B E F G H 中,让我们猜猜有多少个块? (无论转换看起来多么神奇,我们都必须了解数据!)假设它们中没有一个在 Transform 中具有任何非标识比例(因此您不会使用 NonUniformScale 获得一些)。
是 3… 为什么?
您可能希望所有的都具有 Parent,而在 A 的情况下,它可以简单地在其中包含 Entity.Null。但是 Unity 选择不添加根本没有意义的组件,因此使用 ComponentType.Exclude 查询是有效的,这可以在更大的游戏中受益。 (您可以想象,如果该实体首先没有任何父实体,则转换系统可能会忽略某些矩阵工作。)
您会看到添加的其他内部组件/系统状态:LocalToParent、Child、PreviousParent。你不必关心。他们最终会帮助您获得正确的 LocalToWorld。
使用转换时,了解将产生多少不同的原型可能很有用。例如,如果您有一个巨大的游戏对象树,其中一些只是空的用于包装,其中一些确实具有用于转换的 MeshFilter 和 MeshRenderer。你可以想象你至少得到了 6 块。 (3 no parent/both/no child * 2 是否有 RenderMesh) 如果你有不同的网格和材质,它会变得更加碎片化,因为 ISharedComponentData RenderMesh。无论如何,当您尝试针对动态/静态批处理、GPU 实例化或 SRP 批处理器进行优化时,在非 ECS Unity 中已经需要这种想法。使用 Entity Debugger,您可以更清楚地看到有多少事物有效地组合在一起。
“并销毁”行为在停止时也发生了一些变化,停止的树将正确地分离并保存。因为您没有在该部分获得等效的实体,所以您可能仍然希望看到它们。
您会为所有被Disabled 的游戏对象添加禁用组件标签。 (并不是说它们不会被转换。)转换并摧毁下面的顶部立方体可以获得 5 个实体,其中 3 个被禁用。
您还可以将 Convert To Entity (Stop) 放在禁用链中的某个位置,即使对象未处于活动状态,停止组件仍然可以通过转换代码进行查询。如果我在 Cube(1) 和 Cube(4) 上停止,我会得到 3 个实体,Cube (2) 和 Cube (3) 已禁用,Cube 未禁用。
如果 :
之前,假设我在这里有一些诸如Hello或LineRenderer之类的废话,他们会进入转换世界。但是由于没有转换系统会查看 Hello 或 LineRenderer 并对主要实体执行任何操作,因此它们将在转换世界的破坏中丢失。
但是有了“转换和注入”模式,这是可能的。
所以我像往常一样得到 LocalToWorld,因为我已经将 Transform 导入到我的转换世界中。但另外,Transform, Hello 和 LineRenderer 在转换世界被处理之前被导出。再加上这种模式的无破坏行为,这意味着您将获得一个可以返回原始游戏对象的实体。如果我们破坏了原始实体,则此主要实体上的 em.GetComponentObject<Transform/LineRenderer> 将使您为空。
所以你现在看到了为什么以前的模式对破坏原始模式有意义(所以如果转换有效,你不会看到重复)以及为什么这种注入模式对保留原始模式有意义(否则导出的组件对象都会导致为空且无用)。
这意味着如果我有一个带有 MeshRenderer 和 MeshFilter 的立方体但选择注入模式,我将在同一位置渲染 2 个立方体。看到这个,一个 2 * 6 * 2 tris 的立方体(蓝色背景可能是 +2 tris)。使用 convert 和 destroy,它仍然会说 26 tris,因为它已转换为 Hybrid Renderer 可以使用的内容。但是在注入模式下,它可以达到 50 次。因为它保留了原始内容,并且您还可以通过 Hybrid Renderer 使用的转换获得产品。
因此,这不是注入模式的良好用法。注入模式用于当您期望处理转换结果的系统能够检查原始对象时,混合渲染器显然不再需要。
这是您可以使用注入模式做的很酷的事情!您现在可以制作混合 ECS 游戏了。即使您没有任何好的转换系统,您也可以将注入模式视为将您的对象变成系统可工作的。只要能够从转换世界中逃脱的组件对象就足够有用了。然后你可以抛弃所有 MonoBehaviour 中的逻辑,转而使用系统来控制它们。
您可以像添加 IComponentData 一样将 MonoBehaviour 添加到 GameObject。也许有一个“标签 MonoBehaviour”,以便您可以识别它们!以前您可以继承 MonoBehaviour 类来获取公开字段以供重用,现在您可以考虑添加多个组件,而不是像在 ECS 中无法继承 struct 那样。
我没有获得任何性能优势,但在添加系统和使用 ECS 的查询能力方面获得了极大的灵活性。
也许你曾经有一个“管理器优化”,你在每个对象上省略了 Update(),而是有一个 Update() 的管理器有一个所有东西的列表,并且管理器迭代并对其执行工作以节省更新成本.
处理组件对象的系统正是如此!除了查询更加灵活。您可以继续抽出管理您想要的游戏对象子集的管理器。您可以更轻松地在队友之间分配工作。您可以通过更改 UpdateBefore/After 而不是使用脚本执行顺序编号来重新排列工作顺序。
例如这个常规的 UGUI 按钮。我想使用系统为它们提供逻辑,例如交替交互,使它们在变灰状态和恢复正常之间闪烁。
所有按钮都具有转换和注入功能,其中之一缺少 BlinkingButton“标签”游戏对象。它根本没有代码,但我只想在查询中使用类型。
转换结果,我们得到 2 个块,一个块有 3 个实体(橙色条向右移动了一点,所以这个块有 3/160)和一个块有 1 个实体。
然后像这样的系统可以插入 ECS 数据库并从场景中获取所有带有闪烁组件的按钮。借助 ECS 查询的强大功能,这些 GameObject 可以“从无到有”非常有用,您必须声明 GameObject[] 并收集正确的东西的日子已经一去不复返了。即使您对ECS没有兴趣,它也已经是制作普通Unity游戏的好工具。
using Unity.Entities; using Unity.Jobs; using UnityEngine.UI; [UpdateInGroup(typeof(PresentationSystemGroup))] public class BlinkingButtonSystem : JobComponentSystem { EntityQuery blinkingButtonQuery; protected override void OnCreate() { base.OnCreate(); blinkingButtonQuery = GetEntityQuery( ComponentType.ReadOnly<Button>(), ComponentType.ReadOnly<BlinkingButton>() ); } //It is not a good idea to have data in system! float collectTime; bool on; protected override JobHandle OnUpdate(JobHandle inputDeps) { collectTime += Time.DeltaTime; if (collectTime > 0.2f) { collectTime -= 0.2f; on = !on; } Button[] buttons = blinkingButtonQuery.ToComponentArray<Button>(); foreach (var b in buttons) { b.interactable = on; } return default; } }
Entities.ForEach 也可以使用组件对象,只是不要使用 ref 或 in,并防止它使用 Burst 并使用 Run 而不是 Schedule 这样它就不会尝试将引用类型放在线程上。您可以通过这种方式放弃 GetEntityQuery。
Entities.WithAll<BlinkingButton>().ForEach((Button b) => { b.interactable = on; }).WithoutBurst().Run();
您现在可以在完全没有迁移到 ECS 或部分迁移的游戏中使用系统。只需使用注入模式进行转换,这样您就可以在系统中查询实体。
前面的例子有所有接受注入转换的按钮。那些按钮内的文本呢?
它与将所有子项提交到转换世界的销毁模式有点不同。这一次,即使children也有具有注入模式的 ConvertToEntity ,他们也会被忽略。只有前一个得到转换和注入。因此,没有生成具有 Text 组件对象的实体。
为了进一步演示这个规则,如果除了所有按钮之外,我还转换 Canvas 父级。
&esmp; 我不再有闪烁的按钮,因为我似乎只有 Canvas!只是为了有趣地回顾一下子提交的销毁模式,我可以尝试将 Canvas 转换的模式更改回销毁,这样我就可以得到没有组件对象的“纯”实体。
您可以看到它一直到文本,并且组件对象都与转换世界一起被销毁,而不会导出到主要实体。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。