下载 App
《刀光剑影》开发者日志
给游戏配上武侠 BGM(音效试听后撤掉) 本期进展
- 之前完全没有声音
查了一遍:项目里没有任何音频资源和音频代码,连 SoundSource 都没出现过。素材声明里也写着"作品无背景音乐与音效"。这期补上。
- 生成 3 首武侠 BGM
用 AI 音乐模型生成,纯器乐(古筝/琵琶/二胡/战鼓),无人声:
文件 用途 时长 bgm_menu.ogg 菜单、结算 约 3.5 分钟 bgm_battle.ogg 普通战斗 约 2.5 分钟 bgm_boss.ogg 首领决战 约 3.2 分钟 3. 音效做了一版,试听后按需求全部撤掉
最初还生成了 12 个音效(出刀、命中、击杀、受伤、拾取、升级、点击、首领降临等)并接到各个事件上。用户试听后反馈"把所有音效都去掉",于是整体移除:删掉 assets/audio/sfx/ 目录、清掉 4 个脚本里的全部 Audio.Play 调用、AudioManager 也精简成只管 BGM。
教训:音效比 BGM 更容易"吵"。BGM 是铺底,音量低也听得见;音效是离散的,一旦密集触发(比如剑气扫到一群怪、连续拾取剑意)很容易糊成噪音。这次虽然做了每帧预算和节流,但整体听感仍不理想,最终选择不要。
- 新增 audio/AudioManager.lua
只管 BGM,关键设计:
BGM 淡入淡出切换:不同曲目先淡出旧的到静音,再起新的,避免两首叠在一起。同名不重启(currentBgm_ == name 直接返回),所以"每帧请求切换同一首"不会打断播放。 pitch 用绝对频率:这是音频 API 的坑——Play(sound, frequency) 收的是绝对 Hz 不是倍率,直接传 0.95 会被当成 0.95 Hz(一秒的音频拉长到十几小时,等于没声音)。所以需要变调时必须写成 sound:GetFrequency() * pitch。 5. BGM 跟随战况自动切换
菜单、结算 → 江湖远(菜单曲) 普通战斗 → 剑意长歌(战斗曲) 首领在场 → 霜帝降临(决战曲) 切换序列实测正确(每幕:战斗 → 首领 → 战斗),16 次切换全部命中预期。
- BGM"听不见 / 断断续续"——一个被误判两次的状态机 bug
这个 bug 修了两次,第一次完全找错了方向,值得完整记下来。
第一次(错误诊断):用户反馈"BGM 太轻听不见"。我看到增益是 0.55,判断是"数值设小了",直接拉到满增益 1.0。
第二次(真正原因):用户又反馈"BGM 卡得断断续续"。这次才去认真读状态机:
lua
复制 -- 错误版本 if bgmGain_ > 0.001 then -- 只要增益非零就进这个分支 bgmGain_ = math.max(0.0, bgmGain_ - FADE_SPEED * dt) -- 一直递减 bgmSource_.gain = bgmGain_ * BGM_GAIN return -- 永远走不到下面的淡入 end ... if currentBgm_ and bgmGain_ < targetGain_ then -- 淡入分支:够不到 bgmGain_ = math.min(targetGain_, bgmGain_ + FADE_SPEED * dt) end 这个条件只看 bgmGain_ 的大小,没有看方向。 追踪单帧就清楚了:
帧 走哪个分支 结果 1 淡入(上帧归零了) gain = 0.023 2 第一段(>0.001)→ 递减 gain = 0.000 3 淡入 gain = 0.023 4 第一段 → 递减 gain = 0.000 增益在 0 和 0.023 之间每帧来回跳——所以既"极轻"又"断续"。第一次反馈的"听不见"和第二次反馈的"断断续续"是同一个 bug 的两个症状,我上次没有排查到状态机,只是把增益值从 0.55 改成 1.0,掩盖不了它。
修复:用目标值判断方向,而不是用当前值大小。
lua
复制 if targetGain_ <= 0.0 then -- 目标=静音 → 淡出 -- 递减到 0,然后起播 pending 或停止 else -- 目标=播放 → 淡入 -- 递增到目标 end 用等价逻辑跑数值模拟确认:
修复前 修复后 稳态增益 0.023(且每帧抖动) 1.000 切歌行为 无法淡入 淡出→起播→淡入,约 0.7 秒 同时把 BGM_GAIN 从误设的 1.0 调回 0.85——修好状态机后 0.85 已有足够响度,也不用担心压过其他声音。
加了回归断言:Audio.GetStats().bgmGain > 0.5(播放 1 秒后)。我特意把错误版本注回去验证过——断言确实会失败并报"BGM增益未升到稳态(疑每帧被归零)",确认这条断言真能拦住这个 bug。
教训:"音量太小"不要先怀疑数值,先怀疑它有没有升上去。 一个每帧被归零的增益,无论默认值设多大都听不见。这次教训是第一次排查太急,把状态机 bug 误当成参数问题。
问题与风险 ① 音效撤掉后,"打击感"只剩视觉。 出刀、命中、击杀现在都没有声音,靠刀光、命中环、粒子、屏幕震动来表达。是否够"爽"需要真人确认。
② 无头验证听不到声音。 自动化测试只能确认"增益升到稳态、加载无缺失、切换序列正确"(实测 0 次音频缺失、299 项断言通过),不能验证实际听感。
③ 三首 BGM 的响度可能不一致。 三首是分别生成的,没有做统一响度归一化,可能出现"菜单曲正常、战斗曲偏轻"。若确实如此,应离线做响度对齐,而不是给每首配不同的代码增益。
④ 已删除的音效资源不在版本控制里。 12 个音效文件已从磁盘删除,如需恢复要重新生成。
- BGM 改为全程一首、独立循环
接着上面的 bug,用户进一步要求"BGM 不受任何影响,自己在游戏内循环"。原来的设计是跟随战况切歌:
状态 曲目 菜单 / 结算 江湖远 普通战斗 剑意长歌 首领在场 霜帝降临 这个设计的问题:任何状态抖动都会触发切歌。首领刷新、升级暂停、结算弹出、重开——每幕至少要切 2 次,切歌过程本身又是一次淡出淡入(约 1.4 秒听感变化)。配合上面那个增益归零的 bug,就表现为"一直断断续续"。
改法是把切换彻底去掉:
进游戏播一首 bgm_battle,looped = true 之后完全独立循环 main.lua 删掉每帧按 Run.GetState() / Director.GetBoss() 切歌的那整段 六幕、菜单、首领、升级、结算、重开——全程同一首,不切换 AudioManager 删掉 pendingBgm_ / targetGain_ / 淡出逻辑 / StopBGM,只剩"起播 + 开场淡入" 另外加了保活:如果因为设备切换、引擎重置等外部原因让播放停止,Update 检测到 not playing 会自动原地续播,不需要调用方干预。
设计取舍:放弃"战斗/首领不同配乐"的氛围层次,换"绝不中断"的可靠性。 一首循环曲在任何状态下都稳;多曲切换只要有一个状态判断出错就会切坏。用户明确要的是后者。
验证方式:集成测试跑完六幕全流程(菜单→战斗→首领→升级→结算→重开),断言 bgm == "bgm_battle" 且增益未掉。日志确认 BGM 全程只起播一次,没有第二次切换。
- “任何操作都打断音乐”——保活逻辑自己制造的 bug
改完第 7 点后,用户反馈"移动任何操作都会打断音乐"。查下来是我上一轮加的保活逻辑自己造成的:
lua
复制 -- 上一轮加的"保活",已删除 if not bgmSource_.playing and currentSound_ then bgmSource_:Play(currentSound_, 0, bgmGain_ * BGM_GAIN) -- Play 会从头开始 end 原意是"万一播放被外部打断就自动续上",但忽略了一件事:Play() 不是"继续",而是"从头开始"。而 SoundSource.playing 这个属性在主线程卡顿时会瞬时抖动为 false——
移动、出刀、弹窗都会造成帧尖峰 帧尖峰时 playing 抖动 → 保活立刻 Play() → 音乐跳回开头 表现就是"操作一下音乐就断一下" 这解释了为什么是"任何操作都打断"——不是某个操作的问题,是任何造成帧波动的事情都会触发。
修法:删掉保活,Update 里只保留开场淡入,绝不做任何播放控制。循环交给 Sound.looped,Play() 全项目只出现在 PlayBGM 一处,且同名直接 return。
顺带加了音频缓冲:audio:SetMode(100ms, ...) 加大混音缓冲,缓解帧尖峰导致的欠载断音。注意这个 API 必须在音频设备就绪后调用(未就绪时 mixRate=0,会返回 false),失败就沿用默认缓冲。
加了回归断言:Audio.GetStats().playCount == 1。playCount 是起播计数器,只要 Update 里再被塞进任何续播/重播逻辑,走完全流程后它就会大于 1,断言立刻失败。这条断言专门锁死"绝不重播"这个约束。
教训:"保活"这类兜底逻辑,要先确认兜底动作的语义。 Play 是"重播"不是"续播",一个想当然的兜底反而制造了新 bug——而且它比我原本要防的问题更频繁。
- 难度太低:“一刀一片”——伤害成长跑赢了怪物血量
用户反馈"难度不高啊一刀一片"。算了一下确实是:
幕 普通怪 HP 玩家伤害(仅自然成长) 一刀/怪 1 34 20 0.59 2 42 38 0.90 3 52 56 1.08 ← 从这里开始一刀一个 4 64 74 1.16 5 78 92 1.18 6 95 110 1.16 根因是两条成长线叠加:升级每级 +3 伤害、换幕再 +6,而怪物 HP 只从 34 涨到 95。第 3 幕起玩家伤害就超过了怪的血量,之后全程一刀一个。再加上强化乘算(磨刃 ×1.18 可叠 5 层 ≈ ×2.3),溢出更严重。
改法(两条线一起调):
改前 改后 普通怪 HP 34 / 42 / 52 / 64 / 78 / 95 70 / 95 / 125 / 160 / 200 / 250 升级伤害 +3 / 级 +2 / 级 换幕伤害 +6 +4 改后的手感:
只靠自然成长 叠 3 层磨刃后 普通怪 3.1~3.6 刀 1.9~2.2 刀 首领 25~54 刀 — 这样保留了"越打越强"的成长感(叠满强化能压到 2 刀),但不会出现一刀一片——强化选择重新变得有意义,因为不选强化就砍得吃力。
顺手修了 3 条脆断言:测试里写死了 damage==23、damage>40、damage>=strength+16、maxHp==34。这些数值一调就崩,改成趋势断言(damage>20、damage>strength、maxHp==Stages.Get(1).hp)。以后调平衡不会再因为测试写死数值而失败。
下期计划 真机试听 BGM 响度是否合适 真机试玩新难度曲线,重点看第 5、6 幕是否偏难 若仍偏轻:对素材做响度归一化(而非继续改代码增益) 把 assets/image/devlog/ 从构建打包范围排除




![《刀光剑影》开发者日志
第一期:让刀光"是画出来的",而不是贴上去的
本期进展
1. 核心玩法跑通了
玩家在屏幕上画一笔,这一笔的形状直接变成一道飞出去的刀光。画横线出横劈,画弧线出月牙,画爱心就出爱心刀光。笔画越长刀波越大,画得越快伤害越高。
技术上走的是「屏幕笔画 → 归一化形状 → 程序化几何体」这条路:把玩家的采样点降采样、居中、按最大跨度归一化到 [-0.5, 0.5],再沿着轨迹生成带截图](https://img2-tc.tapimg.com/moment/etag/FvfPjVihDzWqTvVHoCG78xT5dKzo_20261009184001.png/_tap_ugc_m.jpg)