game-feel
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGame 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
genre and the engine movement skill. For camera follow/deadzone/orbit framing
use (this skill only triggers the shake). For mixing, ducking, and adaptive
music use . For shader-based dissolves/flashes use and the
engine shader skill. For the concrete tween/particle node APIs, use the engine animation skill
(, ).
platformercamera-systemsaudio-designshader-programminggodot-animationunity-animation- 当某个操作(攻击、跳跃、冲刺、拾取、死亡、按键按下)在机制上正确,但感觉无力、轻飘飘或不满足,你希望它更具响应性和冲击力时使用。
- 用于添加屏幕震动、击中暂停/帧冻结、缓动动画、挤压拉伸、击退效果、闪光效果,或是为单个事件添加多个反馈渠道时使用。
- 用于判断需要多少Juice才足够,以及何时会过度到干扰体验的程度。
不适用场景:如需处理原始控制器运算(跳跃高度、 coyote time),请使用类型及引擎移动技能。如需处理相机跟随/死区/环绕等取景逻辑,请使用(本技能仅触发震动)。如需处理混音、闪避和自适应音乐,请使用。如需处理基于着色器的溶解/闪光效果,请使用及引擎着色器技能。如需具体的tween/粒子节点API,请使用引擎动画技能(、)。
platformercamera-systemsaudio-designshader-programminggodot-animationunity-animationCore 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
核心工作流程
- Confirm the event hooks exist. Juice attaches to discrete events: ,
on_hit,on_land,on_pickup,on_death. If the mechanic doesn't emit these, add them first.on_fire - 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.
- 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.
- Reserve hit-stop and shake for impact. They are the strongest, most abusable tools — short durations, scaled to importance, and never on routine actions.
- 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.
- 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.
- 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?).
- 确认事件钩子存在。Juice需附着在离散事件上:、
on_hit、on_land、on_pickup、on_death。如果机制未触发这些事件,请先添加它们。on_fire - 为每个事件选择反馈渠道(音效、粒子、震动、击中暂停、闪光、击退、tween、数字弹出)。从2-3种开始添加,直到效果达标后停止。
- 使用缓动而非线性运动。将缩放/位置/UI变化通过带有缓动效果的tween实现(使用overshoot效果实现“弹跳感”,ease-out效果实现“稳定感”)。线性运动显得机械生硬。
- 为冲击力保留击中暂停和震动效果。它们是最强、最容易滥用的工具——持续时间要短,根据重要性调整强度,且绝不能用于常规操作。
- 反馈不能干扰核心模拟。震动仅移动相机/视觉元素,而非角色本体;击中暂停使用时间缩放或实时暂停,而非中断游戏逻辑。
- 按重要性层级调整效果。定义小/中/大反馈预设,并将事件分配到对应层级,确保整个游戏的Juice效果一致且比例协调。
- 通过游玩和观察验证。反复触发事件;确认反馈触发后能恢复常态,且不会引起眩晕或阻止输入。记录观察结果(震动是否衰减?击中暂停期间是否仍能注册输入?)。
Patterns
模式示例
1. Screen shake by decaying "trauma" (smooth, not a random jitter)
1. 基于衰减“创伤值”的屏幕震动(平滑而非随机抖动)
gdscript
undefinedgdscript
undefinedGodot 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.
undefinedundefined2. Hit-stop / freeze frame (sell impact by briefly stopping time)
2. 击中暂停 / 帧冻结(通过短暂停止时间强化冲击力)
gdscript
undefinedgdscript
undefinedGodot 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
undefinedgdscript
undefinedGodot 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
.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
.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.
undefinedundefined4. A feedback bundle scaled by importance (keep juice proportional)
4. 按重要性分级的反馈组合包(保持Juice效果比例协调)
gdscript
undefinedgdscript
undefinedOne 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)
undefinedfunc 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)
undefinedPitfalls
常见误区
- 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 / a scaled timer never resumes (at time scale 0 the timer never advances). Use a real-time wait (
WaitForSeconds, or Godot'sWaitForSecondsRealtimetimer).ignore_time_scale - 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.
- 震动玩家/角色本体而非相机偏移会导致碰撞和瞄准不同步。应震动相机(或视觉支点),而非模拟变换。
- 每帧随机偏移会产生类似静电的嗡嗡感。应基于采样噪声/正弦函数和衰减的创伤值驱动震动,确保平滑且自动停止。
- 使用/缩放计时器实现击中暂停会导致无法恢复(时间缩放为0时计时器永远不会推进)。应使用实时等待(
WaitForSeconds,或Godot的WaitForSecondsRealtime计时器)。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
- flash + number-pop recipes, importance-tier presets, and per-engine tween/particle bindings,
read
- 如需创伤值震动算法、缓动曲线速查表(不同缓动效果对应弹跳感/稳定感)、击退+闪光+数字弹出方案、重要性层级预设,以及各引擎的tween/粒子绑定,请阅读。
references/feedback-recipes.md
Related skills
相关技能
- — owns camera follow/deadzone/orbit; this skill only feeds it shake trauma.
camera-systems - ,
godot-animation— concrete tween/AnimationPlayer/particle APIs juice rides on.unity-animation - — the sound layer of every feedback bundle; ducking and SFX variation.
audio-design - — knockback forces and the timestep juice must not destabilize.
physics-tuning - ,
platformer,fps-shooter— genres whose moment-to-moment feel this elevates.roguelike
- — 负责相机跟随/死区/环绕逻辑;本技能仅向其提供震动创伤值。
camera-systems - ,
godot-animation— Juice效果所依赖的具体tween/AnimationPlayer/粒子API。unity-animation - — 每个反馈组合包中的音效层;包括闪避和音效变体。
audio-design - — 击退力及Juice不得干扰的时间步长。
physics-tuning - ,
platformer,fps-shooter— 本技能可提升这些类型游戏的即时手感。roguelike