[Unity ECS] 实体之间如何交互

63 篇文章 11 订阅

我们如何实现实体之间的交互?

  在我们真正回答这个问题之前,我们应该制定实际问题。

概要

  在ECS中,当涉及到实体之间的交互时,有两个问题:读和写访问。事实是,交互并不真正存在,它们隐藏了底层关系的实现。那么,一个关系无非是数据的转变。(更多关于)

  为了推理出创建这些转变的正确工具,我们需要推理我们的代码并问自己以下五个问题。

  1. 我们对哪些数据进行操作?
  2. 我们的域是什么?我们转换的可能输入是什么。
  3. 数据使用的频率是多少?
  4. 我们实际上在转变什么?我们的算法会是什么样子?
  5. 我们多久进行一次转变?

  对于不频繁的读取访问,我们可以轻松地使用ComponentDataFromEntity结构。它允许我们对底层数据进行类似数组的访问。我们不建议使用这种结构进行读取访问,因为在这种情况下,我们放弃了C# Job系统在多线程环境下的安全保证。

  当涉及到写访问时,我们应该考虑使用EnityCommandBuffer。这是一个很好的工具,可以收集一堆我们想执行的命令(动作)。根据我们的需要,缓冲区可以被立即调用或延迟调用。在SystemGroups的情况下,我们可以使用我们自己的CommandBuffer,也可以使用其中一个默认的。

问题所在

  在创建实体之间的互动时,我们主要面临2种类型的问题。

  1. 读取访问。具体来说,这意味着我们必须从一个特定的实体(对象)中读取某些属性,并在此基础上做出反应。就游戏而言。一个角色需要从游戏的另一个部分查询/知道一些信息。例如,在一个任务系统中。所有的任务都完成了吗?
  2. 写入访问。具体来说,这意味着我们必须向一个特定的实体(对象)写入某些属性。

从相互作用到关系的转变

  为了开始这种转变,我们应该快速了解面向数据设计的第一条原则:

数据不是问题域。对于一些人来说,面向数据的设计似乎是大多数其他编程范式的对立面,因为面向数据的设计是一种不容易让问题域如此轻易地进入软件的技术。它丝毫不承认对象的概念,因为数据始终是没有意义的[…]面向数据的设计方法并没有把现实世界的问题建立在代码中。这可以被资深的面向对象开发者看作是面向数据方法的失败,因为很多面向对象设计的成功例子都来自于能够将人类的概念带到机器上,然后在这个中间地带,可以用这种语言写出人类和计算机都能理解的解决方案。面向数据的方法通过将问题域留在设计文档中而放弃了一些人类的可读性,但却阻止了机器在任何层面上处理人类的概念,而只是通过同样的行动–《数据导向的设计书》第1.2章

  这有助于我们认识到,相互作用并不真正存在,它们隐藏了底层关系的实现。一个关系只不过是数据的转换。在ECS的情况下,实体管理器可以被看作是一个数据库,而实体是一个索引组件之间关系的查找表键。系统只是在这里解释这些关系并赋予它们意义。因此,一个系统应该只做一项工作,而且要做得好。系统进行数据的转换。这使得我们可以创建通用的系统,这些系统是解耦的,易于重用,因此,我们应该记住以下几点。

面向数据的设计驱动的应用程序的主要设计目标之一是尽可能通过解耦来关注可重用性。因此,Unix的理念是:编写只做一件事的程序,并且把它做好。写程序是为了协同工作–McIlroy是表达一个系统应该做什么的好方法。

  DOTS或任何ECS都是在考虑到关系的情况下建立的。当我们在编写系统时,我们将数据从一种状态转换到另一种状态,以赋予数据意义。因此系统是定义了数据关系的意义。这种解耦给了我们设计复杂软件(如视频游戏)所需的灵活性。这使得我们可以在以后修改行为,而不破坏任何依赖关系。

我们如何设计系统?

  为了落实上述关系,我们必须下采取几个步骤。我们必须提出以下问题。

  1. 我们要做哪些数据转变,对哪些数据进行转变?这个问题应该导致 “我们需要哪些组件来创建这种关系?” 我们应该总是能够给出一个理由,为什么我们需要这个数据。

  2. 我们可能的域是什么? (我们有什么样的输入?)
    当我们弄清楚这一点时,我们以后就能做出正确的决定,并能对我们的代码进行推理,我们如何实现这种关系?

  3. 数据多长时间改变一次?为了确定我们改变数据的频率,我们逐个组件去讨论我们改变数据的频率。这对以后挑选合适的工具很重要。知道这些数字或趋势对于推理可能的性能瓶颈和我们可以应用优化的地方非常好。

  4. 我们实际上在改变什么?
      写下算法或我们对数据的实际操作的约束是一个很好的解决方案。为了根据计划的算法挑选合适的工具,我们需要考虑我们算法的成本。
      成本是什么意思?它可能意味着从运行时成本到实施成本的任何东西。重要的是首先确定正确的标准是什么。最后的成本使我们能够对代码进行推理。
      要选择正确的工具,我们需要能够推断算法花费我们的成本。在某些情况下,如果我们将运行时性能作为衡量标准,如果我们不经常执行此操作,则可以使用慢速算法,但如果不是这种情况,则应考虑另一种解决方案。

  5. 我们多久执行一次算法/转换?
      基于我们通过定义转化所需的数据而获得的信息,定义执行的频率是很容易的。在判断的时候,实体/对象的总数是已知的,因此我们可以猜测这可能的运行频率。除此之外,我们还讨论了我们怀疑数据被改变的频率,这导致了一个透明度,这让我们对这个代码的成本有了一个很好的了解。

重要提示:当数据发生变化时,问题就会发生变化。因此,我们必须用描述性方法适当地评估可能的结果,并可能改变实施。

读取访问 (ComponentDataFromEntity)

  如果需要从某个实体中读取,ComponentDataFromEntity是正确的工具。这个工具允许我们读取一个实体的指定类型(组件)。它是一个本地容器,提供了对特定类型的组件的类似数组的访问,因此我们可以很容易地从其中读取我们需要的数据。这是一个强大的工具,可以从实体中访问组件数据,但另一方面,它允许随机访问,因此很慢。

重点
你可以在任何Job中安全地从ComponentDataFromEntity中读取,但默认情况下,你不能在并行Job中向容器中的组件写入(包括IJobForEach和IJobChunk)。如果你知道一个并行Job的两个实例永远不能写到容器中的同一个索引,你可以通过向作业结构中的ComponentDataFromEntity字段定义添加NativeDisableParallelForRestrictionAttribute来禁用对并行写的限制。

例子:

//... code
[BurstCompile]
struct MyJob : IJobForEach<MyCmp,Position>{
    [ReadOnly] public ComponentDataFromEntity<Position> data;

    public void Execute([ReadOnly] ref MyCmp mycmp, [ReadOnly] ref Position pos){
        if(!data.Exists(mycmp.Entity)) return;
        Position mycmppos = data[mycmp.Entity];
        //... do some magic
    }
}
///...
protected override JobHandle OnUpdate(...){
    var job = new MyJob(){
         GetComponentDataFromEntity<Position>(true) // true = read only!
    }
    //...
}

写访问 (EntityCommandBuffer)

  在ECS中改变数据(写访问)的正确工具是它利用EntityCommandBuffer,在不经常改变数据的情况下。在不同的情况下,一个更多的价值驱动的方法(直接改变)可能更合适。缓冲区允许我们对命令进行缓存,然后它们将在之后被执行。如果上下文是在多线程环境下工作,那么让``EntityCommandBufferto知道这一点很重要。这将通过thisEntityCommandBuffer.Concurrent`完成。

//... code
[BurstCompile]
struct MyJob : IJobForEach<Target>{
    public EntityCommandBuffer.Concurrent buffer;
    public void Execute(Entity entity, int index,[ReadOnly] ref Target target){
        buffer.AddComponent(index,target.Enity,typeof(...));
    }
}

  重要的是要意识到,在Playback()被调用之前,什么都不会发生。这取决于我们的需要,如果我们想在创建缓冲区后立即调用它,并通过Unity的默认缓冲区填充或延迟。然后,我们需要记住一个游戏的同步点。我们有3个系统组。InitializationSystemGroup SimulationSystemGroup 和 PresentationSystemGroup。如果我们不指定要将命令缓冲区添加到哪里,我们的命令缓冲区就会自动添加到模拟系统组中。我们也可以自己创建。

//...Code
protected override OnCreate(...){
    m_buffer = world.GetOrCreateSystem<InitializationEntityCommandBufferSystem>();
}
protected override JobHandle OnUpdate(...){
    var job = new MyJob(){
        buffer = m_buffer.CreateCommandBuffer().ToConcurrent()
    }.Schedule(this,inputDepends);

    m_buffer.AddJobHandleForProducer(job);
    return job;
}

SystemGroups 的简要概述(默认)

  • InitializationSystemGroup(在播放器循环的初始化阶段结束时更新)
  • SimulationSystemGroup(在玩家循环的更新阶段结束时更新)
  • PresentationSystemGroup(在播放器循环的 PreLateUpdate 阶段结束时更新)

所有这些组都提供2个命令缓冲区,例如BeginPresentationEntityCommandBufferSystem和EndPresentationEntityCommandBufferSystem。这可以用来确定我们何时要执行什么。

结束

以上就是实体之间如何交互全部内容。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值