【问题标题】:Is there a better data structure for storing components and their associated entities?是否有更好的数据结构来存储组件及其关联实体?
【发布时间】:2021-06-01 14:09:07
【问题描述】:

我正在用 Javascript(特别是 Typescript)编写一个小的实体组件系统 (ECS),它目前可以工作,但我想知道它是否可以在引擎盖下更高效。 ECS 的工作方式是实体基本上只是组件包。因此,玩家实体可能有HealthComponentPositionComponentSpriteComponent 等。然后您可以创建一个RenderingSystem,用PositionComponentSpriteComponent 查询所有实体,然后渲染它们。像这样:

for (let entity of scene.query(CT.Position, CT.Sprite) {
  // draw entity
}

为了在查询时提高效率,而不是每次都遍历场景中的每个实体以查看它是否具有Position 组件和Sprite 组件,而是在第一次查询后缓存它调用然后保持更新,因此每个查询调用都可以只返回实体列表,而不是每次都首先遍历所有实体的整个列表。

因此,例如,缓存可能如下所示:

{ "6,1,20" => Map(1) }
{ "2,3,1,6" => Map(1) }
{ "2,3" => Map(31) }
{ "9" => Map(5) }
{ "2,8" => Map(5) }
{ "29,24,2" => Map(5) }

// etc..

数字是指枚举值的值,如CT.PositionCT.Sprite 等。在这种情况下,CT.Position 是 2,CT.Sprite 是 3,并且有 31 个实体具有这两个组件.因此,当查询具有这两个组件的所有实体时,我们可以只返回该实体列表,而不是每次都计算它。

这一切都有效,但效率不高,因为向场景中添加(和删除!)实体是O(n) 操作,并且还涉及大量字符串拆分和连接。您需要遍历缓存中的每个项目,以查看该条目是否包含实体的组件列表。

有什么方法可以改进它,使其更像O(log n) 或者最好是O(1)?让我知道这一切是否都清楚,或者是否有任何细节需要澄清。

这是Typescript Playground URL reproduction example的链接

【问题讨论】:

  • 您能否提供一个适合放入独立 IDE 的准系统实现的minimal reproducible example,以演示其工作原理以及您需要支持哪些操作?
  • @jcalz 我可以举个例子,但我不知道如何分享。我在 Typescript Playground 网站上写了一些代码,但是 URL 太长,无法在此分享。另外,我尝试使用 URL 缩短器,但他们说 URL 太长而无法缩短。
  • 在这里,我创建了一个包含指向操场的 URL 的 pastebin(这太疯狂了..):paste.ee/p/cissL
  • 您应该能够将其编辑到您的问题中,而不仅仅是将其放入 cmets,我想。如果您计划频繁添加/删除实体,我不确定缓存特定查询是否有意义;我想这真的取决于使用模式。如果您不经常添加/删除实体,那么在发生这种情况时丢弃缓存可能是合理的,而不是尝试逐行修复缓存。在任何情况下,我都不愿意在不分析典型用法的情况下对性能做出任何强烈的主张。 ??????
  • 有趣的 sn-p 我得再读一遍。是的,我最近意识到我的问题基本上是“我如何快速找到 N 个集合之间的交集?”所以我在谷歌上搜索。显然布隆过滤器可能在这里工作?我现在正在阅读它.. 不确定我是否理解正确,但看起来您可以在 N 个布隆过滤器上进行设置交集以找到匹配的项目。 stackoverflow.com/a/39176090/962155

标签: javascript typescript algorithm data-structures hashmap


【解决方案1】:

我希望缓存中的查询数量会非常少,因为每个查询都将单独绑定到一组处理结果的代码。因此,遍历查询列表并为每个查询列表执行一些操作不会那么昂贵,但如果您在添加或删除一大堆实体时遇到问题,那么当然可以解决。

首先,用于组件类型子集的字符串表示确实非常低效。有很多选择。也许尝试这样的事情:

  1. 首先,为每个组件类型分配一个整数(您已经这样做了)
  2. 按整数对子集中的组件类型进行排序
  3. 使用每个整数作为字符构建字符串

这种表示并不太花哨,但它允许您使用charCodeAt() 快速获取子集中的组件类型,您可以使用它通过同时遍历两个字符串或遍历一个字符串来测试子集同时在另一个中进行二进制搜索。

然而,真正的改进来自于按照它们所呈现的组件类型的子集对实体进行分组。有很多方法。我认为这样的事情对你有用:

  • 对于每个实体,预先计算其组件类型子集字符串
  • 对于每个正在使用的子集,维护与该子集匹配的缓存查询列表。仅当您引入新查询或新子集时才需要修改此列表。
  • 添加或删除实体时,获取其子集的查询并将其直接添加到结果或从结果中删除。
  • 当您获得新查询时,创建一组匹配的子集,将其添加到这些子集的查询列表中,并检查每个实体以查看其子集是否包含在匹配集中。

【讨论】:

  • 即使整数可以是两位数,这是否有效?我有大约 100 个组件。
  • 是的,您最多可以使用 55000 个字符代码
【解决方案2】:

好的,我认为我对此有一个暂定的答案,因为它似乎可以工作,但是代码对我来说非常复杂,所以我'不确定这是否真的有效,或者它似乎只是并且实际上已经坏了。

因此,对于解决方案,我想保持查询性能,因为每次帧更新都会为每个系统调用查询,因此它的执行频率是实体创建/删除的 1000 倍。当前,通过首先检查缓存是否包含此组件映射,查询作为分期 O(1) 算法工作。如果没有,它会创建与该组件分组(原型)相关联的实体列表,然后从缓存中获取该列表。缓存始终保持最新。

我的问题是,虽然有一个 O(1) 查询操作很好,但希望有更有效的添加和删除操作,因为它们是 O(n*k),其中 n 是不同查询操作(缓存成员)的数量,k 是实体中组件的数量。也就是说,无论何时创建或销毁实体,程序都必须遍历缓存中的每个项目并检查该实体是否应属于该查询操作。如果是,则将其添加到该集合中,如果不是,则将其删除。

我今天早上的想法是实现另一个缓存/映射。也就是说,原始缓存从查询组件列表(原型)映射到包含这些组件的实体集。示例:

{ "6,1,20" => Set(1) }
{ "2,3,1,6" => Set(1) }
{ "2,3" => Set(31) }

假设2 指代PositionComponent3 指代SpriteComponent。这意味着包含这两个组件的所有实体都可以在这组 31 个实体中找到。

因此,我对原始问题的初步解决方案是有一个映射,其中组件列表对应于它们所属的所有缓存条目。也就是说,假设我们有一个包含以下组件的实体:1, 2, 3, 6, 25。那么它在这个缓存中的对应条目将如下所示:

1,2,3,6,25 => [ "2,3,1,6", "2,3" ]

第一次构造该原型的实体(组件列表)时,手动创建该列表。但是,之后它只是保持不变。然后,只要有创建该原型实体的请求,我们就可以简单地查询该缓存以找出我们需要修改哪些缓存条目。

这样,我们不必遍历整个缓存,然后遍历每个缓存项以确定它是否应该是成员,而是只需查询二级缓存以确定它是哪些缓存条目的成员。因此,我相信摊销复杂度从 O(n*k) 缩减到 O(c),其中 c 是它所属的缓存条目数。

【讨论】:

    猜你喜欢
    • 2014-04-23
    • 1970-01-01
    • 2014-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多