« February 2022 | Main | April 2022 »

March 29, 2022

全民大规模新冠检测方案的一些想法

今天吃饭的时候和同事聊起最近上海深圳等超大城市全民新冠检测的问题。我认为现有方案有极大的改进空间。

千万级的全民检测,需要动用的资源是非常可观的,如果我们能优化这个过程,收益也会极其可观。现有的集体检测已经是相对独立个体分别检测做过优化了:通常是很多人放在一起混检,这样减少了检测成本。但我认为这是远远不够的。

现在自测盒的成本已经很低,至少一个自测盒的零售价格远低于我自己去医院做一次检测。所以我认为,如果全民自测的话,单从检测成本上来说,可能也是划算的。而且,对于群体检测,完全可以全家用同一个检测盒,这样如果是阳性,家庭成员就算没有全部感染,也最少是个密接。这比社区混检有效很多。

至于自测盒可能准确度不高的问题,完全可以自行多次自测来弥补。而目前采取的社区集体混检的形式,反而造成了人群密集,增加了病毒传染的风险。

但是,在千万级人口的城市中全民自测,显然结果是不可信的,这是我下面想谈的主要问题:如何设计一个方案,即降低了全民检测的成本,又能保持结果的可信度。即:如果有人自测发现自己阳性(或干脆不想自测),因为恐惧隔离而瞒报的问题。

我认为,当上千万人口中感染者其实不多,大多数社区中并无感染者时,最要紧的是快速排除掉无感染的社区。所以,加入自测环节,不是期盼人们自觉上报感染,而是用自测环节排除掉无感染社区,从而降低整体的成本。

  1. 以小区为单位,每个家庭每天发一个自测盒,全家共用一个居家自测,连续测试多天。
  2. 若自测发现阳性,鼓励主动上报。每个小区首先主动上报阳性的若干家庭,可以享受高标准隔离,例如,可以安排在高档酒店单独隔离。如果高标准隔离场所资源有限,后续自主上报者,也可以获得额外的物质奖励。
  3. 若整个小区无一例阳性上报,则不再对该小区做额外处置。
  4. 若小区至少有一例上报,则退化回原有的社区混检方案。在这个检测中再次发现的额外感染者(无主动上报),去集体方舱隔离。

注意,在这个方案中,并不信任自测主动上报阳性的个例,只是用来触发小区混检的启动。即,用来排除无风险小区,而不是用来找到具体感染者。但是,主动上报是有好处的(享受更好的隔离条件),而隐瞒不报或干脆不测是有风险的(在条件较差的地方隔离)。

March 23, 2022

层次组件的问题

最近思考了 ECS 框架中的一些问题。

具体业务中,有许多组件的构成非常复杂,本质上数据是有层次结构的。用一个二维表的结构很难清晰表述。它还牵扯到另一个问题:是否需要支持一个实体有多个统一类型的组件。例如,玩家实体身上多个装备栏、多个技能 Buf 该如何表达?

我有一些不成熟的想法,还没有具体落地,先记录一下:

在目前的 ECS 框架中,我以内存数据库的形式管理所有的数据。把 Entity/Component (实体、组件)看成是一张二维稀疏表。每个 Entity 占一行,每类 Component 占一列。整张表为一个 world 。

而我最近的反思是,一个世界不应该只有一张表,没有理由把所有数据都塞入同一张二维表。

以物品栏 Inventory 为例,每个角色都会有一个 Inventory ,Inventory 是由诸多物品格 slot 构成的。这就是一个有层级关系的数据结构:角色 -> 物品栏 -> 物品格。

如果我们去掉物品栏这一层抽象,那么,角色这个 Entity 上就需要有复数个物品格 Component 。

通常,如果我们有一个系统 system 是用来处理身上携带物品对角色起的效应,那么最好在 select 物品时,可以把同一个角色身上的所有物品聚合在一起处理。它们之间是有关联的。

相比把这些数据塞入一张大表,一个可能更好的模式或许是为所有的物品格单独建一张表。在这张表中,Entity 指的是单个物品格,同一物品格可以有多个 Component 和 Tag 。有一个 Component 保存的是它的 id ,用来和存放角色 Entity 的那张表发生关联。

在角色 Entity 的表中,我们只维护它身上有几个有效的物品格,真正的数据都放在物品栏的专用表中。

在物品栏的专用表中,为每个物品格生成一个唯一 id ,这个 id 是由角色 id 以及物品格在该角色的 Inventory 中的编号构成的。

使用 ECS 的 select 已经实现的 order 模式(一种特殊的 select 方式,可以根据特定组件 id 排序,以特定的次序而不是构建持续遍历),我们可以做到把同一个角色身上的所有物品格聚在一起遍历。

同时,还可以给物品格打上 tag ,select 时可以挑选出关注的那一部分:例如可以按物品用途分类等。


支持多张表并不需要修改已经写好的 luaecs 的实现,只是原有的名字起的不恰当。单张表不应称为 world ,而需要再加一层 world 来管理多张表。

把数据拆分成多张表后,底层也可以对 order tag 做进一步的优化。因为原有的 order 模式虽然可以让 select 以特定次序遍历 entity ,但相比普通的按构造次序顺序遍历 entity ,查询 entity 的 component 的时间复杂度有可能从 O(1) 下降到 O(log N) 。

我们可以增加一个方法让使用者主动定期按 order tag 整理一张表的内部内存实际布局,提高顺序访问的效率。整个整理工作代价比较大,但分拆数据到多张表可以减少很多整理成本。

March 18, 2022

effekseer 的 shader 转译

我们的游戏引擎一开始是自己实现的粒子系统 。在实现完之后,做配套编辑工具的阶段,开发工具的同学建议换成其它开源的成熟系统,这样就不必花太多精力在维护一套工具了。

他推荐了 Effekseer ,并完成了 Effekseer 和我们引擎的整合工作。

最近,我在推特上看到有个同学也在寻找 bgfx 下的粒子系统的方案,他希望有一个比 bgfx 自带粒子演示更完善的东西,同时又表示整合 popcorn 实在是过于麻烦。我向他推荐了 effekseer 。

集成 effekseer 其实有两个层面的工作。一是对接图形 API ,而是和引擎集成。后者包括和引擎的资源管理集成。bgfx 只是图形 API 的抽象层,可以看成是和 opengl directx metal 这些同级的 api 。光给 effekseer 写一个 bgfx api 的渲染器是不够的,粒子系统还包括了贴图、着色器、模块的加载和管理。如果只在图形 api 级别对接的话,就会让这些资源的加载和生命期管理同时存在游戏引擎和粒子系统两处各一份。这显然是不合理的。

我们在去年做整合的时候,活做的比较粗糙,把两者混在一起了。所以并不好直接拿给另一个基于 bgfx 渲染的项目用。我看了一下推特上那哥们的简历,他曾经是 Crytek 的 Principal R&D Programmer ,做过很多年的 AAA 游戏,相比能力不错,便劝他为自己的项目做 effekseer 的整合工作。他表示既然有人做过,就不要浪费精力了,愿意花钱买我们的代码参考。

我想了想,决定重新设计一下抽象层,把和 bgfx 相关的部分从我们的引擎中剥离出来,单独写一个 effekseer 的 bgfx renderer ,然后开源。和那哥们聊了一下,他也很愿意以后共同维护。想着这样可以白赚一个朋友一起维护一个子模块也不错,多一个项目用也更靠谱些。从上周开始,我就加班开干了。

项目在这里:https://github.com/cloudwu/efkbgfx

目前还在做,这一周下来有太多想吐槽的东西,以后慢慢写。今天先写一下这其中关于 shader 转译的工作。

effekseer 有 6 种内置的材质,可分别用于渲染粒子片(四边形 sprite)或是模型。sprite 和 model 渲染用的 vs 是不同的,但 fs 是共用的,所以一共有 18 个 shader 。

bgfx 用的 shader 有一些自定义的书写格式,不能直接把 effekseer 中的 shader 文件直接拿过来用。去年我们同事做的时候,就根据它里面 opengl 的版本手工转译了几个 shader (并未完全直接全部 6 种内置材质)。这个工作并不难做,花半天时间就能搞定。但去年的 effekseer 版本和目前的最新版本已有差异,去年的工作还需要更新。

我研究了一下 effekseer 的项目,发现它实现了从 DirectX9 到 12 ,OpenGL 2 到 3 ES ,还有 Vulkan Metal 等等很多图形 API 的版本,其实这些不同版本的 shader 都是从 DirectX11 的版本生成出来的。只有 DirectX11 那一版是人工维护。生成工具以 glslang 为基础做的一个专有工具。

理想情况下,我们应该扩展这个工具,让它可以自动生成 bgfx 的 shader 版本。但我评估了一下,这个工作量并不算小。关键是,这个工具并没有留下太好的扩展方法。等于我们需要 fork 其代码再改。那些日后这个 fork 版本的工具同样也有跟进上游的维护成本。还会增加我们项目的构建复杂度。

怎么才能低成本的做成这事呢?

我打算绕开 effekseer 的 shader 转换工具,自己用 lua 写一个。由于 bgfx 的 shader 是基于 glsl 语法的,所以,从它机器生成的 Vulkan 版本转换最为容易。由于,原始版本就是机器生成的,所以格式非常规整。我在实现转译器的时候,甚至没有动用 lpeg ,就用 lua 内置的字符串库就搞定了。前后只花了 4 个小时,100 来行的 lua 程序

为什么写这么一个简易的专有工具就够用了?因为,我们需要转换的只有十几个 shader 文件,并不需要做成通用工具。而且,并不需要没有构建项目的时候都去转换,而只需要在上游代码更新的时候转换,直接把转换结果放在仓库里即可。专有工具不需要考虑太多情况,能把目前的文件处理对就够了。但可以留下足够的检查,一旦发现日后的更新有无法理解的段落,就尽可能的报错,而不是尝试转换。

当未来版本升级时,转换工作是自动化进行,但是人工监督的。一旦发现工具不适用了,改善工具配合更新的工作量并不大。而且专有工具可以配合已经写好的 renderer 代码做更多的细节:比如校验 Uniform 的名字、Vertex Layout 是否和 renderer 代码是否保持一致。而不需要真的去生成 renderer 中的 C++ 代码。这样也简化了 renderer 的实现。