Svelto.ECS项目Wiki的翻译。 适用于Unity3D的ECS框架


哈Ha! 我向您介绍SebastianoMandalà撰写的Svelto.ECS项目的Wiki的翻译。

Svelto.ECS是SOLID原则在Unity游戏开发中多年研究和应用的结果。 这是可用于C#的ECS模式的众多实现之一,并引入了各种独特功能来解决模式本身的缺点。

初看


查看Svelto.ECS基本功能的最简单方法是下载Vanilla Example 。 如果您想确保它的易用性,我将向您展示一个示例:

//  void ApplicationCompositionRoot() { var simpleSubmissionEntityViewScheduler = new SimpleSubmissionEntityViewScheduler(); _enginesRoot = new EnginesRoot(simpleSubmissionEntityViewScheduler); var entityFactory = _enginesRoot.GenerateEntityFactory(); var entityFunctions = _enginesRoot.GenerateEntityFunctions(); _enginesRoot.AddEngine(new BehaviourForSimpleEntityEngine(entityFunctions)); entityFactory.BuildEntity<SimpleEntityDescriptor>(new EGID(1), new[] { new SimpleImplementor() });` } //  class SimpleEntityDescriptor : GenericEntityDescriptor<BehaviourEntityViewForSimpleEntity> { } public class BehaviourEntityViewForSimpleEntity : EntityView { public ISimpleComponent simpleComponent; } public interface ISimpleComponent { public int counter {get; set;} } class SimpleImplementor : ISimpleComponent { public int counter { get; set; } } //  ()    public class BehaviourForSimpleEntityAsStructEngine : IQueryingEntityViewEngine { public IEntityViewsDB entityViewsDB { private get; set; } public void Ready() { Update().Run(); } //   . //    N ,  N    0  1. IEnumerator Update() { Console.Log("Task Waiting"); while (true) { var entityViews = entityViewsDB .QueryGroupedEntityViews<BehaviourEntityViewForSimpleEntity>(0); if (entityViews.Length> 0) { for (var i = 0; i < entityViews.Length; i++) AddOne(entityViews[i].counter); Console.Log("Task Done"); yield break; } yield return null; } } static void AddOne(int counter) { counter += 1; } } 

不幸的是,不可能快速理解该代码背后的理论,这看起来很简单,但同时又令人困惑。 要理解这一点,您需要花时间阅读“文本墙”,并尝试上面的示例。

引言


最近,我一直在与几个或多或少经验丰富的程序员讨论Svelto.ECS 。 我收集了很多反馈,并做了很多笔记,这些笔记将作为我下一篇文章的起点,在此我将更多地讨论理论和良好实践。 有点困扰:我意识到当您开始使用Svelto.ECS时,最大的障碍是更改编程范例 。 与为开发框架而编写的少量代码相比,我不得不写太多的代码来解释Svelto.ECS引入的新概念真是令人惊讶。 实际上,尽管框架本身非常简单且轻巧,但从积极使用继承的OOP或常用的Unity组件过渡到Svelto.ECS提供的“新”模块化和松耦合设计,使人们无法适应该框架。

Svelto.ECS在Freejam中得到了积极使用(译者注-作者是该公司的技术总监)。 由于我总是可以向同事们解释该框架的基本概念,因此他们花更少的时间来了解使用该框架的情况。 尽管Svelto.ECS尽可能坚韧,但不良习惯很难克服,因此用户倾向于滥用一些灵活性,从而使他们无法适应自己习惯的“旧”范式。 由于框架逻辑基础概念的误解或扭曲,可能导致灾难。 这就是为什么我打算撰写尽可能多的文章的原因,特别是因为我确信ECS范例是目前为大型项目编写有效且可维护的代码的最佳解决方案,这些大型项目在几年内多次更改和重做。 RobocraftCardlife就是证明。

我不会谈论太多有关本文基础的理论。 我只会提醒您为什么我拒绝使用IoC容器,而开始仅使用ECS框架:如果使用IoC容器而不了解控制反转的本质,则它是非常危险的工具。 从您以前的文章中可以看到,我区分了创建控制的反转(创建控制的反转)和流程控制的反转(流程控制的反转)。 逆流控制就像好莱坞的原则一样:“不要打电话给我们,我们会打电话给您。” 这意味着绝不能直接通过公共方法使用注入的依赖项,因为您仅使用IoC容器替代任何其他形式的全局注入(例如单例)。 但是,如果基于管理反转(IoC)使用IoC容器,则基本上所有这些归结为重用“模板方法”模式来引入仅用于注册其管理对象的管理器。 在实际的流控制倒置环境中,管理人员始终负责管理实体。 这看起来像ECS模式吗? 当然可以 基于这种推理,我采用了ECS模式并基于该模式开发了一个刚性框架,其使用无异于应用新的编程范例。

组成根和EnginesRoot


Main类是应用程序的成分根。 组合的根源是创建和实现依赖项的地方(我在文章中谈到了很多)。 合成根属于上下文,但是一个上下文可以具有多个合成根。 例如,工厂是构图的根源。 一个应用程序可能具有多个上下文,但这是一种高级方案,在此示例中,我们将不考虑它。

在深入研究代码之前,让我们熟悉Svelto.ECS语言的第一条规则。 ECS是实体组件系统的缩写。 许多作者在文章中都对ECS基础架构进行了很好的分析,但是尽管基本概念很普遍,但实现方式却相差很大。 首先,没有标准的方法可以解决使用面向ECS的代码时出现的某些问题。 我将尽最大努力来解决这个问题,但是稍后或在后续文章中将对此进行讨论。 该理论基于本质,组件(实体)和系统的概念。 尽管我了解为什么历史上会使用系统一词,但从一开始我就认为它不够直观,因此我将引擎用作系统的代名词,根据您的喜好,您可以使用以下术语之一。

EnginesRoot类是Svelto.ECS的核心。 借助它的帮助,您可以注册引擎并设计游戏的所有本质。 动态创建引擎没有多大意义,因此应将它们全部从创建时所在的合成的相同根目录添加到EnginesRoot实例。 由于类似的原因,切勿部署EnginesRoot实例,并且在添加引擎后也不应删除它们。

为了创建和实现依赖关系,我们至少需要组成的一个根。 是的,在一个应用程序中可能不止一个EnginesRoot,但在本文中,我们将不作赘述,而我会尽量简化。 这是引擎创建和依赖项注入时合成根的样子:

 void SetupEnginesAndEntities() { //Engines Root   Svelto.ECS.      EngineRoot // ,  Composition Root     ,   //   . //UnitySumbmissionEntityViewScheduler -  ,   //EnginesRoot,     EntityViews. //    ,   , //         Unity. _enginesRoot = new EnginesRoot(new UnitySumbmissionEntityViewScheduler()); //Engines root      ,   , //   . //   EntityFactory  EntityFunctions. //EntityFactory      //(    ), //   . _entityFactory = _enginesRoot.GenerateEntityFactory(); // EntityFunctions     //   , //  .        var entityFunctions = _enginesRoot.GenerateEntityFunctions(); //GameObjectFactory   Unity GameObject //   // GameObject.Instantiate.      // ,    ,   //       //(  ,   //         //      -  ) GameObjectFactory factory = new GameObjectFactory(); //    3     Svelto.ECS. //        //  . //         //     . var enemyKilledObservable = new EnemyKilledObservable(); var scoreOnEnemyKilledObserver = new ScoreOnEnemyKilledObserver(enemyKilledObservable); //ISequencer   3     Svelto.ECS // .       : //1)       //(   //,   ,     //  ). //2)   ,     // . ISequencer      //  Sequencer playerDamageSequence = new Sequencer(); Sequencer enemyDamageSequence = new Sequencer(); //    Unity. //     . IRayCaster rayCaster = new RayCaster(); ITime time = new Others.Time(); // .         //  . var playerHealthEngine = new HealthEngine(entityFunctions, playerDamageSequence); var playerShootingEngine = new PlayerGunShootingEngine(enemyKilledObservable, enemyDamageSequence, rayCaster, time); var playerMovementEngine = new PlayerMovementEngine(rayCaster, time); var playerAnimationEngine = new PlayerAnimationEngine(); //  var enemyAnimationEngine = new EnemyAnimationEngine(); var enemyHealthEngine = new HealthEngine(entityFunctions, enemyDamageSequence); var enemyAttackEngine = new EnemyAttackEngine(playerDamageSequence, time); var enemyMovementEngine = new EnemyMovementEngine(); var enemySpawnerEngine = new EnemySpawnerEngine(factory, _entityFactory); //    var hudEngine = new HUDEngine(time); var damageSoundEngine = new DamageSoundEngine(); // Sequencer  ,    // ,     . playerDamageSequence.SetSequence( new Steps // ,  ! { { //  //      Next   enemyAttackEngine, new To //        { //      //   Next playerHealthEngine, } }, { //  playerHealthEngine, //      Next   new To //       { //      Next     //DamageCondition.damage { DamageCondition.damage, new IStep[] { hudEngine, damageSoundEngine } }, //      Next     //DamageCondition.dead { DamageCondition.dead, new IStep[] { hudEngine, damageSoundEngine, playerMovementEngine, playerAnimationEngine, enemyAnimationEngine } }, } } }); enemyDamageSequence.SetSequence( new Steps { { playerShootingEngine, new To { enemyHealthEngine, } }, { enemyHealthEngine, new To { { DamageCondition.damage, new IStep[] { enemyAnimationEngine, damageSoundEngine } }, { DamageCondition.dead, new IStep[] { enemyMovementEngine, enemyAnimationEngine, playerShootingEngine, enemySpawnerEngine, damageSoundEngine } }, } } }); // ,     //  _enginesRoot.AddEngine(playerMovementEngine); _enginesRoot.AddEngine(playerAnimationEngine); _enginesRoot.AddEngine(playerShootingEngine); _enginesRoot.AddEngine(playerHealthEngine); _enginesRoot.AddEngine(new PlayerInputEngine()); _enginesRoot.AddEngine(new PlayerGunShootingFXsEngine()); //  _enginesRoot.AddEngine(enemySpawnerEngine); _enginesRoot.AddEngine(enemyAttackEngine); _enginesRoot.AddEngine(enemyMovementEngine); _enginesRoot.AddEngine(enemyAnimationEngine); _enginesRoot.AddEngine(enemyHealthEngine); //  _enginesRoot.AddEngine(new CameraFollowTargetEngine(time)); _enginesRoot.AddEngine(damageSoundEngine); _enginesRoot.AddEngine(hudEngine); _enginesRoot.AddEngine(new ScoreEngine(scoreOnEnemyKilledObserver)); 

该代码来自Survival示例,现在已注释掉该代码,并符合我提议应用的几乎所有良好实践规则,包括使用独立于平台并经过测试的引擎逻辑。 注释将帮助您理解其中的大多数内容,但是如果您是Svelto的新手,那么这个规模的项目可能很难理解。

实体


创建合成的空根和EnginesRoot类的实例之后的第一步是首先确定要使用的对象。 从实体播放器开始是合乎逻辑的。 Svelto.ECS的本质不应与Unity游戏对象(GameObject)混淆。 如果您阅读其他与ECS相关的文章,您可能会发现在许多文章中,实体通常被描述为索引。 这可能是引入ECS概念的最糟糕的方法。 尽管对于Svelto.ECS是正确的,但它已隐藏在其中。 我希望Svelto.ECS用户使用游戏设计域语言来表示,描述和标识每个实体。 代码中的实体必须是游戏设计文档中描述的对象。 任何其他形式的实体定义都将导致一种牵强附会的方式,使您的旧视图适应Svelto.ECS原则。 遵循这一基本原则,您不会出错。 实体类本身在代码中不存在,但您仍不应抽象地定义它。

引擎


下一步是考虑询问实体的行为。 每个行为总是在引擎内部建模;您不能在Svelto.ECS应用程序内的任何其他类中添加逻辑。 我们可以先移动玩家的角色并定义PlayerMovementEngine类。 引擎的名称应非常狭focused,因为它越具体,引擎就越有可能遵循“单一职责规则”。 在Svelto.ECS中正确的类命名是基础。 目标不仅是清楚地表明您的意图,而且还可以帮助您自己“看到”它们。

出于同样的原因,重要的是,您的引擎必须位于非常专业的名称空间中。 如果根据文件夹结构定义名称空间,请适应Svelto.ECS概念。 当在不兼容的命名空间中使用实体时,使用特定的命名空间有助于检测设计错误。 例如,除非目标是打破与对象的模块化和弱耦合相关的规则,否则不假定在敌人的命名空间内将使用任何敌对对象。 这个想法是,特定名称空间的对象只能在该名称空间或父名称空间中使用。 使用Svelto.ECS很难将您的代码转换为意大利细面条,在该意大利面条上左右注入依赖项,当正确地在类之间抽象依赖项时,此规则将帮助您提高代码质量级别。

在Svelto.ECS中,抽象前进了几行,但是ECS实质上有助于从应该处理数据的逻辑中抽象数据。 实体是由其数据而不是其行为决定的。 在这种情况下,引擎是您可以放置​​相同实体的联合行为的地方,这样引擎就可以始终与一组实体一起工作。

Svelto.ECS和ECS范式允许编码器实现纯编程的神圣目标之一,这是逻辑的理想封装。 引擎不应具有公共功能。 必须存在的唯一公共功能是实现框架接口所需的那些功能。 这将导致忘记依赖项注入,并有助于避免在不进行控件反转的情况下使用依赖项注入时发生的错误代码。 切勿将引擎嵌入任何其他引擎或任何其他类型的类中。 如果您认为要实现引擎,则只需在代码设计中犯一个根本性的错误。

与Unity MonoBehaviours相比,引擎已经显示出第一个巨大的优势,那就是能够从相同的代码区域访问这种类型的实体的所有状态。 这意味着代码可以直接从将要执行公共对象逻辑的同一位置直接使用所有对象的状态。 另外,各个引擎可以处理相同的对象,以便引擎可以更改对象的状态,而另一个引擎可以读取对象,从而有效地使用两个引擎通过同一实体数据进行通信。 通过查看PlayerGunShootingEnginePlayerGunShootingFxsEngine引擎可以看到一个示例。 在这种情况下,两个引擎位于同一名称空间中,因此它们可以共享相同的实体数据。 PlayerGunShootingEngine确定播放器(敌人)是否已损坏,并写入IGunAttributesComponent组件(即PlayerGunEntity组件)的lastTargetPosition值。 PlayerGunShootFxsEngine处理武器的图形效果并读取玩家选择的目标的位置。 这是引擎之间通过数据轮询进行交互的示例。 在本文的稍后部分,我将展示如何通过推送数据(数据推送)数据绑定(数据绑定)来允许一种机制在它们之间进行通信。 从逻辑上讲,引擎永远不应存储状态。

引擎不需要知道如何与其他引擎交互。 外部通信是通过抽象发生的,而Svelto.ECS以三种不同的官方方式解决了引擎之间的连接,但我将在稍后讨论。 最好的引擎是不需要任何外部通信的引擎。 这些引擎反映了良好封装的行为,通常通过逻辑循环工作。 循环始终使用Svelto.ECS应用程序中的Svelto.Task任务进行建模。 由于需要在每个物理跳动时更新玩家的动作,因此自然而然地创建一个要在每个物理跳动时执行的任务。 Svelto.Tasks允许您在几种类型的调度程序上运行每种IEnumerator 。 在这种情况下,我们决定在PhysicScheduler上创建一个任务,该任务可让您更新玩家的位置:

 public PlayerMovementEngine(IRayCaster raycaster, ITime time) { _rayCaster = raycaster; _time = time; _taskRoutine = TaskRunner.Instance.AllocateNewTaskRoutine() .SetEnumerator(PhysicsTick()).SetScheduler(StandardSchedulers.physicScheduler); } protected override void Add(PlayerEntityView entityView) { _taskRoutine.Start(); } protected override void Remove(PlayerEntityView entityView) { _taskRoutine.Stop(); } IEnumerator PhysicsTick() { // ,      //  EnginesRoot    . // ,         . var _playerEntityViews = entityViewsDB.QueryEntityViews<PlayerEntityView>(); var playerEntityView = _playerEntityViews[0]; while (true) { Movement(playerEntityView); Turning(playerEntityView); //   yield,     ! yield return null; } } 

Svelto.Tasks任务可以直接执行,也可以通过ITaskRoutine对象执行。 我不会在这里谈论太多Svelto.Tasks,因为我为此写过其他文章。 我决定使用任务例程而不是直接启动IEnumerator实现的原因是非常谨慎的。 我想表明,将播放器的对象添加到引擎后,您可以启动一个循环,而当删除对象时,可以停止循环。但是,为此,您需要知道何时添加和删除对象。

Svelto.ECS引入了添加删除回调,以了解何时添加或删除某些实体。这在Svelto.ECS中是独特的,但是应该明智地使用此方法。我经常看到这些回调被滥用,因为在许多情况下它们足以查询实体。即使将实体引用作为引擎字段,也应将其视为例外而不是规则。

仅在使用这些回调时,才应该从SingleEntityViewEngineMultiEntitiesViewEngine <EntityView1,...,EntityViewN>继承引擎。同样,此数据的使用应该很少,并且它们绝不打算报告引擎将处理的对象。

引擎最常实现IQueryingEntityViewEngine接口。这使您可以访问实体数据库并从中提取数据。请记住,您始终可以从引擎内部请求一个对象,但是当您请求与引擎所在的名称空间不兼容的实体时,您应该了解自己已经在做错事。引擎永远不要假设实体是可访问的,并且应该在一组对象上工作。不能像我在代码示例中那样假设游戏中始终只有一个玩家。在EnemyMovementEngine中 有一种非常通用的方法来请求对象:

 public void Ready() { Tick().Run(); } IEnumerator Tick() { while (true) { var enemyTargetEntityViews = entityViewsDB.QueryEntityViews<EnemyTargetEntityView>(); if (enemyTargetEntityViews.Count > 0) { var targetEntityView = enemyTargetEntityViews[0]; var enemies = entityViewsDB.QueryEntityViews<EnemyEntityView>(); for (var i = 0; i < enemies.Count; i++) { var component = enemies[i].movementComponent; component.navMeshDestination = targetEntityView.targetPositionComponent.position; } } yield return null; } } 

在这种情况下,主机循环直接在预定义的调度程序上开始。勾选()。运行()显示了使用Svelto.Tasks启动IEnumerator的最短方法。 IEnumerator将继续屈服到下一帧,直到找到至少一个敌人目标为止。因为我们知道永远只有一个目标(另一个错误的假设),所以我选择了第一个目标。尽管“敌人目标”的目标只有一个(尽管可能还有更多!),但敌人却很多,而且引擎仍然照顾每个人的运动逻辑。在这种情况下,我作弊,因为我实际上使用了Unity Nav Mesh System,所以我要做的就是将目标设置为NavMesh。老实说,我从未使用过Unity NavMesh代码,因此我什至不确定它是如何工作的,该代码只是继承自原始的Survival演示。

请注意,组件永远不会直接提供Navmesh Unity依赖关系。我将在后面讨论的Entity组件应始终公开值类型。在这种情况下,此规则还使您可以控制代码,因为navMeshDestination字段的值类型可以在以后实现,而无需使用Unity Nav Mesh。

要完成有关引擎的段落,请注意,没有太小的引擎之类的东西。因此,不要害怕编写包含多行代码的引擎,因为您不能在其他地方编写逻辑,并且您需要引擎遵循统一责任规则。

实体表示


在此之前,我们介绍了引擎的概念和本质的抽象定义,现在让我们定义本质的表示形式。我必须承认,在Svelto.ECS构建的5个概念中,实体视图可能是最令人困惑的。我以前称为Node(来自ECS Ash框架的名称),但我意识到名称“ Node”毫无意义。 EntityView也可以是误导,因为程序员通常与概念表示从模板发出相关的模型视图控制器(模型视图控制器),但是Svelto.ECS使用视图,因为EntityView是引擎查看实体的方式。我喜欢这样描述它,因为它看起来是最自然的,但是我也可以称它为EntityMap,因为EntityView显示引擎应该访问的实体的组件。 Svelto.ECS概念的此方案应有所帮助:



我建议从Engine开始,现在我们处于该方案的右侧。每个引擎都有自己的EntityViews集。引擎可以重用与名称空间兼容的EntityView,但是最常见的是,引擎定义其EntityView。引擎不在乎是否真的定义了Player实体,而是声明了它需要PlayerEntityView的事实。上班 编写代码取决于引擎的需求,在了解如何使用它们之前,您不应该创建一个实体及其字段。在更复杂的情况下,名称EntityView可能更具体。例如,如果我们将不得不写处理逻辑和渲染Player播放器图表复杂的引擎(d,或动画,等等。)我们可以有PlayerPhysicEnginePlayerPhysicEntityView,并PlayerGraphicEnginePlayerGraphicEntityViewPlayerAnimationEnginePlayerAnimationEntityView可以使用更特定的名称,例如PlayerPhysicMovementEnginePlayerPhysicJumpEngine (等)。

组成部分


我们认识到引擎对一组实体数据的行为进行建模,并且我们了解到引擎不直接使用实体,而是通过实体表示使用实体组件。我们意识到EntityView是一个只能包含实体公共组件的类。我还暗示了实体组件始终是接口,因此让我们给出一个更好的定义:

实体是数据的集合,而实体组件是访问该数据的一种方式。如果您还没有注意到这一点,则将实体组件定义为接口是Svelto.ECS的另一个非常独特的功能。通常,其他框架中的组件是对象。使用接口可以大大减少代码。如果您遵循原则接口隔离原理”编写了小的组件接口,即使每个组件都有一个属性,您也会注意到您已开始在不同实体中重用组件接口。在我们的示例中,ITransformComponent在许多实体表示中都得到了重用。使用组件作为接口还允许它们实现相同的对象,这在许多情况下使用实体的不同表示形式(或相同,如果可能)简化了看到相同实体的实体之间的关系。

因此,在Svelto.ECS中,实体组件始终是一个接口,并且仅通过Engine内部的EntityView字段使用此接口。实体组件接口然后由所谓的«». , .

组件应始终存储有意义的类型,而字段始终是属性。仅当需要优化时才可以将setter和getter用作使用ref关键字的方法来进行例外处理。这并不意味着代码是面向数据的,但是由于引擎的逻辑不应处理与外部依赖关系的链接,因此它允许您创建用于测试的代码。另外,这可以防止编码人员欺骗框架并使用随机对象的公共功能(可能包括逻辑!)。您可能觉得需要在实体组件的接口内使用链接的唯一原因是要处理第三方依赖性,例如Unity对象。但是,生存示例显示了如何处理此问题,保留引擎测试代码,而不必担心Unity依赖项。


这是实体描述符进行救援以将所有内容组合在一起的地方。我们知道引擎可以通过存储在实体视图中的组件访问实体数据。我们知道引擎是类,EntityView是仅包含Component实体的类,而Components是接口。尽管我对精华有一个抽象的定义,但我们还没有看到一个真正代表精华的类。这对应于作为现代ECS系统中标识符的对象的概念。但是,如果没有正确定义实体,这将迫使编码人员使用具有实体表示的实体来识别实体,这将是灾难性的错误。实体表示是多个引擎可以看到相同实体的方式,但它们不是实体。实体本身应始终被视为通过实体组件定义的一组数据,但这甚至是一个较弱的定义。 EntityDescriptor实例使编码器能够正确确定其实体,而与处理它们的引擎无关。因此,对于实体播放器,我们需要PlayerEntityDescriptor。该类将用于创建实体,尽管它的实际功能是完全不同的,但用户可以编写BuildEntity <PlayerEntityDescriptor>()的事实有助于非常轻松地可视化用于构建实体并将实体传达给他人的实体。编码器。

但是,EntityDescriptor真正要做的是创建一个EntityViews列表!在开发框架的早期阶段,我允许编码人员手动创建此EntityViews列表,这导致代码非常难看,因为它无法再可视化实际发生的情况。

这是PlayerEntityDescriptor的样子

 using Svelto.ECS.Example.Survive.Camera; using Svelto.ECS.Example.Survive.HUD; using Svelto.ECS.Example.Survive.Enemies; using Svelto.ECS.Example.Survive.Sound; namespace Svelto.ECS.Example.Survive.Player { public class PlayerEntityDescriptor : GenericEntityDescriptor<HUDDamageEntityView, PlayerEntityView, EnemyTargetEntityView, DamageSoundEntityView, HealthEntityView, CameraTargetEntityView> { } } 

实体描述符(和实现者)是唯一可以使用多个命名空间中的标识符的类。在这种情况下,PlayerEntityDescriptor定义了一个EntityViews列表,以在创建PlayerEntity时实例化并注入到引擎中。

EntityDescriptorHolder


EntityDescriptorHolder是Unity的扩展,仅应在某些情况下使用。最常见的是创建一种多态性,该多态性存储有关用于构建Unity GameObject的实体的信息。因此,相同的代码可用于创建几种类型的实体。例如,在Robocraft中,我们使用单个多维数据集工厂来构建构成机器的所有多维数据集。用于组装的立方体的类型存储在立方体本身的预制件中。只要多维数据集之间的实现者相同或在GameObject中与MonoBehaviour的实现者相同,这就很好。直接创建实体是可取的,因此仅当您正确理解Svelto.ECS的原理时才使用EntityDescriptorHolders,否则有滥用的风险。示例中的此函数显示了如何使用该类:

 void BuildEntitiesFromScene(UnityContext contextHolder) { //EntityDescriptorHolder -    Svelto.ECS , //       . //         . //      , //    //     IEntityDescriptorHolder[] entities = contextHolder.GetComponentsInChildren<IEntityDescriptorHolder>(); //     Svelto.ECS, ,   //      . //        . //    EntityDescriptorHolder, //    for (int i = 0; i < entities.Length; i++) { var entityDescriptorHolder = entities[i]; var entityDescriptor = entityDescriptorHolder.RetrieveDescriptor(); _entityFactory.BuildEntity (((MonoBehaviour) entityDescriptorHolder).gameObject.GetInstanceID(), entityDescriptor, (entityDescriptorHolder as MonoBehaviour).GetComponentsInChildren<IImplementor>()); } } 

请注意,在此示例中,我使用了一个不太受欢迎的非通用BuildEntity函数我会解释这一点。在这种情况下,实现者是附加到GameObject的MonoBehaviour类。这不是一个好习惯。我应该从示例中删除此代码,但向您展示此特殊情况。我们将在以后看到的实现者仅在必要时才是MonoBehaviours类!

推进器


在创建本质之前,让我们在Svelto.ECS中定义最后一个概念,即Impaler众所周知,实体组件始终是接口,必须实现C#接口。实现这些接口的对象称为“实现器”。实现者具有几个重要特征:

  • 从确定实体数据所需的实体组件数量中解开要组装的对象数量的能力。
  • 由于组件通过属性提供数据,因此可以在不同组件之间交换数据,组件的不同属性可以返回相同的实现字段。
  • 能够为实体组件创建接口组件存根。为了使引擎代码保持测试状态,这很重要。
  • Svelto.ECS (third party) . . Unity, , , Monobehaviour . , Unity, OnTriggerEnter / OnTriggerExit , Unity. , . :

 public class EnemyTriggerImplementor : MonoBehaviour, IImplementor, IEnemyTriggerComponent, IEnemyTargetComponent { public event Action<int, int, bool> entityInRange; bool IEnemyTriggerComponent.targetInRange { set { _targetInRange = value; } } bool IEnemyTargetComponent.targetInRange { get { return _targetInRange; } } void OnTriggerEnter(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), true); } void OnTriggerExit(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), false); } bool _targetInRange; } 

, , . , .

实体创作


假设我们创建了Engines,将它们添加到EnginesRoot,创建了它们的实体视图,这些视图需要将组件作为要在实现者内部实现的接口现在是时候创建我们的第一个精华。始终通过EnginesRoot通过GenerateEntityFactory函数创建 Entity Factory 实例创建实体与EnginesRoot实例不同,可以部署和传输IEntityFactory实例。对象可以在组合根内部构建,也可以在工厂内部动态构建,因此对于后一种情况,您需要通过参数传递IEntityFactory。

IEntityFactory具有一些类似的功能。在本文中,我将跳过解释功能PreallocateEntitySlotsBuildMetaEntity,专注于最常用的功能BuildEntityBuildEntityInGroup

最好始终使用BuildEntityInGroup,但是对于Survival示例,则不需要,所以让我们看看示例中如何使用常规BuildEntity

 IEnumerator IntervaledTick() { //  :       //MonoBehaviour    . //       //   . // ,     , //         . //        ,     . //  ,        //   , //  .      , // ,   ,   . var enemiestoSpawn = ReadEnemySpawningDataServiceRequest(); while (true) { //Svelto.Tasks    yield  Unity, //    . //       . // ,  , //    . yield return _waitForSecondsEnumerator; if (enemiestoSpawn != null) { for (int i = enemiestoSpawn.Length - 1; i >= 0 && _numberOfEnemyToSpawn > 0; --i) { var spawnData = enemiestoSpawn[i]; if (spawnData.timeLeft <= 0.0f) { //          int spawnPointIndex = Random.Range(0, spawnData.spawnPoints.Length); //       . var go = _gameobjectFactory.Build(spawnData.enemyPrefab); //        MonoBehaviour. //      . var data = go.GetComponent<EnemyAttackDataHolder>(); //     MonoBehaviour   // : List<IImplementor> implementors = new List<IImplementor>(); go.GetComponentsInChildren(implementors); implementors.Add(new EnemyAttackImplementor(data.timeBetweenAttacks, data.attackDamage)); //         EntityViews, //     EntityDescriptor. //,       EntityView //  ,     ,  EntityDescriptorHolder //       , //    . _entityFactory.BuildEntity<EnemyEntityDescriptor>( go.GetInstanceID(), implementors.ToArray()); var transform = go.transform; var spawnInfo = spawnData.spawnPoints[spawnPointIndex]; transform.position = spawnInfo.position; transform.rotation = spawnInfo.rotation; spawnData.timeLeft = spawnData.spawnTime; numberOfEnemyToSpawn--; } spawnData.timeLeft -= 1.0f; } } } } 

请记住阅读本示例中的所有注释,它们将帮助您更好地理解Svelto.ECS的概念。由于示例的简单性,我不使用在更复杂的项目中使用的BuildEntityInGroup。在Robocraft中,每个处理功能多维数据集逻辑的引擎都会处理游戏中此特定类型的所有功能多维数据集的逻辑。但是,通常需要知道多维数据集属于哪个车辆,因此为每台机器使用一个组将有助于将相同类型的多维数据集分解为机器,其中机器ID为组ID。这使我们可以执行一些很酷的事情,例如在同一引擎内的机器上运行一个Svelto.Tasks任务,该任务可以使用多线程并行工作。

这段代码显示了一个重要的问题,我可能会在后面的文章中对此进行更详细的介绍...(从注释中开始,如果您尚未阅读的话):

切勿创建仅用于数据存储的MonoBehaviour叶轮。无论数据源如何,都应始终通过服务层检索数据。好处很多,其中包括更改数据源只需要更改服务代码这一事实。在这个简单的示例中,我不使用Service层,但是总的来说,思路很明确。还要注意,在主循环之外,每次启动应用程序时,我仅上传一次数据。如果您所需的数据从未更改,那么您始终可以使用此技巧。

最初,我像一个好的惰性编码器那样直接从MonoBehaviour读取数据。这使我创建了MonoBehaviore只读串行器实现器。如果我们不想抽象数据源,这是可以接受的,但是与从实体组件读取此数据相比,将信息序列化为json文件并在向服务请求时读取它要好得多。

Svelto.ECS上的交流


系统之间的通信是其解决方案从未被任何ECS实施标准化的问题。这是我考虑很多的另一个地方,Svelto.ECS用两种新方法解决了这个问题。第三种方法是使用标准的“观察者/观察者”模式,这在非常具体的情况下是可以接受的。

DispatchOnSet / DispatchOnChange


前面我们看到了如何允许引擎使用数据轮询通过实体组件交换数据。实体组件的属性可以返回DispatchOnSetDispatchOnChange是唯一的引用(非有效类型),但是通用参数T的类型必须是有意义的类型。函数的名称听起来像一个事件分配器,但是应该将它们视为推送数据的方法,与数据轮询相反,后者有点像数据绑定。就是这样,有时候轮询数据很不方便,当我们知道数据很少更改时,我们不想在每一帧都轮询一个变量。DispatchOnSetDispatchOnChange无法在不更改数据的情况下启动,这使我们可以将它们视为数据绑定机制而不是常规事件。也没有要调用的启动函数;相反,必须设置或更改这些类保存的数据的值。在生存的代码没有什么大的例子,但是你可以看到一个布尔场targetHitIGunHitTargetComponentDispatchOnSetDispatchOnChange之间的区别在于,后者仅在数据实际更改时才触发事件,而前者始终触发。

音序器


理想引擎已完全封装,您可以使用Svelto.Tasks和IEnumerators将该引擎的逻辑编写为一系列指令。但是,这并非总是可能的,因为在某些情况下,引擎必须将事件发送给其他引擎。这通常是通过实体数据完成的,尤其是使用DispatchOnSetDispatchOnChange但是,就像在示例中实体“损坏”的情况一样,一系列独立且不相关的引擎对其起作用。在其他情况下,您希望序列按引擎调用的顺序严格,例如在我希望后者总是死亡的示例中。在这种情况下,该序列不仅非常易于使用,而且非常方便!序列重构非常简单。因此,对“垂直”引擎使用IEnumerator Svelto任务,对引擎之间的“水平”逻辑使用序列。

观察者/被观察者


我留下了使用此模式的机会,专门用于遗留代码或不使用Svelto.ECS的代码应与Svelto.ECS引擎交互的情况。对于其他情况,应格外小心,因为可能会滥用该模式,因为Svelto.ECS的大多数新手都熟悉该模式,而Sequencer通常是最佳选择。

Source: https://habr.com/ru/post/zh-CN413107/


All Articles