下载 App
【《上帝的球》开发者日志 #02】—— 有声音了,然后修了bug
修改于10/149 浏览综合 包含 AI 合成内容
日期:2026.10.14 | 开发第 2 天 | 距投稿截止还有 7 天
昨天那篇末尾我写了两件事:加声音,扩充多语言。声音今天做完了。但今天真正吃掉时间的不是声音,是测试——我本来只想"跑一遍看看会不会崩",结果一路挖到底,最后把引擎拆开验了一整夜。这篇分两半,前半是声音,后半是那些 bug。
声音:一条主题曲 + 九个音效
先说结果:assets/bgm/theme.mp3弗拉门戈吉他+ 9 个 UI 音效——6 个不同的"哒"、翻页的 next / prev、关闭的 cancel。
音频引擎走 WebAudio(src/sound.js),两条路是分开走的,因为需求完全相反:
- BGM 用 <audio> + MediaElementSource,不解码。整首 5.3 分钟解成 PCM 大概要 120 MB,手机上一首主题曲就吃这么多内存纯属找死。音量交给 GainNode,淡入 0.8 秒,切设置 0.08 秒就跟上。
- 音效反过来,用 fetch + decodeAudioData 解成 BufferSource。音效只有 4~6 KB,解完常驻内存,点一下 start(0) 就行,躲开 <audio> 那套"同一个元素不能并发播放"的老问题——连点两下按钮,第二声必须能出。
设置页里音乐和音效各一个开关 + 0~100% 滑块,改完立刻生效,跟物理分项共用同一套持久化。
有个细节我自己挺满意:点击音效是 6 选 1 轮着抽,抽完一轮把顺序打乱再抽下一轮。如果每次点击都是同一个"哒",两分钟就会烦;但完全随机又会出现连续三声一模一样。轮抽 + 每轮洗牌是这两件事的折中,听起来像有人在随手敲桌子,不像机器。
(图 1:设置页的声音分组,两行开关 + 滑块)
三个坑,都是浏览器给的
- 不出声。 浏览器要求首次用户手势之后才允许播放音频,所以 AudioContext 不能一开始就 new,得延迟到第一次 pointerdown 才建,建完再补播 BGM。
- 点一下响两声。 音效委托一开始挂在 click 上,结果设置页里 <label> 包着 <input type=checkbox> 的结构会把 click 转发两次。改成 pointerdown 就干净了。
- 切后台还在响。 补了 visibilitychange:切走 suspend,切回来 resume。
这些都是那种"写完听着挺好,一上真机就露馅"的东西。所以下面这句话我要原样留着:声音我只在电脑上验证过,手机浏览器(尤其 iOS Safari)的自动播放策略更严,明天真机必须复验。
然后是我没打算花时间,但花了一整夜的部分
昨天我说"球里塞了 168 种元素"。说出来很爽,但我心里清楚一件事:我验证过的元素,一只手数得过来。 剩下那一百多种,我只能保证"放上去不会崩"。
所以今天写了 .tools/element-audit.mjs:166 种可放置元素 × 4 个场景——A 静置 / B 加热 / C 通电 / D 极寒,每种跑 200 帧,问一个很朴素的问题:放上去之后,它还在吗?它变了吗?它变成的东西对吗?
首轮跑完,11 项异常。修到 166/166 全过。中间挖出来的东西,一个比一个离谱。
bug 1:一个粒子,同时待在 33 个格子里
排查"生命游戏结果和参考实现对不上"的时候发现的:grid.cells 里有 33 个下标指向同一个 Particle 对象。
根因在光子上——光子穿过玻璃时,代码只改了循环里的坐标变量 x = nx; y = ny,没有真的把粒子挪过去。下一次 tryMove 于是拿着错误的原点做交换,把同一个对象写进了两个格子。
后果比听起来严重得多:粒子数组一坏,"清空"就清不干净、粒子数统计虚高、之后所有元素的演化结果全部被污染。也就是说,我昨天看到的那些漂亮的涌现现象,有一部分可能是假的。
修法两条:玻璃折射分支改成先 tryMove 再推进(光子确实走进玻璃格);Simulator.tryMove 加一道安全阀——g.cells[ia] !== p 时直接放弃这次移动。宁可少动一格,也绝不把数组写坏。
bug 2:温度变成 NaN,然后一帧一帧传遍全场
审计报出火柴人(STKM / STK2 / FIGH)字段为 NaN,而且通电场景里 NaN 粒子数从 1 涨到 7——它在扩散。
给 temp 装 setter 抓首次写入栈,落在热传导那一行:next[i] = cur + (sum / (w0+w1+w2+w3) - cur) * RATE[t] * hk。这三个元素的 heatConduct === 0(全项目只有它们三个是 0),四个权重全是 0,sum / 0 = 0/0 = NaN。
温度一旦 NaN,邻居在下一次热传导里读到它 → 邻居也 NaN → 一格一格蔓延,最后整个球都是 NaN,什么都不再反应。
修:den > 0 ? ... : cur(绝热就保持当前温度),落盘前再兜一层 Number.isFinite,任何非有限温度不许写进粒子——从源头掐断传染链。
bug 3:器件自己导电,所以它永远等不到被通电的那一刻
这个是今天最讽刺的一个。所有器件的说明都写着"通电时……",触发条件是"相邻有电火花"。但这些器件自己 conductive: true,于是那颗本该待在导线里的火花会拐进器件体内,器件变成火花本体,onUpdate 根本跑不到。更糟的是火花会倒灌回电池,把电池卡成火花 110 帧(火花寿命),整条电路长时间瘫痪。
对照实验,同一场景只切换 conductive:
| 器件 | 导电=是 | 导电=否 |
|---|---|---|
| TESC 特斯拉线圈 | ✗ 没反应 | ✓ 生效 |
| WIFI 无线链路 | ✗ 没反应 | ✓ 生效 |
| CONV 转换器 | ✗ 没反应 | ✓ 生效 |
| BRAY 光点 / PRAY 激光 / DRAY 复制射线 | ✗ 没反应 | ✓ 生效 |
| BTRY 电池 | 电流倒灌,电路瘫痪 | ✓ 生效 |
原则定了:触发式器件不导电,让那颗火花留在旁边。 11 个器件改成 conductive: false;真正该导电的留下来:导线、PSCN / NSCN 半导体、ETRD、绝缘线、开关、热敏电阻、金属。
光"不崩"不够,还得保证"按说明书干活"
前三轮审计只能证明"这个元素有反应",证明不了"它干的是说明书上写的那件事"。于是又加了几层:
- 元检查(结构层):每个场景跑完扫一遍不变量——粒子数组重复引用、粒子跑到球壁外、字段 NaN、温度失控、空气场(风速/压强/环境温度)NaN 或爆表。上面那个 33 处重复引用,就是这一层抓出来的,行为层永远测不到。
- E 邻物场景:依次贴着水 / 火 / 酸各放一团跑 20 帧——很多反应分支只在有特定邻居时才走得到,静置永远测不到。
- F 混场长跑:元素 + 金属板 + 水 + 墙混在一起跑 200 帧,抓"慢热"的传染型问题。
- 模糊测试:随机挑 1~5 种元素、随机位置/半径/温度、随机开关 7 个物理项、随机球转角,跑 400 次。带种子,失败能原样复现。
- 器件契约测试:给每个器件搭最小实验台,30 条断言验它自己的说明书。
到 03:05 的最终结果:
- 全元素审计 166 / 166,0 异常(200 帧,207 秒)
- 器件契约 30 / 30 通过
- 模糊测试 400 次 0 失败(含边转边跑)
- DOM/渲染/输入集成测试、球径与旋转反解测试 —— 全过
(图 2:审计工具的控制台输出,最后一行 166/166)
顺手干的两件事
改了默认球径:240px → 100px。 昨天那个"满球 7000 多粒 13.5 毫秒"是 240px 下的数字。今天做了各档基准(满球混战 300 帧):
| 球径 | 格数 | 单帧 | 折算 FPS |
|---|---|---|---|
| 100px | 15000 | 3.56 ms | 281 |
| 140px | 29400 | 5.89 ms | 170 |
| 180px | 48600 | 10.34 ms | 97 |
| 240px | 86400 | 15.23 ms | 66 |
240px 在电脑上能跑,低端机上大概率不行。默认降到 100px(快 4.28 倍),240px 留作设置里的可选项。先保证能玩,再谈画面。
验了旋转后的重力方向。 球能自转,但"渲染转 θ、世界重力预转 −θ"这对抵消到底对不对,之前只是我觉得对。设 6 个转角(0/37/90/154/233/300°)各撒一把沙子跑 25 帧,把世界位移按渲染那一步的旋转换算到屏幕:横向偏移均 < 4%(沙子自由堆积的正常离散),纵向 46~49 格。屏幕上沙子永远垂直往下落——对的。
今天的一个片段
(图 3:10 秒演示 .docs/devlog-02-elements.mp4——球体边滚边演:水冲沙、火把水烧开、冰砸下来化成水把火浇灭,全程配主题曲。)
问题与风险
- 真机一次都没跑过。 这是昨天就欠着的债,今天又欠了一天。明天必须补。
- 声音只在桌面验证过,移动端自动播放策略更严,可能要补一次解锁交互。
- 多语言只覆盖了元素名和描述,UI 文案目前只有中 / 繁 / 英三语。
- 审计证明的是"物理正确",证明不了"好玩"。手感这件事只能人来判断。
下期计划
- 加入更多语言:先把 UI 文案的三语补齐,再往日语、韩语扩;语言切换做成热切换,不用刷新。
- 真机跑一遍(安卓 + iOS),重点验触摸和声音解锁。
- 补一个"玩法引导":现在新玩家进来看见一个空球,不知道该干什么。[这游戏谁在意引导啊喂]
最后说个事
今天修完那 33 处重复引用,我重跑了一遍昨天那个场景——沙子倒进水里,堆出一个带弧度的坡。
它还在。弧度一模一样。
那一刻我比第一次看到它的时候更踏实一点:昨天我以为那是涌现,今天我知道那真的是涌现——因为我把底下每一块砖都敲过一遍了。
另外,官方要求开发期内至少更 5 篇日志,这是第 2 篇。AI 使用说明我会在投稿时一并附上:哪些是它写的、哪些是我改的、哪些是我推翻它自己重写的,都会写清楚。
/












