同一座城市,开局第一天和运行第三百天,占用的内存可以差出几倍。原因不在画面,而在居民“记住了什么”。这也是大发娱乐App对设备内存要求偏高的根本原因。

普通游戏的内存花在素材上,模拟游戏的内存花在“记忆”上

一个大型动作游戏占内存,主要因为贴图、模型和音频。关卡载入以后,这部分占用基本不动,玩多久都差不多。城市模拟不一样。素材当然也占一份,但另一份是随着游戏日不断增长的:居民今天走了哪条路、换过几次工作、对哪家店形成了什么印象、上一次涨价时手头有多少钱。这些东西不属于画面,却必须在运行中随时可读。

所以大发娱乐App在运行大型模拟时,内存占用的曲线不像普通游戏那样是一条横线,而更像一条缓慢爬升的斜线,中间还会有几个台阶。要判断自己的设备能跑多大的城市,先得知道这条斜线的斜率由什么决定。

把一个居民拆开:五层状态,增长速度各不相同

以居民Agent的设计思路为例,一个居民的状态大致可以分成五层。它们的特点不同,占用方式也不同。

  • 档案层:年龄、职业、家庭结构、居住地、基础收入。开局确定,很少变化,体量小而稳定。
  • 当前状态层:现在在哪、体力和心情、手头现金、今天的日程。每个模拟小时都在被改写,但只保留“现在”这一份,不会越积越多。
  • 习惯与偏好层:常走的路线、常去的店、对价格的参照点。会慢慢调整,体量中等,增长缓慢。
  • 记忆层:经历过的重要事件的摘要,比如“上周在环线堵了四十分钟”“那家餐厅涨价以后就不去了”。这一层随时间持续增加,是内存的主要增长来源。
  • 关系层:家人、同事、常客之间的关联索引。数量取决于社交范围,增长有限。

关键的区分是:前两层“被覆盖”,后三层“被累积”。被覆盖的状态占用是固定的,被累积的状态才会让曲线往上走。

一个模拟示例:为什么运行三百天后记忆层占了大头

下面的数字是说明用的假设值,不是App的实际数据结构,也不是实测结果,只用来展示比例关系。假设一座城市有4000个居民:

状态层假设的单人占用 4000人合计运行300天后
档案 + 当前状态约2 KB 约8 MB 基本不变
习惯与偏好 约4 KB 约16 MB略有增长
关系 约1 KB 约4 MB略有增长
记忆(假设每天新增约0.3 KB) 开局为0 开局为0 约90 KB/人,合计约350 MB

在这组假设里,开局时全部居民状态合计不到30 MB,运行三百天以后,仅记忆层就超过了三百兆,而且只要不做整理,还会继续线性增长。这就是为什么“开局很顺、越玩越慢”在模拟游戏里很常见。反过来看,居民数量翻一倍,记忆总量也翻一倍;游戏天数翻一倍,同样翻一倍。内存占用是“规模 × 时间”的乘积,而不是只跟其中一个有关。

记忆不能无限增长,也不能随手删掉

让智能体有记忆,并不是大发娱乐独有的想法。Park等人的团队于2023年在arXiv发表的Generative Agents一文,提出了由记忆流、反思、检索与规划组成的智能体架构:先把经历记录下来,再周期性地归纳成更高层的结论,需要时再检索出来用。这套结构之后被城市、交通、经济等不少模拟研究沿用。放到游戏里,它带来的直接工程问题就是:记录一多,内存和检索代价都在涨。

常见的处理办法是分级保留。最近发生的事保留细节;较旧的琐碎记录合并成摘要;带有强烈情绪或后果的事件(比如一次搬家、一次失业)被标记为重要,长期保留。删得太狠,居民会重新“失忆”:三个月前那次涨价被忘掉,价格参照点回到初始水平,降价促销就重新变得有效;上个月的堵车被忘掉,路线选择又回到了最短路径。模拟表面上依旧在运行,其中的因果链却已经断了。

所以理想的做法是压缩,而不是清空:把“十次经过环线、每次都堵”压缩成“这条路在早高峰不可靠”这一条结论。保留的是判断依据,丢掉的是重复的流水。至于什么时候唤醒哪些居民、哪些居民值得完整思考,则属于事件驱动而不是每秒调用的调度问题,它决定了这些记忆被读取的频率,而不是记忆的总量。

世界数据也在长:账本、库存和历史曲线

除了居民,世界本身的数据同样在增加。World State,也就是世界状态,包括路网与建筑、企业的库存与现金、各类商品的价格与成交记录,以及供玩家查看的历史曲线。前面几类是“当前值”,体量固定;历史曲线和交易记录是“追加型”,天数越多越长。

还有一类经常被忽略的数据:事件日志。历史与战略类模拟需要保证因果一致,也就是某件事改变以后,后续事件要重新判断,而不是原样触发。要做到这一点,系统必须留有“发生过什么”的记录,才能在改变发生时回溯哪些后续依赖需要重算。这部分日志的增长速度取决于事件的粒度,记录得越细,越能保证一致,占用也越大,这是一个需要取舍的地方。

还有一层是派生缓存,比如预先算好的路径、聚合后的统计。它们可以随时重算,属于“用内存换速度”,内存紧张时是最先可以放掉的一层。

内存吃紧时,先动哪些东西

把上面的分层对应到实际操作,优先级大致如下。这里的每一项是否在App中提供,以及名称叫什么,以实际页面为准:

  1. 先释放可重算的:退出后重新进入存档,让派生缓存重新生成,通常能立刻回收一部分内存。
  2. 再缩短历史:如果有历史曲线保留长度一类的选项,缩短它,几乎不影响模拟本身的因果。
  3. 然后降低规模:居民数量直接决定记忆层的总量,是最有效也最彻底的一档。
  4. 最后才考虑动记忆:任何要求“清空居民记忆”的做法,都会改变城市的行为,应当视为重置,而不是优化。

要区分两个概念:存档占的是存储空间,运行占的是内存。存档文件可能只有几十兆,载入之后却要展开成远大于它的运行时结构,所以“存档不大”不等于“跑起来不吃内存”。想排查具体的卡顿,可以对照CPU、内存和模拟规模的排查顺序;如果关心这些数据换设备时是否随账户走,则是存档同步那边的问题。

三类世界的内存账并不一样

上面的估算以城市为例,换到别的世界,增长的主项会变。大发娱乐经营AI的世界里,居民更多以消费者群体的形式出现,记忆里装的是价格印象和购买习惯,企业Agent则要保存自己的库存、定价历史和对竞争对手的观察。大发娱乐体育经理的世界里,球员的成长轨迹、出场记录、合同和士气变化都要逐赛季保留,联赛越长,这些追加型数据越多。战略与历史类世界中,事件日志和因果依赖的记录占比更高。

共同点是:每一类世界里,真正让内存持续上涨的,都是“为了让后来的决定有依据而保留的过去”。这也解释了一个直觉上有点奇怪的现象:一个只有几百居民、却已经运行了很久的世界,有时比一个刚开局的大城市更吃内存。

判断一台设备能跑多大的城市,先看可用内存,再看处理器。前者决定“能不能长期玩下去”,后者只决定“玩起来顺不顺”。