下载 App
《我真没让他们建城啊!》开发者日志 02
修改于10/168 浏览
先让人看懂,再让数据说话 一个涌现游戏最大的敌人,不是"世界不够复杂",而是"玩家不知道这一切跟自己有什么关系"。 第二大的敌人是:你以为你测出来的数,是真的。
日期: 2026-10-09 当前进度: 新手引导 + 数值调参层完成;数值体检发现战争链路与可复现性问题
写在前面:从"能跑"到"能被看懂" 上一篇日志(01)讲的是怎么让世界长出来——七张热度场、Utility AI、20 项文明现象图鉴。
今天要解决两个新问题:
世界长出来了,但新玩家不知道自己在看什么。 屏幕上是一群小豆子在乱走,没有任务栏、没有血条、没有"点击建造"。玩家大概率乱点两下就退了。 数值调完了,但我不知道自己调得对不对。 手感是主观的,需要一套能跑的数据来告诉我"这一局到底发生了什么"。 所以今天的三件事是一条线:
新手引导——第一次进来该看到什么、什么时候看到(Tutorial.lua) 数值统一——把所有调参入口收进一个文件(GameBalanceConfig.lua) 无头体检——跑 900 秒 × 3 个种子,用数据验收(scripts/_proc/) 结果是:第 1、2 件做完了,第 3 件顺藤摸出了两个真问题。
一、新手引导:只在玩家第一次需要时教 设计文档 §63 里写死了一条红线:
新手引导绝对不能开场连续弹 8 个框。
错误示范:
text
复制 欢迎来到游戏 → 这是人口 → 这是资源 → 这是神之手 → 这是性格 → 这是道路 → 这是市场 → 这是规则卡 → 这是文明图鉴 玩家看完的唯一感受是:这个游戏系统好多。然后关掉。
正确的做法只有一句:
只在玩家第一次需要某个东西时,才教它。
1.1 节奏:五拍,每拍只教一件事 Tutorial.lua 只维护状态,不画任何东西——所有画面由 Render 读它的字段完成。教学逻辑和渲染彻底解耦,改节奏不用碰渲染代码。
阶段 触发 做什么 intro 开局 两句话,约 4.8 秒,不弹完整教学 place intro 结束 常驻提示"试着在左侧「资源」里放一些食物",同时地图高亮食物落点 quiet 玩家真的投下食物 20~40 秒不弹任何教学 road 第一条道路形成 6 秒全屏过场,整个新手流程的高潮 free 过场结束 自由观察,一次性提示按需触发 两个刻意的设计:
第二拍的提示是"常驻"的(hintSticky = true)。它不会被计时器清掉,只有玩家真的投下食物才消失。因为玩家读完提示还要摸索怎么拖拽——如果提示读得慢就消失了,教学就白做了。
第三拍是刻意的留白。20~40 秒什么都不教,让玩家盯着小豆人看。这段时间里小豆人会开始吃东西、走动、踩出第一道痕迹——是游戏在教玩家,不是我在教玩家。
1.2 把"第一条道路"做成高潮 整个新手流程唯一一个"大场面"给了第一条道路(§68)。
因为道路是这个游戏第一个"WOW 时刻"——玩家第一次亲眼看到自己什么都没做,世界却变了。这一刻必须在教学里被放大,不能一闪而过。
顺序很重要:先让玩家看到现象,再告诉他"你可以做更多"。 反过来就变成说明书了。
1.3 HUD 渐进开放 设计文档 §73 要求首局隐藏复杂功能,所以工具栏和图鉴是逐步解锁的:
功能 解锁条件 资源(食物/木材/石头) 始终开放 文明图鉴 第一条道路形成后 世界规则 玩家选过第一张规则卡后 危险(狼群/野火) 第一次遇到狼,或 300 秒后 诱因(篝火/宝藏/图腾/钟) 240 秒后 玩家开局看到的工具栏是短的。随着他理解世界,工具栏才一点点变长。
1.4 一个容易忽略的细节:教学不能污染模拟 写 Tutorial.Reset() 时卡了一下:安静观察期的时长要有点随机性(20~40 秒),不然每局都一样。但不能用 math.random。
因为模拟是固定步长、确定性伪随机的——资源分布、小豆人性格全部由地图种子推导。如果引导在这里消耗一次随机数,"玩家是否触发过教学"就会改变世界演化,同一颗种子在不同玩家手里会跑出不同结果。
所以安静时长从地图种子推导:
lua
复制 Tutorial.quietDuration = TC.QuietMin + math.floor(Config.Hash2(B.MapConfig.Seed, 97) * (TC.QuietMax - TC.QuietMin + 1)) 一条原则:教学是"观察者",不能污染模拟。 这和 01 里"涌现系统只能命名、不能创造"是同一条原则的延伸。
二、数值统一:把参数收进一个文件 项目现在约 7000 行 Lua,参数散落在 World、Agent、Emergence、Systems 各处。调一个繁殖概率要翻三个文件,改一个阈值不知道哪里还在引用。
于是建了 GameBalanceConfig.lua,按模块归类:
lua
复制 B.TimeConfig -- 固定步长、各阶段起始时间 B.MapConfig -- 地图尺寸、资源点、出生点、种子 B.AgentConfig -- 人口、寿命、性格、繁殖、感知 B.NeedConfig -- 饥饿、疲劳、恐惧、社交 B.ResourceConfig -- 资源容量、采集时长、再生 B.HeatMapConfig -- 七张热度场的衰减与阈值 B.SettlementConfig -- 聚落形成、升级、迁徙 B.TribeConfig -- 部落独立距离、关系、敌意阈值 B.CombatConfig -- 战斗、狼群、战争判定 B.RuleCardConfig -- 12 张规则卡 B.EventConfig -- 7 个随机事件 B.TutorialConfig -- 新手引导节奏 两个刻意的约定 约定一:单位写进文件头。
-- 时间:游戏秒;距离:Cell。Config.CELL 仅负责将逻辑距离换算为绘图坐标。 这个项目里有三种"长度":逻辑格子(Cell)、绘图像素、屏幕逻辑像素。混用一次就是一场 debug。把单位钉在文件顶部,比任何注释都管用。
约定二:阈值旁边写"为什么是这个数"。
lua
复制 -- IndependentDistance 必须 <= SettlementConfig.CoreGap(10),否则间距落在 -- 两者之间的新聚落会被永久并入第一个部落,第二部落永不诞生,战争无从发生。 IndependentDistance = 10, 这条注释是 01 里"部落分裂距离"坑的产物。数值文件里最值钱的不是数字,是数字旁边那句为什么。
三、无头体检:让数据说话 数值调参最怕两件事:凭感觉调、改了 A 不知道 B 崩了。
所以写了一组无头诊断脚本,放在 scripts/_proc/,用 .cli/UrhoXRuntime 直接跑,不开图形界面:
脚本 干什么 balance_audit.lua 3 个种子 × 900 秒,输出关键指标 + 首次现象时间对照设计目标 war_diag.lua 专测战争:战斗热度峰值 / 部落数量峰值 / 跨部落击杀 balance_diag.lua 死亡构成、交易时间分布、人口曲线 determinism.lua 单次运行,供跨进程对比确定性 leak_diag.lua 同进程连跑多局,用状态指纹定位跨局状态泄漏 3.1 三颗种子的体检报告 跑完 900 秒(一局完整时长):
指标 seed 21021 seed 777 seed 4242 均值 峰值人口 132 139 121 130.7 出生 / 死亡 215 / 113 226 / 126 212 / 115 217.7 / 118.0 道路长度 150 140 150 146.7 聚落数 1 2 2 1.7 城市数 0 2 0 0.7 交易次数 62 345 117 174.7 商路数 0 1 1 0.7 图鉴解锁 6 10 9 8.3 战争次数 0 0 0 0 三个种子都 standardPassed = true——但最后一行那个全 0 太扎眼了。
3.2 首次现象时间 vs 设计目标 体检脚本内置了一张对照表(设计文档 §一 的目标时间):
现象 设计目标(秒) seed 21021 实测 结论 首次采集 10~20 11.5 ✅ 首次道路 45~180 58.1 ✅ 首次交易 120~240 34.7 ⚠ 偏早 首次冲突 240~420 46.0 ⚠ 偏早 首次死亡 240~420 264.9 ✅ 新手引导最关键的那条——首次道路 58 秒——正好落在"3 分钟内看到明显涌现"的验收区间里。这是今天最想确认的数,它成立了。
但交易和冲突来得太早了:设计上希望玩家先安静观察一分钟,再看到交易和打架;实测 35 秒就交易、46 秒就打架。这会让 quiet 那段留白变得很吵——玩家还没来得及"看",世界就已经热闹起来了。
四、追一个"从不发生的战争" 三颗种子的 wars 全是 0。这不是"偶尔没打起来",是三局都没打起来。一个 GameJam 里最戏剧化的系统,等于不存在。
4.1 先画出战争的三层门槛 代码里战争是这样触发的(Emergence.lua):
text
复制 第一层:至少存在 2 个部落 └─ 聚落人口 ≥ IndependentPopulation(8) 且 与最近聚落距离 > IndependentDistance(10)
第二层:两个部落的关系 ≤ HostileThreshold(-20) └─ 关系从 +10 开始,抢劫 -5 / 攻击 -8 / 击杀 -15 只有"敌意"关系才会主动攻击
第三层:战场内战斗热度 ≥ WarHeat(150) 且 30 秒窗口内 同一对部落 的跨部落死亡 ≥ 3 └─ 满足才记一次"战争" 4.2 诊断数据(真实运行结果) war_diag.lua 每秒采样,跑完 900 秒 × 3 种子:
种子 战斗热度峰值(需 ≥150) 部落峰值 跨部落击杀 关系极值 战争 21021 154.3 1 0 0.0 0 777 387.0 2 0 +14.0 0 4242 453.0 2 35 +100.0 5 这张表比任何猜测都清楚——三颗种子卡在了三个不同的层:
21021 卡在第一层:部落峰值只有 1。战斗热度 154.3 刚过线,但只有一个部落,不存在"跨部落"这回事。第二聚落没能长成独立部落。 777 卡在第二层:部落有 2 个,战斗热度高达 387,但关系极值是 +14(正的!),跨部落击杀 0。两个部落过得太好了,从没结仇,所以从不互相攻击。 4242 反而打通了:2 个部落、关系推到 +100(说明有大量往来)、35 次跨部落击杀、5 场战争。 我原本以为是"地图太小,聚落长不到 10 格远"这一个原因。数据证明我错了——是三个不同的门槛各自在不同的局里失效。
顺带一提:三局里狼击杀全部为 0。狼群会生成、会过境(日志里能看到 生成狼 wolf1..wolf5),但没有杀死过任何小豆人。这又是另一个"写了的系统没真正生效"的信号。
4.3 一个更麻烦的发现:900 秒下同种子不可复现 排查到一半,我注意到一件更严重的事:
同一颗种子 4242,在 22:00 的体检里是 0 场战争,在 22:17 的单跑里是 5 场战争。
两次运行之间没有改过任何代码(最后一次构建是 21:23)。
于是先验证确定性。determinism.lua 跑 seed 777 × 400 秒,三个独立进程结果完全一致:
[det] seed=777 峰值=67 出生=62 死亡=17 道路=74 聚落=1 交易=54 ← 三次完全相同 400 秒尺度是确定的。但 900 秒尺度下,同代码、同种子给出了 0 和 5 两种结果。
这意味着:我上午那份体检报告本身是不可信的。 它可能只是恰好跑到了某个分支。基于它的所有调参结论(包括"首次交易偏早")都要打问号。
这比"战争没发生"严重得多——它意味着我的测量工具坏了。 在修好它之前,任何数值调整都是盲调。
目前定位到的方向(还没定论):
400 秒一致、900 秒不一致,说明分歧出现在长跑的后段,很可能是某个累积状态或迭代顺序在长时间后才放大成可见差异; leak_diag.lua 就是为这个写的:同进程连跑 [4242] 和 [777, 4242],逐秒记录状态指纹(人口 + 所有小豆人坐标/需求之和 + 部落数 + 道路长度),找第一个分歧秒,从而二分定位到具体模块。 这一步是明天的第一件事。
五、今天踩到的三个"看不见的耦合"
- 教学随机数会污染模拟。 新手引导看起来和玩法无关,但只要它用了一次 math.random,确定性就没了。观察者必须零副作用。
- 一个阈值"对不对",取决于地图尺度。 IndependentDistance = 10 单看合理,但地图只有 64×64、聚落间距普遍小于 10,这个数字就可能永远不满足。数值不能脱离尺度单独评审。
- 三层门槛,可能三层同时失效,而且失效原因各不相同。 战争不是"一个 bug",是三个独立的门:部落没分裂、关系没恶化、规模不够。修一个不等于通。
六、下一步 [ ] 最高优先级:修可复现性。 用 leak_diag.lua 找 900 秒分歧点,在测量工具可信之前不调任何数值 [ ] 修战争链路:三个门槛逐一验证——部落分裂(21021)、关系恶化(777)、规模(4242) [ ] 查"狼击杀 = 0":狼生成了却没造成任何死亡,同样是"写了没生效" [ ] 压前段节奏:把首次交易、首次冲突推回设计目标区间 [ ] 补 UI/UX 文档 §66/§72:明确"新手教学不教的内容"(AI 原理、性格、规则卡机制一律不教) [ ] 结算界面、宣传 GIF、投稿材料 写在最后 01 里我说,这个项目最爽的瞬间是"等等,他们为什么突然开始干这个了?"
今天补上了它的两个前提:
第一,玩家得先愿意留下来看。 新手引导不是"教玩法",是保护玩家第一次看到涌现时的那份好奇。少弹一个框、晚一秒解释、多留二十秒空白,都是在给那一刻让路。
第二,我得先能相信自己在看的东西。 一个测不准的系统,比一个没做好的系统更危险——因为你会拿着假数据,自信地往错误的方向调。
今天最有价值的产出,不是新功能,是那张三颗种子卡在三个门槛的表,和那句"同一颗种子跑出两个结果"。涌现游戏里,"没发生"和"发生了"一样重要,而"测不准"最要命。
#聚光灯gamejam开发者日志
#聚光灯gamejam开发者日志










