下载 App
《我真没让他们建城啊!》开发者日志 04
修改于10/1624 浏览综合
—— 先让每局都不一样,再让他们学会反抗 今天被玩家的话打断了三次。 第一次他说「有些不好玩」,第二次说「中段不知道往哪投」, 第三次说「我知道我想玩什么了」。 这三句话,把这一天的方向从头改到了尾。
日期: 2026-10-12 当前进度: 每局地图随机化;文明编年史 + 世界态势面板;叛神/觉醒系统上线(慈神 0 觉醒 vs 暴神引爆对抗);跨局图鉴存档;叛神全套像素视觉
写在前面:一次「不好玩」的诊断 02 和 03 我都在讲「怎么让世界更可信」——先让人看懂,再让数据说话,最后让世界可复现。那是内部的功夫。
今天第一次直面一个更硬的问题:它好玩吗?
玩家给了一句非常笼统的反馈:「有些不好玩啊。」
这种话最怕的就是拍脑袋改。所以我没有直接动手,而是先问清楚「不好玩」是哪一种。他勾了四个:每局都一样、看着无聊、看不懂在发生什么、结局没意义。
四个全中。那就不是某一个旋钮的问题,是内容与反馈的系统性缺口。于是今天做了四件事,每一件对应一个诊断:
诊断 做了什么 每局都一样 每局注入真实种子 + 地图骨架随种子变化 看不懂在发生什么 文明编年史(HUD 按 C)+ 结算页时间线 中段不知道往哪投 世界态势面板(常驻可点击定位) 结局没意义 跨局图鉴存档 + 叛神系统(有胜负了) 但真正让今天变得不一样的,是玩家最后那句话——「我知道我想玩什么了」。他说:
人天生喜欢破坏和创造。小人并不会意识到存在一个上帝,我希望给他们加一个规则,当他们的不幸值增加,有概率反抗玩家,反抗神。知识大爆炸,玩家需要对抗这些小人。因为玩家可以选择是参与创造或者参与破坏。
这句话把游戏从「看风景」推到了「有对手」。
一、每局都一样:一个被忽略了一整局的 bug 1.1 先复现,再动手 「每局都一样」这种反馈,最容易被当成「玩家玩腻了」。但它是可以验证的——我写了个探针,不设种子连开两局,打印开局世界指纹:
text
复制 RUN[A] fp0=18346247 RUN[B] fp0=18346247 ← 一模一样 指纹完全相同。 不是玩家玩腻了,是真的每局同一张地图。
1.2 两个叠加的原因 查下去发现是两个问题叠在一起,而且第二个更隐蔽。
原因一:主游戏从头到尾没调用过 math.randomseed。
只有 BalanceTests 和 _proc/ 下的测试脚本设过种子。引擎默认种子固定,所以 math.random 序列每局一样——NPC 出生分布、狼群落点、随机事件选择,全都一样。
原因二:地图骨架本身是写死的。 这一条才是关键——就算换了 math.random 种子,地形大形也不会变:
lua
复制 -- 修复前 local riverX = 36.0 for cy = 0, grid - 1 do local wx = math.floor(riverX + math.sin(cy * 0.22) * 3.0) -- 无种子 ... end -- 两片森林用不含种子的固定哈希 if W.terrain[idx] == Config.T_GRASS and Config.Hash2(cx * 3 + 1, cy * 5 + 2) < 0.62 then 河流永远在第 36 列,两片森林永远长在同一片区域,资源簇坐标固定在配置里。换种子也没用,因为地形根本不吃种子。
1.3 修复:种子要喂进骨架里 lua
复制 -- main.lua:每局取真实种子(os.time 混高精度时钟,同秒重开也不同) local function NewMapSeed() local ok, t = pcall(function() return os.time() end) ... local jitter = math.floor(os.clock() * 1000) % 100000 return math.floor(t) % 1000000 * 100000 + jitter end lua
复制 -- World.Generate:河流的基准列/摆幅/频率/倾斜/相位全部由种子派生 local riverBase = 32.0 + Config.Hash2(mapSeed, 101) * 8.0 local riverAmp = 2.0 + Config.Hash2(mapSeed, 103) * 3.0 local riverFreq = 0.16 + Config.Hash2(mapSeed, 107) * 0.12 森林的范围与疏密、资源簇的中心位置(±4 Cell 抖动)也全部接上种子。
关键取舍:测试脚本不注入种子时,回落到配置里的固定种子。 这样 03 辛苦建立的跨进程可复现性一点没丢——测试仍然逐字节一致,只有真实游玩每局不同。
1.4 验证:结局差异才是重玩动力 12 个种子 × 300 秒,0 失败,而且结局差异很大:
text
复制 OK seed=111111 pop=49 roads=70 era=3 disc=8 OK seed=222222 pop=70 roads=107 era=1 disc=7 OK seed=333333 pop=65 roads=98 era=4 disc=10 OK seed=777777 pop=59 roads=92 era=4 disc=10 ... SUMMARY ok=12 fail=0 纪元 1~4、现象 4~10、人口 49~70。同一套规则,每局跑出不同的历史——这正是重玩动力该有的样子。
二、看不懂 & 不知道往哪投:两块面板 2.1 编年史:数据一直在记,界面完全没用 「看不懂在发生什么」这条,查下去发现一个很荒诞的事实:数据全都在记,界面上一条都没用。
W.deathLog、W.migrationLog、W.eventLog、W.firstTimings——死亡、迁徙、事件、纪元晋级,全都在后台累积。但 Render.lua 只在结算页显示几个聚合数字:交易 78 次、死亡 52 人。过程一条都看不到。
所以写了 Systems.BuildChronicle(),把这些聚成一条按时间排序的叙事线。但直接铺开会刷屏——实测 600 秒有 64 条死亡记录,其中绝大多数是老死。于是做了两个降噪:
老死(old)不进编年史:太频繁,会淹没叙事 同因死亡按 20 秒窗口聚合:否则一次狼群入侵刷出十几条「有人死于狼口」 效果:54 条 → 29 条,一条完整的故事线:
text
复制 67.1s 路 「第一条道路」出现了 131.1s 墓 「墓地」出现了 289.1s 瘴 瘴气:低洼密集处毒雾弥漫 299.1s 村 「村落」出现了 307.9s 迁 有 11 人结伴迁徙 327.2s ☠ 有 4 人死于狼口 344.1s 🌾 他们走进了农耕时代 358.9s 迁 难民成群离开了家园 2.2 态势面板:设计文档要求了,但从来没做 「中段不知道往哪投」这条,查设计文档时发现一行:
§12 世界事件状态:右上角只显示当前最重要的一件事。例如 🐺 狼群正在接近北部。点击展开。
这个一直没实现。 玩家只有转瞬即逝的横幅和 toast——中段一旦平静下来,屏幕上就没有任何持续的、可行动的信号。
于是做了「世界态势」面板:常驻显示现在最该关注的几件事,每条都能点击,镜头直接跳到事发地。
text
复制 🌍 世界态势 · 点击可定位 🐺 狼群在活动 —— 投食物分散,或投狼群互斗 ← 点击跳转 💀 近期有 25 人死亡 —— 这里出了大事 ← 点击跳转 平静期则给一条可行动的引导(按进度递进):
还没交易过 → 「把食物投在两聚落之间,看他们走出商路」 聚落不足 2 个 → 「投食物引他们去远处,再养出一个聚落」 还没战争 → 「把两个部落拉近些,迟早会打起来」 2.3 顺手修的一个老 bug 做面板时发现:Camera.RegisterUI(id, x, y, w, h, nil) 传 nil 回调等于没注册。所以选中工具后点面板,会穿透到世界、误放工具——图鉴和编年史面板都有这毛病。加了个空回调统一修掉。
三、叛神:让他们学会反抗 这是今天最重的一块。
3.1 设计:苦难是觉醒的唯一条件 玩家的原话是「不幸值增加,有概率反抗」。所以核心是一本苦难账,每聚落独立记。
但设计过程中撞到一个很关键的问题,我把它单独拎出来讲,因为它差点让整个系统不成立:
最初我要求「至少农耕时代才能觉醒」。 听起来合理——得先有余力想「为什么」吧。
实测结果:暴神打崩了经济 → 时代停滞 → 觉醒永远不发生 → 只剩灭族。
text
复制 [暴神] seed=111111 天罚=6 pop=0 觉醒=0 结束=extinction [暴神] seed=555555 天罚=7 pop=0 觉醒=0 结束=extinction 镇压到灭族,反抗根本没机会成型。这和设计意图正好相反。
所以改掉了:AwakenEraMin = 1。苦难本身就是觉醒的唯一条件,不需要文明先富起来。改完之后,对抗链条立刻活了。
3.2 两个量:创伤只涨不消,怨气会消退 第二个坑更隐蔽。最初我用一个值 value 同时干两件事,结果发现永远点不醒:
text
复制 t=155 炮1 不幸=55 t=215 炮2 不幸=1 ← 60 秒后掉回 1 怨气以 0.35/秒消退,两次天罚间隔就退干净了。于是拆成两个量:
grief(创伤):只涨不消。觉醒看它——创伤不会被时间抹掉。 value(怨气):会消退。回心转意看它——持续的善可以让怨气平息。 这个拆分是整个系统的地基。
3.3 核心张力:慈神 vs 暴神 修完之后,两条路线的差异非常清楚(同种子 600 秒):
玩法 觉醒 叛神者 通天塔 慈神(投食物) 0 0 0 暴神(降天罚) 8 18 70% 当慈神完全不会引发反抗;当暴神则引爆完整对抗链。 玩家的选择第一次真正有了代价。
而且「善」是有回报的:每次恩赐会累积信仰,抬高该聚落的觉醒门槛——不是抹掉创伤,而是让他们更愿意相信神是善的。
3.4 反抗长什么样 叛神者(Agent 的 rebel 状态)去拆神的造物——篝火、图腾、钟、宝藏。不碰他们自己建的房子,这个边界很重要:他们恨的是神,不是自己的生活。
而如果长期无人压制,觉醒聚落会开始造通天塔。塔建到顶——
「神权崩塌:他们造了一座塔,爬上去,看见了你。」
这就是这一局的失败条件。
3.5 一个必须加的细节:叛神者抗天罚 最初天罚能直接劈死叛神者。结果是一轮镇压就灭族,对抗无从谈起。
所以让叛神者对天罚有抗性——他们已经不信神了,落雷吓不住他们,只重伤不死。镇压要变成「对抗」,而不是「单方面屠杀」。
四、图鉴存档:为什么要玩第二局 这块是玩家点名要先修的,理由他说得很清楚:「影响的是为什么要玩第二局这个根本问题。」
查文档,Day 17-18 明确写着:
加入:文明现象图鉴。至少 15 个。这是玩家玩第二局的重要动力。
但代码里零持久化——全项目没有任何存档调用。关掉游戏,31 种现象全部清零,第二局打开还是满屏 ???。驱动被剪断了。
修复的核心是一个设计决策:把「本局的发现」和「这辈子的发现」彻底分开。
Systems.discovered —— 本局发现了什么,每局清空 Collection.unlocked —— 这辈子见过什么,永久保存 混用会导致第二局的「首次发现」被误判成重复。分开之后,才能区分出「★ 首次收录」和「本局又见」。
验证必须跨进程——同进程内读写会假阳性:
text
复制 第一次运行:PHASE5 文件内容: {"runs":1,"total":3,"unlocked":["road","village","war"],...} 第二次运行:PHASE1 loaded=true count=3 runs=1 ← 真的读回来了 PHASE2 首次收录 road=false ← 已收录的正确识别为重复 端到端再验一次:一局发现 14 种,其中 12 种首次收录(road/village/war 已在库里,正确排除),累计 3+12=15。同种子重跑,首次收录 = 0。
五、叛神的视觉:从「看不见」到「一眼认出」 系统做完之后,玩家指出一个很准的问题:「这是你最新、最独特的系统,却最看不见。」
确实——只有 HUD 上一个数字和态势面板的文字,地图上完全没表现。按项目的美术规范(烘焙像素精灵,不用矢量),补了 6 张:
通天塔 4 级(22×26 → 26×40 → 30×56 → 34×74)
关键设计:画布尺寸逐级变大,所以塔是真的「长高」,不是同一张图缩放。锥形塔身,塔顶那只眼睛 2 级起睁开——那是他们想爬上去看见的东西。>75% 时整塔泛红光 + 加速脉动,这是明确的失败警告。
叛神者(站立 + 移动两帧)
红头带 + 举拳 + 破袍下摆 + 怒目斜线。红头带是唯一识别特征,我特意合成了一张模拟场景图确认它在人群里一眼可辨。
配套的还有:觉醒聚落头顶红金光环(「他们看见了你」)、被拆造物的左右摇晃 + 耐久条(绿→黄→红,让玩家判断还能撑几下)。
5.1 一个差点污染确定性的坑 Apostasy.AttackStructure 最初写的是:
struct.shakeT = 0.25 -- 倒计时 如果渲染侧每帧 -dt 来衰减它,就改了世界状态——这会破坏 03 辛苦建立的跨进程可复现性。改成绝对时间戳:
struct.shakeUntil = W.time + 0.25 -- 渲染只读 03 那条原则今天再次生效:模拟是权威的,表现只能看,不能碰。
六、今天踩到的坑
- 「觉醒要有时代门槛」——一个听起来合理、实际致命的直觉。 它会让「镇压」直接通向「灭族」,反抗永远来不及发生。设计直觉必须用数据验,不能靠推演。
- 一个值干两件事,会互相抵消。 怨气既要驱动觉醒、又要能消退,结果就是永远点不醒。拆成「只涨不消的创伤」和「会消退的怨气」之后,系统才成立。
- 觉醒会疯狂抖动。 门槛太接近时,实测 155 秒内觉醒 9 次、回心转意 8 次,非常出戏。加了回心转意的滞后(门槛降到 0.25 倍 + 需连续保持 25 秒)才稳住。
- 按 tribeId 选叛神者,一个人都选不出来。 归属要驻留 60 秒才生效,早期居民 tribeId 全是 0。改成按到聚落中心的距离选。
- Lua 的 local function 不提升。 DrawBabels 放在文件后面,但被更早的 DrawWorld 调用,LSP 直接报 undefined-global。顺序必须正确。
- 烘焙器里不能引用 W。 叛神者带尾本来想用 W.animT 做飘动,但烘焙脚本没有 W,会 nil 崩。带尾改成静态,飘动交给渲染侧。
七、当前状态 项 数值 代码 14 个玩法模块,8724 行 Lua(含测试工具共 8906 行) 神之手工具 12 个(资源 / 危险 / 诱因 / 天罚) 文明现象图鉴 34 条(今天新增 3 条叛神线) 像素精灵 33 张(今天新增 6 张) 音效注册 51 条 八、下一步 [ ] 叛神音效:觉醒/建塔/拆造物目前是静默的(或复用别的音),AudioManager 里 0 条叛神音效 [ ] 通天塔警告期:>75% 已有红光,可再加全场闪烁 + 音效 [ ] 小地图:64×64 地图 + 相机缩放,玩家仍不知道全局格局 [ ] 教学覆盖新系统:引导只教了「投食物」和「看道路」,叛神/天罚/态势面板/编年史都没教 [ ] 发布素材:project.json 里 icon/screenshots/promotional_image 全为空,且标题被截断成「我真没让他们建」(少了「城啊!」) [ ] 战争率:把冲突判定限定在不同部落之间 [ ] 收纪元阈值:蒸汽时代比预期更易达到 写在最后 03 我写的是:一个自己会进化的世界,前提是一个能被你反复重放的世界。
今天补上了它的下一句:
一个能被反复重放的世界,还得有值得你重放的理由。
如果每局都是同一张地图、同一个结局、同一批「???」,那么「可复现」就只是一项技术成就,而不是一个好玩的游戏。
所以今天最有价值的产出,不是任何一行代码,而是那张表:
玩法 觉醒 叛神者 通天塔 慈神 0 0 0 暴神 8 18 70% 同一套规则,玩家做了不同的选择,世界给出了完全不同的回应。
这才是我一开始想做的东西——不是「一个会自己运转的模拟」,而是**「一个会因为你的存在而改变的模拟」**。
他们一直不知道你的存在。今天,他们抬头了。#聚光灯gamejam开发者日志











![[2026聚光灯开发日志]Day19 收尾&游戏简单介绍截图](https://img2-tc.tapimg.com/moment/etag/Fj-UxPfAIpdIrNNFyUuLWZxepgWE_20261019211934.png/_tap_ugc_m.jpg)

