下载 App
《变成黑洞》开发日志
日期:2026-10-23 代码规模:14022 行 / 20 个文件


一、本期主线:三个「不应该是这样」的物理
这一期的问题全部来自同一个地方:物理规则是对的,但套错了对象。
- 两块陨石撞一下,变成碎石被吃掉 —— 「同归于尽」变成了「一方吃掉另一方」
- 两块等大的球撞上,双双停在原地 —— 像粘住,不像天体
- 玩家撞成重伤,进度条纹丝不动 —— 掉血和进度是两套独立计量
三次排查都经历同一个过程:先怀疑参数,测完发现是判据用错了维度。


二、撞击降阶 → 被当成食物吞掉
现象
用户反馈:「我是陨石状态全速相向撞另一块陨石,撞了一下他直接成一个陨石状的碎石了,然后触发吞噬直接吃掉了。」
复现
写脚本让两颗等质量陨石(各 6.02e4)以 2250 相对速度对撞:
f=25 阶段变化 A:2->1 B:2->1
质量 6.024e4 -> 5.427e4(各掉 10%)两块同时降阶。
根因:三处 stage <= 1 判据
撞击剥落扣 10% 质量后,Collision.Sync 按质量反推阶段,跌破第 2 阶门槛 → stage = 1。而第 1 阶在物理层被当作「可吃的东西」:
lua-- ① Contact 里的捕食判定 local aMin0 = (a.stage or 1) <= 1 local aPrey0 = (aFood0 or aMin0) and not a.isPlayer -- ② Collect 里的同一套判断 local aMin = (a.stage or 1) <= 1 local aPrey = aFood or aMin -- ③ 双第 1 阶静默跳过 local aMin, bMin = (a.stage or 1) <= 1, (b.stage or 1) <= 1 if aMin and bMin then return end
三处的本意都是「第 1 阶就是碎石」,但它们把因撞击降阶的正常天体也一并划进了碎石行列。
修复
捕食判据统一改用 FoodSupply.IsFood()——只看 isFragment / isDust:
lua-- 只有真正的碎片/尘云才是食物 local aPrey0 = aFood0 local bPrey0 = bFood0
剥落再加一道限制:最多降一阶,不能一路打穿:
lualocal target = max(1, (stage or 1) - 1) local floorM = Collision.Threshold(target) * 1.001 return min(loss, max(0, m - floorM))
⚠ 这里不能直接把损失钳到 0。进化时质量会被补齐到门槛,天体长期贴着门槛,钳到 0 等于「永远无法被撞伤」。
验证
10 种对撞组合(2v2 到 4v8,速度 ×3~×8),全部双方存活并保持原阶。


三、碰撞弹性:从「像台球」改回「像天体」
一个绕远路的过程
修完降阶问题后,我发现两块等质量球对撞会双双停在原地。当时的判断是「缺弹性」,于是加了 BounceRestitution = 0.35:
等质量对撞 → ±280 弹开,间距从 0 拉到 1355用户立刻指出方向错了:
「角色之间的相撞哪那么容易被弹开啊,大部分都化成了热量和爆炸对不对。」
确实错了。 真实天体撞击的恢复系数 e 在 0.1~0.3,而且尺度越大越低(星球级 e≈0)。绝大部分动能变成热、塑性变形、碎裂,不是把两块球弹开。
修复
Impact 和 Contact 的所有冲量调用统一用 e = 0:
lualocal lost = Impulse(a,b,nx,ny,0, ...) local launch = removed > 0 and lost * 0.2 or 0 -- 20% 给碎屑 ShareHeat(a,b,lost-launch) -- 80% 给热量
删掉了 BounceRestitution 配置、bounceRestitution 变量、Impact 的 canFuse 参数。
验证
初始动能 4.879e10
撞后动能 2.962e5 ← 剩 0.0006%
热量 3.518e10 ← 动能几乎全转成热
保留率 72.1% ← 其余变成剥落碎屑带走的能量反思
加弹性系数是为了解决「撞上后粘在原地」,但根因不在缺弹性。真正的原因在别处,加系数只是把症状盖住了。这是本期最大的弯路。


四、撞伤不掉血:一个 stage <= 1 又中招
现象
「玩家发生碰撞进度条没损失啊」
实测
玩家第 1 阶撞上恒星:
f=40 质量 1.901e4(变化 0.0%) 进度 10/20(不变)
f=80 质量 1.901e4(变化 0.0%) 进度 10/20(不变)
f=... 玩家死亡质量一点没掉,人就死了。
根因(第一个)
还是 stage <= 1:
lua-- 本意:碎石不可再碎出更小的碎片 if (a.stage or 1) <= 1 then da = 0 end if (b.stage or 1) <= 1 then db = 0 end
玩家开局就是第 1 阶,于是在第 1 阶完全免疫撞击。撞上恒星零损失,然后被吞。
改为只对碎片生效:
luaif a.isFragment or a.isDust then da = 0 end
根因(第二个)
进度条读的是 stageEats(本阶吃了几口),撞伤扣的是 mass。两套独立计量,永不联动。
加了一条从物理层到 UI 的通路:
lua-- CosmicPhysics:把本步损失比例传出去 local playerLossFrac = da / ma ctx.OnImpact(..., playerLossFrac) -- Game / OnImpact:折算成扣几口 local lost = math.max(1, playerLossFrac * eatsPer) local maxLostThisHit = math.max(1, math.floor(cur * 0.5)) lost = math.min(lost, maxLostThisHit) S.stageEats = math.max(0, cur - lost)
为什么单次最多扣一半
第一版没有上限,实测撞恒星(掉 18% 质量)直接把 20 格进度一次清零。
质量账本是真实的,但进度条是「离下一阶还差多久」的感知。一次撞击抹平所有努力,玩家会觉得「白玩了」。
验证
撞前:质量 1.201e4,进度 7/20 = 35%
撞后:质量 9846(-18%),进度 4/20 = 20%

五、本期新增:一键升阶调试按钮
调试进化动画要一阶一阶吃,太慢。加了个按钮(HUD 右下角):
luafunction Game.DebugEvolveOneStage() S.stageEats = Config.EatsForStage(idx) return true end
关键:只填满口数,不直接改阶段。 下一帧由 CheckEvolution 完成真正的升级,这样外观、动画、无敌时间、阶段判定全部和正常进化一致。
验证:12 次点击从碎石升到黑洞并通关。


六、食物密度与刷新点位(未完成)
这一块花了很多时间,但结论是没做完,如实记录。
用户确定的方向
「全场均匀分布加上视野之外刷」
已改
- 开局:全场面积均匀(r = √u × R),不再围着玩家
- 补给:rejection sampling,落在视野矩形外接圆之外
达到的效果
- 视野内 5 块(目标达成)
- 95% 的补给落在视野外
卡住的地方
视野半径 4133 世界单位,玩家速度 260~500/秒,穿过整个视野要 10~16 秒。视野内只有 5 块时,找目标太慢,全程走不完。
这个矛盾没有解决。
顺带修的真 bug
| Bug | 影响 |
|---|---|
| MassLimit 把「玩家质量×2」用在自然碎石上 | 后期 NaturalMax 完全失效(改成 45/70/100,全场都卡在 53 块) |
| Fits 用「全部食物质量」判断自然碎石 | 后期一次大碰撞产生上亿质量碎片 → 自然补给永久停止(实测 295 次后断供) |
| batch 只按缺口算 | 存量满时每轮只补 1 块,追不上消耗 |
第二条尤其隐蔽:补给停了,但 Fits 从未报错,因为挡住它的是「总质量」而不是「自然碎石质量」。现在自然碎石有独立的 naturalMass 预算。


七、方法上的收获
这一期三次排查都验证了同一件事:
「物理公式正确」≠「用对了地方」。
| 问题 | 表面现象 | 真实原因 |
|---|---|---|
| 撞一下被吃掉 | 吞噬逻辑有问题 | 捕食用 stage 判断,降阶天体被误判 |
| 撞成重伤不掉血 | 撞伤公式失效 | 同一个 stage <= 1 又挡在前面 |
| 两球撞上粘住 | 缺少弹性 | (错误结论) |
第二个和第一个是同一个 bug 的两个面:改第一处时没意识到还有第二处用了同样的判据。
教训:改判据时,全文件搜一遍同样的写法。


八、验证
- 10 种对撞组合:全部双方存活、保持原阶
- 5 分钟正常游玩:0 次降阶、0 次死亡、无崩溃,稳定升到第 6 阶
- 等质量对撞:动能 99.999% 转成热,不弹开
- 撞伤:质量 -18%,进度 35% → 20%
- 一键升阶:12 次点击通关
- 冒烟测试通过
- LSP 全程 0 Error


九、已知边界
- 食物密度与刷新速度的矛盾未解决:视野内 5 块 + 视野外刷新,玩家找目标太慢。需要调整视野大小、玩家速度,或接受更密的分布。
- 撞伤单次最多扣一半进度,是游戏性取舍,不是物理结果。
- presentedStage 是模块级状态,直接改 p.stage 会被下一帧钉回原值(调试时注意)。
- 手机实机帧率仍未测。






