Game-specific coding guidance during implementation. Use when implementing game code, coding game mechanics, building game loop, setting up state machine, game architecture during development, writing game systems, structuring game update logic, or adding game input handling. Covers game loop setup, state management, input handling, frame budget, entity architecture, delta time.
Provides prescriptive patterns for implementing game loops, state machines, input handling, and performance-critical systems.
npx claudepluginhub smileynet/game-spiceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Prescriptive patterns for building game systems correctly from the start. Follow these defaults unless you have a measured reason to diverge.
| Area | Default Pattern | Upgrade When |
|---|---|---|
| Game Loop | Fixed update + variable render | Never (this is the correct answer) |
| State Management | Enum + switch FSM | States > 4: hierarchical FSM |
| Input Handling | Action mapping layer | Day 1 (no upgrade, start here) |
| Frame Budget | Measure first, optimize second | Profiler shows a bottleneck |
| Entity Architecture | Composition / components | Entity count > ~100: consider ECS |
| Delta Time | Every movement * delta | Never skip this |
Build this: fixed update with variable rendering.
The accumulator pattern decouples physics from framerate. Physics runs at a fixed rate. Rendering interpolates between physics states.
FIXED_TIMESTEP = 1.0 / 60.0 # Physics rate
accumulator = 0.0
while running:
frame_time = clock.get_delta()
accumulator += min(frame_time, 0.25) # Cap to prevent spiral of death
while accumulator >= FIXED_TIMESTEP:
update_physics(FIXED_TIMESTEP) # Fixed step, deterministic
accumulator -= FIXED_TIMESTEP
alpha = accumulator / FIXED_TIMESTEP
render(alpha) # Interpolate for smooth display
Key decisions:
alpha for smooth visuals between physics ticksFIXED_TIMESTEP as your single constant — don't pass delta to physics(see architecture-audit → Game Loop Patterns)
Build this: enum + switch FSM from the start. Replace boolean flags immediately.
Boolean flags (is_jumping, is_ducking, is_firing) create combinatorial explosion. Every new flag doubles possible states. Most combinations are invalid.
enum State { IDLE, RUNNING, JUMPING, FALLING, DEAD }
current_state = State.IDLE
func update(delta):
match current_state:
State.IDLE:
if input.move: transition(State.RUNNING)
if input.jump: transition(State.JUMPING)
State.JUMPING:
if velocity.y < 0: transition(State.FALLING)
State.FALLING:
if on_ground: transition(State.IDLE)
State.DEAD:
pass # No transitions out
func transition(new_state):
exit_state(current_state)
current_state = new_state
enter_state(new_state)
Key decisions:
transition() — never set the enum directly(see architecture-audit/detailed-audits.md for State Management)
Build this: an action mapping layer from day 1. Never check raw keys in gameplay code.
Three layers, always:
| Layer | Responsibility |
|---|---|
| Raw Input | Hardware signals — keyboard events, gamepad axes |
| Mapping | Translates to named actions, supports remapping |
| Action | Game systems consume actions with buffering |
# Mapping layer (configure once)
input_map = {
"jump": [Key.SPACE, Gamepad.A],
"move_left": [Key.A, Key.LEFT, Gamepad.LEFT_STICK_LEFT],
"fire": [Key.F, Gamepad.RIGHT_TRIGGER],
}
# Action layer (game code reads this)
if actions.just_pressed("jump"):
player.jump()
Key decisions:
(see architecture-audit/detailed-audits.md for Input Handling)
Measure before optimizing. No per-frame allocations. Know your budget.
| Target | Budget per Frame | Typical Split |
|---|---|---|
| 30 FPS | 33ms | ~10ms logic, ~10ms physics, ~13ms render |
| 60 FPS | 16.6ms | ~5ms logic, ~5ms physics, ~6.6ms render |
Key decisions:
new/malloc in update loops)Common frame budget violations:
| Violation | Fix |
|---|---|
new / malloc in update loop | Pre-allocate, use object pools |
| Nested entity collision loops | Add spatial partitioning (grid, quadtree) |
| String type checks per frame | Use enum or integer IDs |
| Unbounded entity spawning | Pool with fixed max, recycle oldest |
(see architecture-audit/detailed-audits.md for Performance Smells)
Build this: composition first. Switch to ECS when entity count justifies it.
| Scale | Pattern | Why |
|---|---|---|
| < ~100 entities | Component-based composition | Simple, fits scene-graph engines |
| > ~100 entities, perf-critical | ECS (Bevy, EnTT, etc.) | Cache-friendly, scales linearly |
| < 10 entity types, no runtime flex | Inheritance (max 3 deep) | Acceptable only at tiny scale |
# Composition: attach behaviors as components
entity = Entity()
entity.add(PositionComponent(x=0, y=0))
entity.add(VelocityComponent(speed=200))
entity.add(SpriteComponent(texture="player.png"))
entity.add(HealthComponent(max_hp=100))
Key decisions:
(see architecture-audit/detailed-audits.md for Entity Architecture)
Every movement value multiplied by delta. Speed expressed in units-per-second.
# WRONG: framerate-dependent movement
position.x += 5
# RIGHT: framerate-independent movement
speed = 300 # units per second
position.x += speed * delta
Key decisions:
deltaFIXED_TIMESTEP instead of deltaCommon delta time mistakes:
| Mistake | Symptom | Fix |
|---|---|---|
position += 5 | Game speed changes with framerate | position += speed * delta |
| Speed as "pixels per frame" | Inconsistent on different hardware | Speed as "units per second" |
| Timer using frame count | Timer speed varies with FPS | Timer using elapsed seconds |
Note: In fixed update loops, do NOT multiply by delta — use the FIXED_TIMESTEP constant directly. This is correct behavior, not a bug.
(see architecture-audit → Architecture Health Checklist)(see antipatterns)(see scoping → MLP Scoping Process)