下载 App
《气味之城》开发日志4 亲眼看着系统自己长出来的东西,一共六件
修改于10/2015 浏览综合
这几天我一直在做一件很无聊的事:什么都不干,盯着街区看。
因为这是一篇"涌现"主题的游戏,如果我只贴自己设计的东西,那就等于什么都没证明。所以这篇我想反过来写:我只写我没设计的东西。 六件事,每件都按同一个格式拆给你——我做了什么、代码里哪几行在算、街区最后变成了什么样。
如果你想自己复现,文末有命令。
① 橙色撞上橙色
我做了什么: 我在一块墙角连按空格蹭地盘,那块地在气味视觉里变成了亮橙色。十分钟后,一只路过的猫走进了那片橙色。
底层怎么算: 领地信息素沉积时会顺便记下"这是谁留下的"(scripts/pheromone_grid.gd:deposit()),读取靠 owner_at()(同文件 :109-113)。每只猫每 0.4 秒做一次决策,第一件事就是查脚下格子的归属(scripts/cat_base.gd:_think() 开头的领地冲突检测):归属者不是自己,且(双方好感 < -15 或 这只猫胆量 > 0.65 且掷出 12% 的骰子)——才进入对峙,同时往脚下写一圈危险气味,冷却 40 秒。
街区最后变成什么样: 事件日志里多了一行"×× 撞见 ×× 的地盘,压低身子对峙",配一声压得很低的低吼。两秒两秒后如果还在对峙状态机里,就进入结算。
写这段的时候我又确认了一遍:代码里没有任何一行叫"两猫开战"。 全部条件就是上面那三个判断。
② 有时候它不打
我做了什么: 什么都不做。看着两只猫对峙,一方是块头最大的大橘(胆量 0.9),一方是最小的煤球(胆量 0.2)。
底层怎么算: 结算的时候先算"是不是欺软"(scripts/cat_base.gd:_resolve_confront() 里那句 absf(boldness - other.boldness) > 0.25):两只猫的胆量差 > 0.25,就不动手。胆小的一方安全值 -30,转身进逃跑状态跑出 260 像素(lose_confront());胆大的一方安全值只 -10,继续待在原地。
街区最后变成什么样: 日志写的是"煤球压低耳朵退让了,大橘抖了抖毛守住地盘"。
这条规则是上期日志里说的"调参三处收紧"里加的。改之前,同样的 soak 测试里猫群 3 个游戏日打了 71 场架;加上它之后是 17 场,其中 10 场被这条退让规则消化掉了。打架从背景噪音变回了事件。README.md 里记着这两个数。
(口径:Godot 4.7 无头模式跑 3 个游戏日,统计事件日志里"打了一架"的行数;命令见文末。这一对数字在 README.md 和 docs/TASK-BOARD.md 里都有记录。)
③ 迁徙不是事件,是余烬熄灭的样子
我做了什么: 什么都不做,只看那只清洁工走到垃圾桶旁边。
底层怎么算: 三段拼起来:
- 垃圾桶每 3 秒按自己剩余的饭量往外散一次食物气味,量 = 0.15 × 存量 + 0.1(scripts/food_source.gd:103-110);
- 清洁工每隔 70 秒随机挑一个还有剩饭的垃圾桶,清空它,并在原地写一圈危险气味(scripts/human_npc.gd:74-81 与 :88-93);
- 正在赶路的猫并不认识"清洁工"这个概念,它只是每帧算一次周围 8 个格子的浓度,往上坡走(scripts/pheromone_grid.gd:gradient() + scripts/cat_base.gd:_move() 的 SEEK_FOOD 分支)。
街区最后变成什么样: 垃圾桶一空,食物气味开始衰减,"上坡"没了,走在半路的猫在原地转圈;等气味彻底散掉,它就放弃目标,散开去别处找方向。
再往另一半看,投喂阿姨是 45 秒一趟(scripts/human_npc.gd:69-74),到了投喂点补 5 份饭,并散一大圈友好气味(:83-87)。所以你会看到饭来猫聚、饭走猫散——这段迁徙在任何一行代码里都不存在,它只是两股气味此消彼长。
④ 老鼠带来的生态位分化
我做了什么: 什么都不做,就数谁抓到过老鼠。
底层怎么算: 猫饿了超过 72、胆量不低于 0.4、掷 30% 的骰子、并且 240 像素内正好有老鼠,才会去追(scripts/cat_base.gd:_hunt_check());贴到 22 像素内就算抓住,饥饿 -55(_move() 里 State.HUNT 的捕获分支);抓住后 18 秒补种一只(scripts/main.gd:catch_mouse())。
街区最后变成什么样: 胆量 0.2 的煤球几乎抓不到,胆量 0.9 的大橘常年蹲在鼠的路上。同一件事——抓老鼠——在不同猫身上长出了完全不同的行为模式。没人分工,是性格参数自己分出来的。
⑤ 你被记住了
我做了什么: 我叼着食物(Q 键)走了一路,一份一份分给路边的猫。
底层怎么算: 一份食物让对方饥饿 -40、对你的好感 +26(scripts/cat_base.gd:on_player_action() 的 "food" 分支)。好感过 30 这只猫就进入"信任玩家"状态。回头看它找谁社交:社交需求过 68 且合群度高于 0.4 才会主动找人(scripts/cat_base.gd:_think() 的 State.WANDER 分支),而它挑人的时候只挑已经有好感的对象,对玩家是按好感 ×1.5 打分、打分要高于 5 分才上门(scripts/main.gd:nearest_friend())。
街区最后变成什么样: 日志开始出现"×× 主动过来蹭你,给你舔了舔手"。而声望是十只猫对你好感值的平均值(scripts/main.gd:_update_hud()),跨过 40 就是「猫王候选」,全街播报、给你戴皇冠、播那段猫王 BGM(同函数后半段)。
反过来也成立,而且我试过:G 键哈气会在身边写一圈危险气味,130 像素内的猫安全感直接 -28、好感 -8、全部逃跑(scripts/player_cat.gd:_try_hiss() + scripts/cat_base.gd:on_player_action() 的 "hiss" 分支)。哈几天,你的声望会一路掉到「孤僻的独行者」。
结论很土,但很真:猫不认人,只认你做过的事。
⑥ 我碰翻了一只花盆
我做了什么: 按 C。
底层怎么算: 55 像素内有没被碰倒的花盆就碰倒它,换上倒花盆贴图,在原地写一圈危险气味,并让 110 像素内的猫安全值 -12(scripts/player_cat.gd:_try_knock() + scripts/flower_pot.gd:knock())。
街区最后变成什么样: 安全值掉到 30 以下就会切换到躲藏状态,目标是最近的纸箱(scripts/cat_base.gd:_think() 的 safety < 30.0 与 _enter(State.HIDE),纸箱查询在 scripts/main.gd:nearest_shelter())。所以你看到的是一排猫同时往纸箱里钻。
一只花盆,搅动了半径 110 像素内的所有猫——然后什么都没留下,花盆不会复原到原位(knocked 是单向的),但街区记得刚才那一瞬间。


顺带说说:声望是怎么算的
顺手交代一下,HUD 上那个称号不是表演,是十只猫的好感平均值过了三道线(scripts/main.gd:_update_hud() 里的 var title := "孤僻的独行者" 往下三行):
| 称号 | 条件 |
|---|---|
| 孤僻的独行者 | 平均好感 < 0 |
| 脸熟的流浪猫 | ≥ 0 |
| 街区红猫 | ≥ 15 |
| 猫王候选 | ≥ 40(触发全街加冕) |
旁边的领地格数是实时数你名下有多少个格子(scripts/main.gd:_update_hud())——那也是涌现出来的,你蹭多少次,它就多多少。
想自己复现的话
游戏自带开发用启动参数,命令行加在 -- 之后:
bash"D:/godot/4.7/Godot_v4.7-stable_win64_console.exe" --headless --path . --quit-after 7200 -- --evlog --fast
- --evlog:涌现事件同步打印到控制台(scripts/event_log.gd 的 verbose,由 _ready() 读命令行参数打开);
- --fast:游戏时间 10 倍速,用于 soak;
- 一个游戏日 = 240 秒(scripts/main.gd 的 day_len)。
打开它,你就和我看到的是同一条街。
最后,帮我个忙
在评论区告诉我:这六件事里,哪一件是你自己跑出来过的?
我特别想知道——如果你实测出来的现象和这里写的不一样,那才是最有价值的一条。把你看到的原样发上来(包括截图和事件日志),点赞最高的那条我会写进 docs/GDD.md 的规则清单和游戏里的行为。
还有个小问题:如果只能加一个新行为进游戏,你希望是什么? 上期到现在,社区给我的答案我已经攒了一堆,但还没塞进代码——因为我想先把上面这六件事讲完。
下期预告:安卓双端适配踩坑。这是我最不想写、但必须写的一篇——一个人把游戏送上手机屏幕,中间卡了多少步。写完这篇,上传就在倒计时里了。
#聚光灯gamejam #涌现 #猫咪游戏 #独立开发 #游戏开发 #独立游戏#聚光灯gamejam开发者日志











