February 06, 2023

同一 Entity 包含多个同类 Component 的问题

ECS 中,同一个 Entity 是否可以由多个同类型的 Component 构成?在 Unity 中,答案是可以。我们的引擎在设计之初也是可以的。

当时有一个问题:在 Lua 中,如何访问同类型的 Component ?如果有多个同类 Component ,最自然的方式是把它们放在一个数组里。但是、绝大多数情况下我们用不上这个特性,每次访问 Component 都加一次 [1] 或 [0] 的数组索引显得画蛇添足。若单个 Component 不用数组,多个才用数组,写起来又有极大的心智负担。因为这样做,它们就成了两个不同的类型。

后来,我们干脆利用 Lua 的特性,把数组和 Component 本身放在一个 table 中。如果有多个 Component 就把这个数组直接放在第一个 Component 的 table 内。就这样用了一段时间后,最后还是受不了这个脏技巧。等到用 C 编写 luaecs 后,就砍掉了这个特性。

ECS 不是万能灵药。如果需要让相同的 Component 聚合在一起,那么就使用额外的数据结构,或是不只使用一个 world 。这是我们目前实践给出的答案。去年在 luaecs 的 issue 9 也讨论过类似问题。

阅读全文 "同一 Entity 包含多个同类 Component 的问题" »

January 20, 2023

最近开发中解决的一些性能问题

今天是年前最后一天工作,我想对最近做的一些事情做一些记录。

我们在使用自研引擎开发游戏时,遇到了不少和预期设计有距离的问题,针对问题再反思了原有的设计并做出改进。我认为、凭空设计一个通用游戏引擎是不可能的,必须结合实际项目,做许多针对性的实现。但同时应不断反思,避免过多的技术债。在最近一年,我参与的引擎具体编码工作很少,更多的是站在一个反思者位置,监督代码和设计的演变。

我们的引擎主体框架是基于 Lua 的,受益于 Lua 的动态性,可以很方便的把各个模块粘合在一起。但是、Lua 和 C/C++ 相比,又有两个数量级的性能差异,对于渲染代码而言,若将和 GPU 沟通的 API 完全放在 Lua 的 binding 层,对于对象数量巨大的场合,很容易出现性能问题。我估计、在手机环境这个“巨大” 差不多在 10K 这个数量级吧,而 PC 环境还能支撑到 100K 左右。

Lua 项目的优化无非两条路:使用 jit 技术、把热移到 C side 。正如 Roberto 所言,"Finally, keep in mind that those two options are somewhat incompatible" 这两条路需要的操作往往是互斥的。而我不喜欢 jit 带来的复杂度和不确定性,所以选择了后者。

为了解决大数量对象的批量操作问题,我在 2021 年进入了 luaecs 。这个库现在已经是引擎的核心,专门用来解决 Lua 处理大数据重复事务的性能问题。通过 luaecs ,我们可以把对象的初始化这个繁杂的过程使用 Lua 编写,而每帧都迭代的事务用 C 来实现。

一般来说,如果 10K 是场景可渲染对象的数目的话,那么对于很多游戏场景还是适用的。但如果Lua 调用的图形 API 放在太底层,手机上(以 Apple 的 A9 芯片为下线)一个流畅的场景却很难支撑到 10K 对象。这是因为,每个可渲染对象需要一系列参数传到图形 API 层,设置 VB/IB 渲染状态,尤其是大量的 uniform ,这些 API 差不多会在 10 个调用左右,它们会吃掉一个数量级,最终 Lua 层能流畅处理的数量大约只剩下 1K 左右了。

阅读全文 "最近开发中解决的一些性能问题" »

January 10, 2023

虚拟文件系统的 mod 机制

上次谈游戏引擎的虚拟文件系统是去年 10 月了。最近我们又做了一些修改。

我们的虚拟文件系统有两个工作模式。一种模式主要用于编辑器和开发环境,所有的文件名和路径都基于原生文件系统,我们叫它编辑器模式;另一种模式被称为运行时模式,主要用于运行时环境。文件均用类似 git 仓库的形式,用文件内容的 hash 值作文件名,储存在一颗 Merkle tree 上,并通过一个专门的 file server 为运行环境提供资源的更新服务。

在编辑器模式下,并不是直接映射原生文件系统的整棵目录树的,而是增加了一个 mount 配置,可以把很多不同的目录装配在一起。当初这么设计是希望提供一定的灵活度,方便游戏项目可以把引擎提供的基础功能和资源组装进来。

阅读全文 "虚拟文件系统的 mod 机制" »

December 20, 2022

covid-19 感染康复记录

月初还和同事调侃,这次谁也躲不掉,感染是迟早的事。最近全家都中招了,好在病毒这次还算轻柔,差不多都痊愈了。在此记录一下这次的历程。

起点是广州的全面开放。老婆在家已经被关了快一个月,憋得不行。开放第一个周末,就约了同事出去玩,周五晚上密室、周日下午(12 月 11 日)出去拍照。

我觉得广州已经放开,这次病毒的 R0 相当高,快速传播开无可避免,第一个周末外出有非常大的风险。所以取消了原本每个周末的攀岩活动,和两个孩子继续关在家中。儿子一直有点咳嗽,我有一些感冒,有流鼻涕的症状。

Day 0 (12 月 12 日周一)

老婆被通知一起玩的同事已经确诊,需要她居家办公,观察一天再考虑回办公室。

我当天正常上班了。这天我之前的感冒症状(流鼻涕)已经完全消失,精神状态非常好。

晚上她有一点低烧(38 度),自测抗原阴性,觉得是因为感冒。这一晚,女儿和妈妈睡觉。

阅读全文 "covid-19 感染康复记录" »

December 13, 2022

触摸屏上的一些交互设计问题

我们现在制作的游戏以手机优先,交互都是围绕着触摸屏来做。如果开发的时候总是在 PC 上用鼠标测试,很多交互问题都容易被忽视掉。所以,我们自研的引擎花了很大气力在手机设备上即改即所得。在开发时推荐开发人员直接在手机上修改、调试。这样更容易发掘出平时用鼠标操作难以察觉的问题。

btw,直接在手机上开发比在 PC 上开发还有另一个好处,就是随时能关注画面元素(尤其是字体)的大小是否合适。

阅读全文 "触摸屏上的一些交互设计问题" »

November 21, 2022

间断储存的字符串

绝大部分的基础数据结构都是定长的,很容易针对优化它们的内存管理。但字符串是一个例外。

内存管理和其它资源管理有明显的不同。管理内存有点像切蛋糕,从整块蛋糕上切下需要的那块,但归还的时候却因为支离破碎难以合并起来满足后续用途。举个极端的例子:如果内存堆有 2G 大小,如果碰巧在正中间分配了几个字节而从未释放,这个堆就被永久的分成了两个不足 1G 的分片。之后再无可能从这个 2G 大小的堆中分配出 1G 的内存块。

改进内存分配算法或许可以减轻内存碎片的危害,但即使是在此方面做了相当多努力的 jemalloc ,其表现也大大低于一般用户的预期。以我的经验,一个 16G 的内存堆,对于长期运行,需要大量反复分配释放内存的程序,通常能做到 10G 左右的峰值有效内存占用就不错了。这里说的有效内存使用,指你调用 malloc 传入的字节数之和。根据应用程序使用内存的方式不同,这个数字会有很大的不同。

60% 的内存使用率其实已经很好了,但还是会有很多人问,我的内存都去哪了?

阅读全文 "间断储存的字符串" »

November 17, 2022

skynet 1.6.0

最近半个月,因为广州防疫政策,我一直居家办公。找了点时间,给 skynet 做了一年一度的 release 。

这次的 1.6.0 版,相比去年的 1.5.0 版,没有太大的变化。主要是平时积累的一些 bugfix 。它所依赖的第三方库,例如 Lua , jemalloc 我都更新到了最新的版本。

值得一提的是,mongo 的 driver 也更新了。因为 mongo 在最新的版本中已经淘汰了旧的 wire protocol ,如果再不更新,就无法连接新版 mongo server 了。具体讨论可以参看 PR #1649 。因为 mongo 自己的设计原因,我们无法给出一个同时兼容新老版本协议的方案。从这点来看,我个人认为 redis 的底层协议设计就稳定的多。 这么多年更迭基本不需要修改。

阅读全文 "skynet 1.6.0" »

November 09, 2022

被干眼症结膜炎困扰的这几年

这几年我一直被干眼症和结膜炎困扰。疫情之前,每次洗澡后就双眼通红。但似乎并无其他影响,也没怎么在意。后来慢慢的,眼睛就容易发干,经常疲劳。严重的时候眼睛几乎睁不开。最受影响是在开车的时候,因为无法休息眼睛,特别难受。到晚上环境光线不足的时候,感觉远处看不清楚。

然后,视力突然断崖式下降。三年时间,每年体检,从 1.2 直落 0.7 左右。而我在 35 岁之前一直是双眼 1.5 的视力。本来 2020 年就想约眼科医生看看的,因为疫情去看五官科不方便就一直拖着,直到 2021 年底才去看了医生。

初步诊断结果是结膜炎、干眼症、视疲劳。另外,验光结果表明我有 150 度近视加 50 度散光。因为并不影响日常生活,便没有配眼镜。而且,我的视力是时好时坏。从办公室眺望珠江对面的楼顶招牌时,有时那些字清晰可见,有时则是重影。我感觉如果以后能解决干眼症和结膜炎的话,视力能稳定一些。

阅读全文 "被干眼症结膜炎困扰的这几年" »

October 17, 2022

电信宽带 ipv6 折腾记

周末在家里折腾 BT 下载,发现 UPnP 无法工作,便去光猫那里看看是什么情况。

我发现现在电信宽带默认已经是由光猫来拨号,家中的路由器是作为一个二级路由在使用,如果想让 UPnP 或 DMZ 工作起来,需要在光猫里设置。

查看光猫设备上印的默认管理员密码登录上去,没有找到太多设置选项。在网上搜了一下,需要一个超级管理员身份才能看到全部设置菜单。一般的说法是打电话问一下装宽带的师傅,但天色已晚,不想麻烦人。转念一想,有这么多家宽带设备要管理,电信应该用的是统一密码,或是有一些简单手段可以拿到这个密码才对。否则建立一整套管理系统成本实在太高了。

阅读全文 "电信宽带 ipv6 折腾记" »

October 08, 2022

引擎 IO 模块的变化和发展

我们游戏引擎的 IO 模块其实一直在修改。最初的版本到现在有四年多了

一直没有定稿的一部分原因是因为我们给引擎设定了一个比较高的需求:引擎本身也是可以从网络自更新的。而更新引擎本身必然依赖 IO ,包括 IO 模块自身。而我们引擎又基于一个多线程版本的 Lua 框架 ltask ,ltask 本身也是需要依赖 IO 模块启动的。这些后续的设计要晚于最初 IO 模块的设计,反复重构也就是必然了。

阅读全文 "引擎 IO 模块的变化和发展" »

September 30, 2022

货物和货车的匹配

我们在制作一款类似 Factorio 以自动化为主题的游戏。在游戏开发中遇到了不少算法上的问题,解决过程都非常有趣。比如之前就写过一篇 流体系统

和 Factorio 体验的最大不同在于,我们不使用传送带做主要物流手段,而是依靠公路网和货车。这种形式并不新,我在 20 多年前的凯撒3 (以及同系列的法老王、宙斯、龙之崛起等)中就玩到过。采用这种物流方式的游戏一直不曾间断,这几年我玩过放逐之城、缺氧、环世界、海狸浮生记也是这种。

这类游戏的共性是需要靠城市资源供养一批工人,然后这些工人干活来供养城市,其中一个重要的工作就是物流。一般受资源产能影响,不会养太多工人,不然很难产生正循环。所以物流所占工作比例并不大。大部分工人的工作(在生产建筑中)从事生产。

阅读全文 "货物和货车的匹配" »

September 07, 2022

多线程串行运行 Lua 虚拟机

ltask 是一个功能类似 skynet 的库。我主要将它用于客户端环境。由于比 skynet 后设计,所以我在它上面实验了许多新想法。

最近碰到一个需求:我们的游戏引擎中嵌入的第三方 C/C++ 模块以 callback 形式提供了若干接口。在把这些模块嵌入 ltask 的服务中运行时,这些 callback 函数是难以使用全部 ltask 的特性。比如,我们的 IO 操作全部在一个独立服务中,引擎读取文件时,很可能是通过网络异步远程加载数据的。这些第三方模块通常没有考虑异步 IO 操作,都是以同步 IO 方式给出一个读文件的 callback 函数让使用者填写。

那么,怎样才能在这个 C callback 中挂起当前任务,等待 IO 的异步完成呢?

阅读全文 "多线程串行运行 Lua 虚拟机" »

September 02, 2022

路网中路径的储存

我们正在制作的游戏中,交通和物流是基于公路网的。公路网其实是以路口为顶点,路为边构成的(无向)图。因为我们有大量的车辆行驶在这个路网中,所以,我需要一个空间高效的方法储存这些车辆的路径。

在大部分情况下,我们的车辆都选择唯一的最短路径,也就是说如果从 A 路口到 B 路口存在一条制定好的路径的话,所有的车都会走这条路。基于这一点,我们可以对多条路径合并储存。

我选择用当前路口 id 和目的地路口 id 做 key ,把下一站路口 id 作为 value ,保存在一张 hash 表中。这样,先用 dijkstra 算法求出起点到终点的最短路线后,再把每一段路线用该结构保存下来即可。

当车辆到达某个路口,只需要用当前路口和它自己的目的地就可以索引到应该往哪个方向开。这个结构很适合保存在 Lua table 中,用元表触发尚未计算过的路径。而且这样一个路径表只是一个 cache ,随时可以清理重建。

阅读全文 "路网中路径的储存" »

Misc

Categories

Archives

Recent Comments