Repository navigation
Spikes Paddle Animation Collision
Resolves the two paddle spikes #587
(animation states) and #865
(sprite collisions) as one decision, because both rework the same Sprite/collider
seam on paddle.gd and the collider question only has an answer once the animation
states exist.
Paddles render through an AnimatedSprite2D whose SpriteFrames resource holds
named placeholder animations (idle, ready, swing, walk). A movement state machine
owns the paddle's gameplay state and drives the animation downstream; the
animation layer never holds gameplay state. Swing is reactive animation (the paddle
plays it because a hit happened, it is not a player input), so collision does not
change with it: the paddle has one fixed authored collider and the sprite animates
over it. No AnimationTree.
Real character art lands later as a SpriteFrames swap with matching animation
names; no GDScript changes. This satisfies #587's
acceptance criterion.
The three Godot 4 ways to drive sprite animation, and why this picks the first:
| Approach | Fit | Verdict |
|---|---|---|
| AnimatedSprite2D + code state map | Self-contained; all frames in one SpriteFrames; swap art by swapping the resource |
Chosen |
| AnimationPlayer owns sprite + tracks | One lockstep timeline (sprite, collider, audio) | Rejected: tracks live in the node, so new art means re-keying per sheet, breaking the swap-with-no-code-change goal |
| AnimationTree (state-machine mode) | Blended, composed locomotion | Rejected: no blending wanted; cannot natively drive AnimatedSprite2D (proposal #567), so adopting it forces giving up the swap-friendly node for a feature we do not use |
AnimatedSprite2D is the only option where dropping in real art is a resource swap. The official Godot tutorial introduces it as the primary tool for frame-based sprite animation; it carries less per-instance overhead than AnimationPlayer (2D sprite animation).
Gameplay state (idle / ready / walk, and the transient swing action) lives in a movement state machine on the paddle. The animation layer is a downstream consumer: the FSM decides the state, then tells the sprite which animation to play. State must never live in the animation, or the ball physics would depend on which sprite frame is showing, coupling collision to art.
The paddle has no movement FSM today. State is implicit: velocity.y != 0 reads as
moving, == 0 as idle, and the only explicit machine is TimeoutController's
walk-off/equip/walk-on enum, which sits outside the paddle and sets the paddle's
drive_blocked flag (declared on paddle.gd). Wiring named animation states is the occasion to make
movement state explicit, with the FSM as the single owner the animation reads from.
The animation FSM consumes the paddle's movement state directly (velocity-driven idle vs
moving), not any controller's internal phase enum. During a timeout the paddle is still
moving, so the FSM animates that movement the same way; it does not need to be hard-wired
to TimeoutController.get_state(). The constraint the build inherits: do not couple the
FSM to TimeoutController's current class shape, because that shape is under separate
review (the timeout/equip responsibilities are being re-homed by ownership in their own
refactor). Keying the FSM to paddle movement keeps it independent of how any errand is
orchestrated.
Swing is a transient action that can fire while the paddle is idle or walking, not a
mode the paddle is wholly in. The hit is currently an instantaneous paddle_hit
signal with no held pose (paddle.gd:53): a brief animation that plays on top of a
persistent movement state and then hands back.
Composing a transient animation over a movement state does not require an
AnimationTree. A tree earns its cost when
the composition is blended and continuous (speed-graded locomotion, per-bone upper
body over lower body). Ours is discrete: the movement state picks the body animation,
and a swing plays its animation on top when a hit fires. Sprite frames cannot
meaningfully blend (a frame is a discrete picture), so a tree here would be a state
selector with extra ceremony, while also forcing the loss of AnimatedSprite2D.
Discrete composition is a few lines in the FSM. Revisit a tree only if blended
locomotion or live multi-layer sprite compositing is ever wanted; both are additive
over this.
The collider does not change across animation states. Swing is not a player input;
it is reactive animation, the paddle plays a swing because a hit happened, not a
timed action that extends reach or alters the hitbox. The ball always strikes the
same body. So there is no per-state silhouette to track, no per-frame collision
shape, and nothing to toggle: the paddle has one authored collider, and the sprite
animates freely over it. (This argues swing specifically; if a future ready pose
ever extends the paddle's reach, the collider question reopens for that state alone.)
This also keeps the bounce stable. Paddle.get_half_height() (paddle.gd) feeds the
ball's return-angle contact-offset denominator; because the collider is one fixed
shape, that reference never moves with the animation. The sprite varies; the angle
does not.
The collider's only legitimate size change is the stat-driven loadout resize in
_apply_size() (item effects growing/shrinking the paddle), which is per-loadout, not
per-frame, and is the existing foot-anchor path, untouched by this work.
Today _apply_size() ends by scaling sprite.scale.y to match the collider, the one
line that couples the visual to the physics shape. Once the sprite animates, that
derivation drifts. The build removes that final sprite-scale line; the collider-resize
and the position.y foot-anchor in _apply_size stay untouched, so
TimeoutController._half_height() (which reads the collider, not the sprite) is
unaffected. After that, the collider is authored on its own and the sprite is authored
on its own; neither derives from the other.
Character art is authored as individual PNGs per frame, dropped into a SpriteFrames
resource. The project has no sprite art convention to inherit (nine PNGs today, all
single images, no spritesheets, no SpriteFrames), so this spike sets the
convention. Individual images are the most swap-friendly and artist-friendly path for
AnimatedSprite2D; a spritesheet would suit AnimationPlayer's region keying, which is
not the chosen node.
Three paddle scenes, all 2D CharacterBody2D, share paddle.gd:
| Scene | Root | Visual node today |
|---|---|---|
scenes/player_paddle.tscn |
CharacterBody2D | child Sprite2D "Sprite", PlaceholderTexture2D
|
scenes/partner_paddle.tscn |
CharacterBody2D | child Sprite2D "Sprite", PlaceholderTexture2D
|
scenes/partners/martha_paddle.tscn |
CharacterBody2D | child Sprite2D "Sprite" + cosmetic "Bow" overlay |
sprite is already @export (paddle.gd); today its only animation-relevant logic is
_apply_size() ending by scaling sprite.scale.y to the collider. The build swaps the
Sprite2D export to AnimatedSprite2D across paddle.gd and the three scenes, removes
that final sprite-scale line from _apply_size (the collider-resize and position.y
foot-anchor in _apply_size stay, so TimeoutController._half_height() is unaffected),
and wires the movement FSM to drive the animation. The single collider is untouched.
Whether to wrap the AnimatedSprite2D in a small reusable child node (instanced across the
three scenes) or keep it a direct child driven by paddle.gd is a build-time structural
taste call, not load-bearing here.
- Real character art. Placeholder frames only; art swaps in later.
- New gameplay states.
readyandswingare defined animation hooks the FSM can enter; the gameplay that triggers a heldreadyor a swing windup is separate work. - The movement FSM's full design beyond what animation needs to read.
- The
TimeoutControllerreshape. Its timeout/equip responsibilities are being re-homed by ownership under a separate refactor; the spike only constrains the FSM not to couple to that controller's current shape, and does not decide or execute the reshape. - Any
AnimationPlayerorAnimationTreeadoption. Both are additive later if blended locomotion or frame-synced multi-track events are ever wanted.
Contributing
The game
The Cast
Process
Reference