Sprite Attachment System
The SpriteAttachment system provides a unified, type-safe foundation for all sprite attachments in martini-kit - including directional arrows, health bars, name tags, and custom indicators.
Why Use SpriteAttachment?
Pit of Success Design:
- ✅ Auto-update by default - No manual update() calls needed
- ✅ Auto-cleanup - Destroys when sprite/scene is destroyed
- ✅ Type-safe - Full TypeScript support
- ✅ Reusable - One pattern for all attachment types
- ✅ Efficient - Uses Phaser’s event system
Core Interface
Every sprite attachment implements this simple interface:
interface SpriteAttachment {
update: () => void; // Called every frame (if autoUpdate: true)
destroy: () => void; // Called when sprite/scene is destroyed
getGameObject?: () => Phaser.GameObjects.GameObject | null;
} Basic Usage
Creating a Custom Attachment
import { createSpriteAttachment } from '@martini-kit/phaser';
function createGlowEffect(
scene: Phaser.Scene,
sprite: any,
color: number = 0xffff00
) {
// Create visual element
const glow = scene.add.circle(sprite.x, sprite.y, 40, color, 0.3);
// Wrap in attachment system - auto-updates and auto-destroys!
return createSpriteAttachment(scene, sprite, {
update: () => {
// Follow sprite position
glow.setPosition(sprite.x, sprite.y);
// Pulse effect
const scale = 1 + Math.sin(Date.now() / 200) * 0.1;
glow.setScale(scale);
},
destroy: () => {
glow.destroy();
},
getGameObject: () => glow
});
}
// Usage in SpriteManager:
onCreate: (key, data) => {
const sprite = this.add.sprite(data.x, data.y, 'player');
// Attach glow - auto-updates every frame!
createGlowEffect(this, sprite);
return sprite;
} That’s it! The glow will:
- Update every frame automatically
- Destroy when sprite is destroyed
- Destroy when scene shuts down
- Never cause memory leaks
Manual Update Mode
For advanced use cases where you need fine control:
const attachment = createSpriteAttachment(scene, sprite, {
update: () => { ... },
destroy: () => { ... }
}, {
autoUpdate: false // Disable automatic updates
});
// Then in your scene's update loop:
update() {
attachment.update();
} Built-in Attachments
martini-kit provides several pre-built attachments that use this system:
DirectionalIndicator
Shows which direction a sprite is facing.
import { attachDirectionalIndicator } from '@martini-kit/phaser';
onCreate: (key, data) => {
const car = this.add.rectangle(data.x, data.y, 30, 20, data.color);
attachDirectionalIndicator(this, car, {
shape: 'triangle',
offset: 20,
color: 0xffffff
});
return car;
} HealthBar (Coming Soon)
Auto-updating health bar above sprites.
import { createHealthBar } from '@martini-kit/phaser';
createHealthBar(scene, sprite, {
maxHealth: 100,
currentHealth: 75,
offset: { y: -40 }
}); NameTag (Coming Soon)
Text label that follows sprites.
import { createNameTag } from '@martini-kit/phaser';
createNameTag(scene, sprite, {
text: 'Player 1',
offset: { y: -50 },
style: { fontSize: '16px', color: '#fff' }
}); Advanced Patterns
Multiple Attachments
Attach several components to the same sprite:
import { createSpriteAttachments } from '@martini-kit/phaser';
onCreate: (key, data) => {
const sprite = this.add.sprite(data.x, data.y, 'player');
// Attach multiple components at once
createSpriteAttachments(this, sprite, [
createDirectionalArrow(this, sprite),
createHealthBar(this, sprite, { maxHealth: 100 }),
createNameTag(this, sprite, { text: data.name })
]);
return sprite;
} Composite Attachments
Combine multiple visual elements into one attachment:
import { createCompositeAttachment } from '@martini-kit/phaser';
function createPlayerIndicator(scene: Phaser.Scene, sprite: any) {
// Create multiple visual elements
const arrow = scene.add.triangle(...);
const glow = scene.add.circle(...);
const label = scene.add.text(...);
// Combine into composite - all update/destroy together
return createCompositeAttachment(scene, sprite, [
{
update: () => {
arrow.setPosition(sprite.x, sprite.y - 30);
arrow.setRotation(sprite.rotation);
},
destroy: () => arrow.destroy()
},
{
update: () => {
glow.setPosition(sprite.x, sprite.y);
glow.setScale(1 + Math.sin(Date.now() / 200) * 0.1);
},
destroy: () => glow.destroy()
},
{
update: () => {
label.setPosition(sprite.x, sprite.y + 40);
},
destroy: () => label.destroy()
}
]);
} Conditional Updates
Only update when certain conditions are met:
function createConditionalAttachment(
scene: Phaser.Scene,
sprite: any,
shouldUpdate: () => boolean
) {
const circle = scene.add.circle(sprite.x, sprite.y, 10, 0xff0000);
return createSpriteAttachment(scene, sprite, {
update: () => {
// Only update if condition is true
if (shouldUpdate()) {
circle.setPosition(sprite.x + 20, sprite.y);
circle.setVisible(true);
} else {
circle.setVisible(false);
}
},
destroy: () => circle.destroy()
});
}
// Usage:
createConditionalAttachment(
this,
sprite,
() => this.adapter.getMyPlayerId() === playerId // Only show for local player
); State-Driven Attachments
React to game state changes:
function createStateIndicator(
scene: Phaser.Scene,
sprite: any,
runtime: GameRuntime,
playerId: string
) {
const indicator = scene.add.circle(sprite.x, sprite.y, 15, 0x00ff00);
return createSpriteAttachment(scene, sprite, {
update: () => {
const state = runtime.getState();
const player = state.players[playerId];
// Update position
indicator.setPosition(sprite.x, sprite.y - 30);
// Change color based on state
if (player?.isPoweredUp) {
indicator.setFillStyle(0xffff00); // Yellow when powered up
} else if (player?.isInvulnerable) {
indicator.setFillStyle(0x00ffff); // Cyan when invulnerable
} else {
indicator.setFillStyle(0x00ff00); // Green normally
}
},
destroy: () => indicator.destroy()
});
} Performance-Optimized Updates
Throttle updates for expensive operations:
function createThrottledAttachment(
scene: Phaser.Scene,
sprite: any,
updateIntervalMs: number = 100
) {
const visual = scene.add.graphics();
let lastUpdate = 0;
return createSpriteAttachment(scene, sprite, {
update: () => {
const now = Date.now();
// Only update every N milliseconds
if (now - lastUpdate < updateIntervalMs) {
return;
}
lastUpdate = now;
// Expensive operation (e.g., complex graphics rendering)
visual.clear();
visual.lineStyle(2, 0xff0000);
visual.strokeCircle(sprite.x, sprite.y, 50);
// ... more expensive operations
},
destroy: () => visual.destroy()
});
} Creating Reusable Attachment Helpers
Package your attachments as reusable functions:
// my-game/attachments/createPowerUpGlow.ts
import { createSpriteAttachment, type SpriteAttachment } from '@martini-kit/phaser';
export interface PowerUpGlowConfig {
color?: number;
radius?: number;
pulseSpeed?: number;
}
export function createPowerUpGlow(
scene: Phaser.Scene,
sprite: any,
config: PowerUpGlowConfig = {}
): SpriteAttachment {
const color = config.color ?? 0xffff00;
const radius = config.radius ?? 40;
const pulseSpeed = config.pulseSpeed ?? 200;
const glow = scene.add.circle(sprite.x, sprite.y, radius, color, 0.4);
glow.setBlendMode(Phaser.BlendModes.ADD);
return createSpriteAttachment(scene, sprite, {
update: () => {
glow.setPosition(sprite.x, sprite.y);
const scale = 1 + Math.sin(Date.now() / pulseSpeed) * 0.2;
glow.setScale(scale);
},
destroy: () => glow.destroy(),
getGameObject: () => glow
});
}
// Usage across your game:
import { createPowerUpGlow } from './attachments/createPowerUpGlow';
onCreate: (key, data) => {
const sprite = this.add.sprite(data.x, data.y, 'player');
if (data.hasPowerUp) {
createPowerUpGlow(this, sprite, {
color: 0xff00ff,
pulseSpeed: 150
});
}
return sprite;
} API Reference
createSpriteAttachment
Creates a sprite attachment with automatic lifecycle management.
function createSpriteAttachment(
scene: Phaser.Scene,
sprite: any,
attachment: SpriteAttachment,
config?: SpriteAttachmentConfig
): SpriteAttachment Parameters:
scene- Phaser scene instancesprite- Sprite to attach toattachment- Attachment implementation (update + destroy functions)config- Optional configurationautoUpdate?: boolean- Enable automatic updates (default:true)
Returns: Enhanced attachment with lifecycle management
createSpriteAttachments
Creates multiple attachments at once.
function createSpriteAttachments(
scene: Phaser.Scene,
sprite: any,
attachments: SpriteAttachment[],
config?: SpriteAttachmentConfig
): SpriteAttachment[] createCompositeAttachment
Combines multiple child attachments into one.
function createCompositeAttachment(
scene: Phaser.Scene,
sprite: any,
children: SpriteAttachment[],
config?: SpriteAttachmentConfig
): SpriteAttachment Best Practices
✅ Do
- Use autoUpdate (default) - It’s the pit of success
- Attach in onCreate/onAdd - Ensures proper lifecycle
- Package as reusable helpers - DRY principle
- Keep update() fast - Called every frame
- Use composite for complex attachments - Better organization
- Leverage TypeScript - Type-safe configs
❌ Don’t
- Don’t disable autoUpdate unless you have a specific reason
- Don’t create attachments in update() - Too expensive
- Don’t forget destroy cleanup - Always destroy visual elements
- Don’t call update() manually with autoUpdate: true - Redundant
- Don’t use setInterval/setTimeout - Use scene events instead
Performance Considerations
Memory Usage
Each attachment with autoUpdate: true adds:
- 1 event listener on
scene.events - 1 event listener on
sprite(for destroy) - Minimal overhead (~100 bytes per attachment)
Rule of thumb: < 100 attachments = negligible performance impact
Update Performance
The update() function is called every frame (60 fps).
Keep it fast:
// ✅ Good - simple position update
update: () => {
circle.setPosition(sprite.x + 10, sprite.y - 20);
}
// ❌ Bad - expensive every frame
update: () => {
// Complex physics calculations
// Network requests
// Heavy graphics rendering
} For expensive operations, use throttling or conditional updates (see examples above).
Cleanup Performance
Cleanup is automatic and efficient:
- Event listeners removed immediately
- Visual elements destroyed via Phaser’s pooling
- No manual tracking required
Troubleshooting
Attachment Not Updating
Problem: Visual element doesn’t follow sprite.
Solutions:
- Ensure
autoUpdate: true(default) - Check that
update()is implemented - Verify sprite has valid x/y properties
// Debug: Log to verify updates
update: () => {
console.log('Updating attachment', sprite.x, sprite.y);
visual.setPosition(sprite.x, sprite.y);
} Memory Leak
Problem: Attachments persist after scene change.
Solutions:
- Use
autoUpdate: truefor automatic cleanup - If using manual mode, call
destroy()explicitly - Don’t prevent sprite destroy events
// ✅ Good - auto-cleanup
createSpriteAttachment(scene, sprite, { ... });
// ❌ Bad - manual cleanup required
const attachment = createSpriteAttachment(scene, sprite, { ... }, { autoUpdate: false });
// Must call attachment.destroy() manually! Double Destroy Error
Problem: “Cannot read property of null” on scene change.
Solution: The system prevents double-destroy automatically. If you see this:
- Don’t call
destroy()manually with autoUpdate: true - Don’t override sprite’s destroy event
See Also
- Directional Indicator - Built-in arrow attachment
- Sprite Manager - Sprite lifecycle management
- Phaser Helpers - All available helpers