game-feel

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Game feel (juice)

游戏手感(Juice)

The difference between a mechanic that works and one that feels good is feedback: the layered, slightly-exaggerated response an action provokes. This skill covers the engine- neutral techniques — screen shake, hit-stop, easing, squash & stretch, knockback, and stacked feedback — and tells you how to apply them without burying the underlying simulation. It adds polish on top of an existing mechanic; it does not implement the mechanic.
一个“能用”的机制和一个“好用”的机制之间的区别在于反馈:即操作触发的分层、略带夸张的响应。本技能涵盖独立于引擎的技术——屏幕震动、击中暂停(hit-stop)、缓动、挤压拉伸(squash & stretch)、击退效果以及叠加反馈——并告诉你如何在不干扰底层模拟的情况下应用这些技术。它是在现有机制之上添加打磨效果;而非实现机制本身。

When to use

适用场景

  • Use when an action (hit, jump, dash, pickup, death, button press) is mechanically correct but feels weak, weightless, or unsatisfying, and you want it to feel responsive and punchy.
  • Use to add screen shake, hit-stop/freeze frames, eased motion, squash & stretch, knockback, flashes, or to layer multiple feedback channels onto one event.
  • Use to decide how much juice is enough and where it crosses into noise.
When not to use: for the raw controller math (jump height, coyote time) use the
platformer
genre and the engine movement skill. For camera follow/deadzone/orbit framing use
camera-systems
(this skill only triggers the shake). For mixing, ducking, and adaptive music use
audio-design
. For shader-based dissolves/flashes use
shader-programming
and the engine shader skill. For the concrete tween/particle node APIs, use the engine animation skill (
godot-animation
,
unity-animation
).
  • 当某个操作(攻击、跳跃、冲刺、拾取、死亡、按键按下)在机制上正确,但感觉无力、轻飘飘或不满足,你希望它更具响应性和冲击力时使用。
  • 用于添加屏幕震动、击中暂停/帧冻结、缓动动画、挤压拉伸、击退效果、闪光效果,或是为单个事件添加多个反馈渠道时使用。
  • 用于判断需要多少Juice才足够,以及何时会过度到干扰体验的程度。
不适用场景:如需处理原始控制器运算(跳跃高度、 coyote time),请使用
platformer
类型及引擎移动技能。如需处理相机跟随/死区/环绕等取景逻辑,请使用
camera-systems
(本技能仅触发震动)。如需处理混音、闪避和自适应音乐,请使用
audio-design
。如需处理基于着色器的溶解/闪光效果,请使用
shader-programming
及引擎着色器技能。如需具体的tween/粒子节点API,请使用引擎动画技能(
godot-animation
unity-animation
)。

Core principle: feedback is layered and exaggerated

核心原则:反馈需分层且适度夸张

One satisfying hit is usually 5–8 tiny responses firing together within ~100 ms: a sound, a particle burst, a brief hit-stop, a flash, a knockback, a small screen shake, and a number popping up. Each is cheap; stacked, they read as "impact". Two rules keep it from becoming a mess: (1) exaggerate briefly and return to rest (juice is transient, not a new resting state); (2) scale juice to event importance — a footstep is not a boss death.
一次令人满意的攻击通常会在约100毫秒内同时触发5-8个微小响应:音效、粒子爆发、短暂的击中暂停、闪光、击退、轻微的屏幕震动以及数字弹出。每个单独效果都很简单,但叠加在一起就会传递出“冲击力”。有两条规则可避免效果混乱:(1)短暂夸张后恢复常态(Juice是瞬时效果,而非新的静止状态);(2)根据事件重要性调整Juice强度——脚步声的效果不能和Boss死亡的效果一样。

Core workflow

核心工作流程

  1. Confirm the event hooks exist. Juice attaches to discrete events:
    on_hit
    ,
    on_land
    ,
    on_pickup
    ,
    on_death
    ,
    on_fire
    . If the mechanic doesn't emit these, add them first.
  2. Pick feedback channels per event from the menu (sound, particles, shake, hit-stop, flash, knockback, tween, number pop). Start with 2–3; add until it reads, then stop.
  3. Make motion eased, not linear. Route scale/position/UI changes through a tween with an ease (overshoot for "pop", ease-out for "settle"). Linear motion feels robotic.
  4. Reserve hit-stop and shake for impact. They are the strongest, most abusable tools — short durations, scaled to importance, and never on routine actions.
  5. Keep feedback off the critical simulation. Shake moves the camera/visual, not the body; hit-stop uses time scale or a real-time pause, not a gameplay-logic stall.
  6. Tune by importance tiers. Define small/medium/large feedback presets and assign events to a tier, so the whole game's juice stays consistent and proportional.
  7. Verify by playing and watching. Trigger the event repeatedly; confirm the feedback fires, returns to rest, and is not nauseating or input-blocking. Report what you observed (does shake decay? does input still register during hit-stop?).
  1. 确认事件钩子存在。Juice需附着在离散事件上:
    on_hit
    on_land
    on_pickup
    on_death
    on_fire
    。如果机制未触发这些事件,请先添加它们。
  2. 为每个事件选择反馈渠道(音效、粒子、震动、击中暂停、闪光、击退、tween、数字弹出)。从2-3种开始添加,直到效果达标后停止。
  3. 使用缓动而非线性运动。将缩放/位置/UI变化通过带有缓动效果的tween实现(使用overshoot效果实现“弹跳感”,ease-out效果实现“稳定感”)。线性运动显得机械生硬。
  4. 为冲击力保留击中暂停和震动效果。它们是最强、最容易滥用的工具——持续时间要短,根据重要性调整强度,且绝不能用于常规操作。
  5. 反馈不能干扰核心模拟。震动仅移动相机/视觉元素,而非角色本体;击中暂停使用时间缩放或实时暂停,而非中断游戏逻辑。
  6. 按重要性层级调整效果。定义小/中/大反馈预设,并将事件分配到对应层级,确保整个游戏的Juice效果一致且比例协调。
  7. 通过游玩和观察验证。反复触发事件;确认反馈触发后能恢复常态,且不会引起眩晕或阻止输入。记录观察结果(震动是否衰减?击中暂停期间是否仍能注册输入?)。

Patterns

模式示例

1. Screen shake by decaying "trauma" (smooth, not a random jitter)

1. 基于衰减“创伤值”的屏幕震动(平滑而非随机抖动)

gdscript
undefined
gdscript
undefined

Godot 4.x. Store trauma 0..1; shake = trauma^2 so small hits barely move, big hits punch.

Godot 4.x. Store trauma 0..1; shake = trauma^2 so small hits barely move, big hits punch.

Drives a Camera2D OFFSET (the visual), never the player body. Decays every frame.

Drives a Camera2D OFFSET (the visual), never the player body. Decays every frame.

@export var decay := 1.2 # trauma lost per second @export var max_offset := Vector2(12, 8) @export var max_roll := 0.1 # radians var trauma := 0.0 var _t := 0.0
func add_trauma(amount: float) -> void: trauma = clampf(trauma + amount, 0.0, 1.0) # hits ADD; they don't reset
func _process(dt: float) -> void: if trauma <= 0.0: return trauma = maxf(trauma - decay * dt, 0.0) var shake := trauma * trauma # quadratic: gentle low, sharp high _t += dt * 30.0 # Smooth pseudo-random via sampled noise/sin, NOT rand each frame (that buzzes). offset = Vector2(max_offset.x * shake * sin(_t * 1.7), max_offset.y * shake * sin(_t * 2.3)) rotation = max_roll * shake * sin(_t * 1.1)
@export var decay := 1.2 # trauma lost per second @export var max_offset := Vector2(12, 8) @export var max_roll := 0.1 # radians var trauma := 0.0 var _t := 0.0
func add_trauma(amount: float) -> void: trauma = clampf(trauma + amount, 0.0, 1.0) # hits ADD; they don't reset
func _process(dt: float) -> void: if trauma <= 0.0: return trauma = maxf(trauma - decay * dt, 0.0) var shake := trauma * trauma # quadratic: gentle low, sharp high _t += dt * 30.0 # Smooth pseudo-random via sampled noise/sin, NOT rand each frame (that buzzes). offset = Vector2(max_offset.x * shake * sin(_t * 1.7), max_offset.y * shake * sin(_t * 2.3)) rotation = max_roll * shake * sin(_t * 1.1)

Unity 6: identical model on a CinemachineCamera via CinemachineBasicMultiChannelPerlin

Unity 6: identical model on a CinemachineCamera via CinemachineBasicMultiChannelPerlin

(set AmplitudeGain/FrequencyGain from trauma^2) — see camera-systems.

(set AmplitudeGain/FrequencyGain from trauma^2) — see camera-systems.

undefined
undefined

2. Hit-stop / freeze frame (sell impact by briefly stopping time)

2. 击中暂停 / 帧冻结(通过短暂停止时间强化冲击力)

gdscript
undefined
gdscript
undefined

Godot 4.x. Drop time scale, then restore after a REAL-TIME delay (unaffected by time_scale).

Godot 4.x. Drop time scale, then restore after a REAL-TIME delay (unaffected by time_scale).

func hit_stop(duration := 0.08, scale := 0.05) -> void: Engine.time_scale = scale # 4th arg ignore_time_scale=true → the timer still fires while the game is frozen. await get_tree().create_timer(duration, true, false, true).timeout Engine.time_scale = 1.0

```csharp
// Unity 6 (C#). WaitForSecondsRealtime ignores Time.timeScale, so the timer still elapses.
IEnumerator HitStop(float duration = 0.08f, float scale = 0.05f) {
    Time.timeScale = scale;
    yield return new WaitForSecondsRealtime(duration);
    Time.timeScale = 1f;            // RIGHT: real-time wait. WRONG: WaitForSeconds (never resumes at scale 0)
}
func hit_stop(duration := 0.08, scale := 0.05) -> void: Engine.time_scale = scale # 4th arg ignore_time_scale=true → the timer still fires while the game is frozen. await get_tree().create_timer(duration, true, false, true).timeout Engine.time_scale = 1.0

```csharp
// Unity 6 (C#). WaitForSecondsRealtime ignores Time.timeScale, so the timer still elapses.
IEnumerator HitStop(float duration = 0.08f, float scale = 0.05f) {
    Time.timeScale = scale;
    yield return new WaitForSecondsRealtime(duration);
    Time.timeScale = 1f;            // RIGHT: real-time wait. WRONG: WaitForSeconds (never resumes at scale 0)
}

3. Squash & stretch + overshoot via an eased tween (the "pop")

3. 挤压拉伸(squash & stretch)+ 带缓动的tween弹跳效果(“弹跳感”)

gdscript
undefined
gdscript
undefined

Godot 4.x. Conserve volume: stretch one axis, squash the other, then spring back with overshoot.

Godot 4.x. Conserve volume: stretch one axis, squash the other, then spring back with overshoot.

func pop(node: Node2D) -> void: node.scale = Vector2(1.3, 0.7) # instant squash on the event var tw := create_tween() tw.tween_property(node, "scale", Vector2.ONE, 0.18)
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) # BACK = overshoots past 1, settles
func pop(node: Node2D) -> void: node.scale = Vector2(1.3, 0.7) # instant squash on the event var tw := create_tween() tw.tween_property(node, "scale", Vector2.ONE, 0.18)
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) # BACK = overshoots past 1, settles

RIGHT: ease back (TRANS_BACK/ELASTIC) for life. WRONG: linear tween → mechanical, dead.

RIGHT: ease back (TRANS_BACK/ELASTIC) for life. WRONG: linear tween → mechanical, dead.

undefined
undefined

4. A feedback bundle scaled by importance (keep juice proportional)

4. 按重要性分级的反馈组合包(保持Juice效果比例协调)

gdscript
undefined
gdscript
undefined

One call per event; the tier decides intensity so the whole game stays consistent.

One call per event; the tier decides intensity so the whole game stays consistent.

func feedback(event_pos: Vector2, tier: String) -> void: match tier: "small": AudioBus.play("tick"); Camera.add_trauma(0.15) "medium": AudioBus.play("hit"); Camera.add_trauma(0.4); hit_stop(0.05); spawn_particles(event_pos, 6) "large": AudioBus.play("boom"); Camera.add_trauma(0.8); hit_stop(0.12); spawn_particles(event_pos, 30); flash_white(0.06)
undefined
func feedback(event_pos: Vector2, tier: String) -> void: match tier: "small": AudioBus.play("tick"); Camera.add_trauma(0.15) "medium": AudioBus.play("hit"); Camera.add_trauma(0.4); hit_stop(0.05); spawn_particles(event_pos, 6) "large": AudioBus.play("boom"); Camera.add_trauma(0.8); hit_stop(0.12); spawn_particles(event_pos, 30); flash_white(0.06)
undefined

Pitfalls

常见误区

  • Shaking the player/body instead of the camera offset desyncs collision and aim. Shake the camera (or a visual pivot), never the simulated transform.
  • Random offset every frame buzzes like static. Drive shake from sampled noise/sin and a decaying trauma value so it's smooth and self-ending.
  • Hit-stop with
    WaitForSeconds
    / a scaled timer
    never resumes (at time scale 0 the timer never advances). Use a real-time wait (
    WaitForSecondsRealtime
    , or Godot's
    ignore_time_scale
    timer).
  • Hit-stop on every frame of a held attack locks the game. Trigger it once per impact.
  • Linear tweens everywhere feel robotic. Ease almost everything; reserve overshoot (BACK/ELASTIC) for "pop" and ease-out for "settle".
  • Permanent exaggeration (scale never returns, shake never decays) becomes the new normal and stops reading as feedback. Juice must return to rest.
  • Over-juicing routine actions (full shake + hit-stop on every footstep) causes nausea and hides real impacts. Scale to importance; add a "reduce screen shake"/"reduce flashing" accessibility option.
  • Feedback that blocks input (long freeze, un-cancelable animation) hurts responsiveness. Keep juice short and let input buffer through it.
  • 震动玩家/角色本体而非相机偏移会导致碰撞和瞄准不同步。应震动相机(或视觉支点),而非模拟变换。
  • 每帧随机偏移会产生类似静电的嗡嗡感。应基于采样噪声/正弦函数和衰减的创伤值驱动震动,确保平滑且自动停止。
  • 使用
    WaitForSeconds
    /缩放计时器实现击中暂停
    会导致无法恢复(时间缩放为0时计时器永远不会推进)。应使用实时等待(
    WaitForSecondsRealtime
    ,或Godot的
    ignore_time_scale
    计时器)。
  • 在持续攻击的每帧触发击中暂停会锁定游戏。应仅在每次冲击时触发一次。
  • 所有动画都使用线性tween会显得机械生硬。几乎所有动画都应使用缓动;保留overshoot(BACK/ELASTIC)效果用于“弹跳感”,ease-out效果用于“稳定感”。
  • 永久夸张效果(缩放永不恢复,震动永不衰减)会成为新的常态,不再被视为反馈。Juice必须恢复到常态。
  • 常规操作过度添加Juice(每步都添加完整震动+击中暂停)会引起眩晕,并掩盖真实冲击效果。应根据重要性调整强度;添加“降低屏幕震动”/“降低闪光”的无障碍选项。
  • 阻止输入的反馈(长时间冻结、无法取消的动画)会损害响应性。Juice效果应保持短暂,并允许输入在期间缓冲。

References

参考资料

  • For the trauma-shake math, easing-curve cheat sheet (which ease for pop vs settle), knockback
    • flash + number-pop recipes, importance-tier presets, and per-engine tween/particle bindings, read
      references/feedback-recipes.md
      .
  • 如需创伤值震动算法、缓动曲线速查表(不同缓动效果对应弹跳感/稳定感)、击退+闪光+数字弹出方案、重要性层级预设,以及各引擎的tween/粒子绑定,请阅读
    references/feedback-recipes.md

Related skills

相关技能

  • camera-systems
    — owns camera follow/deadzone/orbit; this skill only feeds it shake trauma.
  • godot-animation
    ,
    unity-animation
    — concrete tween/AnimationPlayer/particle APIs juice rides on.
  • audio-design
    — the sound layer of every feedback bundle; ducking and SFX variation.
  • physics-tuning
    — knockback forces and the timestep juice must not destabilize.
  • platformer
    ,
    fps-shooter
    ,
    roguelike
    — genres whose moment-to-moment feel this elevates.
  • camera-systems
    — 负责相机跟随/死区/环绕逻辑;本技能仅向其提供震动创伤值。
  • godot-animation
    ,
    unity-animation
    — Juice效果所依赖的具体tween/AnimationPlayer/粒子API。
  • audio-design
    — 每个反馈组合包中的音效层;包括闪避和音效变体。
  • physics-tuning
    — 击退力及Juice不得干扰的时间步长。
  • platformer
    ,
    fps-shooter
    ,
    roguelike
    — 本技能可提升这些类型游戏的即时手感。