下载 App
【《上帝的球》开发者日志 #03】—— 光被墙卡住了 249 帧
10/1551 浏览综合 包含 AI 合成内容
日期:2026.10.14 | 开发第 3 天 | 距投稿截止还有 6 天
昨天那篇末尾我列了三条待办:补多语言、跑真机、加引导。今天一件都没做。
今天一整天都在跟光较劲。起因很小——我自己随手在球里摆了两面镜子想让一束光来回弹,结果光粘在玻璃上了,一动不动。我盯着看了半分钟,然后意识到:这不是摆法的问题,是我写的光子逻辑从根上就错了。
这篇只讲一件事:把一个元素修到"我敢说它是对的"。
先说结论
光子这一块,我数出来四个独立的毛病,一个比一个隐蔽:
| # | 症状 | 我最后查到的地方 |
|---|---|---|
| 1 | 光撞到玻璃就被"吸"住,永远停在玻璃表面 | 穿透明介质时只改了坐标变量,没真的把粒子挪过去 |
| 2 | 光飞出球的边界后不消失,变成一颗永远停在网格边缘的幽灵 | 销毁条件是"寿命 > 0 才递减",而我在边界上把寿命直接设成了 0 |
| 3 | 光撞墙反射一次,寿命就短一截 | 反射分支里顺手砍了寿命,纯属我写的时候想当然 |
| 4 | 贴着球壁的曲面飞,会在同一个格子里原地反射到天荒地老 | 对角方向走不通时没有退路,整条光卡在楼梯口 |
第 4 个我是用"最长原地停留帧数"这个指标抓出来的。修之前,同一个场景里最长的一次停留是 249 帧——八秒多,一束光贴在球壁上抖了八秒。
修完是 2 帧。
一个一个说
① 玻璃反而成了唯一能粘住光的材质。
这条最讽刺。我把"透明"理解成"光能穿过去",于是写了:光碰到玻璃就把折射后的新方向算出来,赋值给循环里的坐标变量,然后接着往下走。
问题是那只是三个局部变量。粒子对象本身还在原来的格子里。下一次移动拿着一个错误的原点去做交换,把同一个粒子对象写进了两个格子——33 个格子下标指向同一个粒子。数组一坏,后面的演化结果全部不可信。
修法:穿透明介质时老老实实走一次真正的位移。另外在移动函数最底下加了一道安全阀——if (格子里的对象 !== 我要移动的对象) 直接放弃这次移动。宁可少动一格,也绝不把数组写坏。
② 幽灵光子。
飞出边界的处理,我原来的写法是"把寿命设成 0,让它自然消亡"。但粒子引擎的销毁条件是"寿命大于 0 时才每帧递减",寿命已经是 0 的粒子永远不递减、永远不回收。于是它变成一颗停在网格最边上、不吃不喝也不动的光子,你还能看见它。
改成飞出边界直接回收——不啰嗦。
③ 反射不该扣寿命。
这条纯粹是我偷懒。撞墙反射的时候我顺手把寿命截短了,写的时候想的是"光撞墙总得损耗点吧"。但这是错的:镜子反射的光不会因为反射而从世界上消失得更快。光飞多久,只该由它自己的寿命决定。
④ 249 帧——曲面上的楼梯口。
这条最有意思,也花了我最久。
球的边界在整数网格上不是一个光滑的圆,是一圈楼梯。一束光斜着撞上楼梯的拐角时,它想去的那个对角格恰好是墙壁,于是原地反射;反射后的方向又指向另一个对角格,还是墙,再反射回来……
我原来的反射法线只有一种取法:从球心指向外。在楼梯拐角上这个法线会把光又推回墙里,配合整数坐标就成了死锁。
修法是给移动加"退路":对角走不通,就先试试只走 x 那一轴;还走不通,再试只走 y 那一轴。光于是不撞死在拐角上,而是沿着墙滑开。
这个思路其实很朴素——现实里你贴着墙走路,撞到东西也不会原地弹回来,你会侧一下身绕过去。我把它写进了移动里。
修完之后我单独试了一颗贴着壁飞的光子:300 帧里它飞了 92 格,一路顺着球壁滑过去,没有一次停顿。
顺手修掉的三个别的东西
光修光了,但既然已经在这儿了,就把我先前记录下的几个"看着不对劲"的地方一起处理了:
植物和水在一起,水瞬间就没了。 原来是"相邻即反应",一帧就把整片水推成氧气。改成按接触面积缓慢消耗——植物慢慢把周围的水分解掉,变成氧气冒出去,水面是慢慢下去的,不是一刀切。
氧气和二氧化碳直接往底下坠。 这个是物理上就不对:气体凭什么被重力像石头一样拽到球底?改成上浮、扩散、往上走。现在球里的气体是分层的,重的在下面、轻的在上面。
火柴人会滑到固体下方,甚至穿出球外。 补了两道约束:与固体的碰撞、以及球壁的硬边界。火柴人现在站在沙面上,不会整个人钻进地里。
然后是那一整晚的测试
我不敢只靠肉眼。写了一个专门盯光子的检查脚本,23 条断言:穿透、折射、全内反射、撞墙、出界回收、以及"不许原地卡住"。
这里踩了一个我自己的坑,值得写下来:
"原地卡住"这个指标我一开始写错了。
我的第一版是这样统计的:看某一个格子下标连续多少帧是光子。跑出来一堆误报——一个明明在正常飞行的光束被判成"卡住"。
原因是我忘了自己写的东西:**光子沿直线每帧走 3 格,整排光点的格子下标集合根本不变。**第 0 帧占了 3、6、9…,第 1 帧还占 3、6、9…,因为整排一起平移了一格。集合不变,但那是在飞。
改成按粒子对象的身份追踪——谁是谁,这才是"同一个东西停了多久"。改完误报清零。
同样的错我还犯了一次:另一个探针只比较 x 坐标,结果把"在沙柱里竖直弹跳"的光子判成原地卡住(它 x 没变,其实在上下飞)。改成比较完整坐标之后,各元素的停滞帧数全部归零。
这两件事让我确认了一件事:测出来的"异常",先怀疑度量,再怀疑代码。
到凌晨的结果:
- 光子检查 23 / 23 全过
- 全元素冒烟(166 种 × 多场景)全过,1500 帧混战单帧 30.12 ms
- 生态长跑:四组场景,最长的一组跑满 6000 帧没崩,另外两组分别在 96.3 秒和 56.4 秒走到终点
- 火柴人碰撞、设备旋转、DOM/输入集成 —— 全过
今天的一个片段
这段片子是用游戏本身一帧一帧渲出来的(900 帧、1080×1920、30 fps),过程中撞出来的坑比写游戏还密:
- 一格只能放一颗光子。想画一条实心光束,只在一个位置每帧投一颗是不够的——光子每帧前进 3 格,光点间距正好等于步长,整条光带变成一把梳子。得沿光束方向错开 1 格同时投三颗,三组相位首尾相接才接得上。我一开始还自作聪明地"并排投三行",结果只加粗不填缝,白忙。
- 斜着飞的一串光子会互相顶住。所以光束必须是轴对齐的。为了让"屏幕上的水平"在世界坐标里也是水平,我得先把球的自转倒回 0。
- **两条光束绝不能交叉。**交叉点上双方各顶一下,1 格间距的光带会被连锁推到整条僵死。
- **Math.sin(Math.PI) 不是精确的 0。**一个 1e-16 的正数会让"竖直光束"被判成斜向——因为方向判断写的是 vy > 0 ? 1 : vy < 0 ? -1 : 0。我现在会在投光之前把 1e-15 量级的残差掐成 0。
- 自转的单位是弧度/帧,不是角度。我一开始误当成角度,填了 0.16,等于 9°/帧,几百帧就把球里的东西搅成一锅均匀的糊。另外粒子会滞后于球体的转动:实测 0.0064 rad/帧还能保住沙堆,再快整团被搅散。片子最后定在 0.0012。
问题与风险
- **真机还是没跑过。**这是第一天就欠的债,今天第三天,还欠着。这是我现在最大的风险。
- 光子的斜向飞行仍然不可靠:单颗没问题,一串就会互顶。我这次的绕法是"只用轴对齐光路",也就是说游戏里用激光器随便斜着打一束光,很可能还是糊的。这是下一件必须解决的事。
- 生态平衡偏紧:四组长跑里有两组会走到终点(96.3 秒 / 56.4 秒)。不是崩,是耗尽了。这是设计问题,不是 bug,但得调。
- 审计证明的是"物理正确",证明不了"好玩"。手感只能人来判断。
下期计划
- 把斜向光束彻底修掉——这是今天绕过去的,不是我解决的。要查的是"每帧一步、粒子在扫描过程中被重复更新"这条路径。
- 真机跑一遍(安卓 + iOS),重点验触摸和声音解锁。
最后说个事
今天最爽的一刻,不是那 23 条断言全绿。
是我把"对角走不通就退而走单轴"那三行加进去、重新跑那个 249 帧停滞的场景——2 帧。
然后我让那颗光子贴着球壁飞。它顺着那圈楼梯滑下去,一格一格,一路滑到对面。屏幕上就是一条弧线,干干净净。
一束光贴着一个圆的内壁走,这件事在数学上没什么了不起。但那一瞬间我脑子里想的是另一件事:这个球是我写的。
它里面的每一条规矩——光会拐弯、会弹回来、会在墙上滑一下——都是我一条条定下来、又一条条验过的。所以那条弧线不是"看起来像",它就是。
另外,官方要求开发期内至少更 5 篇日志,这是第 3 篇。AI 使用说明我会在投稿时一并附上:哪些是它写的、哪些是我改的、哪些是我推翻它自己重写的,都会写清楚。












