« January 2022 | Main | March 2022 »

February 25, 2022

ECS 中的对象引用

我们很难避免在 ECS 系统中相互引用 Entity 。而我对 ECS 模式的使用是鼓励去引用的。为此,我对许多常见依赖引用的模式给了对应的解决方案。

最近的一个 luaecs 开发版本中,提供了一种 Lua 层面的引用方案 :在创建 Entity 时,可以指定一个 table 作为该对象的引用。系统会更新它,让它保持为一个有效的(形如 select 过程中的)迭代器。这样,业务层就可以随时通过它 sync entity 中的数据。

我一直不是太喜欢这个方案,所以一直再考虑不同的解决方法。念念不忘 必有回响。昨天,我尝试了一个新的、更满意一点的方案。

老方法的问题是:这个方法高效的前提是,大部分 Entity 一旦创建,之后就不再需要对业务层暴露引用。如果我们对每个 Entity 都预留一个引用,它就没太大意义作为一个可选部件存在。

但这样,我们就需要在一开始就觉得要还是不要这个引用,实际开发中,非常难把握。

另外,从一个组件快速找到兄弟组件,时间复杂度其实是 O(log N) 的。因为同类组件之间是以有序 id 进行排列,用二分法查找。但 luaecs 对循序遍历做了特别优化,这样,在 select 循环中,兄弟组件的查找可以接近 O(1) 的复杂度。

从引用找到 Entity 其它组件的过程却是个随机访问的过程,不能被同样的策略优化。从引用定位到用来拣选的 Tag 的过程优化到 O(1) 复杂度意义不大。


和老方案不同,新方案是非侵入式的,不修改任何 luaecs world 已有的数据结构。新的方案更像是内存数据库模型。我们可以挑选一个特定的组件作为 key ,为它额外建一个索引结构体。这个结构体完全是一个 cache 。也就是说,当我们对 ECS 的 world 做持久化时,完全可以扔掉这些 index cache 。同时,你也可以对多个 key 建立不同的索引 。

可做索引的 key 必须是整数类型。索引结构就是一个 hash 表。用户需要引用一个 Entity 时,可以给 Entity 添加一个唯一 id 的组件,并记录下 id 。需要解引用时,用 id 去查询 hash 表,构建出访问 Entity 用的迭代器。

在第一次引用时,使用 O(n) 的遍历算法找到对应的 Entity ,并记录下位置。之后,不必再重复构造这个迭代器。Cache 的大小是固定的,发生 hash 碰撞时,老的记录会被覆盖掉。由于现在 luaecs 中的数据会因为删除 Entity 而发生移动,所以每次获取迭代器都会做二次校验,如果之前的记录已经失效,则会重新矫正。

这个方案中相关的优化基于以下几点:

我们可以自由的为所有的 Entity 保留引用,只需要记录下任意 id 即可。但是,我认为,解引用的频次是非常低的。只有少部分情况才需要通过之前保留的引用找到特定 Entity 。高频操作依旧推荐在 select 循环中完成:即,批量处理对象和对象之间的关系。

如果一个引用被经常使用,那么让它的解引用过程足够快;如果引用只是备用,那么可以接受更低的效率。相比 Enitty 的总数量而言,常用的 Entity 之间的关系对的数据是比较少的。

如果一个 Entity 被创建出来后需要保留其引用,那么首次解引用的时机会非常早(创建出来后和解引用之间只会创建少量 Entity)。也就是说,如果你保留了一个引用,你越久不使用它,就越不太可能用它。

关于最后一点,我针对的是我在我们项目的实际用法:解引用大多发生在一批 Entity 批量创建的过程中。

实现上做了一个简单的优化:通过 id 遍历查找 Entity 时,是按创建次序的逆序进行的。虽然算法是 O(n) 的,但是会更快的找到。同时、当 Entity 的数据因为有其它 Entity 被删除而移动时,虽然原来的迭代器会失效,但寻找新的位置只需要从原来的位置向前找即可。因为,移动总是向前的。

February 23, 2022

信息茧房

最近一个月,无论我打开推特,还是微博豆瓣,每天都被“徐州八孩母亲事件”刷屏。在公司吃工作餐的时候,也和同事聊过这个事件。从我的角度看,此事的热度之高,持续时间之久,近年罕见。能读到我的 blog 这一篇的同学,相信都对这件事的来龙去脉不会太陌生。如果你不了解,那么 google 一下“徐州八娃女”或是“丰县铁链女” 就能找到很多信息。

但是,我很清醒的知道,热度只存在于自己所在的这么个小圈子。我相信在中国社会中,不知道此事的人群才是大多数。就算在微博上百万转发,也不会有所改变。毕竟,翻墙的人还是少数。而墙内的微博微信,都帮助人们把各自的信息茧房建设的很好。

我也看到很多人在努力的线下传播。有人在地铁上向公众发言、多地的书店为丰县八孩母亲案设专柜。但这种信息传播力太脆弱了。只要主流媒体不报道,公众几乎无法了解此事。无需用墙隔开信息,微信之流只需稍使手段,就能协助人自己树立信息之墙。茧房其实是自发建立起来的,技术助了把力。

各位同学,不妨把整件事情在脑海里整理清楚,然后讲给你身边的亲朋好友听听。我相信不少人会诧异你身边人的孤陋寡闻。

February 14, 2022

Factorio 的乐趣

我玩 Factorio 有记录的时间已经超过 2000 小时了。从它上 steam 的第一天我就开始玩,但大部分时间花在最近两年。

我在 steam 上的推荐语最初只有一句话,很能代表我初见这个游戏的感觉:“这个游戏满足了我对城市建设、物流调配、自动化建造、甚至还有铁路模拟,等等类型游戏的一切梦想。”

小时候最爱凯撒系列,它代表了当时城市建设和物流游戏的巅峰,我从中发现了无穷的乐趣,这是让我第一天就买下 Factorio 的缘故。

我是不太喜欢沙盒类型的游戏的,需要游戏有强烈的目标感。初期的 Factorio 虽然更像是一个沙盒,但自带了一些小的任务关,我在上面花掉了上百小时。但我甚至没能发一发火箭(原版游戏的大目标)就搁置了。

疫情来临之前,同事告诉我说,他一直在玩 Factorio 尤其是打上 Mod 后有更多的乐趣。有一个叫做太空探索 space exploration 的 mod ,更是把原版游戏的玩法拓展了不只一个数量级。他告诉我,这个游戏现在的版本已经和最初的版本大为不同。比如:火车系统被极大完善了,有了液罐车等;传送带也改进了很多,不再需要复杂的电路就能做各种复杂的物流系统。游戏中后期也不再强制打虫子,可以专心搞建设,而我当初玩的时候,后期的瓶子全部靠打虫巢得来。

说的我有点动心,刚好疫情来了,我在家中宅了很久,最终通关了 space exploration 。


之后,我们的游戏引擎趋近完成,我需要一个实际的游戏项目来驱动它的完善。我和同事讨论说做点什么。我觉得要做就做一个大家本身都喜爱的游戏类型,然后就选择了 Factorio 这种自动化建设类别。

我现在对这个游戏定义的主类型就是自动化。它让玩家不断的走出自己的舒适区,不做重复机械的活动。 对比大部分网游的设计理念,主流网游主要用重复活动(肝)来留住玩家,玩家追求的是目标而不是过程。同时在用付费的方法来减轻肝度。在这种设计理念驱使下,我不太喜欢它们。

在 Factorio 里面,几乎一切都可以自动化。当你熟悉了一种玩法后,一旦觉得需要重复已经熟悉的过程最终就可以达成目标,那么,你最好想出某种自动化的方法来代替已经做过的工作,而游戏基础机制也鼓励你这样做,游戏不在这方面为难玩家,甚至想法设法的提供更方便的自动化设施来方便你实施。即使原版游戏未提供,也会有 Mod 把它创造出来,而玩家社区也鼓励这种 Mod 。

例如,最早的火车是不可以直接运输液体的,你需要把液体装在桶中,运达目的地后再倾倒出来,并想法把空桶回收。随之,有 Mod 把这个过程完全自动化,把装桶,回收空桶做在车站内部,细节对玩家隐藏起来。这个 Mod 很受玩家社区的欢迎,不久官方版本就直接做出了液罐车。

又比如,早期的传送带二分器是没有筛选功能的,玩家用电路和 Bug 制作了各种神奇的拆分系统。既然,玩家对此有强烈的需求,后来的版本里干脆就让二分器带上了非常丰富的筛选功能。

这些在网游中是不可思议的。相当于把各种外挂内置在游戏中。一切让玩家觉得厌烦的重复活动,都变成游戏主动帮玩家解决困难。去年和一个策划朋友吃饭时聊起,他们拿下了一个颇受玩家欢迎的单机游戏的网游改编权。但是在设计时却犯难了。单机版本一般人也就不到 100 小时可以完美通关,但这对现在的网游运营来说,无论如何也是不可接受的。所以只好加入了一些繁复的任务消费玩家的时间。而我说,看看 Factorio 吧,一个 Mod 就让我玩了几百小时毫无重复感,可以预料我还能再玩几百小时。而现在网游能让人玩进去一千小时是不是已经够运营的需求了?

Factorio 如何做到?因为只要游戏设计得当,每当你克服了一个阶段,全自动化运作后,就会迎来下一个难题。

在游戏中,一件事情有不同的完成方法。方案之间相互比较的话,效率会有优劣差。但实施方案本身有学习和动手的成本。所以大部分玩家并不会也不需要遵循一致的玩法。这样就极大的避免了:游戏很复杂,看别人玩一遍之后,觉得自己已经了解了游戏,就不想再玩了。(云游戏玩家的心理)

游戏中有很多的基础系统,拿物流来说,你可以用传送带、也可以用无人机网络、还可以用铁路;铁路也可以分成没有信号控制的简单路线和复杂的道路网络。在 space exploration 中,还拓展了火箭运输和飞船运输。每种物流系统,人工操作和自动化都是非常不同的。

很多玩家在熟悉了某种物流系统后,倾向于用一个锤子去锤所有的钉子,直到问题规模达到一定后,痛定思痛,学习新的游戏方式,就好像打开了新世界的大门,过去繁杂的问题烟消云散。这个过程因人而异,有人玩几百小时也不愿意跨出舒适区(学习新玩法),有人几十个小时就进阶了。

但这种玩法差异是无所谓的,因为每个人都在游戏过程中获得了乐趣。尤其是跨出舒适区的快感,早来一点晚来一点都没问题,你并不需要和别人比赛,而是在挑战自我。

在游戏中,99% 的事情最终看的都只是时间成本,而不用反悔。一切都是积累,一切可以重来。这样就避免了:我看高玩玩过,我觉得自己达不到那个水平,所以害怕玩这个游戏。

大部分长 Mod 都有非常长的游戏过程,动则数百小时一局。所以玩家几乎只会玩一盘。在漫长的游戏时间里,他们会接受自己的失误,最终让单局游戏演化出个人的特色。而在这一局游戏中,也会需要不断的打破自己的舒适区。这依靠的是游戏过程给自己树立的一个个短期目标,做起来很有目标感。例如:改造一篇旧厂区,升级产能。开辟一片新疆土。


玩太空探索时,我突然发现,原本单机游戏的 Load/Save 大法似乎一定程度的失效了。这让我觉得很有趣。它是怎样做到的呢?我觉得是游戏机制拉大了反馈循环。特意制造出一些大循环的反馈。

例如我第一次上太空,在太空上漏建了陨石防御系统。结果一颗陨石砸坏了我的主仓库,物资洒了一地,自动化也停止了。为什么我不可以 Load 档补建防御系统呢?这是因为我的游戏进程已经离开了太空站 4 个小时。如果 Load 回离开前的档案,等于回档了 4 小时,这相比我再去一次太空站善后的成本要高得多。

这也表明,同样的自动化,短期自动化和长期自动化的游戏体验是可以不同的。当我们设计的系统复杂到一定程度后,就很难一眼看穿,尤其是对玩家的一次游戏过程来说,势必将玩家引到不同的方向。每个人都会有不同的游戏体验。

我安利给三个人玩太空探索,看过五个人的游戏过程,没有一个是一样的。


Factorio 有了 Mod 后,几乎成了一个可以玩一辈子的游戏。它一开始就是这样设计的,只提供基础机制,再在这些机制上挖掘发挥,甚至原版游戏就是自己的一个 Mod 。列一下我的两千多小时有过的体验吧。

首先是原版自带的一些小关卡,可以算是熟悉游戏机制用的小品,大约可以玩上几十小时,解决一个个短期的难题。像是一些 puzzle game 。

在原版中发射一枚火箭升天。在不熟悉游戏的时候,这个过程可能需要数十甚至上百小时。但后面回头来看,原版那点设计真是小儿科。原版有个挑战,8 小时发射火箭可以挑战一下,还是颇有难度的。但据说高玩可以把这个时间缩短到 2 个小时之内( speedrun 上目前最快记录是 1 小时 25 分)。

原版在设计时就留下了足够的火箭后的内容。关注游戏社区的玩家应该知道千(万)瓶工厂的概念:一分钟生产一千个以上的科技瓶。主要的难点是足够快的自动化发火箭(拿到白瓶)。比新手发火箭阶段,玩到这里,必须去理解怎么围绕插件塔布局,研究机械爪和传送带的配合。没有用过无人机,火车的,多半都需要用上一些。信号控制也会成为基本功。

我最喜欢的大型 Mod 是太空探索 space exploration 。它可以视为是原版游戏的深度扩展。在原版中的发射火箭,真的成了开始游戏、迈入太空的第一步。Mod 极大的拓展了游戏过程,进入太空后的游戏时长比原版高出两个数量级,面临的难题也多上一个数量级。几乎每个阶段都会发现需要解决的新(自动化)问题。每几十个小时都能发现新玩法新体验,而不是对过去的重复。我更愿意把这个 Mod 看成是一系列的 puzzle ,是 Mod 作者做出来挑战玩家的。如果你找不到方法,可能需要花上几百倍的时间才能通关。

比如,最后期的曲虹球,和游戏的终极折跃引擎,如果不查别人的方案的话,着实需要动一番脑筋。我通关的时候比较早,同时作者也在迭代,所以网上能参考的不多。最后那个折跃引擎的制作方案,我是万事俱备后,用一个周末 10 个小时才设计出来,非常的满足。

同期贴吧上有个系列帖,那个同学一直在记录他玩太空探索的经历,前面都比我更有经验,更紧凑。但是他宣称最后那个任务因为受到曲虹球的产能限制,需要挂机 400 个小时才能完成。这就是掉入了思维陷阱,认为必须把飞船盖得足够大才能满足通关要求,而没有找到合理的方案。

我推荐所有喜欢 Factorio 原版的同学一定要尝试一下太空探索,一定会发现一些在原版在无法体会到的乐趣。


社区很多玩家推荐把 Krastorio 2 简称 K2 这个 Mod 和太空探索一起玩。

我觉得 K2 也是一个很好玩的 Mod ,它极大的拓展了原版发火箭前的玩法。可以说 K2 在宽度上扩展了原版游戏,而 SE 在深度上极大的加强,结合起来似乎更香。

但我个人有不同的观点。毕竟这是两个作者的作品,思路无法完全一致。虽然他们已经尽力做到兼容了。但从平衡性和难度来说,独立开 SE 应该更符合 Mod 作者的设计难度一些。K2 + SE 会增加整体的游戏时长,但因为 K2 引入的更多机制,其实难度会下降一些。喜欢挑战的话,单玩 SE 更好。而且根本不用担心内容丰富程度。

单独玩 K2 也是同样好玩的。


我玩的时长第二的 Mod 叫做 Nullius 。它的主题是地球化改造。星球上是没有生物的,所以也不存在煤和石油。你得从空气和海水的分解做起,从空气中分离出二氧化碳、海水中提取出氯元素,电解水得到氢气和氧气。然后再用这些基础元素逐步合成高分子化合物,直到做出有机物。

更奇妙的是,游戏的后半期,你又从培育出的植物和动物,基于生物科技逐步分解出简单的化合物,改进工厂的产能。这种倒过来做产线的方式让人耳目一新。旧产线简单稳定低效、新产线复杂高效但更容易出问题,难以自动化。整个游戏过程不断的在推倒旧事物引入新方法。虽然游戏的配方比原版复杂了两个数量级以上,随便做个零件都需要异常复杂的工艺。强迫你必须把所有制造工作自动化,不再能依赖过去手搓的生产方式。但游戏过程设计的非常丝滑。最后通关时庞大的自动化基地给人异常的成就感。

玩游戏的给人的刺激与编写代码的乐趣相当,都是在不断的解决有趣的问题,完成一个个的挑战。程序员应该会爱上它的。毕竟,写程序也是在命令机器自动化工作罢了。玩 Nullius 的这段时间,我写代码的量几乎是这些年最少的。这是在玩别的游戏时不曾遇到的。因为大部分游戏给我大脑的刺激都很难比得上写代码。

关键是 Nullius 的节奏控制的非常好。同样类似主题的还有个叫做 Py 的 Mod 组。也是极尽难事。原版中几个铁片和铜板就可以合成的初级红色瓶,在 Py 中需要折腾几个小时才能出来:你需要通过复杂的工艺吹出玻璃瓶,从种橡胶树开始制作软木塞。但总体给我的感觉就是:不好玩。Nullius 其实同样复杂,但要好玩的多。这证实了我的另一个观点:游戏机制的复杂不等于有趣。

btw, Nullius 极大的挖掘了原版的流体系统。这是我在其它大型 Mod 中都没有过的体验。它让我发现,原版的基础流体模块原来还可以玩出这么多花样来。这甚至影响了我们自己开发的游戏中的流体系统,我在玩过 Nullius 后,直接把写好的流体模块重构了一次。


我另一个喜爱的 Mod 叫做工业革命,现在已经是第二版了 Industrial Revolution 2 。它在一定程度上和 Nullius 一样,都强调了自动化制作机器的必要性。而在原版中,如果你不是去做千瓶工厂,通常仅限于自动化做零件,大部分机器还是手搓出来的。

IR2 的特点是革命。你会在蒸汽时代停留很久。在电力时代之前,能量是通过流体管道中的蒸汽传输的。玩起来比架设电网有着截然不同的体验。而时代的更迭需要你大规模的改造旧工厂,用新思路建设新基地。玩起来还是颇有意思的。


如果脱离自动化建设主题,以战斗系统为主,也可以非常有趣。在太空探索中,中期也会有大量的时间和虫子搏斗。开着吉普或坦克,围着虫巢 Hit and Run ;在铁路上放上轨道炮,自动化清理蔓延到基地附近的虫群;甚至乘坐防御环线列车视察边疆,都是很有趣的游戏体验。

不过 Warptorio2 这个 Mod 硬生生的把游戏改造成了塔防游戏,其中别样的乐趣我是玩之前没有想到的。原本我以为,想玩塔防类型的游戏,我有的选择太多了。何必在 Factorio 里面玩呢。玩了之后我发现,这个 Mod 太适合我了。你需要在有限时间有限空间中构建出完整的生产系统,这和把一小段代码优化到极致的乐趣是差不多的。

我一开始上手就玩的 Warptorio2 Expansion 。听名字感觉是 Warptorio2 的扩展,肯定更好玩。结果一个周末下来,都被虐哭了。动用了 L/S 大法步步为营还是感觉太难了。后来才看到 Mod 主页上的作者警告:绝对不要一个人 solo 这个 Mod ,一定要和朋友一起联机合作。否则后期的一些任务是几乎不可能完成的。

我还是退回去玩 Warptorio2 了。