心流与动态难度
让游戏自己判断玩家是焦虑、投入还是无聊,再去调难度,让人多待在心流里。做了一个浏览器里能玩的 demo。
- TYPE
- 游戏里的自适应难度,业余项目
- STATUS
- demo 能玩,没做过玩家测试
- DEMO
- vampire-survivors-ai-dda.vercel.app(放在 Vercel 上,国内直连打不开,要走代理)
- WHEN
- 2025 至今。这一页写的是 2026 年 1 月的版本,在线 demo 是 5 月的版本
- BUILT WITH
- TypeScript、React、Canvas,控制器是手写的,没用现成的库
- WHO
- 和一位合作者一起做的
这是什么
一个类似 Vampire Survivors 的小游戏,在浏览器里跑。玩家只管移动,武器自动开火,杀敌掉经验、金币和回血,一局 15 分钟。
特别的地方在后台。常见的做法是按时间表加难度,过一分钟敌人多一成。这一版不这么做,后台有一个控制器,大约每秒看一次战况,估计玩家现在是焦虑、心流还是无聊,然后决定要不要多放点敌人、多掉点回血。
FIG. 1 · 控制回路
虚线框里是控制器。它只读战况,只动三个参数。
它看什么
控制器只看三样东西,都是游戏里现成的数。
- 血量。当前血量除以上限,分 5 档。
- 包围。玩家周围一定距离以内,8 个方向里有几个方向有敌人,分 4 档。数的是方向,不是敌人的个数。
- 连杀。上次挨打之后杀了几个,分 5 档。
没有摄像头,没有心率,也不问玩家。所以“焦虑、心流、无聊”只是模型里三个状态的名字,它们和玩家真实的感受对不对得上,没有验证过。
怎么从信号读出状态
控制器里有三张表(图 2),写的是“如果玩家正处在某个状态,看到某一档信号的可能性有多大”。比如玩家焦虑的时候,血量危急的可能性是 0.6,心流的时候只有 0.05。这些数是手填的假设,没有拿玩家数据统计过。
FIG. 2 · 三张表
每一格是“处在这一列的状态时,看到这一行的信号”的概率,每一列加起来是 1,颜色越深概率越大。数字照抄自代码。
每一圈,控制器把上一圈的估计往前推一步,乘上这三张表里对应的数,再归一化,得到三个加起来等于 1 的概率。开局的估计是焦虑 0.1、心流 0.8、无聊 0.1。
图 3 是按这些表算出来的六个例子,从开局的估计出发,只看一次观测。它是算出来的,没有对局记录在里面。
FIG. 3 · 六种战况各被读成什么
右边三个数依次是焦虑、心流、无聊的概率。
怎么调
控制器手里有七个动作,三个参数各往上或往下调一格,再加一个“不动”(图 4)。它不直接改敌人的血量、速度和伤害。
选哪个动作,用的是 active inference(主动推断)里的预期自由能。大意是,对每个动作,先预测做了之后会看到什么样的战况,再算两样东西,一是预测的战况离“想看到的战况”有多远,二是预测本身有多含糊。两样加起来越小,这个动作被选中的概率越大。最后按概率抽一个,不直接取最好的。
“想看到的战况”也是写死的,血量在中间那一档,被围着但有缺口,连杀在 11 以上(图 2 里带三角的那几行)。
FIG. 4 · 三个参数,七个动作
横线是参数能取的范围,黑块是开局的值,每一小格是一个动作调的幅度。时间按每秒 60 次更新折算。
做到哪了
下面说的是 2026 年 1 月那一版。在线 demo 是 5 月的版本,控制器后来改过,这一页还没跟上。
- demo 能构建、能玩。8 种敌人,5 种武器,5 种被动,5 个武器进化配方。
- 三个状态没有校准过,没有玩家的主观报告,也没有别的独立测量。
- 没有存对局数据,所以这一页没有“难度随时间怎么变”的实测曲线。上面的图要么是从模型里的数算出来的,要么是照着代码画的。
- 设计文档里写了让那三张表跟着对局结果慢慢更新,代码里还没有。
- 参数调到头的时候,调不动的那个动作还留在候选里,会被抽中,然后什么都不发生。
- 类型检查有 6 个错,没有测试。
另一个版本
还有一版是文字互动叙事,机制不一样,按回合走,每一轮决定加不加挑战、把文字停在哪儿把选择交还给玩家。这一版的材料我还没整理,先不展开。