Loading recipe…
Walking wild done with the engine primitive. No stage-level ghost sprites; the engine reparents a pooled ReelSymbol to viewport.unmaskedContainer for the flight, tweens it across column boundaries, releases it back to the pool on land, and fires hooks at every transition so you can drive animation state on Spine assets.
The core loop
async function walkPinsLeft() {
for (const pin of [...reelSet.pins.values()]) {
if (pin.col <= 0) {
reelSet.unpin(pin.col, pin.row);
continue;
}
await reelSet.movePin(
{ col: pin.col, row: pin.row },
{ col: pin.col - 1, row: pin.row },
{ duration: 350 },
);
}
}
Call walkPinsLeft() between spins and you have a walking wild.
Driving flight animation. onFlightCreated / onFlightCompleted
movePin exposes the pooled flight ReelSymbol via two callbacks:
reelSet.movePin(from, to, {
duration: 350,
onFlightCreated: (flight) => {
// Before the tween starts. Kick off a flight animation here.
},
onFlightCompleted: (flight) => {
// After the tween, before the pool recycles the instance.
},
});
The demo uses them for a simple scale pulse (the sprite asset has no “run” animation). For a Spine-based walking wild, this is where you set the run animation track:
import type { SpineSymbol } from 'pixi-reels';
reelSet.movePin(from, to, {
duration: 600,
onFlightCreated: (flight) => {
// Your SpineSymbol subclass exposes a method to set an animation track.
(flight as SpineSymbol).setAnimation?.('run', true);
},
onFlightCompleted: (flight) => {
(flight as SpineSymbol).setAnimation?.('idle', true);
},
});
The flight symbol is a fresh acquire from the symbol factory, so its default state is whatever your SpineSymbol.onActivate() sets (typically idle). The callback kicks it out of idle for the flight duration.
Driving overlay animation. pin:overlayCreated / pin:overlayDestroyed
The sticky part of a walking wild. its behaviour while the reel is spinning, before it moves. is managed by the engine’s per-pin overlay. Two events expose that too:
reelSet.events.on('pin:overlayCreated', (pin, overlay) => {
// Overlay created on spin:start for every active pin.
// For Spine: (overlay as SpineSymbol).setAnimation?.('idle-sticky', true);
});
reelSet.events.on('pin:overlayDestroyed', (pin, overlay) => {
// Fires before the overlay is released to the pool.
// Stop any tweens / listeners you attached.
});
In the demo we fade the overlay rhythmically so the “sticky during spin” state reads differently from the landed state.
Lifecycle overview
pin(col, row, 'wild', { turns: 'permanent' })
fires pin:placed
spin() begins
_ensurePinOverlay runs for every pin
fires pin:overlayCreated <- Spine: play idle-sticky
reels scrolling; overlay keeps the wild visible at its cell
spin lands
fires pin:overlayDestroyed <- Spine: cleanup
overlay released to pool; real cell shows the wild
game code calls movePin(from, to)
onFlightCreated fires before tween <- Spine: play run
onFlightCompleted fires after tween <- Spine: play idle
fires pin:moved
next spin begins - new overlay at the NEW position
Constraints
movePinmust be called at rest (throws duringisSpinning).- Destination must be in-grid and unoccupied by another pin.
from === tois a no-op that still firespin:moved. safe in generic loops.- Callback errors are swallowed so a bug in animation code can’t break the move.
- Walk pins sequentially, not in parallel. If two wilds try to move to the same destination cell, the second
movePinthrows ("a pin already exists at…"). The example above iterates withawaitinside the loop so each move completes and the destination is free before the next one starts. Do not usePromise.allacross multiplemovePincalls.
Related
- Sticky wild (CellPin). persistent pin, no movement
- Pins guide. the full pin lifecycle and Spine animation hooks