16 KiB
Game Development Guidelines
Overview
This document establishes coding standards, architectural patterns, and development practices for 2D game development using Phaser 3 and TypeScript. These guidelines ensure consistency, performance, and maintainability across all game development stories.
TypeScript Standards
Strict Mode Configuration
Required tsconfig.json settings:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}
Type Definitions
Game Object Interfaces:
// Core game entity interface
interface GameEntity {
readonly id: string;
position: Phaser.Math.Vector2;
active: boolean;
destroy(): void;
}
// Player controller interface
interface PlayerController {
readonly inputEnabled: boolean;
handleInput(input: InputState): void;
update(delta: number): void;
}
// Game system interface
interface GameSystem {
readonly name: string;
initialize(): void;
update(delta: number): void;
shutdown(): void;
}
Scene Data Interfaces:
// Scene transition data
interface SceneData {
[key: string]: any;
}
// Game state interface
interface GameState {
currentLevel: number;
score: number;
lives: number;
settings: GameSettings;
}
interface GameSettings {
musicVolume: number;
sfxVolume: number;
difficulty: 'easy' | 'normal' | 'hard';
controls: ControlScheme;
}
Naming Conventions
Classes and Interfaces:
- PascalCase for classes:
PlayerSprite,GameManager,AudioSystem - PascalCase with 'I' prefix for interfaces:
IGameEntity,IPlayerController - Descriptive names that indicate purpose:
CollisionManagernotCM
Methods and Variables:
- camelCase for methods and variables:
updatePosition(),playerSpeed - Descriptive names:
calculateDamage()notcalcDmg() - Boolean variables with is/has/can prefix:
isActive,hasCollision,canMove
Constants:
- UPPER_SNAKE_CASE for constants:
MAX_PLAYER_SPEED,DEFAULT_VOLUME - Group related constants in enums or const objects
Files and Directories:
- kebab-case for file names:
player-controller.ts,audio-manager.ts - PascalCase for scene files:
MenuScene.ts,GameScene.ts
Phaser 3 Architecture Patterns
Scene Organization
Scene Lifecycle Management:
class GameScene extends Phaser.Scene {
private gameManager!: GameManager;
private inputManager!: InputManager;
constructor() {
super({ key: 'GameScene' });
}
preload(): void {
// Load only scene-specific assets
this.load.image('player', 'assets/player.png');
}
create(data: SceneData): void {
// Initialize game systems
this.gameManager = new GameManager(this);
this.inputManager = new InputManager(this);
// Set up scene-specific logic
this.setupGameObjects();
this.setupEventListeners();
}
update(time: number, delta: number): void {
// Update all game systems
this.gameManager.update(delta);
this.inputManager.update(delta);
}
shutdown(): void {
// Clean up resources
this.gameManager.destroy();
this.inputManager.destroy();
// Remove event listeners
this.events.off('*');
}
}
Scene Transitions:
// Proper scene transitions with data
this.scene.start('NextScene', {
playerScore: this.playerScore,
currentLevel: this.currentLevel + 1,
});
// Scene overlays for UI
this.scene.launch('PauseMenuScene');
this.scene.pause();
Game Object Patterns
Component-Based Architecture:
// Base game entity
abstract class GameEntity extends Phaser.GameObjects.Sprite {
protected components: Map<string, GameComponent> = new Map();
constructor(scene: Phaser.Scene, x: number, y: number, texture: string) {
super(scene, x, y, texture);
scene.add.existing(this);
}
addComponent<T extends GameComponent>(component: T): T {
this.components.set(component.name, component);
return component;
}
getComponent<T extends GameComponent>(name: string): T | undefined {
return this.components.get(name) as T;
}
update(delta: number): void {
this.components.forEach((component) => component.update(delta));
}
destroy(): void {
this.components.forEach((component) => component.destroy());
this.components.clear();
super.destroy();
}
}
// Example player implementation
class Player extends GameEntity {
private movement!: MovementComponent;
private health!: HealthComponent;
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'player');
this.movement = this.addComponent(new MovementComponent(this));
this.health = this.addComponent(new HealthComponent(this, 100));
}
}
System Management
Singleton Managers:
class GameManager {
private static instance: GameManager;
private scene: Phaser.Scene;
private gameState: GameState;
constructor(scene: Phaser.Scene) {
if (GameManager.instance) {
throw new Error('GameManager already exists!');
}
this.scene = scene;
this.gameState = this.loadGameState();
GameManager.instance = this;
}
static getInstance(): GameManager {
if (!GameManager.instance) {
throw new Error('GameManager not initialized!');
}
return GameManager.instance;
}
update(delta: number): void {
// Update game logic
}
destroy(): void {
GameManager.instance = null!;
}
}
Performance Optimization
Object Pooling
Required for High-Frequency Objects:
class BulletPool {
private pool: Bullet[] = [];
private scene: Phaser.Scene;
constructor(scene: Phaser.Scene, initialSize: number = 50) {
this.scene = scene;
// Pre-create bullets
for (let i = 0; i < initialSize; i++) {
const bullet = new Bullet(scene, 0, 0);
bullet.setActive(false);
bullet.setVisible(false);
this.pool.push(bullet);
}
}
getBullet(): Bullet | null {
const bullet = this.pool.find((b) => !b.active);
if (bullet) {
bullet.setActive(true);
bullet.setVisible(true);
return bullet;
}
// Pool exhausted - create new bullet
console.warn('Bullet pool exhausted, creating new bullet');
return new Bullet(this.scene, 0, 0);
}
releaseBullet(bullet: Bullet): void {
bullet.setActive(false);
bullet.setVisible(false);
bullet.setPosition(0, 0);
}
}
Frame Rate Optimization
Performance Monitoring:
class PerformanceMonitor {
private frameCount: number = 0;
private lastTime: number = 0;
private frameRate: number = 60;
update(time: number): void {
this.frameCount++;
if (time - this.lastTime >= 1000) {
this.frameRate = this.frameCount;
this.frameCount = 0;
this.lastTime = time;
if (this.frameRate < 55) {
console.warn(`Low frame rate detected: ${this.frameRate} FPS`);
this.optimizePerformance();
}
}
}
private optimizePerformance(): void {
// Reduce particle counts, disable effects, etc.
}
}
Update Loop Optimization:
// Avoid expensive operations in update loops
class GameScene extends Phaser.Scene {
private updateTimer: number = 0;
private readonly UPDATE_INTERVAL = 100; // ms
update(time: number, delta: number): void {
// High-frequency updates (every frame)
this.updatePlayer(delta);
this.updatePhysics(delta);
// Low-frequency updates (10 times per second)
this.updateTimer += delta;
if (this.updateTimer >= this.UPDATE_INTERVAL) {
this.updateUI();
this.updateAI();
this.updateTimer = 0;
}
}
}
Input Handling
Cross-Platform Input
Input Abstraction:
interface InputState {
moveLeft: boolean;
moveRight: boolean;
jump: boolean;
action: boolean;
pause: boolean;
}
class InputManager {
private inputState: InputState = {
moveLeft: false,
moveRight: false,
jump: false,
action: false,
pause: false,
};
private keys!: { [key: string]: Phaser.Input.Keyboard.Key };
private pointer!: Phaser.Input.Pointer;
constructor(private scene: Phaser.Scene) {
this.setupKeyboard();
this.setupTouch();
}
private setupKeyboard(): void {
this.keys = this.scene.input.keyboard.addKeys('W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT');
}
private setupTouch(): void {
this.scene.input.on('pointerdown', this.handlePointerDown, this);
this.scene.input.on('pointerup', this.handlePointerUp, this);
}
update(): void {
// Update input state from multiple sources
this.inputState.moveLeft = this.keys.A.isDown || this.keys.LEFT.isDown;
this.inputState.moveRight = this.keys.D.isDown || this.keys.RIGHT.isDown;
this.inputState.jump = Phaser.Input.Keyboard.JustDown(this.keys.SPACE);
// ... handle touch input
}
getInputState(): InputState {
return { ...this.inputState };
}
}
Error Handling
Graceful Degradation
Asset Loading Error Handling:
class AssetManager {
loadAssets(): Promise<void> {
return new Promise((resolve, reject) => {
this.scene.load.on('filecomplete', this.handleFileComplete, this);
this.scene.load.on('loaderror', this.handleLoadError, this);
this.scene.load.on('complete', () => resolve());
this.scene.load.start();
});
}
private handleLoadError(file: Phaser.Loader.File): void {
console.error(`Failed to load asset: ${file.key}`);
// Use fallback assets
this.loadFallbackAsset(file.key);
}
private loadFallbackAsset(key: string): void {
// Load placeholder or default assets
switch (key) {
case 'player':
this.scene.load.image('player', 'assets/defaults/default-player.png');
break;
default:
console.warn(`No fallback for asset: ${key}`);
}
}
}
Runtime Error Recovery
System Error Handling:
class GameSystem {
protected handleError(error: Error, context: string): void {
console.error(`Error in ${context}:`, error);
// Report to analytics/logging service
this.reportError(error, context);
// Attempt recovery
this.attemptRecovery(context);
}
private attemptRecovery(context: string): void {
switch (context) {
case 'update':
// Reset system state
this.reset();
break;
case 'render':
// Disable visual effects
this.disableEffects();
break;
default:
// Generic recovery
this.safeShutdown();
}
}
}
Testing Standards
Unit Testing
Game Logic Testing:
// Example test for game mechanics
describe('HealthComponent', () => {
let healthComponent: HealthComponent;
beforeEach(() => {
const mockEntity = {} as GameEntity;
healthComponent = new HealthComponent(mockEntity, 100);
});
test('should initialize with correct health', () => {
expect(healthComponent.currentHealth).toBe(100);
expect(healthComponent.maxHealth).toBe(100);
});
test('should handle damage correctly', () => {
healthComponent.takeDamage(25);
expect(healthComponent.currentHealth).toBe(75);
expect(healthComponent.isAlive()).toBe(true);
});
test('should handle death correctly', () => {
healthComponent.takeDamage(150);
expect(healthComponent.currentHealth).toBe(0);
expect(healthComponent.isAlive()).toBe(false);
});
});
Integration Testing
Scene Testing:
describe('GameScene Integration', () => {
let scene: GameScene;
let mockGame: Phaser.Game;
beforeEach(() => {
// Mock Phaser game instance
mockGame = createMockGame();
scene = new GameScene();
});
test('should initialize all systems', () => {
scene.create({});
expect(scene.gameManager).toBeDefined();
expect(scene.inputManager).toBeDefined();
});
});
File Organization
Project Structure
src/
├── scenes/
│ ├── BootScene.ts # Initial loading and setup
│ ├── PreloadScene.ts # Asset loading with progress
│ ├── MenuScene.ts # Main menu and navigation
│ ├── GameScene.ts # Core gameplay
│ └── UIScene.ts # Overlay UI elements
├── gameObjects/
│ ├── entities/
│ │ ├── Player.ts # Player game object
│ │ ├── Enemy.ts # Enemy base class
│ │ └── Collectible.ts # Collectible items
│ ├── components/
│ │ ├── MovementComponent.ts
│ │ ├── HealthComponent.ts
│ │ └── CollisionComponent.ts
│ └── ui/
│ ├── Button.ts # Interactive buttons
│ ├── HealthBar.ts # Health display
│ └── ScoreDisplay.ts # Score UI
├── systems/
│ ├── GameManager.ts # Core game state management
│ ├── InputManager.ts # Cross-platform input handling
│ ├── AudioManager.ts # Sound and music system
│ ├── SaveManager.ts # Save/load functionality
│ └── PerformanceMonitor.ts # Performance tracking
├── utils/
│ ├── ObjectPool.ts # Generic object pooling
│ ├── MathUtils.ts # Game math helpers
│ ├── AssetLoader.ts # Asset management utilities
│ └── EventBus.ts # Global event system
├── types/
│ ├── GameTypes.ts # Core game type definitions
│ ├── UITypes.ts # UI-related types
│ └── SystemTypes.ts # System interface definitions
├── config/
│ ├── GameConfig.ts # Phaser game configuration
│ ├── GameBalance.ts # Game balance parameters
│ └── AssetConfig.ts # Asset loading configuration
└── main.ts # Application entry point
Development Workflow
Story Implementation Process
-
Read Story Requirements:
- Understand acceptance criteria
- Identify technical requirements
- Review performance constraints
-
Plan Implementation:
- Identify files to create/modify
- Consider component architecture
- Plan testing approach
-
Implement Feature:
- Follow TypeScript strict mode
- Use established patterns
- Maintain 60 FPS performance
-
Test Implementation:
- Write unit tests for game logic
- Test cross-platform functionality
- Validate performance targets
-
Update Documentation:
- Mark story checkboxes complete
- Document any deviations
- Update architecture if needed
Code Review Checklist
Before Committing:
- TypeScript compiles without errors
- All tests pass
- Performance targets met (60 FPS)
- No console errors or warnings
- Cross-platform compatibility verified
- Memory usage within bounds
- Code follows naming conventions
- Error handling implemented
- Documentation updated
Performance Targets
Frame Rate Requirements
- Desktop: Maintain 60 FPS at 1080p
- Mobile: Maintain 60 FPS on mid-range devices, minimum 30 FPS on low-end
- Optimization: Implement dynamic quality scaling when performance drops
Memory Management
- Total Memory: Under 100MB for full game
- Per Scene: Under 50MB per gameplay scene
- Asset Loading: Progressive loading to stay under limits
- Garbage Collection: Minimize object creation in update loops
Loading Performance
- Initial Load: Under 5 seconds for game start
- Scene Transitions: Under 2 seconds between scenes
- Asset Streaming: Background loading for upcoming content
These guidelines ensure consistent, high-quality game development that meets performance targets and maintains code quality across all implementation stories.