下载 App
《变成黑洞》开发日志
日期:2026-10-22 代码规模:13821 行 / 20 个文件(RenderEvolutionGiant.lua 920 → 954)


一、本期主线:把「能跑通」变成「说得清」
上一期把重复实现收敛掉了。这一期暴露的是另一类问题: 代码能跑,但跑出来的东西经不起一句追问。
- 每阶都是 20 口 —— 「碎石和红巨星凭什么一个节奏?」
- 黑洞吃 2000 口只长 4% —— 「那通关是怎么通的?」
- 黑洞明明显示 160 口满,却要 277 口才通关 —— 「那 117 口去哪了?」
三个问题都是同一类:没有实测,只有推理。这期的做法是先把数字测出来,再改。


二、冰巨星极光:把「线」换成「光」
用户原话:「冰巨星的特效那是什么呀,说了不要用线啊。」
查下来是三处描边:
| 位置 | 原实现 | 视觉效果 |
|---|---|---|
| 极光帘幕 | 2 侧 × 7 根 nvgStroke | 14 根硬线,像雷达图 |
| 霜壳包裹 | Core.LightBand | 内部是 5 圈同心圆描边 |
| 残留霜辉 | 同上 | 同上 |
极光帘幕 → 渐隐光幕
真实极光是面状发光体,不是线。改为填充四边形:
lua-- 沿自身方向渐变(青白 → 全透明) local grF = nvgLinearGradient(nvg, fx, fy, ex, ey, nvgRGBA(190, 250, 255, math.floor(fa * 0.95)), nvgRGBA(120, 200, 255, 0)) -- 宽度从根部到顶端收窄到 16% local halfW0 = baseR * (0.030 + fw * 0.038) * (1 - ft * 0.45) local halfW1 = halfW0 * 0.16
从 7 根增到 9 道,填充之间互相叠合,配合 NVG_LIGHTER 自然融成一片。
顺手补的光晕底层
原来极光柱边缘太干净,像贴上去的色块。加一层宽度 2 倍、亮度 26% 的渐变垫底。
霜壳 → Fx.SoftRing
新写的纯填充环形辉光原语(RenderAnimFx.lua:130):
lua-- 内半边 [r-halfW, r]:透明 → 中线 nvgCircle(nvg, cx, cy, r) nvgCircle(nvg, cx, cy, r - halfW) nvgPathWinding(nvg, NVG_HOLE) -- 挖洞 nvgFillPaint(nvg, nvgRadialGradient(...)) -- 外半边 [r, r+halfW]:中线 → 透明,同理
两半环各用放射渐变填充,中线最亮、内外缘透明。 视觉上还是柔光带,但零 nvgStroke。
结果:第 8 阶动画里 nvgStroke 出现次数 = 0。


三、进化口数:从「一刀切 20」到按阶段分级
先测基线
写了个 headless 驱动的测量脚本(追最近可吃目标,模拟会主动觅食的玩家):
第 2 阶 陨石 20 口 6s
第 3 阶 小行星 20 口 5s
...
第 12 阶 黑洞 20 口 4s
合计 220 口 · 45 秒通关问题很清楚:从 12 质量的碎石到 310 半径的红巨星, 量级跨了几十个数量级,节奏却是同一条直线。
新口数表
| 阶 | 进化 | 口数 | 设计意图 |
|---|---|---|---|
| 1 | 碎石→陨石 | 20 | 开局 6 秒内看到第一次进化 |
| 2 | 陨石→小行星 | 27 | |
| 3 | 小行星→原行星 | 34 | |
| 4 | 原行星→矮行星 | 42 | 进入"真天体",节奏开始放缓 |
| 5 | 矮行星→类地 | 50 | |
| 6 | 类地→冰巨星 | 58 | |
| 7 | 冰巨星→气巨 | 66 | |
| 8 | 气巨→恒星 | 74 | |
| 9 | 恒星→红巨星 | 82 | |
| 10 | 红巨星→中子星 | 90 | |
| 11 | 中子星→黑洞 | 95 | 致密星:小而极强,蓄势到位 |
| 12 | 黑洞→通关 | 160 | 终局加倍 |
| 合计 | 798 |
实现上从单值改成查表:
luaEatsPerStageList = { 20, 27, 34, 42, 50, 58, 66, 74, 82, 90, 95, 160 }, EatsPerStage = 20, -- 兜底(表外阶段用) function Config.EatsForStage(stage) -- 越界/缺项时回落
调用点收敛为两处:Game.CheckEvolution(判定)和 main.UpdateHUD(进度条)。


四、黑洞永远打不通
这是本期最严重的问题,而且一直在线上。
发现
原判定只在前 11 阶有效:
luaif idx < #Config.Stages then local need = Collision.Threshold(idx + 1) * 1.0001 if p.mass < need then p.mass = need end -- 补齐到下一阶 ... end
黑洞是最后一阶,idx + 1 不存在 —— 整段什么都不做。 黑洞只能靠吃碎石带来的真实质量增长。
实测 210 秒吃了 2000 口:
| 时间 | 半径 | 质量 |
|---|---|---|
| t=40s | 114.8 | 1.19e+08 |
| t=210s | 119.9 | 1.36e+08 |
2000 口只长了 4%。 按这个速度长到 r=10000 需要 6 小时。 也就是说:玩家打到黑洞,游戏就再也结束不了了。
修复
改为「每吃一口按几何比例膨胀」:
lualocal p01 = eaten / eatsPer local wantR = baseR * (targetR / baseR) ^ p01
从 114.8 平滑撑到 10000,160 口走完。 不用「满 160 口一次性跳到通关线」,是因为那样「吞掉宇宙」会变成一次瞬移 —— 玩家要能看见黑洞一口口撑大。


五、两个让口数失真的 bug
修完第四节,实测仍要 277 口。继续查。
1. 浮点误差卡在通关线上
Collision.Sync 会从质量反算半径:
luap.mass = Collision.MassFromRadius(wantR, p.stage) Collision.Sync(p) -- 内部再算回来
往返换算后 10000 变成 9999.99999, 差一点点够不到 p.r >= worldR * VictoryRadiusRatio 的判定。 黑洞卡在线上反复吃,多花了 108 口。
修复:满口数时直接把半径对齐到目标值(并同步 collisionR)。
2. 一帧吃多口,统计越界
黑洞半径巨大,一帧内会同时接触十几颗碎石, stageEats 一次跨过 160,继续累加到 235、320。
修复:stageEats 超过目标口数即钳为满。
修复前:VICTORY se=320
修复后:VICTORY se=160 r=10000.0

六、食物密度:从「满屏」到「够用」
用户反馈「食物密度太高了要正常一点」。
先测,不猜。写探针统计视野矩形内的天体数:
t=5s inView=136
t=30s inView=124
t=60s inView=199视野里塞了 120~199 块碎石。
调整
| 参数 | 原值 | 新值 |
|---|---|---|
| NaturalMax | 450 | 200 |
| InitialCount | 450 | 200 |
| MaxVisibleMass | 900000 | 400000 |
| SpawnInterval | 0.15s | 0.20s |
补充频率跟着放缓:总量减半后不需要那么勤,约 5 块/秒仍能跟上消耗。
结果
t=5s inView=67
t=30s inView=58
t=60s inView=46稳定在 45~67 块。
副作用(可接受)
进化节奏本身没变(每阶口数逐一核对无差异),但总时长从 106s 涨到 149s —— 碎石稀了,每口之间找目标更慢。这是降低密度的自然结果。


七、本期的做法变化
前几期是「发现问题 → 推理原因 → 改代码」。 这期改成「先测,再改」,因为推理在这三件事上都失效了:
| 问题 | 推理会得出的结论 | 实测真相 |
|---|---|---|
| 黑洞打不通 | 「质量门槛公式有问题」 | 黑洞分支根本不存在 |
| 黑洞要多花口数 | 「几何膨胀公式算错了」 | 浮点误差 + 一帧多口 |
| 食物太密 | 「调小 SpawnInterval」 | 是 NaturalMax 太高 |
测量脚本都留在 .tmp/(evolve_pace.py / bh_t3.py / food_density.py), 后续调数值可以直接复用。


八、验证
- 12 阶口数逐阶核对:20/27/34/42/50/58/66/74/82/90/95/160,与配置一致
- 黑洞通关:se=160 r=10000.0 mode=victory,不再超额
- 视野食物:45~67 块(原 120~199)
- 第 8 阶动画 nvgStroke 出现次数:0
- LSP 全程 0 Error
- 构建通过


九、已知边界
- 黑洞 160 口里,前期每口膨胀幅度小(前 20 口 r 只到 206), 视觉反馈偏弱;后期才开始明显变大。可考虑改成前快后慢的曲线。
- 食物密度降到 200 后,AI 通关要 149s;真人停停看看,估计 6~7 分钟一局。 若嫌长,优先调 SpawnInterval 而不是 NaturalMax(后者影响观感)。
- 手机实机帧率仍未测。








