默认参数下的跳跃我量了 12 组。重力 2400、起跳初速 900 时,最高点实测 161.3 px,理论值 168.8 px,差 4.40%。滞空实测 0.733 秒,理论 0.750 秒,差 2.22%。
同一个脚本玩家跑 25 局,障碍间距保持默认的 280~520 px,中位只有 848 米。把间距改成 700~1200 px,中位直接摸到 3000 米的测试上限,25 局里 21 局一次没死。
劝退还是耐玩,不由障碍数量决定,由时间窗决定。间距写死在像素上,速度却随米数一直涨,两个障碍之间的可用时间一直在缩短。
在线案例与源码:https://atomgit.com/deli007/demo_park/tree/main/codearts-endless-runner-game
页面是一个 3.7 万字符的单文件,参数面板、物理解算、自检全在里面跑,不发任何网络请求。
一、为什么值得搞清楚:间距是像素,难度是时间
障碍间距在页面里是一个区间,默认 280 到 520 px,每次随机取。
速度是另一条式子:速度 = 起步速度 + 每米增速 × 米数。默认 350 起步,每米加 0.35。
两条式子放在一起,就得到一件反直觉的事:间距不动,时间窗一直在缩。
我把三档间距在不同速度下的时间窗算了一遍,单位毫秒。
| 当前速度 | 对应米数(默认增速) | 280 px 间距 | 400 px 间距 | 520 px 间距 |
|---|---|---|---|---|
| 350 px/s | 0 米 | 800 ms | 1143 ms | 1486 ms |
| 500 px/s | 429 米 | 560 ms | 800 ms | 1040 ms |
| 700 px/s | 1000 米 | 400 ms | 571 ms | 743 ms |
| 1000 px/s | 1857 米 | 280 ms | 400 ms | 520 ms |
| 1350 px/s | 2857 米 | 207 ms | 296 ms | 385 ms |
| 1600 px/s | 3571 米 | 175 ms | 250 ms | 325 ms |
再放一把尺子:默认参数下起跳一次,人要离开地面 0.733 秒。
两个数一摆就清楚了。1000 米处的 280 px 间距只剩 400 毫秒,这 400 毫秒里还夹着一整次跳跃。
二、准备环境:在码道 Web 里把要求写死
浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwsf&sourcead=dmzntgwwbwz,登录后就能在对话窗口输入需求,不需要装软件。
码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。
需求我写得比较死,四块内容直接钉进去。
- 参数面板:重力、跳跃初速、基础速度、每米增速、障碍最小间距、障碍最大间距,六个输入框都能现场改
- 物理面板:最大跳跃高度与滞空时间,实测值和理论值并排,附相对误差
- 自检面板:至少 20 条断言,把结果逐条渲染到页面上
- 三种障碍:绿色矮箱直接跳过,蓝色中箱要算起跳时机,红白高梁必须滑铲钻过去
最值钱的是第三条。自检和游戏共用同一套公式,页面上量出来的数字才算数。
三、12 组跳跃实测:离散积分比理论低 2% 到 8%
我把重力和初速拆成 12 组,每组都走游戏自己的起跳与逐帧积分,再和解析公式对一遍。
| 重力 | 初速 | 最高点实测 | 理论 v²/2g | 误差 | 滞空实测 | 理论 2v/g | 误差 |
|---|---|---|---|---|---|---|---|
| 2400 | 900 | 161.3 px | 168.75 px | 4.40% | 0.7333 s | 0.7500 s | 2.22% |
| 2400 | 700 | 96.3 px | 102.08 px | 5.63% | 0.5667 s | 0.5833 s | 2.86% |
| 2400 | 1200 | 290.0 px | 300.00 px | 3.33% | 0.9833 s | 1.0000 s | 1.67% |
| 2400 | 500 | 48.0 px | 52.08 px | 7.84% | 0.4000 s | 0.4167 s | 4.00% |
| 2400 | 1600 | 520.0 px | 533.33 px | 2.50% | 1.3167 s | 1.3333 s | 1.25% |
| 1600 | 900 | 245.7 px | 253.13 px | 2.95% | 1.1167 s | 1.1250 s | 0.74% |
| 3200 | 900 | 119.1 px | 126.56 px | 5.89% | 0.5500 s | 0.5625 s | 2.22% |
| 1200 | 900 | 330.0 px | 337.50 px | 2.22% | 1.4833 s | 1.5000 s | 1.11% |
| 3600 | 900 | 105.0 px | 112.50 px | 6.67% | 0.4833 s | 0.5000 s | 3.33% |
| 1000 | 600 | 175.0 px | 180.00 px | 2.78% | 1.1833 s | 1.2000 s | 1.39% |
| 5000 | 1400 | 184.4 px | 196.00 px | 5.90% | 0.5500 s | 0.5600 s | 1.79% |
| 4000 | 1000 | 116.7 px | 125.00 px | 6.67% | 0.4833 s | 0.5000 s | 3.33% |
12 组的实测高度全部低于理论值,滞空也一样。差值来自积分方式:每帧先加速度、再走位移,属于半隐式欧拉,永远摸不到解析上界。
初速越大、重力越小,误差越小,反方向就放大。初速 1600 那一组误差只有 2.50%,重力 4000 那一组是 6.67%。
四、快速下落:把 0.733 秒的滞空压到 0.2 秒
空中按 ↓,下落速度会被直接顶到 1400 px/s。
我在起跳后的第 N 帧按下 ↓,试了 7 组,记录这一跳的最高点和总滞空。
| 按 ↓ 的时机 | 最高点 | 总滞空 | 落地用了几帧 |
|---|---|---|---|
| 不按(基准) | 161.3 px | 0.7333 s | 44 |
| 第 0 帧 | 0.0 px | 0.0167 s | 1 |
| 第 2 帧 | 28.0 px | 0.0667 s | 4 |
| 第 4 帧 | 53.3 px | 0.1167 s | 7 |
| 第 8 帧 | 96.0 px | 0.2000 s | 12 |
| 第 14 帧 | 140.0 px | 0.3333 s | 20 |
| 第 20 帧 | 160.0 px | 0.4500 s | 27 |
| 第 30 帧 | 161.3 px | 0.6000 s | 36 |
这张表解释了「跳完接着钻高梁」为什么成立:跳一个矮障碍只要 0.05 秒左右的滞空,落地后还剩大半秒可以滑铲。
第 14 帧那行是分水岭。等到从最高点开始下落再按 ↓,滞空只压到 0.333 秒,省不了多少。想省时间就得早按,代价是高度,矮障碍够用,中箱可能就撞上去。
五、跑满 1000 米要多久:模拟与解析差 0.2% 以内
我把 12 组速度参数各跑一遍:逐帧累加算模拟耗时,公式算解析耗时。
| 起步速度 | 每米增速 | 1000 米处速度 | 模拟耗时 | 解析耗时 | 误差 |
|---|---|---|---|---|---|
| 350 | 0 | 350 px/s | 142.867 s | 142.857 s | 0.0067% |
| 350 | 0.35 | 700 px/s | 99.033 s | 99.021 s | 0.0124% |
| 350 | 1 | 1350 px/s | 67.517 s | 67.496 s | 0.0301% |
| 350 | 2 | 2350 px/s | 47.633 s | 47.606 s | 0.0575% |
| 200 | 0.35 | 550 px/s | 144.533 s | 144.514 s | 0.0131% |
| 500 | 0.35 | 850 px/s | 75.817 s | 75.804 s | 0.0167% |
| 700 | 0.35 | 1050 px/s | 57.933 s | 57.924 s | 0.0168% |
| 1000 | 0.35 | 1350 px/s | 42.883 s | 42.872 s | 0.0262% |
| 1500 | 0.35 | 1850 px/s | 29.967 s | 29.960 s | 0.0220% |
| 350 | 5 | 5350 px/s | 27.300 s | 27.269 s | 0.1130% |
| 350 | 10 | 10350 px/s | 16.967 s | 16.934 s | 0.1927% |
| 600 | 1.5 | 2100 px/s | 41.783 s | 41.759 s | 0.0588% |
12 组误差全部小于 0.2%,并且随增速单调变大。增速越大,最后一帧的余量占比越高。
默认参数跑满 1000 米要 99 秒。想压成一局 30 秒的短跑,把每米增速调到 5 就行。
六、障碍间距:三档区间各生成 2000 次
| 间距区间 | 实测最小 | 实测最大 | 均值 | 越界次数 | 去重后的取值 |
|---|---|---|---|---|---|
| 280 ~ 520 px | 280.127 px | 519.967 px | 398.46 px | 0 | 1994 |
| 400 ~ 800 px | 400.138 px | 799.663 px | 596.87 px | 0 | 1997 |
| 700 ~ 1200 px | 700.196 px | 1199.869 px | 947.62 px | 0 | 1995 |
每档 2000 次采样,越界 0 次,去重后仍有近 2000 个不同取值,说明随机是均匀铺开的,不是少数几个档位。
生成函数的写法很好懂:间距 = 最小间距 + 随机数 × 区间宽度。真正需要防的是另一头。
最小间距填得比最大间距大时,面板会拒绝这次输入,并保留上一个有效值。
两个提示的原文是「minGap(1200) 不能大于当前 maxGap(520),已保留上一有效值 280」和「abc 不是合法数字,已保留上一有效值 2400」。
七、脚本玩家 25 局:默认参数中位 848 米
我写了一个脚本玩家,规则只有三条:遇到矮箱和中箱先算弧线,算得出就在撞上之前起跳;遇到高梁滑铲;空中发现落地来不及就按快速下落。
四档增速配三档间距,每组各跑 25 局。
| 每米增速 | 间距 280~520 | 间距 400~800 | 间距 700~1200 |
|---|---|---|---|
| 0 | 3000 米(25/25 触顶) | 3000 米(25/25 触顶) | 3000 米(25/25 触顶) |
| 0.35 | 848 米(661~1012) | 1667 米(1392~2030) | 3000 米(21/25 触顶,最低 2759) |
| 1 | 346 米(233~475) | 671 米(485~873) | 1262 米(1044~1441) |
| 2 | 205 米(159~322) | 374 米(285~528) | 669 米(562~775) |
括号里是中位之外的最小值与最大值。
增速为 0 的三组全部跑满测试上限,一局没死。默认增速下间距放宽一档,中位从 848 米涨到 1667 米,再放宽一档就触顶了。
把每米增速从 0.35 提到 2,其他参数不动,中位从 848 米掉到 205 米。
死因统计更直白:默认那组 25 局里有 21 局,撞击发生在人还在空中的瞬间,也就是「还没落地,后一个障碍已经追上来」。
八、两障碍简化扫描:最小可过间距随速度涨
我把场景压到极简:只有两个障碍,中间固定一个间距,速度恒定。
| 恒定速度 | 矮箱接高梁 | 中箱接高梁 |
|---|---|---|
| 350 px/s | 80 px | 80 px |
| 500 px/s | 80 px | 80 px |
| 700 px/s | 80 px | 100 px |
| 900 px/s | 120 px | 120 px |
| 1200 px/s | 200 px | 200 px |
| 1600 px/s | 240 px | 260 px |
350 和 500 那两档,80 px 就能过。两个障碍几乎贴在一起时,脚本玩家靠「跳过第一个、立刻按快速下落」硬挤过去,第四节那张表就是它的底气。
1200 px/s 之后不行了。矮箱接高梁的最小可过间距涨到 200 px,1600 px/s 时要 240 到 260 px。
再对照第七节的默认间距下限 280 px:到 1600 px/s 附近,随机出来的最短间距已经贴住这个下限,过不过得去开始靠运气。
九、验证与踩坑
页面自检在加载时跑完,26 条全过。我把这 26 条逐条读过,也在上面补了几轮真跑。
第一处坑是我自己挖的。脚本玩家第一版把「已经越过」判断错了,只比了障碍的左边缘和玩家身体前缘,结果人还压在障碍上就触发了快速下落,一头撞在矮箱顶上。
那批失败很好认:死因全是「空中撞上矮箱」,而不是高梁。改成比对碰撞盒后缘之后,同样的参数下中位从 443 米涨到 848 米。
第二处坑在页面自检自己身上。第 1 条断言要求实测高度与理论值的相对误差小于 6%,可离散积分的误差本来就随重力和初速变化。
我把重力从 100 一路扫到 10000,每 100 一组:从 3300 开始误差就过 6%,这一条断言会自己判失败。
面板允许重力填到 10000。也就是说,读者完全可以在合法范围内调出一组参数,让页面自己判自己不合格。
第三处是个设计判断,不算缺陷。碰撞盒比看起来小一圈,站立时是 34 × 62 px,滑铲时压到 34 × 28 px,高梁下面的净空是 60 px。
宽容度给得偏大,钻梁实际上比看上去轻松。如果你想让它更难,把净空改成 40 px 就够了。
十、本地复现:两条命令加一段控制台脚本
把页面存成 index.html,起一个静态服务器就能跑。
python-m http.server 8000# 浏览器打开 http://127.0.0.1:8000/index.html想自己复现第三节的 12 组跳跃,把下面这段粘进浏览器控制台。
functionprobe(g,v0){params.gravity=g;params.jumpVelocity=v0;resetGame();obstacles.length=0;lastRight=1e9;lastType=null;state='running';player.y=GROUND-STAND_H;player.h=STAND_H;player.vy=0;player.onGround=true;startJump(player);lett=0,maxH=0;while(!player.onGround&&t<3){step(1/60);t+=1/60;maxH=Math.max(maxH,GROUND-(player.y+player.h));}constth=theoJump();return{g,v0,maxH:+maxH.toFixed(2),th:+th.h.toFixed(2),err:+((Math.abs(maxH-th.h)/th.h)*100).toFixed(2)};}[2400,3200,4000].forEach(g=>console.log(probe(g,900)));这些函数跑在页面脚本的顶层作用域,控制台里可以直接调,量到的就是我表格里那些数。
十一、直接下载试玩
在线案例与源码都在这里:https://atomgit.com/deli007/demo_park/tree/main/codearts-endless-runner-game
目录里有单文件 index.html 和 5 张截图。双击 index.html 就能玩,改右边那六个输入框,物理面板和游戏行为会立刻跟着变。
十二、使用码道的体会
需求里写清楚「面板 + 自检渲染到页面」,一轮对话就能拿到能自证的产物。这次我只提了一轮需求,生成的页面就带着 26 条断言和两块实测面板。
用中文写物理量比写画面有用。把重力、初速、滞空、碰撞盒这些词直接写进需求,出来的代码里就能找到对应的常量和函数。
它给的默认参数偏激进。每米 +0.35 的增速看着温和,实测到 1000 米处时间窗只剩 400 毫秒。这个结论是我自己跑出来才敢下的。
十三、小结与下一步
三句话收尾。
跳跃的实测值永远低于解析上界,2% 到 8% 的差额是积分方式的固有代价,不是写错了。
0.733 秒的滞空是这游戏难度的基准尺。任何间距和速度的组合,都要按「时间窗够不够落地」来反推。
按实测数据,默认的 280 px 最小间距在 1600 px/s 附近就贴住极限了。
下一步我会做三件事:把默认最小间距从 280 提到 400;给高梁单独拉长与上一个障碍的距离;把自检第 1 条的固定百分比阈值改成按帧长归一化。