输入:按下和响应之间那几十毫秒
玩家跑到平台边缘,按下跳 —— 但他晚了 40 毫秒,角色已经离地了,于是那次按键什么也没发生,他直挺挺地掉了下去。他会说什么?他不会说「这游戏没有土狼时间」,他会说「这游戏手感很差」,或者更糟:「我怎么这么菜」。这一章讲的就是替玩家的手误买单这件事 —— 以及它到底值多少个百分点。
人的手是不准的
先接受一个事实:玩家永远按不准。
这不是水平问题,是生理问题。从「我决定要跳」到「手指真的按下去」,中间有神经传导、肌肉收缩、按键行程。这段延迟本身有 100–200 毫秒,而且每次都不一样 —— 抖动量级在 ±50 毫秒上下。
再叠上游戏这边的延迟:输入采样(最多 1 帧)、逻辑处理(1 帧)、渲染(1–2 帧)、显示器(1 帧以上)。60 帧下这就是 50–80 毫秒。
所以「玩家在正确的一帧按下正确的键」这件事,从来不会发生。你的游戏必须为此设计。
在跳跃这件事上,玩家的手误只有两个方向,各有一味解药:
| 场景 | 手误 | 解药 | |
|---|---|---|---|
| 按晚了 | 跑下平台想起跳 | 按下时人已经在空中了 | 土狼时间(Coyote Time) |
| 按早了 | 下落中想一落地就跳 | 按下时还没沾地,这次输入被吃掉 | 输入缓冲(Input Buffer) |
两者是对称的:一个把「地面」的有效期往后延,一个把「按键」的有效期往后延。
它们到底值多少
不空谈。下面这台东西真的在跑物理:每次你动滑块,它就用上一章那台引擎完整模拟几千次起跳和落地,数有多少次成功。
模拟的方式是:假设玩家的按键时机相对「完美时刻」有一个高斯抖动(默认 σ = 60 毫秒,这是普通人的量级),然后统计成功率。
默认设置(σ=60ms)下的结果:
场景 A · 跑下平台边缘想起跳 不开土狼时间:45.3% 开 100 ms :92.9% ← 救回 47.6 个百分点 场景 B · 空中下落想一落地就跳 不开输入缓冲:60.1% 开 120 ms :97.8% ← 救回 37.7 个百分点
没有土狼时间时,玩家在平台边缘的起跳,有超过一半是失败的。
而且是悄无声息地失败 —— 没有报错,没有提示,角色只是径直掉了下去。玩家的大脑会把这解释成「我按晚了」或者「这游戏有问题」,而两种解释都会消耗他对你游戏的耐心。
十行代码,把它变成 92.9%。这大概是整个游戏开发里投入产出比最高的十行代码。
试试把 σ 拖大
把「玩家手抖」拖到 100 毫秒以上(模拟一个不熟练的玩家 / 用手机触屏的玩家)。你会看到不开宽容窗口时成功率进一步崩塌,而开了之后仍然很高。
宽容窗口的价值,随着玩家水平的下降而上升。这意味着:你(这个游戏玩了几百遍的人)是最感觉不到它的人,而你的新玩家是最需要它的人。
土狼时间:怎么写
名字来自动画片里那只跑出悬崖后还能在空中跑几步才掉下去的土狼。
实现是一个计时器:离开地面后的一小段时间内,仍然允许起跳。
const COYOTE_TIME := 0.10 # 秒
var coyote_left: float = 0.0
func _physics_process(delta: float) -> void:
# 关键:在 move_and_slide() 之后判断,此时 is_on_floor() 才是本帧的最新结果
if is_on_floor():
coyote_left = COYOTE_TIME
else:
coyote_left = maxf(0.0, coyote_left - delta)
if Input.is_action_just_pressed("jump") and coyote_left > 0.0:
velocity.y = -JUMP_SPEED
coyote_left = 0.0 # ← 用掉就清零,否则能连跳两次
坑一:忘了用掉就清零。如果起跳后不把 coyote_left 归零,玩家跳起来的那 0.1 秒内还满足 coyote_left > 0,可以再跳一次 —— 白送一个二段跳。这是最常见的 bug。
坑二:土狼时间对「非跳跃的离地」也生效。正常跳跃起跳后立刻清零了,所以没问题。但如果你有别的方式让角色离地(被弹飞、冲刺、被击退),记得也清零,否则玩家能在被打飞的途中凭空跳一下。
坑三:窗口开太大。超过 0.15 秒,玩家会开始感觉到不对劲 —— 角色明明已经掉出去半个身位了还能跳,像在作弊。宽容的上限是「玩家察觉不到」。
输入缓冲:怎么写
对称的思路:记住这次按键,在接下来一小段时间内如果条件满足了就执行。
const BUFFER_TIME := 0.12
var buffer_left: float = 0.0
func _physics_process(delta: float) -> void:
# 1. 按下时,往缓冲里记一笔
if Input.is_action_just_pressed("jump"):
buffer_left = BUFFER_TIME
else:
buffer_left = maxf(0.0, buffer_left - delta)
# 2. 每一帧都检查:缓冲里有没有一次没兑现的跳?现在能跳吗?
if buffer_left > 0.0 and coyote_left > 0.0:
velocity.y = -JUMP_SPEED
buffer_left = 0.0
coyote_left = 0.0
注意第二段:把土狼时间和输入缓冲合在一起判断。这两个机制天然配合 —— 一个管「地面还算不算数」,一个管「按键还算不算数」,两个条件都满足就跳。
没有宽容窗口的写法,逻辑是:「按键这一刻,条件满足吗?」
有宽容窗口的写法,逻辑是:「最近有没有一个还没兑现的意图?现在能兑现了吗?」
后者把输入从一个瞬时事件变成了一个有生命周期的意图。这个思维转变会一路影响你的整个输入系统 —— 攻击、闪避、交互、换弹,全都适用。
缓冲适用于几乎所有动作
输入缓冲不只用在跳跃上。任何「有前置条件、条件很快会满足」的动作都该缓冲:
| 动作 | 前置条件 | 不缓冲的后果 |
|---|---|---|
| 跳跃 | 在地上 | 落地瞬间按的跳被吃掉 |
| 攻击 | 上一次攻击的硬直结束 | 连击必须精确掐点,非常难受 |
| 闪避 / 翻滚 | 不在硬直中 | 危险时刻疯狂按闪避,一次都没生效 |
| 冲刺 | 冷却结束 | 玩家要盯着 CD 条按,注意力被抢走 |
| 下一句对话 | 当前文字显示完 | 点快了会漏掉一句 |
第三行特别值得注意。玩家在紧张时会连按 —— 这是本能。如果你的闪避不缓冲,那么玩家在最需要闪避的时刻(惊慌、连按),成功率反而最低。
还有哪些宽容窗口
土狼时间和输入缓冲是最有名的两个,但同一个思路能生出一整族机制。全部都是「悄悄替玩家买单」:
| 机制 | 做什么 | 玩家的感受 |
|---|---|---|
| 边角宽容 (Corner Correction) | 跳跃时头顶擦到平台边角,把角色推开一点让他过去,而不是撞停 | 「这一跳过去了」(其实差一点没过) |
| 地面吸附 | 下坡时贴住地面,而不是一小段一小段地弹 | 跑起来很顺 |
| 攻击判定放宽 | 命中框比视觉模型稍大 | 「打中了」 |
| 受击判定收窄 | 受伤框比视觉模型稍小 | 「我躲过去了」 |
| 致命一击豁免 | 满血时受到的致命伤害,保留 1 点血 | 「好险!」(其实是被救了) |
| 无敌帧 | 受伤后短暂无敌 | 不会被连续伤害瞬间打空 |
这是平台游戏里第三重要的宽容机制(前两个是土狼和缓冲),但知道的人少得多。
场景:玩家跳起来,头顶差几个像素擦到了上方平台的角。物理上正确的处理是撞停,纵向速度归零,直接掉下去 —— 而玩家的感受是「我明明跳过去了啊?」
解法:检测到这种「只擦到边角一点点」的碰撞时,把角色水平推开几个像素,让他顺利过去。
《马力欧》《蔚蓝》《空洞骑士》全都有。它在物理上是彻底的作弊,但它匹配了玩家的意图 —— 玩家瞄的是那个缝,不是那个角。
攻击判定大、受击判定小
表格里那两行放在一起看,会发现一件有点脸红的事:游戏在系统性地偏袒玩家。
你打敌人时判定框比看起来大,敌人打你时你的受伤框比看起来小。两边都在往「玩家赢」的方向偏。
这不是懒惰或者放水,这是补偿:玩家看到的是屏幕上的 2D 画面,他对距离的判断本来就有误差,而这个误差全部由他承担是不公平的。判定框的偏袒,是在把这个误差还给系统。
宽容的边界在哪
那能不能一直宽容下去?土狼时间给 0.5 秒,判定框大三倍?
不能。有一条清晰的线:
宽容的上限,是玩家察觉不到它存在的那个点。
一旦玩家意识到「哦这游戏在放水」,宽容就从「让我感觉自己很准」变成了「让我感觉这游戏很水」—— 而后者会直接摧毁成就感。
经验值:
- 土狼时间:0.08–0.15 秒。超过 0.2 秒会被察觉。
- 输入缓冲:0.10–0.20 秒。它比土狼时间容忍度高(因为它只是「早按了」,视觉上没有异常)。
- 判定框偏差:不超过角色宽度的 15%。
怎么验证?把窗口调到两倍,自己玩。如果你开始觉得「奇怪」,那就是超了。然后调回一半。
一个例外:不该宽容的时候
有一类游戏刻意不给宽容,而且这是对的:那些把「精确」本身当成核心乐趣的游戏。
节奏游戏就是典型 —— 判定窗口就是玩法本身,把它放宽等于把游戏删掉。同理还有硬核平台跳跃里的某些设计(初代《波斯王子》的固定跳距、《Getting Over It》的完全不宽容)。
判断标准:玩家失败时,他能不能明确知道自己错在哪?
- 节奏游戏:能。屏幕上写着「早了 30ms」。那这个失败是有教育意义的,可以不宽容。
- 平台跳跃:不能。他只知道自己掉下去了。那这个失败只有挫败,没有信息,应该宽容。
关于延迟本身
最后说一下宽容窗口解决不了的那部分:系统延迟。
从按键到画面变化,你的游戏至少要花:
输入采样 0 – 16.7 ms (取决于按键落在帧的哪个位置) 游戏逻辑 16.7 ms (1 帧) 渲染 + 呈现 16.7 – 33 ms (1 – 2 帧,看是否有垂直同步和多缓冲) 显示器响应 5 – 20 ms ──────────────────────────── 合计 约 40 – 85 ms
好消息是玩家对绝对延迟的容忍度比想象中高(80 毫秒以内大多数人感觉不到)。坏消息是他们对延迟的波动极其敏感 —— 忽快忽慢比一直慢难受得多。
- 输入判断放在
_physics_process里,不要放在_process里。物理帧是固定步长的,逻辑一致;渲染帧会抖。 - 但用
Input.is_action_just_pressed()时要小心:它是按渲染帧刷新的。如果物理帧率和渲染帧率不同,可能漏掉或重复触发。稳妥做法是在_input()里捕获事件、置一个标志位,在_physics_process里消费并清除 —— 这本身就是一个最小的输入缓冲。 - 物理帧率:
Engine.physics_ticks_per_second默认 60。动作游戏可以提到 120,手感会明显更跟手(本书那台引擎跑的就是 120)。 - 不要在
_process里做重活。一次卡顿造成的延迟波动,比恒定的高延迟更伤手感。
本章小结
- 玩家永远按不准(生理抖动 ±50ms + 系统延迟 40–85ms)。你的游戏必须为此设计。
- 两个方向的手误,两味解药:按晚了 → 土狼时间;按早了 → 输入缓冲。
- 实测(本页引擎跑 4000 次):土狼时间 45.3% → 92.9%;输入缓冲 60.1% → 97.8%。
- 思维转变:输入不是瞬时事件,是有生命周期的意图。这个转变适用于所有动作,不只跳跃。
- 还有一整族宽容机制:边角宽容、地面吸附、攻击框偏大、受击框偏小、致命一击豁免。
- 宽容的黄金律:上限是玩家察觉不到它。土狼 0.08–0.15s,缓冲 0.10–0.20s。
- 例外:当失败本身带有明确信息时(节奏游戏),不该宽容。
- 卡牌:宽容 = 落位吸附和误触保护。拖到目标附近就吸上去;打出高消耗牌时要求二次确认。「输入缓冲」的等价物:动画播放期间的点击要排队,而不是丢掉 —— 否则玩家会在结算动画时疯狂点击然后漏操作。
- 射击:宽容 = 近失补偿(差一点没打中算打中)、瞄准辅助(准星靠近目标时轻微吸附)、子弹判定球比模型大。手柄射击游戏几乎全部依赖这些,否则手柄根本没法跟鼠标比。
- 塔防 / 策略:宽容 = 可撤销。放错塔能在 2 秒内全额退款;误点的移动能撤销。这类游戏的「手误」代价往往是几分钟的进度,所以撤销的价值远高于任何判定放宽。
- 模拟经营:宽容 = 批量操作的容错(框选时轻微超出边界也算进去)和撤销栈。另外一条常被忽略的:暂停。允许随时暂停思考,本身就是最大的宽容。
- 叙事 / 视觉小说:宽容 = 回看历史(点快了漏掉的能翻回去)和点击缓冲。这两个功能的缺失是这类游戏最常见的差评来源。
- 节奏游戏:刻意不宽容,但要给出精确反馈(早了多少毫秒)。规律仍然成立:失败若带信息,可以不宽容;失败若只有挫败,必须宽容。