打击停顿不该用秒来计时
手感在代码层就是三件事:谁在计时、冻在哪一帧、什么排在什么之后。附一份我自己用的 hitstop 写法。
上一篇写手感是一堆有参数的功能。这篇写它们在代码里长什么样 —— 因为我看过的多数"手感丢失"bug,最后都不是设计问题,是计时问题。
别把动画和秒绑在一起
Jan Willem Nijman(《Fistful of Momentum》那篇随笔)有条我觉得最朴素也最容易违反的规则:
如果动画是 10 帧、播放时长 0.5 秒,那么所有和它绑定的特效、逻辑、声音,都应该依赖这个动画本身,而不是依赖秒。
违反的典型症状我在自己代码里见过,Martin Jonasson 和 Petri Purho 的《Juice it or lose it》(2012)里也描述过同一个坑:并行的两套计时器一旦不同步就会鬼畜 —— 你的粒子计时器写 1 秒,动画实际 0.95 秒,几次循环之后闪光就开始在动作之前的帧上炸开,玩家看到的是"闪错了地方"。
我现在把这两类时钟明确分开:game_time(受 hitstop / slow-mo 影响,驱动世界和动画)和 ui_time(永不受影响,驱动菜单、HUD、结算动画)。慢动作把敌人拖成 0.25 倍速时,UI 绝不能跟着慢 —— 这条一旦混了就很难拆回来。
打击停顿:一个数字,两种实现
hitstop(受击/命中瞬间把世界钉住几帧)是格斗和动作游戏里最常用的"让打击有分量"的手法。可用的数字:
- Nijman 给的通用起点是 100ms。
- Michael Lutz 的量化更贴近帧:60~90ms、大约 10 帧,"按接触强度取大小",而且他明确警告一次只能停一个对象 —— 同时冻多个对象会把帧时间吃掉,产生卡顿感。
真正的坑是实现方式。最省事的写法是让角色空转一个"暂停计数":
// 常见写法:命中时 HitStop = 0.08f
void Update() {
if (hitStop > 0f) { hitStop -= Time.unscaledDeltaTime; return; } // ← 动画也停了
animator.Update(Time.deltaTime);
}
它的问题很隐蔽:动画被冻在了那一帧上。如果这一帧不是接触帧,玩家看到的就是"拳头停在脸前面 80ms" —— 停顿反而破坏了它想强调的那一下。
正确的写法是让暂停只作用在位移和逻辑上,而把动画快进到接触帧:
void Hit(string contactAnim, float seconds) {
animator.Play(contactAnim); // 直接切到接触那一帧
animator.Update(0f); // 冻住它,但姿态是对的
hitStopRemaining = seconds; // 只对位移/逻辑生效
Scheduling.Ease(() => hitStopRemaining = 0f, seconds, Ease.OutQuad);
}
要点:hitstop 期间位移速度为零,但精灵仍然被渲染 —— 玩家看到的是"卡在一个正确的姿势上",这才叫打击感。
冻结帧的演化:为什么现在更流行"把怪打飞 + 慢动作"
早期做法是命中瞬间整块屏幕停住。现在更多游戏改成:生成一个受击对象的副本、把它按击退向量甩出去、同时全场慢动作,视觉冲击一样强,但不打断玩家的连续输入。Juice it or lose it 把这个转变讲成"juice 的代际差异"。
下面这组是我自己项目里在用的起点值(不是我从某篇文章抄来的,是我调完之后固定下来的默认):
| 手法 | 我的起点值 |
|---|---|
| 慢动作 | 全局 timeScale 0.2~0.5,持续 0.05~0.35s,结束前缓动回 1.0 |
| 死亡表现 | 不冻结整屏,改为"副本被击退 + 慢动作" |
| 精灵闪光 | 白色覆盖,衰减别短于 0.2s,否则像一次不自然的眨眼(这条 Karunaratne 与 Lutz 给的是同一量级) |
受击位移的规格也是同一类数字:把 sprite 沿击退方向偏移约 8 像素、200ms 缓出衰减(Karunaratne 的综述里给的就是这个量级)。还有两条是顺序相关的:
- 挤压变形的峰值要落在接触那一帧,不是接触之后 —— squash & stretch 是"强调接触时刻"的手法,不是"接触后的反应"。竖直挤压不超过 30%,否则玩家认不出自己的角色。
- 确认性反馈(粒子、爆浆、飘字)排在击杀事件之后,不要排在命中之前 —— Lutz 的排序原则就是"先确认击杀,再喷溅"。命中反馈和击杀反馈混在一个时机里,玩家就永远读不出"这一下打死了吗"。
时间缩放是设计工具,不只是效果工具
Karunaratne 的综述里提了一类我没怎么想过的用法:hitstop 和全局慢动作可以做成难度参数。他举的例子是把《Bomb Jack》的 hitstop 拉长到 1 秒 —— 停顿变长意味着玩家的输入窗口被压缩,同一个机制立刻变难。也就是说手感层可以承担一部分难度曲线,而不需要动数值表。
(顺手一条容易写错的:hitstop 不要乘在 Time.deltaTime 上,要乘在"该实体自己的速度"上。前者的副作用范围是整个游戏,后者才只影响被打的那个东西。)
最后:手感会掩盖机制空洞
所有 juice 手法共同的副作用 —— Jonasson 和 Purho 那篇演讲的核心论点恰恰是"juice 能让平庸的机制感觉很好",这是把刀。我现在的做法是把"手感演示"和"机制演示"分开评审:去掉所有特效、音效、停顿之后,这个技能还成立吗?如果不成立,那 100ms 的 hitstop 只是止痛药。
Karunaratne 引的 Swink 有个说法我拿来当检查项:反馈必须及时、自洽、且超出玩家预期。三条里"超出预期"最难,因为它意味着你加的东西在玩家按下按键之前就已经排好队了。
参考来源
- Juice it or lose it — Martin Jonasson & Petri Purho(2012 演讲;原幻灯片页已不可达,此处为演讲的报道页)
- Making Music, Indie Game Design Dogma, and a Little Juice(The Game Design Forum #21) — Jan Willem Nijman 访谈
- The Hidden Pitfalls Of Game Interpolation — Michael Lutz(该站拒绝命令行抓取,需浏览器打开)(hitstop 帧数、单对象限制、偏移参数)
- Game Feel: A Systematic Review(Visual Impact, Part 1) 与 Part 2(动态与反馈) — Anuradhi Karunaratne