@hiddentao/clockwork-engine
Version:
A TypeScript/PIXI.js game engine for deterministic, replayable games with built-in rendering
295 lines (235 loc) • 8.51 kB
Markdown
# Rendering System Architecture
The Clockwork engine provides a complete 2D rendering system built on PIXI.js, designed for high-performance game graphics with deterministic behavior.
## Core Components
### GameCanvas
`GameCanvas` is an abstract base class that creates and manages a PIXI.js application with viewport controls.
**Key Features:**
- PIXI.js application lifecycle management
- Viewport integration with pixi-viewport for camera controls (pan, zoom, drag)
- Automatic update loop integration with game engine
- Responsive canvas resizing
- Event handling and user input processing
- Game layer initialization through abstract methods
**Usage:**
```typescript
class MyGameCanvas extends GameCanvas {
protected initializeGameLayers(): void {
// Register renderers for different game object types
this.addRenderer("Player", new PlayerRenderer(this.gameContainer))
this.addRenderer("Enemy", new EnemyRenderer(this.gameContainer))
}
}
// Create canvas instance
const canvas = await MyGameCanvas.create(domElement, {
width: 800,
height: 600,
worldWidth: 2000,
worldHeight: 2000,
backgroundColor: 0x1a1a2e,
antialias: false,
enableDrag: true,
enablePinch: true,
enableWheel: true,
minZoom: 0.5,
maxZoom: 2.0,
initialZoom: 1.0
})
```
### AbstractRenderer<T>
`AbstractRenderer<T>` is a generic base class for creating game object renderers with built-in PIXI container management.
**Key Features:**
- Generic typing for specific game object types
- Automatic PIXI container lifecycle management
- Helper methods for common graphics operations
- Efficient batch operations (add/update/remove/setItems)
- Named child management for complex graphics
**Required Methods:**
- `create(gameObject: T): PIXI.Container` - Creates initial PIXI graphics
- `getId(gameObject: T): string` - Returns unique identifier for the game object
**Optional Methods:**
- `updateContainer(container: PIXI.Container, gameObject: T): void` - Updates existing container
**Helper Methods:**
- `createRectangle(width, height, color, x?, y?)` - Creates rectangle graphics
- `createCircle(radius, color, x?, y?)` - Creates circle graphics
- `addNamedChild(parent, child, name)` - Adds child with name reference
- `getNamedChild(parent, name)` - Retrieves child by name
**Example Implementation:**
```typescript
export class SnakeRenderer extends AbstractRenderer<Snake> {
constructor(gameContainer: PIXI.Container) {
super(gameContainer)
}
protected create(snake: Snake): PIXI.Container {
const container = new PIXI.Container()
this.updateSnakeSegments(container, snake)
return container
}
protected updateContainer(container: PIXI.Container, snake: Snake): void {
this.updateSnakeSegments(container, snake)
}
private updateSnakeSegments(container: PIXI.Container, snake: Snake): void {
const segments = snake.getSegments()
const cellSize = GAME_CONFIG.CELL_SIZE
// Clear existing segments
container.removeChildren()
// Create graphics for each segment
segments.forEach((segment, index) => {
const isHead = index === 0
const color = isHead ? 0x00ff00 : 0x008800
const segmentGraphic = this.createRectangle(
cellSize - 2,
cellSize - 2,
color,
1, // center within cell
1
)
const segmentContainer = new PIXI.Container()
segmentContainer.position.set(
segment.position.x * cellSize,
segment.position.y * cellSize
)
segmentContainer.addChild(segmentGraphic)
container.addChild(segmentContainer)
})
}
public getId(snake: Snake): string {
return snake.getId()
}
}
```
### BaseRenderer Interface
`BaseRenderer` defines the standard interface that all renderers must implement:
```typescript
interface BaseRenderer {
add(item: any): void
update(item: any): void
remove(itemId: string): void
setItems(items: any[]): void
}
```
## Positioning and Coordinates
The rendering system uses a grid-based coordinate system where:
- **Game coordinates** are logical positions (e.g., grid cells)
- **PIXI coordinates** are pixel positions on screen
- **Conversion**: `pixelPosition = gamePosition * cellSize`
### Best Practices for Positioning
1. **Grid-aligned objects** (walls, tiles):
```typescript
container.position.set(
gamePosition.x * cellSize,
gamePosition.y * cellSize
)
```
2. **Centered objects** (circles, sprites):
```typescript
container.position.set(
gamePosition.x * cellSize + cellSize / 2,
gamePosition.y * cellSize + cellSize / 2
)
```
3. **Inset objects** (with borders):
```typescript
const graphic = this.createRectangle(
cellSize - 2, // smaller size
cellSize - 2,
color,
1, // offset to center
1
)
```
## Integration with Game Engine
The rendering system integrates seamlessly with the Clockwork game engine:
### Canvas Setup
```typescript
// 1. Create game engine
const engine = new MyGameEngine()
// 2. Create and configure canvas
const canvas = await MyGameCanvas.create(domElement, config)
// 3. Associate engine with canvas
canvas.setGameEngine(engine)
// 4. Engine automatically calls renderers during update loop
```
### Automatic Rendering
The GameCanvas automatically:
- Calls renderers when game objects are added/updated/removed
- Manages PIXI container lifecycle
- Handles viewport updates and camera controls
- Processes user input events
### Renderer Registration
Renderers are registered by game object type in `initializeGameLayers()`:
```typescript
protected initializeGameLayers(): void {
// Type string must match GameObject.getType()
this.addRenderer("Snake", new SnakeRenderer(this.gameContainer))
this.addRenderer("Apple", new AppleRenderer(this.gameContainer))
this.addRenderer("Wall", new WallRenderer(this.gameContainer))
}
```
## Performance Considerations
### Efficient Updates
- Use `updateContainer()` for dynamic objects that change frequently
- Minimize `removeChildren()` calls - reuse containers when possible
- Use object pooling for frequently created/destroyed objects
### Graphics Optimization
- Use `PIXI.Graphics` for simple shapes
- Use `PIXI.Sprite` for textured objects
- Consider `PIXI.ParticleContainer` for many similar objects
- Set `antialias: false` for pixel-perfect graphics
### Memory Management
- AbstractRenderer automatically handles container cleanup
- Use `destroy()` methods on PIXI objects when manually managing lifecycle
- Avoid memory leaks by removing event listeners
## Example: Complete Renderer Implementation
```typescript
export class AppleRenderer extends AbstractRenderer<Apple> {
constructor(gameContainer: PIXI.Container) {
super(gameContainer)
}
protected create(apple: Apple): PIXI.Container {
const cellSize = GAME_CONFIG.CELL_SIZE
const position = apple.getPosition()
// Create container positioned at cell center
const container = new PIXI.Container()
container.position.set(
position.x * cellSize + cellSize / 2,
position.y * cellSize + cellSize / 2
)
// Create main apple body
const appleBody = this.createCircle(
(cellSize - 4) / 2, // radius
0xff0000 // red color
)
this.addNamedChild(container, appleBody, "body")
// Create highlight
const highlight = this.createCircle(
(cellSize - 4) / 8, // smaller radius
0xff6666, // lighter red
-(cellSize - 4) / 6, // offset left
-(cellSize - 4) / 6 // offset up
)
this.addNamedChild(container, highlight, "highlight")
return container
}
protected updateContainer(container: PIXI.Container, apple: Apple): void {
const cellSize = GAME_CONFIG.CELL_SIZE
const position = apple.getPosition()
// Update position
container.position.set(
position.x * cellSize + cellSize / 2,
position.y * cellSize + cellSize / 2
)
// Update alpha based on age
container.alpha = apple.getAlpha()
// Apply pulse effect
const pulse = Math.sin(Date.now() * 0.005) * 0.1 + 0.9
const body = this.getNamedChild(container, "body")
const highlight = this.getNamedChild(container, "highlight")
if (body) body.scale.set(pulse)
if (highlight) highlight.scale.set(pulse)
}
public getId(apple: Apple): string {
return apple.getId()
}
}
```
This rendering system provides a powerful, flexible foundation for creating high-performance 2D games with the Clockwork engine.