SpriteManager
SpriteManager automatically handles sprite creation, physics setup, and synchronization between host and clients. It eliminates boilerplate code for managing sprite lifecycles in multiplayer games.
Quick Start
// Create sprite manager
const playerManager = adapter.createSpriteManager({
onCreate: (key, data) => {
return this.add.sprite(data.x, data.y, 'player');
},
onCreatePhysics: (sprite) => {
this.physics.add.existing(sprite);
sprite.body.setCollideWorldBounds(true);
}
});
// Add sprites (host only)
if (adapter.isHost()) {
playerManager.add('player-1', { x: 100, y: 100 });
playerManager.add('player-2', { x: 700, y: 100 });
}
// Sprites automatically appear on clients! API Reference
interface SpriteManager<TData extends SpriteData> {
// Properties
readonly namespace: string;
readonly group: Phaser.GameObjects.Group;
// Methods
add(key: string, data: TData): Phaser.GameObjects.Sprite;
remove(key: string): void;
get(key: string): Phaser.GameObjects.Sprite | undefined;
has(key: string): boolean;
getAllSprites(): Map<string, Phaser.GameObjects.Sprite>;
destroy(): void;
}
interface SpriteManagerConfig<TData> {
onCreate: (key: string, data: TData) => Phaser.GameObjects.Sprite;
onCreatePhysics?: (sprite: any, key: string, data: TData) => void;
onUpdate?: (sprite: any, data: TData) => void;
onDestroy?: (sprite: any, key: string) => void;
onAdd?: (sprite: any, key: string, data: TData, context: AddContext) => void;
staticProperties?: string[];
sync?: SyncConfig;
label?: LabelConfig;
namespace?: string;
} Creating a SpriteManager
const manager = adapter.createSpriteManager<PlayerData>({
onCreate: (key, data) => Phaser.GameObjects.Sprite,
// ... other options
}); onCreate (Required)
Factory function to create sprites. Called on both host and clients.
onCreate: (key: string, data: TData) => Phaser.GameObjects.Sprite Example:
const manager = adapter.createSpriteManager({
onCreate: (key, data) => {
const sprite = this.add.sprite(data.x, data.y, data.texture);
sprite.setTint(data.color);
return sprite;
}
}); onCreatePhysics (Optional)
Setup physics bodies. HOST ONLY - automatically skipped on clients.
onCreatePhysics?: (sprite: any, key: string, data: TData) => void Example:
const manager = adapter.createSpriteManager({
onCreate: (key, data) => this.add.sprite(data.x, data.y, 'player'),
onCreatePhysics: (sprite, key, data) => {
// Enable physics
this.physics.add.existing(sprite);
// Configure physics body
sprite.body.setCollideWorldBounds(true);
sprite.body.setBounce(0.2);
sprite.body.setGravityY(300);
// Add colliders
this.physics.add.collider(sprite, this.platforms);
}
}); onUpdate (Optional)
Update sprite properties when data changes. Clients only.
onUpdate?: (sprite: any, data: TData) => void Example:
interface PlayerData extends SpriteData {
x: number;
y: number;
color: number;
health: number;
}
const manager = adapter.createSpriteManager<PlayerData>({
onCreate: (key, data) => this.add.sprite(data.x, data.y, 'player'),
onUpdate: (sprite, data) => {
// Update color based on health
if (data.health < 30) {
sprite.setTint(0xff0000); // Red when low health
} else {
sprite.setTint(data.color);
}
// Update scale
sprite.setScale(data.health / 100);
}
}); onDestroy (Optional)
Cleanup when sprite is removed.
onDestroy?: (sprite: any, key: string) => void Example:
const manager = adapter.createSpriteManager({
onCreate: (key, data) => {
const sprite = this.add.sprite(data.x, data.y, 'player');
// Attach particle emitter
sprite.particles = this.add.particles(sprite.x, sprite.y, 'particle');
sprite.particles.startFollow(sprite);
return sprite;
},
onDestroy: (sprite, key) => {
// Clean up particles
if (sprite.particles) {
sprite.particles.destroy();
}
console.log(`${key} was removed`);
}
}); onAdd (Optional)
Called after sprite is fully created and ready. Fires for both initial and late-joining sprites.
onAdd?: (sprite: any, key: string, data: TData, context: AddContext) => void
interface AddContext {
manager: SpriteManager;
allSprites: Map<string, any>;
} Use cases:
- Inter-sprite setup
- Collision detection between specific sprites
- Attaching effects that depend on other sprites
Example:
const manager = adapter.createSpriteManager({
onCreate: (key, data) => this.add.sprite(data.x, data.y, 'player'),
onAdd: (sprite, key, data, { allSprites }) => {
// Add collision with boss (if it exists)
if (this.boss) {
this.physics.add.collider(sprite, this.boss, () => {
console.log(`${key} hit boss!`);
});
}
// Add collision with all existing players
for (const [otherKey, otherSprite] of allSprites) {
if (otherKey !== key) {
this.physics.add.collider(sprite, otherSprite);
}
}
console.log(`${key} joined! Now ${allSprites.size} players`);
}
}); sync (Optional)
Configure automatic property synchronization.
sync?: {
properties?: string[]; // Default: ['x', 'y', 'rotation', 'alpha']
direction?: 'toState' | 'toSprite'; // Default: 'toState'
interval?: number; // Default: 50ms (20 FPS)
} Example:
// Default: Sync sprite → state (physics-driven)
const manager = adapter.createSpriteManager({
onCreate: (key, data) => this.add.sprite(data.x, data.y, 'player'),
sync: {
properties: ['x', 'y', 'rotation', 'alpha', 'scaleX', 'scaleY'],
interval: 33 // 30 FPS
}
});
// Rare: State → sprite (state-driven, use StateDrivenSpawner instead)
sync: {
properties: ['x', 'y'],
direction: 'toSprite'
} staticProperties (Optional)
Properties that sync once and don’t change.
staticProperties?: string[] Example:
interface PlayerData extends SpriteData {
x: number;
y: number;
role: 'fire' | 'ice'; // Static
color: number; // Static
name: string; // Static
}
const manager = adapter.createSpriteManager<PlayerData>({
onCreate: (key, data) => {
const sprite = this.add.sprite(data.x, data.y, `player-${data.role}`);
sprite.setTint(data.color);
return sprite;
},
staticProperties: ['role', 'color', 'name']
// These are sent once, not every sync interval
}); label (Optional)
Automatically render text labels above sprites.
label?: {
getText: (data: TData) => string;
offset?: { x?: number; y?: number };
style?: Phaser.Types.GameObjects.Text.TextStyle;
} Example:
const manager = adapter.createSpriteManager<PlayerData>({
onCreate: (key, data) => this.add.sprite(data.x, data.y, 'player'),
label: {
getText: (data) => `${data.name}\nHP: ${data.health}`,
offset: { x: 0, y: -40 },
style: {
fontSize: '14px',
color: '#ffffff',
align: 'center',
backgroundColor: '#000000',
padding: { x: 5, y: 2 }
}
}
}); namespace (Optional)
Custom namespace for sprite data in state.
namespace?: string // Default: '_sprites' Example:
// Separate managers for different sprite types
const playerMgr = adapter.createSpriteManager({
namespace: 'players', // → state.players.*
onCreate: (key, data) => this.add.sprite(data.x, data.y, 'player')
});
const enemyMgr = adapter.createSpriteManager({
namespace: 'enemies', // → state.enemies.*
onCreate: (key, data) => this.add.sprite(data.x, data.y, 'enemy')
}); Methods
add()
Add a sprite. HOST ONLY.
add(key: string, data: TData): Phaser.GameObjects.Sprite Example:
if (adapter.isHost()) {
manager.add('player-1', { x: 100, y: 100, health: 100 });
manager.add('player-2', { x: 700, y: 100, health: 100 });
} remove()
Remove a sprite.
remove(key: string): void Example:
manager.remove('player-1'); get()
Get a sprite by key.
get(key: string): Phaser.GameObjects.Sprite | undefined Example:
const sprite = manager.get('player-1');
if (sprite) {
sprite.setAlpha(0.5);
} has()
Check if sprite exists.
has(key: string): boolean Example:
if (manager.has('player-1')) {
console.log('Player 1 is alive');
} getAllSprites()
Get all managed sprites.
getAllSprites(): Map<string, Phaser.GameObjects.Sprite> Example:
for (const [key, sprite] of manager.getAllSprites()) {
console.log(`${key} at (${sprite.x}, ${sprite.y})`);
} Properties
group
Phaser Group containing all managed sprites. Perfect for collision detection.
readonly group: Phaser.GameObjects.Group Example:
const playerMgr = adapter.createSpriteManager({ ... });
const enemyMgr = adapter.createSpriteManager({ ... });
// Collision between all players and all enemies
this.physics.add.collider(
playerMgr.group,
enemyMgr.group,
(player, enemy) => {
console.log('Player hit enemy!');
}
);
// Collision with a single object
this.physics.add.collider(ball, playerMgr.group); Why use group?
// ❌ BAD: Forgot to add collider for new players
manager.add('player-1', { ... });
this.physics.add.collider(manager.get('player-1'), this.boss);
manager.add('player-2', { ... });
// Oops! Forgot collider for player-2
// ✅ GOOD: Automatic collision for ALL players
this.physics.add.collider(manager.group, this.boss);
manager.add('player-1', { ... }); // Auto-collides
manager.add('player-2', { ... }); // Auto-collides Complete Example
import Phaser from 'phaser';
import { PhaserAdapter } from '@martini-kit/phaser';
interface PlayerData {
x: number;
y: number;
role: 'fire' | 'ice';
health: number;
name: string;
}
class GameScene extends Phaser.Scene {
private adapter!: PhaserAdapter;
private playerManager!: SpriteManager<PlayerData>;
create() {
this.adapter = new PhaserAdapter(runtime, this);
// Create player manager
this.playerManager = this.adapter.createSpriteManager<PlayerData>({
// Create sprite visual
onCreate: (key, data) => {
const sprite = this.add.sprite(data.x, data.y, `player-${data.role}`);
return sprite;
},
// Add physics (host only)
onCreatePhysics: (sprite, key, data) => {
this.physics.add.existing(sprite);
sprite.body.setCollideWorldBounds(true);
sprite.body.setBounce(0.2);
this.physics.add.collider(sprite, this.platforms);
},
// Update visuals based on health
onUpdate: (sprite, data) => {
if (data.health <= 0) {
sprite.setAlpha(0.3);
} else if (data.health < 30) {
sprite.setTint(0xff0000);
} else {
sprite.clearTint();
}
},
// Setup after sprite is ready
onAdd: (sprite, key, data) => {
console.log(`${data.name} joined as ${data.role}`);
},
// Cleanup
onDestroy: (sprite, key) => {
console.log(`${key} left the game`);
},
// Show player name and health
label: {
getText: (data) => `${data.name}\n${data.health} HP`,
offset: { y: -40 },
style: { fontSize: '12px', color: '#fff' }
},
// Static data
staticProperties: ['role', 'name'],
// Sync config
sync: {
properties: ['x', 'y', 'rotation', 'health'],
interval: 50
}
});
// Host: Create initial players
if (this.adapter.isHost()) {
this.playerManager.add('player-1', {
x: 200,
y: 400,
role: 'fire',
health: 100,
name: 'Alice'
});
this.playerManager.add('player-2', {
x: 600,
y: 400,
role: 'ice',
health: 100,
name: 'Bob'
});
}
// Both host and client: Setup collisions
this.physics.add.collider(
this.playerManager.group,
this.boss,
this.handlePlayerHitBoss,
undefined,
this
);
}
handlePlayerHitBoss(player: any, boss: any) {
console.log('Player hit boss!');
}
} Best Practices
✅ Do
- Use
onCreatefor visuals - Both host and clients - Use
onCreatePhysicsfor physics - Host only (automatic) - Use
groupfor collisions - Handles late-joiners automatically - Use
labelfor player names - Automatic positioning - Use
staticPropertiesfor metadata - Reduce bandwidth
❌ Don’t
- Don’t call
add()on clients - Host only - Don’t add physics in
onCreate- UseonCreatePhysics - Don’t forget
destroy()- Memory leaks - Don’t manually track sprites - Use
get()/has()
See Also
- PhaserAdapter - Creating sprite managers
- InputManager - Handling input
- Phaser Integration Guide - Full guide