Loading...
Loading...
Add "juice" and game feel that makes actions satisfying — screen shake, hit-stop/freeze frames, tweened/eased motion, squash & stretch, knockback, and layered audio-visual feedback — as engine-neutral techniques that pair with the detected engine's tween, particle, and camera APIs. Use when the user mentions game feel, juice, "make it feel good/punchy", screen shake, hit stop, screen freeze, easing, squash and stretch, impact frames, or feedback/polish on hits, jumps, pickups, and deaths.
npx skill4agent add gamedev-skills/awesome-gamedev-agent-skills game-feelplatformercamera-systemsaudio-designshader-programminggodot-animationunity-animationon_hiton_landon_pickupon_deathon_fire# 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.
@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
# (set AmplitudeGain/FrequencyGain from trauma^2) — see camera-systems.# 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// 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)
}# 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
# RIGHT: ease back (TRANS_BACK/ELASTIC) for life. WRONG: linear tween → mechanical, dead.# 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)WaitForSecondsWaitForSecondsRealtimeignore_time_scalereferences/feedback-recipes.mdcamera-systemsgodot-animationunity-animationaudio-designphysics-tuningplatformerfps-shooterroguelike