Skip to content
Back to skills

Game Arch Patterns

BSecurity

Game architecture code patterns: a fixed timestep loop with accumulator, an EventBus, ECS-style composition in Godot, Resource-based game data, an autoload service locator, and a game state stack. Use when laying out the core structure of a game or untangling one that has grown messy.

  • 65 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 1, 2026
developmentgonode

Security analysis

B75/100
  • criticalAccesses sensitive system or user directories

Pro shows the line behind each finding and how to fix it

Scanned October 1, 2026

npx -y skills add HermeticOrmus/claude-code-game-development --skill game-arch-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Game Arch Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Game Arch Patterns
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/hermeticormus-game-arch-patterns/badge)](https://www.skillsdirectory.com/skills/hermeticormus-game-arch-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: game-arch-patterns
description: "Game architecture code patterns: a fixed timestep loop with accumulator, an EventBus, ECS-style composition in Godot, Resource-based game data, an autoload service locator, and a game state stack. Use when laying out the core structure of a game or untangling one that has grown messy."
---

# Game Architecture Patterns

## Fixed Timestep Game Loop with Accumulator

```gdscript
# Fixed physics timestep decoupled from variable render rate
# Based on Glenn Fiedler's "Fix Your Timestep!" (2004)
class_name GameLoop extends Node
const PHYSICS_DELTA: float = 1.0 / 60.0  # Fixed 60Hz physics
const MAX_DELTA: float = 0.25            # Spiral-of-death prevention

var _accumulator: float = 0.0
var _previous_time: float = 0.0
var _alpha: float = 0.0  # Interpolation factor for rendering

func _ready() -> void:
    _previous_time = Time.get_ticks_msec() / 1000.0

func _process(_delta: float) -> void:
    var current_time := Time.get_ticks_msec() / 1000.0
    var frame_time := min(current_time - _previous_time, MAX_DELTA)
    _previous_time = current_time
    _accumulator += frame_time

    # Fixed timestep physics integration
    while _accumulator >= PHYSICS_DELTA:
        _physics_step(PHYSICS_DELTA)
        _accumulator -= PHYSICS_DELTA

    # Render interpolation between previous and current state
    _alpha = _accumulator / PHYSICS_DELTA
    _render(_alpha)

func _physics_step(delta: float) -> void:
    # Deterministic physics: same delta every time
    pass

func _render(alpha: float) -> void:
    # Interpolate render positions between previous_state and current_state
    # position = lerp(previous_position, current_position, alpha)
    pass
# Note: Godot's built-in _physics_process IS a fixed timestep loop.
# Use _physics_process for physics, _process for visual interpolation.
```

## EventBus (Decoupled Signal System)

```gdscript
# Global EventBus autoload - replaces Singleton coupling
# Add as autoload: EventBus -> res://systems/event_bus.gd
class_name EventBus extends Node

# Typed event signals - define all game events here
signal player_died(player_id: int)
signal enemy_spawned(enemy: Node, position: Vector3)
signal item_collected(item_id: StringName, player_id: int)
signal level_completed(level_id: int, score: int)
signal health_changed(entity_id: int, old_health: float, new_health: float)
signal game_state_changed(old_state: int, new_state: int)

# Usage from any script (no direct reference needed):
# EventBus.player_died.emit(player.id)
# EventBus.player_died.connect(_on_player_died)
```

```gdscript
# Resource-as-event-channel pattern (decentralized, typed)
class_name GameEvent extends Resource
signal triggered(data: Dictionary)

func trigger(data: Dictionary = {}) -> void:
    triggered.emit(data)

# Usage: create GameEvent Resources in editor, reference by @export
# Emitter:  player_died_event.trigger({"player_id": id})
# Receiver: player_died_event.triggered.connect(_on_player_died)
```

## ECS Approximation in Godot (Composition Pattern)

```gdscript
# Godot uses scene tree composition, not true ECS.
# Approximate ECS with component nodes.

# "Entity" = CharacterBody3D with component children
# Node: Player (CharacterBody3D)
#   ChildNode: HealthComponent
#   ChildNode: MovementComponent
#   ChildNode: WeaponComponent
#   ChildNode: InputComponent

class_name HealthComponent extends Node
signal died(entity: Node)
signal health_changed(old_val: float, new_val: float)

@export var max_health: float = 100.0
var current_health: float:
    get: return _current_health
    set(value):
        var old := _current_health
        _current_health = clamp(value, 0.0, max_health)
        health_changed.emit(old, _current_health)
        if _current_health <= 0.0:
            died.emit(get_parent())

var _current_health: float

func _ready() -> void:
    _current_health = max_health

func take_damage(amount: float) -> void:
    current_health -= amount

func heal(amount: float) -> void:
    current_health += amount
```

## Resource-Based Game Data (ScriptableObject Equivalent)

```gdscript
# Data container as Resource - store in .tres files
class_name WeaponData extends Resource
@export var weapon_name: String
@export var damage: float = 10.0
@export var fire_rate: float = 0.5         # shots per second
@export var projectile_speed: float = 30.0
@export var ammo_capacity: int = 30
@export var reload_time: float = 1.5
@export var projectile_scene: PackedScene
@export var fire_sound: AudioStream
@export var weapon_icon: Texture2D

# Use in weapon script:
# @export var weapon_data: WeaponData
# var damage := weapon_data.damage
```

## Service Locator (Autoload Pattern)

```gdscript
# Services autoload - controlled global access
# Avoids direct cross-singleton coupling
class_name Services extends Node
var _registry: Dictionary = {}

func register(service_name: StringName, service: Object) -> void:
    _registry[service_name] = service

func get_service(service_name: StringName) -> Object:
    if service_name in _registry:
        return _registry[service_name]
    push_error("Service not registered: %s" % service_name)
    return null

# Null service pattern: register no-op service at startup
func register_null_service(service_name: StringName, null_service: Object) -> void:
    if service_name not in _registry:
        _registry[service_name] = null_service

# Usage:
# Services.register(&"AudioManager", $AudioManager)
# var audio: AudioManager = Services.get_service(&"AudioManager")
```

## Game State Stack

```gdscript
class_name GameStateStack extends Node
enum GameState { GAMEPLAY, PAUSE, INVENTORY, DIALOGUE, GAME_OVER, MAIN_MENU }

var _state_stack: Array[GameState] = []
signal state_changed(new_state: GameState, old_state: GameState)

func push_state(state: GameState) -> void:
    var old := current_state()
    _state_stack.push_back(state)
    _on_state_enter(state)
    state_changed.emit(state, old)

func pop_state() -> void:
    if _state_stack.is_empty():
        return
    var old := _state_stack.pop_back()
    _on_state_exit(old)
    if not _state_stack.is_empty():
        state_changed.emit(current_state(), old)

func current_state() -> GameState:
    return _state_stack.back() if not _state_stack.is_empty() else GameState.MAIN_MENU

func _on_state_enter(state: GameState) -> void:
    match state:
        GameState.PAUSE:
            get_tree().paused = true
            EventBus.game_state_changed.emit(GameState.GAMEPLAY, GameState.PAUSE)
        GameState.GAMEPLAY:
            get_tree().paused = false

func _on_state_exit(state: GameState) -> void:
    match state:
        GameState.PAUSE:
            get_tree().paused = false
```

## Anti-Patterns

- **God object / God scene**: Single 2000-line script managing everything. Decompose into component nodes with single responsibilities.
- **Everything as Autoload**: Autoloads (singletons) create hidden global state. Use them only for truly global systems (AudioManager, SaveSystem, EventBus). Not for gameplay objects.
- **Signals with too many parameters**: If a signal needs 6+ parameters, the event carries too much data. Use a typed data class/dictionary payload instead.
- **Direct scene path strings**: `get_node("/root/Game/Player/Health")` breaks if scene structure changes. Use @onready, signals, or the Service Locator.
- **Variable timestep physics**: Never use `_process(delta)` for physics simulation. Non-deterministic, framerate-dependent behavior. Use `_physics_process(delta)`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…