Roguelike Camera Architecture: Multi-Layer Viewport, Spatial Query, and LOD Systems
A complete pattern guide for building performant camera systems in roguelike games across Phaser 3, Unity, and Godot engines.
---
Why Single-Camera Systems Fail in Roguelikes
Traditional single-camera approaches break down in roguelikes because the camera must serve multiple roles simultaneously:
- Multi-layer world: Background tilemap, foreground tilemap, entities, particles, UI — each with different parallax scroll speeds and LOD requirements
- Spatial query hub: "What entities are currently visible?", "Are there enemies within 5 tiles of the player?"
- Entity activation hub: Entities outside camera view need reduced update frequency or deactivation for performance
- Event trigger: Entities entering/exiting screen trigger summons, script events, or audio cues
This guide presents a 5-layer CameraArchitecture pattern that integrates all these responsibilities.
---
Layer 1: Camera Rig (Smart Tracking)
class CameraRig {
private state: CameraState = CameraState.FOLLOW;
private bounds: Rectangle;
private target: GameObject;
update(delta: number) {
switch (this.state) {
case CameraState.FOLLOW:
const targetPos = this.predictPosition(this.target, delta);
this.virtualCam.panTo(targetPos, delta);
break;
case CameraState.LOCKED:
this.clampTargetToView();
break;
case CameraState.TRANSITION:
this.followPath(delta);
break;
}
this.clampToBounds();
}
private predictPosition(target: GameObject, dt: number): Vector2 {
return target.position.add(target.velocity.multiply(dt * 5));
}
}
Room-Based Camera Lock (Dead Cells Pattern)
class RoomCameraLock {
private currentRoom: Rectangle | null = null;
update(playerPos: Vector2) {
if (this.currentRoom && this.currentRoom.contains(playerPos)) {
this.state = CameraState.LOCKED;
return;
}
const nextRoom = this.findRoomAt(playerPos);
if (nextRoom && nextRoom !== this.currentRoom) {
this.transitionTo(nextRoom);
}
}
}
---
Layer 2: LOD & Culling Controller
Distance-based 3-zone activation system:
┌──────────────────────────────────┐
│ FAR ZONE (Dormant) │ No updates
│ ┌────────────────────────┐ │
│ │ MID ZONE (Low Freq) │ │ 5fps updates
│ │ ┌──────────────┐ │ │
│ │ │ NEAR ZONE │ │ │ Full 60fps
│ │ │ (Active) │ │ │
│ │ │ ◉ Player │ │ │
│ │ └──────────────┘ │ │
│ └────────────────────────┘ │
└──────────────────────────────────┘
enum EntityActivity {
ACTIVE, // Full updates every frame
LOW_FREQ, // 5-10fps updates
DORMANT, // Updates stopped
SLEEPING, // Virtualized in memory
}
class ActivityManager {
update(camera: Camera) {
const camCenter = camera.center;
for (const entity of this.world.entities) {
const dist = Vector2.distance(camCenter, entity.position);
if (dist < this.nearRadius) {
entity.activity = EntityActivity.ACTIVE;
} else if (dist < this.midRadius) {
entity.activity = EntityActivity.LOW_FREQ;
} else {
entity.activity = EntityActivity.DORMANT;
}
}
}
}
---
Layer 3: Spatial Query System
Essential for roguelike game logic:
class SpatialHashGrid {
private cellSize: number = 64;
private cells: Map<string, Set<Entity>>;
key(x: number, y: number): string {
const cx = Math.floor(x / this.cellSize);
const cy = Math.floor(y / this.cellSize);
return `${cx},${cy}`;
}
queryRect(rect: Rectangle): Entity[] {
// O(1) lookup for entities in rectangle area
}
queryCircle(center: Vector2, radius: number): Entity[] {
// Range queries for AoE effects
}
}
---
Layer 4: Screen Effects Pipeline
class ScreenEffectsPipeline {
onDamage(event: DamageEvent) {
if (event.isCritical) {
this.shake.intensity(8, 150);
this.hitStop.duration(80);
this.slowMotion.scale(0.3, 200);
}
}
onPlayerDeath() {
this.shake.intensity(12, 500);
this.hitStop.duration(300);
this.slowMotion.scale(0.1, 500);
}
}
---
Layer 5: Viewport Composer
Three-camera setup for final rendering:
class CameraComposition {
constructor(scene) {
this.worldCamera = new CameraRig(cam);
this.fxCamera = scene.add.camera(0, 0, cam.width, cam.height);
this.uiCamera = scene.add.camera(0, 0, cam.width, cam.height);
}
}
---
Engine-Specific Implementation Notes
| Game | Camera Style | Active System | Spatial Query |
|---|---|---|---|
| Hades | Dynamic zoom | Room-based | Room-level |
| Dead Cells | Room-lock | Room toggle | None (room-level) |
| Risk of Rain 2 | 3D tracking | Distance-based | Sphere cast |
| Vampire Survivors | Player follow | Full active | None |
---
Performance Budget (60fps Reference)
16.6ms frame budget:
├─ Rendering: 8ms (48%)
├─ Spatial Query: 1ms (6%)
├─ Effects: 2ms (12%)
└─ Entity Update: 4ms (24%)
---