Architectural Patterns
This guide covers common architectural patterns for building scalable martini-kit games.
1. Component-Based Entities
For complex games, use component pattern:
// Define components
interface Position { x: number; y: number }
interface Velocity { vx: number; vy: number }
interface Health { current: number; max: number }
interface Sprite { key: string; scale: number }
// Compose entities
interface GameState {
entities: Record<string, {
position: Position;
velocity?: Velocity; // Optional components
health?: Health;
sprite?: Sprite;
}>;
}
// Systems operate on components
actions: {
applyVelocity: {
apply(state, context) {
for (const entity of Object.values(state.entities)) {
if (entity.position && entity.velocity) {
entity.position.x += entity.velocity.vx;
entity.position.y += entity.velocity.vy;
}
}
}
}
} 2. State Machines
For entity AI and game modes:
type EnemyState = 'idle' | 'patrol' | 'chase' | 'attack';
interface Enemy {
x: number;
y: number;
state: EnemyState;
target?: string; // Player ID
}
actions: {
updateEnemies: {
apply(state, context) {
for (const [id, enemy] of Object.entries(state.enemies)) {
switch (enemy.state) {
case 'idle':
// Check for nearby players
const nearbyPlayer = findNearestPlayer(state, enemy);
if (nearbyPlayer) {
enemy.state = 'chase';
enemy.target = nearbyPlayer;
}
break;
case 'chase':
// Move toward target
if (enemy.target && state.players[enemy.target]) {
const player = state.players[enemy.target];
const distance = Math.hypot(player.x - enemy.x, player.y - enemy.y);
if (distance < 50) {
enemy.state = 'attack';
}
} else {
enemy.state = 'idle';
enemy.target = undefined;
}
break;
case 'attack':
// Attack logic
break;
}
}
}
}
} 3. Event System
For decoupled game events:
interface GameState {
players: Record<string, Player>;
events: Array<{
type: 'player-hit' | 'item-collected' | 'level-complete';
data: any;
timestamp: number;
}>;
}
actions: {
hit: {
apply(state, context, { targetId }) {
state.players[targetId].health -= 10;
// Emit event
state.events.push({
type: 'player-hit',
data: { targetId, damage: 10 },
timestamp: Date.now()
});
}
}
}
// In scene - consume events
this.adapter.onChange((state, prevState) => {
const newEvents = state.events.slice(prevState?.events.length || 0);
for (const event of newEvents) {
switch (event.type) {
case 'player-hit':
this.playHitSound();
this.showDamageNumber(event.data.damage);
break;
// ...
}
}
}); 4. Input Buffering
For responsive controls:
interface GameState {
players: Record<string, {
x: number;
y: number;
inputBuffer: Array<{ action: string; timestamp: number }>;
}>;
}
actions: {
bufferInput: {
apply(state, context, { action }) {
const player = state.players[context.playerId];
player.inputBuffer.push({
action,
timestamp: Date.now()
});
// Keep only recent inputs
player.inputBuffer = player.inputBuffer.slice(-10);
}
},
processInputs: {
apply(state, context) {
for (const player of Object.values(state.players)) {
// Process buffered inputs
for (const input of player.inputBuffer) {
// Apply input action
}
player.inputBuffer = [];
}
}
}
} 5. Pointer Input with Camera Scrolling
**Critical:** Always use world coordinates for pointer input, not screen coordinates!
When your camera follows the player (or scrolls for any reason), pointer.x/y gives screen coordinates, not world coordinates. This causes bugs where clicks appear at the wrong position.
❌ Wrong - Uses screen coordinates:
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
runtime.submitAction('move', {
x: pointer.x, // ⚠️ WRONG! Screen position, not world position
y: pointer.y
});
}); ✅ Correct - Uses world coordinates:
// Option 1: Use pointer.worldX/worldY directly
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
runtime.submitAction('move', {
x: pointer.worldX, // ✅ World position (accounts for camera scroll)
y: pointer.worldY
});
});
// Option 2: Use PhaserAdapter helper
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
const worldPos = adapter.pointerToWorld(pointer);
runtime.submitAction('move', {
x: worldPos.x,
y: worldPos.y
});
});