game-feel is a skill from the Summer skill library: a guide your AI agent reads when the task matches, so it builds the good version on the first try. Add juice: hit-flash, trauma camera shake and audio ducking, one hit fires all three.
When your agent uses it
- A Summer game feels flat, lacks impact, needs juice or punch
- The user asks for hit feedback, screen shake, camera shake, or audio ducking
Install
summer setup installs every skill for your agent. To add only this one:
claude-code for your agent, such as codex or cursor. See Skills for every option.
The skill
Particles do not make a game feel good. Screen shake alone does not either. What makes a hit land is three systems firing on the same frame: a sub-frame hit flash on the body that got hit, trauma-based camera shake with quadratic falloff so big hits saturate, and audio ducking that briefly squashes Music/Ambient so the impact SFX punches through. This skill installs the canonical Summer Engine stack as three small systems wired to one signal.Step 1 — Ask what feels flat
Open with one focused question. Wait for the answer.What feels flat? Pick one to start: combat hits / pickups / explosions / spell casts / something else.If the user says “all of it”, anchor on combat hits — the hit-flash + shake + duck trio is the loudest perceived improvement per minute, and pickups/explosions reuse the same building blocks afterwards.
Step 2 — Read the current scene state
Before adding anything, see what’s there. Preferred (Summer MCP):MeshInstance3D on the hurt body (for the flash), an existing Camera3D on the player (don’t spawn a new one), and the names of the audio buses already present (Master + at least one of Music, Ambient, SFX).
If there’s no Camera3D under the player, stop and ask before adding one — there may be a level camera elsewhere you’d be fighting.
If the only bus is Master, ducking will do nothing useful. See Common mistakes below.
Fallback (no MCP): ask the user to paste the relevant .tscn and the project’s default_bus_layout.tres (or describe the bus list).
Step 3 — Propose the bundle
Don’t drip the systems in one at a time. The whole point is the trio fires together — install all three, wire one signal, see it land.I’m about to add:HitFlashon./World/Enemy,CameraShakescript on./World/Player/Camera3D,AudioDuckerautoload, and connect thedamagedsignal so one hit fires all three. About 130 lines of GDScript across 3 files. May I proceed?
Step 4 — Install Section 1 (Hit Flash)
Goal: when a body takes damage, its mesh pulses to white for ~0.08s, then tweens back. Reads instantly even at 60 fps. Approach: keep a reference to the originalalbedo_color. On flash(), write white directly to the mesh’s material_override (or build a flash override the first time), then create_tween() back to the original over duration seconds. Tween, not await + sleep — tweens auto-cancel if flash() is re-called mid-flash, which matters during multi-hit combos.
Node tree:
scenePath):
scripts/vfx/hit_flash.gd (via summer_write_file, which requires exactly
one of create_only or expected_sha256):
duration 0.06–0.12s — under 0.06 reads as a flicker, over 0.12 looks slow. emission_energy_multiplier 1.5–3.0.
Note:summer_set_prop(scenePath="res://levels/world.tscn", path="./World/Enemy/Mesh", key="material_override", value="StandardMaterial3D")creates an inlineStandardMaterial3Dsub_resource thatsummer_set_resource_property(scenePath="res://levels/world.tscn", nodePath="./World/Enemy/Mesh", resourceProperty="material_override", subProperty="albedo_color", value="Color(r, g, b, a)")then edits andsummer_save_scenepersists — so the inline route, the script above, and a savedmaterials/flash.tresare all valid. What breaks is the value shape: a JSON object instead of a Godot literal string, a misspelledkey/subProperty, or a wrong-typed value returnsok:trueyet silently no-ops or destructively coerces on current engines (dict → material cleared; dict →Color(0,0,0,1); newer engines reject these withunknown_property/bad_value_shape/type_mismatch). Always pass class names andColor(...)/Vector3(...)strings, and confirm the result in the saved.tscnor a snapshot diff — never fromokalone. Seesummer_docs_readidreference/mcp-tools-reference.
Step 5 — Install Section 2 (Trauma Camera Shake)
Goal: any system canadd_trauma(amount) to the camera. The camera accumulates trauma 0–1, decays it every frame, and shakes by trauma² × random_offset. The square is the trick: a 0.3 hit shakes very little (0.09), a 0.8 hit shakes a lot (0.64). Big hits feel huge, small hits don’t pollute the screen.
Approach: put the shake on the existing Camera3D. Two variables — _trauma (0–1 accumulator) and _shake_offset (last frame’s applied rotation). Every frame, subtract last frame’s offset first so the next offset doesn’t drift, then apply the new one. This is the non-obvious trick.
Node tree: the script attaches to the existing ./World/Player/Camera3D. No new nodes.
scripts/vfx/camera_shake.gd:
trauma_decay 3.0 (long-tail, cinematic) ↔ 6.0 (snappy, twitch-shooter feel). max_shake_angle_deg 2.0 (subtle) ↔ 4.0 (action-game). Keep yaw_factor ≤ 0.5 — equal pitch/yaw shake feels uniform and wrong; eyes are far more sensitive to vertical motion.
Step 6 — Install Section 3 (Audio Ducking)
This is the system most projects skip and the one that makes the biggest perceived difference. On a hit, briefly attenuate Music + Ambient + Announcer (whatever non-impact buses you have) so the SFX bus stabs through the mix. Then smoothly tween volumes back. Crucially: store original bus volumes once at startup, and apply ducking as an offset. Snapping volumes to absolute values destroys the user’s volume preferences. Approach: an autoload (AudioDucker) so any system can call AudioDucker.duck(0.5, 0.4) from anywhere. Internally:
_initial_volumes: Dictionary[StringName, float]captured once in_ready()fromAudioServer.get_bus_volume_db()for each named bus.duck(amount, duration)lowers each non-impact bus byamount × per_bus_weight, scheduled via Tween, and tweens back overduration.- Per-bus weights let Music duck more than Ambient (Music 0.9 = -90% intensity, Ambient 0.7, Announcer 0.5).
- Kill any in-flight tween before starting a new one so rapid-fire hits don’t compound the duck and silence the mix.
scripts/vfx/audio_ducker.gd:
scripts/vfx/audio_ducker.gd
as AudioDucker.)
Calling it:
set_bus_volume_db("Music", -24.0) directly, the restore tween will pull Music up to -24’s “neutral” which is louder than the user wanted. Capturing _initial_volumes at autoload _ready() and only ever offsetting from those values preserves the user’s mix forever.
Step 7 — Wire the trio to one signal
The whole point: one hit, all three fire on the same frame. Keep this in the hurt body’s damage handler — not on a global event bus, because you want the trauma amount and flash to scale per-hit.player_camera from its own _ready() (Step 5). Node has no
groups property, so summer_set_prop(key="groups", ...) cannot do this — and
summer_set_prop only accepts a string, number, or boolean anyway, never an array.
The two ways that do work are the add_to_group() call in _ready() or a groups = ["player_camera"] line written into the .tscn node header.
Or wire the signal explicitly via MCP if you prefer (emitter/receiver, not
from/to, and scenePath is required):
Step 8 — Scaling intensity per hit weight
The same code makes a glancing parry feel different from a critical hit. Calibrate the curve once:
Because trauma is squared inside the camera, the felt difference between 0.35 (→ 0.12 shake) and 0.75 (→ 0.56 shake) is roughly 5×. That’s the whole point — small hits stay clean, big hits saturate the screen.
Step 9 — Verify
- Flash invisible →
emission_energy_multiplier1.5 → 3.0, or check that the mesh actually has a material to override. - Shake feels jittery → reduce
max_shake_angle_deg, or checkyaw_factoris ≤ 0.5. - Ducking inaudible → check
_initial_volumesgot populated (autoload running before the first hit?), and thatMusic/Ambientbus names actually match yourdefault_bus_layout.tres. - Ducking too loud → reduce per-bus weight, not the
amountargument. Weights are per-project; amount is per-hit.
Common mistakes
Collaborative protocol
This skill writes 3 GDScript files and adds 1 node + 1 autoload entry. Always ask before each section is applied. Group related writes into one ask: “I’m about to add HitFlash + CameraShake + AudioDucker autoload, wire one signal. OK?” Seesummer_docs_read id reference/collaborative-protocol.
When NOT to use this skill
- 2D games — the trauma curve and dB ducking still apply, but the camera-shake script needs a
Camera2Drewrite (offsetinstead ofrotation). Use this skill as a reference, not a drop-in. - Turn-based / strategy — game-feel here lives in UI animation, not body-hit feedback. Skip the trio; reach for tween-based UI polish instead.
- No audio buses configured — ducking with one Master bus is a no-op. Set up
Music/Ambient/SFXfirst via the audio-direction skill, then come back. - Networked games where the hurt body is remote — the flash should run client-side on visual hit confirm, not on damage authority. Hook to a
hit_visual_receivedsignal, not the authoritative damage signal.
See also
summer_docs_readidreference/mcp-tools-reference— full MCP tool list, especiallysummer_set_resource_propertyon inline sub_resources and which failures are loud vs. silentsummer_docs_readidreference/godot-version— Summer compatibility and version-sensitive API notessummer_docs_readidreference/collaborative-protocol— “May I write” patternsummer_docs_readidreference/gd-style— typed GDScript conventionsaudio-direction— bus layout setup (prerequisite for ducking)vfx-hit-spark— particles to layer on top of the triodebugging-game-feel— when the trio is installed and it still feels wrong
From a shell
No Summer MCP tools in this session? Run each one as a command; whensummer --version is missing or below 4.0, use npx -y summer-engine@latest in place of summer. Fields are flags in kebab-case (--scene-path res://main.tscn) or one --input '<json>', and summer ops describe <op> prints the exact input. The summer-cli skill has the rest.
Related skills
audio-direction: Define the game’s sound (music style, SFX vocabulary, dynamic music) as .summer/audio-bible.md.
Source:
library/skills/game-feel/SKILL.md, rendered from summer-engine@4.3.0 on npm.
