Lesson 01 · 为什么角色会“听话”?
先亲手跳一次,再直接看刚才那次操作的回放:你什么时候离开平台、什么时候按下跳跃、为什么跳成功或失败。这条时间轴记录的就是上面小游戏的真实操作,不需要额外设置。
① 手感实验:先玩一关
📱 手机操作:在游戏画面上水平拖动并按住移动,轻点画面跳跃;移动时可用第二根手指点按跳跃。松开停止。横屏玩能看得更清楚。
电脑操作:A/D 或方向键移动 · 空格/W/↑ 跳 · R 重置。目标:从左侧平台跳到右侧平台。
刚才这一跳 · 自动复盘
直接连接上面小游戏。每次按跳跃,会用游戏实际记录的时刻更新这里;不需要选实验、调整毫秒或手动播放。
先在上方小游戏按一次跳跃;成功、未生效或跌落后,这里会自动说明原因。
提示:在平台上直接跳、走出边缘后补跳、落地前抢先按键,得到的复盘结论会不一样。
查看这一跳的精确毫秒数据
操作任务
- 先直接按一次跳跃,看复盘是否显示「普通起跳」。
- 走到左侧平台边缘,刚踩空后再按一次跳跃,看是否显示「离地后补跳」。
- 关掉“土狼时间”,再试一次;这次如果失败,复盘会解释为什么。
- 跳向右侧平台,在落地前提前轻按一次,观察它是被缓存后执行,还是过期丢失。
可选进阶:独立参数模拟器(不是刚才的操作记录)
只有想自己指定“离地后第几毫秒按键”时才需要展开。默认折叠,不影响上方真正的跳跃复盘。
模型边界:这是可确定性重演的课堂场景,沿用原版两个窗口的数值;不是对原游戏完整运动、硬直和长按输入的逐帧仿真。
可选进阶:Deepnight 的多动作输入队列实验
自己制造、消费和清理输入事件
这里是另一个独立实验,专门研究原版 ControllerQueue:Jump、Dash、Shoot、600ms 懒过期与事件顺序。不属于上方小游戏的操作回放。
① 先 Dash 后 Jump,直接消费 Jump 会怎样?② 改用“忽略顺序消费 Jump”,Dash 还在吗?③ 在锁定期间按 Jump,500ms 后解锁再消费,结果如何?
这里是单独编写的可测试队列模型,重现原版主要事件语义。输入锁模拟的是 Hero.controlsLocked();不是输入设备本身失活。真实 ControllerQueue 还支持 Release 查询、长按、清队列与锁到松开等更多行为。
③ 对照源码与性能审计
观察完再阅读同目录 LESSON.md 解析文档,里面有作者 Haxe 源文件的具体位置、示意时序图与本课的验收题。
体验完成后展开:Deepnight 在 Haxe 里怎么实现容错?
在他原版的 Hero.hx 的 frameUpdate() 中,落地时给玩家保留了短暂的跳跃资格:
if( onGround && options.justInTimeJump )
cd.setS("allowJitJump",0.15);
// 发起正常跳跃的判定条件:
if( ( onGround || cd.has("allowJitJump") ) && ... )
注意原版这里是 0.15 秒,本课演示用的是 0.12 秒。上面的省略号是便于课堂阅读的缩写,完整判断请打开源文件。
输入缓冲则沿着 ControllerQueue、checkControlPress()、consumePress() 追踪。你会发现“什么时候记录意图”和“什么时候准许动作”是两种职责。
进阶源码:Deepnight 的输入缓存为何不只是一个 Boolean?
源代码在 ControllerQueue.hx。Hero 为 Jump、Shoot、Dash 各注册一个动作监听器,每帧采样时只记录按下、松开两种边沿事件的时间戳。
// 核心逻辑摘取与简化,不是原文件连续代码
if (!wasDown && ca.isDown(action))
presses.push(curTimeS);
// 保存输入最长 0.6 秒;需要的时候才检查过期
while (presses.length > 0 &&
curTimeS - presses[0] > 0.6)
presses.shift();
在玩家控制被短暂锁定时,输入依然可以先存入队列。等可以行动,再用 consumePress() 取走一次按下事件;consumePressOrDown() 还支持持续按住的动作。
原版为 600ms;本页面游戏的输入缓存仅为 120ms。 原版默认还会考虑不同按键之间的事件先后顺序,所以严格说这是多动作事件队列,而不是“只记住一次跳跃”。
性能观察:这里用“时间戳 + 查询时清理”做了懒过期;但每帧仍要采样被监听的动作,消费时可能扫描其他动作队列,且 Array.shift() 不是严格 O(1)。
架构审计:输入队列有什么成本?什么情况值得简化?
只遍历监听的动作,Hero 默认三个:Jump / Shoot / Dash。粗略复杂度 O(W),W 是监测的动作数。
保存 Press / Release 的时间戳,在查询、新事件等时机清理旧记录;不是逐帧给每条记录减剩余时间。
默认查询还检查其他动作的先后顺序,使用 Array.shift() 删除旧事件,因此不能把整个消费操作当成严格 O(1)。
如果只用一个 Jump,并只保存最后一次短按,一个 pressedAt 时间戳通常够用。连招、不同动作顺序和松开事件才体现完整队列的优势。
设计结论:和上一部分讨论过的 Cooldown 不一样,输入缓存已经用了时间戳及懒过期。值得再评估的是:600ms 太长会不会误触发?跨动作排序真的符合玩法吗?
一课三问:检验是否真的理解
直接点答案查看解释,不上传数据、不记录成绩。