Expert25 min readJuly 28, 2025

Game Architecture Patterns in Lua: Building Scalable Games

By Michael Thompson

Building maintainable and scalable games requires thoughtful architecture from the start. This comprehensive guide explores proven architectural patterns specifically adapted for Lua game development, helping you create games that can grow in complexity without becoming unmaintainable nightmares.

The Foundation: Why Architecture Matters

Game development is unique in software engineering. Games require real-time performance, complex state management, and the ability to iterate quickly on gameplay ideas. Poor architecture decisions early in development can compound into technical debt that strangles productivity and performance.

In Lua, where dynamic typing and flexible data structures provide both power and pitfalls, following established architectural patterns becomes even more crucial. The patterns we'll explore have been battle-tested in countless games and adapted specifically for Lua's strengths and characteristics.

Core Architectural Principles

  • Separation of Concerns: Each module has a single responsibility
  • Loose Coupling: Components don't depend directly on each other
  • High Cohesion: Related functionality is grouped together
  • Extensibility: New features can be added without massive refactoring
  • Testability: Components can be tested in isolation

Pattern 1: Entity Component System (ECS)

The Entity Component System is perhaps the most popular architectural pattern in modern game development. It provides excellent flexibility for managing game objects with varying behaviors and properties.

ECS Fundamentals

ECS separates data (Components) from behavior (Systems) and uses lightweight identifiers (Entities) to associate them. This creates a highly flexible and performant architecture.

Basic ECS Implementation

-- Entity Component System implementation
local ECS = {}
ECS.__index = ECS

function ECS:new()
    return setmetatable({
        entities = {},
        components = {},
        systems = {},
        nextEntityId = 1,
        componentTypes = {}
    }, ECS)
end

-- Entity management
function ECS:createEntity()
    local id = self.nextEntityId
    self.nextEntityId = self.nextEntityId + 1
    self.entities[id] = {}
    return id
end

function ECS:destroyEntity(entityId)
    -- Remove all components
    for componentType, _ in pairs(self.entities[entityId] or {}) do
        self:removeComponent(entityId, componentType)
    end
    self.entities[entityId] = nil
end

-- Component management
function ECS:addComponent(entityId, componentType, data)
    if not self.entities[entityId] then
        error("Entity does not exist: " .. entityId)
    end
    
    if not self.components[componentType] then
        self.components[componentType] = {}
    end
    
    self.components[componentType][entityId] = data
    self.entities[entityId][componentType] = true
end

function ECS:removeComponent(entityId, componentType)
    if self.components[componentType] then
        self.components[componentType][entityId] = nil
    end
    if self.entities[entityId] then
        self.entities[entityId][componentType] = nil
    end
end

function ECS:getComponent(entityId, componentType)
    if self.components[componentType] then
        return self.components[componentType][entityId]
    end
    return nil
end

function ECS:hasComponent(entityId, componentType)
    return self.entities[entityId] and 
           self.entities[entityId][componentType] == true
end

-- System management
function ECS:addSystem(system)
    system.ecs = self
    table.insert(self.systems, system)
    if system.init then
        system:init()
    end
end

function ECS:update(dt)
    for _, system in ipairs(self.systems) do
        if system.update then
            system:update(dt)
        end
    end
end

-- Query entities with specific components
function ECS:queryEntities(...)
    local requiredComponents = {...}
    local results = {}
    
    for entityId, entityComponents in pairs(self.entities) do
        local hasAll = true
        for _, componentType in ipairs(requiredComponents) do
            if not entityComponents[componentType] then
                hasAll = false
                break
            end
        end
        
        if hasAll then
            table.insert(results, entityId)
        end
    end
    
    return results
end

-- Component definitions
local Components = {}

Components.Position = function(x, y)
    return {x = x or 0, y = y or 0}
end

Components.Velocity = function(vx, vy)
    return {vx = vx or 0, vy = vy or 0}
end

Components.Sprite = function(texture, width, height)
    return {
        texture = texture,
        width = width or 32,
        height = height or 32,
        rotation = 0,
        scale = 1
    }
end

Components.Health = function(max)
    return {
        current = max or 100,
        maximum = max or 100
    }
end

Components.Input = function()
    return {
        left = false,
        right = false,
        up = false,
        down = false,
        action = false
    }
end

-- System implementations
local MovementSystem = {}
MovementSystem.__index = MovementSystem

function MovementSystem:new()
    return setmetatable({}, MovementSystem)
end

function MovementSystem:update(dt)
    local entities = self.ecs:queryEntities("Position", "Velocity")
    
    for _, entityId in ipairs(entities) do
        local pos = self.ecs:getComponent(entityId, "Position")
        local vel = self.ecs:getComponent(entityId, "Velocity")
        
        pos.x = pos.x + vel.vx * dt
        pos.y = pos.y + vel.vy * dt
    end
end

local RenderSystem = {}
RenderSystem.__index = RenderSystem

function RenderSystem:new()
    return setmetatable({}, RenderSystem)
end

function RenderSystem:update(dt)
    local entities = self.ecs:queryEntities("Position", "Sprite")
    
    for _, entityId in ipairs(entities) do
        local pos = self.ecs:getComponent(entityId, "Position")
        local sprite = self.ecs:getComponent(entityId, "Sprite")
        
        -- Render sprite at position
        -- love.graphics.draw(sprite.texture, pos.x, pos.y, 
        --                   sprite.rotation, sprite.scale, sprite.scale)
    end
end

-- Usage example
local function createGame()
    local ecs = ECS:new()
    
    -- Add systems
    ecs:addSystem(MovementSystem:new())
    ecs:addSystem(RenderSystem:new())
    
    -- Create player entity
    local player = ecs:createEntity()
    ecs:addComponent(player, "Position", Components.Position(100, 100))
    ecs:addComponent(player, "Velocity", Components.Velocity(0, 0))
    ecs:addComponent(player, "Sprite", Components.Sprite(playerTexture, 32, 32))
    ecs:addComponent(player, "Health", Components.Health(100))
    ecs:addComponent(player, "Input", Components.Input())
    
    -- Create enemy
    local enemy = ecs:createEntity()
    ecs:addComponent(enemy, "Position", Components.Position(200, 200))
    ecs:addComponent(enemy, "Velocity", Components.Velocity(-50, 0))
    ecs:addComponent(enemy, "Sprite", Components.Sprite(enemyTexture, 24, 24))
    ecs:addComponent(enemy, "Health", Components.Health(50))
    
    return ecs
end

Advanced ECS Features

A production-ready ECS needs additional features like component dependencies, system priorities, and efficient memory management:

-- Advanced ECS features
local AdvancedECS = setmetatable({}, {__index = ECS})

function AdvancedECS:new()
    local ecs = ECS.new(self)
    ecs.systemPriorities = {}
    ecs.componentPools = {}
    ecs.eventQueue = {}
    return ecs
end

-- System priority management
function AdvancedECS:addSystem(system, priority)
    ECS.addSystem(self, system)
    self.systemPriorities[system] = priority or 0
    
    -- Sort systems by priority
    table.sort(self.systems, function(a, b)
        return (self.systemPriorities[a] or 0) > (self.systemPriorities[b] or 0)
    end)
end

-- Component pooling for performance
function AdvancedECS:addComponent(entityId, componentType, data)
    if not self.componentPools[componentType] then
        self.componentPools[componentType] = {}
    end
    
    -- Try to reuse pooled component
    local component = table.remove(self.componentPools[componentType])
    if component then
        -- Reset component data
        for k, v in pairs(data) do
            component[k] = v
        end
        for k in pairs(component) do
            if not data[k] then
                component[k] = nil
            end
        end
    else
        component = data
    end
    
    ECS.addComponent(self, entityId, componentType, component)
end

function AdvancedECS:removeComponent(entityId, componentType)
    local component = self:getComponent(entityId, componentType)
    if component then
        -- Return to pool
        table.insert(self.componentPools[componentType], component)
    end
    
    ECS.removeComponent(self, entityId, componentType)
end

-- Event system for decoupled communication
function AdvancedECS:emit(eventType, data)
    if not self.eventQueue[eventType] then
        self.eventQueue[eventType] = {}
    end
    table.insert(self.eventQueue[eventType], data)
end

function AdvancedECS:processEvents()
    for eventType, events in pairs(self.eventQueue) do
        for _, system in ipairs(self.systems) do
            if system.handleEvent then
                for _, eventData in ipairs(events) do
                    system:handleEvent(eventType, eventData)
                end
            end
        end
        self.eventQueue[eventType] = {}
    end
end

function AdvancedECS:update(dt)
    -- Process events first
    self:processEvents()
    
    -- Update systems
    ECS.update(self, dt)
end

-- Example: Collision System with events
local CollisionSystem = {}
CollisionSystem.__index = CollisionSystem

function CollisionSystem:new()
    return setmetatable({}, CollisionSystem)
end

function CollisionSystem:update(dt)
    local entities = self.ecs:queryEntities("Position", "Sprite")
    
    -- Simple collision detection
    for i = 1, #entities do
        for j = i + 1, #entities do
            local entity1 = entities[i]
            local entity2 = entities[j]
            
            local pos1 = self.ecs:getComponent(entity1, "Position")
            local pos2 = self.ecs:getComponent(entity2, "Position")
            local sprite1 = self.ecs:getComponent(entity1, "Sprite")
            local sprite2 = self.ecs:getComponent(entity2, "Sprite")
            
            if self:checkCollision(pos1, sprite1, pos2, sprite2) then
                -- Emit collision event
                self.ecs:emit("collision", {
                    entity1 = entity1,
                    entity2 = entity2
                })
            end
        end
    end
end

function CollisionSystem:checkCollision(pos1, sprite1, pos2, sprite2)
    return pos1.x < pos2.x + sprite2.width and
           pos1.x + sprite1.width > pos2.x and
           pos1.y < pos2.y + sprite2.height and
           pos1.y + sprite1.height > pos2.y
end

-- Health System that responds to collision events
local HealthSystem = {}
HealthSystem.__index = HealthSystem

function HealthSystem:new()
    return setmetatable({}, HealthSystem)
end

function HealthSystem:handleEvent(eventType, eventData)
    if eventType == "collision" then
        -- Apply damage to entities with health
        local entity1 = eventData.entity1
        local entity2 = eventData.entity2
        
        local health1 = self.ecs:getComponent(entity1, "Health")
        local health2 = self.ecs:getComponent(entity2, "Health")
        
        if health1 and health2 then
            health1.current = health1.current - 10
            health2.current = health2.current - 10
            
            -- Check for death
            if health1.current <= 0 then
                self.ecs:emit("entity_death", {entity = entity1})
            end
            if health2.current <= 0 then
                self.ecs:emit("entity_death", {entity = entity2})
            end
        end
    elseif eventType == "entity_death" then
        self.ecs:destroyEntity(eventData.entity)
    end
end

Pattern 2: State Machine Architecture

State machines provide structure for managing complex game states and transitions. They're particularly useful for AI, game flow, and animation systems.

Hierarchical State Machine

Advanced State Machine Implementation

-- Hierarchical State Machine
local HSM = {}
HSM.__index = HSM

function HSM:new(initialState)
    return setmetatable({
        currentState = nil,
        states = {},
        globalState = nil,
        previousState = nil,
        stateStack = {},
        owner = nil
    }, HSM)
end

-- State class
local State = {}
State.__index = State

function State:new(name)
    return setmetatable({
        name = name,
        subStates = {},
        currentSubState = nil,
        parent = nil
    }, State)
end

function State:enter(owner) end
function State:execute(owner, dt) end
function State:exit(owner) end
function State:handleMessage(owner, message) return false end

-- Add sub-state functionality
function State:addSubState(subState)
    subState.parent = self
    self.subStates[subState.name] = subState
end

function State:changeSubState(newSubStateName)
    if self.currentSubState then
        self.currentSubState:exit(self.owner)
    end
    
    self.currentSubState = self.subStates[newSubStateName]
    if self.currentSubState then
        self.currentSubState:enter(self.owner)
    end
end

-- HSM methods
function HSM:addState(state)
    self.states[state.name] = state
end

function HSM:changeState(newStateName)
    if self.currentState then
        self.currentState:exit(self.owner)
        self.previousState = self.currentState
    end
    
    self.currentState = self.states[newStateName]
    if self.currentState then
        self.currentState.owner = self.owner
        self.currentState:enter(self.owner)
    end
end

function HSM:pushState(newStateName)
    if self.currentState then
        table.insert(self.stateStack, self.currentState)
    end
    self:changeState(newStateName)
end

function HSM:popState()
    if #self.stateStack > 0 then
        local previousState = table.remove(self.stateStack)
        self:changeState(previousState.name)
    end
end

function HSM:update(dt)
    if self.globalState then
        self.globalState:execute(self.owner, dt)
    end
    
    if self.currentState then
        self.currentState:execute(self.owner, dt)
        
        -- Update sub-states
        if self.currentState.currentSubState then
            self.currentState.currentSubState:execute(self.owner, dt)
        end
    end
end

function HSM:handleMessage(message)
    -- Try current sub-state first
    if self.currentState and self.currentState.currentSubState then
        if self.currentState.currentSubState:handleMessage(self.owner, message) then
            return true
        end
    end
    
    -- Try current state
    if self.currentState and self.currentState:handleMessage(self.owner, message) then
        return true
    end
    
    -- Try global state
    if self.globalState and self.globalState:handleMessage(self.owner, message) then
        return true
    end
    
    return false
end

-- Example: AI Character States
local IdleState = setmetatable({}, {__index = State})
function IdleState:new()
    local state = State.new(self, "Idle")
    return setmetatable(state, IdleState)
end

function IdleState:enter(owner)
    print(owner.name .. " is now idle")
    owner.velocity = {x = 0, y = 0}
    owner.idleTimer = 0
end

function IdleState:execute(owner, dt)
    owner.idleTimer = owner.idleTimer + dt
    
    -- Look for nearby enemies
    local nearbyEnemies = findNearbyEnemies(owner, 100)
    if #nearbyEnemies > 0 then
        owner.target = nearbyEnemies[1]
        owner.stateMachine:changeState("Chase")
        return
    end
    
    -- Random movement after idle time
    if owner.idleTimer > 3 then
        owner.stateMachine:changeState("Patrol")
    end
end

local ChaseState = setmetatable({}, {__index = State})
function ChaseState:new()
    local state = State.new(self, "Chase")
    return setmetatable(state, ChaseState)
end

function ChaseState:enter(owner)
    print(owner.name .. " is chasing " .. owner.target.name)
end

function ChaseState:execute(owner, dt)
    if not owner.target or owner.target.health <= 0 then
        owner.target = nil
        owner.stateMachine:changeState("Idle")
        return
    end
    
    -- Move towards target
    local dx = owner.target.x - owner.x
    local dy = owner.target.y - owner.y
    local distance = math.sqrt(dx * dx + dy * dy)
    
    if distance < 30 then
        owner.stateMachine:changeState("Attack")
        return
    end
    
    if distance > 200 then
        owner.target = nil
        owner.stateMachine:changeState("Idle")
        return
    end
    
    -- Normalize direction and set velocity
    owner.velocity.x = (dx / distance) * owner.speed
    owner.velocity.y = (dy / distance) * owner.speed
end

local AttackState = setmetatable({}, {__index = State})
function AttackState:new()
    local state = State.new(self, "Attack")
    
    -- Add sub-states for complex attack behavior
    local windupState = State:new("Windup")
    windupState.enter = function(self, owner)
        owner.attackTimer = 0
        owner.velocity = {x = 0, y = 0}
    end
    windupState.execute = function(self, owner, dt)
        owner.attackTimer = owner.attackTimer + dt
        if owner.attackTimer >= 0.5 then
            self.parent:changeSubState("Strike")
        end
    end
    
    local strikeState = State:new("Strike")
    strikeState.enter = function(self, owner)
        -- Deal damage to target
        if owner.target then
            owner.target.health = owner.target.health - owner.damage
        end
        owner.attackTimer = 0
    end
    strikeState.execute = function(self, owner, dt)
        owner.attackTimer = owner.attackTimer + dt
        if owner.attackTimer >= 0.3 then
            self.parent:changeSubState("Recovery")
        end
    end
    
    local recoveryState = State:new("Recovery")
    recoveryState.execute = function(self, owner, dt)
        owner.attackTimer = owner.attackTimer + dt
        if owner.attackTimer >= 0.2 then
            owner.stateMachine:changeState("Chase")
        end
    end
    
    state:addSubState(windupState)
    state:addSubState(strikeState)
    state:addSubState(recoveryState)
    
    return setmetatable(state, AttackState)
end

function AttackState:enter(owner)
    print(owner.name .. " is attacking " .. owner.target.name)
    self:changeSubState("Windup")
end

-- Create AI character with state machine
local function createAICharacter(name, x, y)
    local character = {
        name = name,
        x = x, y = y,
        health = 100,
        speed = 50,
        damage = 25,
        velocity = {x = 0, y = 0},
        target = nil,
        stateMachine = HSM:new()
    }
    
    character.stateMachine.owner = character
    
    -- Add states
    character.stateMachine:addState(IdleState:new())
    character.stateMachine:addState(ChaseState:new())
    character.stateMachine:addState(AttackState:new())
    
    character.stateMachine:changeState("Idle")
    
    return character
end

Pattern 3: Observer Pattern for Events

The Observer pattern enables loose coupling between game systems by allowing objects to subscribe to and receive notifications about events without direct dependencies.

-- Event Manager with Observer Pattern
local EventManager = {}
EventManager.__index = EventManager

function EventManager:new()
    return setmetatable({
        listeners = {},
        eventQueue = {},
        processing = false
    }, EventManager)
end

function EventManager:subscribe(eventType, listener, callback)
    if not self.listeners[eventType] then
        self.listeners[eventType] = {}
    end
    
    table.insert(self.listeners[eventType], {
        listener = listener,
        callback = callback
    })
end

function EventManager:unsubscribe(eventType, listener)
    if not self.listeners[eventType] then return end
    
    for i = #self.listeners[eventType], 1, -1 do
        if self.listeners[eventType][i].listener == listener then
            table.remove(self.listeners[eventType], i)
        end
    end
end

function EventManager:emit(eventType, data)
    local event = {
        type = eventType,
        data = data,
        timestamp = os.clock()
    }
    
    if self.processing then
        -- Queue events during processing to avoid infinite loops
        table.insert(self.eventQueue, event)
    else
        self:processEvent(event)
    end
end

function EventManager:processEvent(event)
    local listeners = self.listeners[event.type]
    if not listeners then return end
    
    for _, listener in ipairs(listeners) do
        if listener.callback then
            listener.callback(listener.listener, event.data)
        elseif listener.listener[event.type] then
            listener.listener[event.type](listener.listener, event.data)
        end
    end
end

function EventManager:processQueue()
    self.processing = true
    
    while #self.eventQueue > 0 do
        local event = table.remove(self.eventQueue, 1)
        self:processEvent(event)
    end
    
    self.processing = false
end

-- Global event manager
local Events = EventManager:new()

-- Example: Game Systems Using Events
local ScoreSystem = {}
function ScoreSystem:new()
    local system = {score = 0, multiplier = 1}
    
    -- Subscribe to events
    Events:subscribe("enemy_killed", system, function(self, data)
        self.score = self.score + (data.points * self.multiplier)
        Events:emit("score_changed", {score = self.score})
    end)
    
    Events:subscribe("powerup_collected", system, function(self, data)
        if data.type == "multiplier" then
            self.multiplier = self.multiplier * 2
            Events:emit("multiplier_changed", {multiplier = self.multiplier})
        end
    end)
    
    return system
end

local UISystem = {}
function UISystem:new()
    local system = {scoreText = "", multiplierText = ""}
    
    Events:subscribe("score_changed", system, function(self, data)
        self.scoreText = "Score: " .. data.score
    end)
    
    Events:subscribe("multiplier_changed", system, function(self, data)
        self.multiplierText = "x" .. data.multiplier
    end)
    
    Events:subscribe("player_health_changed", system, function(self, data)
        -- Update health bar
        self.healthPercent = data.current / data.maximum
    end)
    
    return system
end

local SoundSystem = {}
function SoundSystem:new()
    local system = {sounds = {}}
    
    Events:subscribe("enemy_killed", system, function(self, data)
        self:playSound("enemy_death")
    end)
    
    Events:subscribe("player_hit", system, function(self, data)
        self:playSound("player_hurt")
    end)
    
    Events:subscribe("powerup_collected", system, function(self, data)
        self:playSound("powerup_" .. data.type)
    end)
    
    function system:playSound(soundName)
        -- Play sound implementation
        print("Playing sound:", soundName)
    end
    
    return system
end

-- Achievement System using events
local AchievementSystem = {}
function AchievementSystem:new()
    local system = {
        achievements = {},
        stats = {
            enemiesKilled = 0,
            powerupsCollected = 0,
            totalScore = 0
        }
    }
    
    -- Define achievements
    system.achievements.firstKill = {
        name = "First Blood",
        description = "Kill your first enemy",
        unlocked = false,
        condition = function(stats) return stats.enemiesKilled >= 1 end
    }
    
    system.achievements.collector = {
        name = "Collector",
        description = "Collect 10 powerups",
        unlocked = false,
        condition = function(stats) return stats.powerupsCollected >= 10 end
    }
    
    system.achievements.highScore = {
        name = "High Scorer",
        description = "Reach 10,000 points",
        unlocked = false,
        condition = function(stats) return stats.totalScore >= 10000 end
    }
    
    -- Subscribe to events
    Events:subscribe("enemy_killed", system, function(self, data)
        self.stats.enemiesKilled = self.stats.enemiesKilled + 1
        self:checkAchievements()
    end)
    
    Events:subscribe("powerup_collected", system, function(self, data)
        self.stats.powerupsCollected = self.stats.powerupsCollected + 1
        self:checkAchievements()
    end)
    
    Events:subscribe("score_changed", system, function(self, data)
        self.stats.totalScore = data.score
        self:checkAchievements()
    end)
    
    function system:checkAchievements()
        for id, achievement in pairs(self.achievements) do
            if not achievement.unlocked and achievement.condition(self.stats) then
                achievement.unlocked = true
                Events:emit("achievement_unlocked", {
                    id = id,
                    name = achievement.name,
                    description = achievement.description
                })
            end
        end
    end
    
    return system
end

Pattern 4: Command Pattern for Input and Undo

The Command pattern encapsulates actions as objects, enabling features like input remapping, action queuing, and undo/redo functionality.

Command Pattern Implementation

-- Command base class
local Command = {}
Command.__index = Command

function Command:new()
    return setmetatable({}, self)
end

function Command:execute() 
    error("Command:execute() must be implemented") 
end

function Command:undo() 
    -- Optional: implement for undoable commands
end

function Command:canExecute() 
    return true 
end

-- Concrete command implementations
local MoveCommand = setmetatable({}, {__index = Command})
function MoveCommand:new(actor, direction, distance)
    local cmd = Command.new(self)
    cmd.actor = actor
    cmd.direction = direction
    cmd.distance = distance or 1
    cmd.previousPosition = nil
    return setmetatable(cmd, MoveCommand)
end

function MoveCommand:execute()
    if not self:canExecute() then return false end
    
    -- Store previous position for undo
    self.previousPosition = {x = self.actor.x, y = self.actor.y}
    
    -- Execute movement
    if self.direction == "up" then
        self.actor.y = self.actor.y - self.distance
    elseif self.direction == "down" then
        self.actor.y = self.actor.y + self.distance
    elseif self.direction == "left" then
        self.actor.x = self.actor.x - self.distance
    elseif self.direction == "right" then
        self.actor.x = self.actor.x + self.distance
    end
    
    return true
end

function MoveCommand:undo()
    if self.previousPosition then
        self.actor.x = self.previousPosition.x
        self.actor.y = self.previousPosition.y
    end
end

function MoveCommand:canExecute()
    -- Check bounds, obstacles, etc.
    return self.actor and self.actor.canMove
end

local AttackCommand = setmetatable({}, {__index = Command})
function AttackCommand:new(attacker, target)
    local cmd = Command.new(self)
    cmd.attacker = attacker
    cmd.target = target
    cmd.damageDealt = 0
    return setmetatable(cmd, AttackCommand)
end

function AttackCommand:execute()
    if not self:canExecute() then return false end
    
    self.damageDealt = self.attacker.attack
    self.target.health = self.target.health - self.damageDealt
    
    Events:emit("attack_executed", {
        attacker = self.attacker,
        target = self.target,
        damage = self.damageDealt
    })
    
    return true
end

function AttackCommand:undo()
    self.target.health = self.target.health + self.damageDealt
end

function AttackCommand:canExecute()
    return self.attacker and self.target and 
           self.target.health > 0 and
           getDistance(self.attacker, self.target) <= self.attacker.range
end

-- Command Manager for queuing and undo/redo
local CommandManager = {}
CommandManager.__index = CommandManager

function CommandManager:new()
    return setmetatable({
        commandQueue = {},
        history = {},
        historyIndex = 0,
        maxHistorySize = 100
    }, CommandManager)
end

function CommandManager:queueCommand(command)
    table.insert(self.commandQueue, command)
end

function CommandManager:executeQueue()
    while #self.commandQueue > 0 do
        local command = table.remove(self.commandQueue, 1)
        self:executeCommand(command)
    end
end

function CommandManager:executeCommand(command)
    if command:execute() then
        -- Add to history for undo
        self:addToHistory(command)
    end
end

function CommandManager:addToHistory(command)
    -- Remove any commands after current index (for redo functionality)
    for i = self.historyIndex + 1, #self.history do
        self.history[i] = nil
    end
    
    table.insert(self.history, command)
    self.historyIndex = #self.history
    
    -- Limit history size
    if #self.history > self.maxHistorySize then
        table.remove(self.history, 1)
        self.historyIndex = self.historyIndex - 1
    end
end

function CommandManager:undo()
    if self.historyIndex > 0 then
        local command = self.history[self.historyIndex]
        if command.undo then
            command:undo()
        end
        self.historyIndex = self.historyIndex - 1
        return true
    end
    return false
end

function CommandManager:redo()
    if self.historyIndex < #self.history then
        self.historyIndex = self.historyIndex + 1
        local command = self.history[self.historyIndex]
        command:execute()
        return true
    end
    return false
end

-- Input Manager using Command Pattern
local InputManager = {}
InputManager.__index = InputManager

function InputManager:new(commandManager)
    return setmetatable({
        commandManager = commandManager,
        keyBindings = {},
        inputBuffer = {},
        bufferSize = 10
    }, InputManager)
end

function InputManager:bindKey(key, commandFactory)
    self.keyBindings[key] = commandFactory
end

function InputManager:handleInput(key, pressed)
    if pressed and self.keyBindings[key] then
        local command = self.keyBindings[key]()
        if command then
            self.commandManager:queueCommand(command)
            
            -- Add to input buffer for combo detection
            table.insert(self.inputBuffer, {key = key, time = os.clock()})
            if #self.inputBuffer > self.bufferSize then
                table.remove(self.inputBuffer, 1)
            end
        end
    end
end

function InputManager:checkCombo(sequence)
    if #self.inputBuffer < #sequence then return false end
    
    local bufferStart = #self.inputBuffer - #sequence + 1
    for i, expectedKey in ipairs(sequence) do
        if self.inputBuffer[bufferStart + i - 1].key ~= expectedKey then
            return false
        end
    end
    
    -- Check timing (all inputs within 2 seconds)
    local firstTime = self.inputBuffer[bufferStart].time
    local lastTime = self.inputBuffer[#self.inputBuffer].time
    return (lastTime - firstTime) <= 2.0
end

-- Usage Example
local function setupGameInput()
    local player = {x = 100, y = 100, canMove = true, health = 100, attack = 25, range = 50}
    local commandManager = CommandManager:new()
    local inputManager = InputManager:new(commandManager)
    
    -- Bind keys to command factories
    inputManager:bindKey("w", function()
        return MoveCommand:new(player, "up", 32)
    end)
    
    inputManager:bindKey("s", function()
        return MoveCommand:new(player, "down", 32)
    end)
    
    inputManager:bindKey("a", function()
        return MoveCommand:new(player, "left", 32)
    end)
    
    inputManager:bindKey("d", function()
        return MoveCommand:new(player, "right", 32)
    end)
    
    inputManager:bindKey("space", function()
        local nearestEnemy = findNearestEnemy(player)
        if nearestEnemy then
            return AttackCommand:new(player, nearestEnemy)
        end
        return nil
    end)
    
    -- Combo attacks
    inputManager:bindKey("combo_fireball", function()
        if inputManager:checkCombo({"s", "d", "s", "d", "space"}) then
            return FireballCommand:new(player)
        end
        return nil
    end)
    
    return inputManager, commandManager
end

Pattern 5: Factory Pattern for Object Creation

The Factory pattern centralizes object creation, making it easier to manage different types of game objects and their initialization logic.

-- Abstract Factory for Game Objects
local GameObjectFactory = {}
GameObjectFactory.__index = GameObjectFactory

function GameObjectFactory:new()
    return setmetatable({
        templates = {},
        constructors = {}
    }, GameObjectFactory)
end

function GameObjectFactory:registerTemplate(name, template)
    self.templates[name] = template
end

function GameObjectFactory:registerConstructor(type, constructor)
    self.constructors[type] = constructor
end

function GameObjectFactory:create(templateName, overrides)
    local template = self.templates[templateName]
    if not template then
        error("Unknown template: " .. templateName)
    end
    
    local constructor = self.constructors[template.type]
    if not constructor then
        error("No constructor for type: " .. template.type)
    end
    
    -- Merge template with overrides
    local config = {}
    for k, v in pairs(template) do
        config[k] = v
    end
    if overrides then
        for k, v in pairs(overrides) do
            config[k] = v
        end
    end
    
    return constructor(config)
end

-- Base game object
local GameObject = {}
GameObject.__index = GameObject

function GameObject:new(config)
    local obj = setmetatable({
        id = generateUniqueId(),
        x = config.x or 0,
        y = config.y or 0,
        width = config.width or 32,
        height = config.height or 32,
        active = true,
        components = {}
    }, self)
    
    return obj
end

function GameObject:addComponent(component)
    self.components[component.type] = component
    component.owner = self
end

function GameObject:getComponent(type)
    return self.components[type]
end

function GameObject:update(dt)
    for _, component in pairs(self.components) do
        if component.update then
            component:update(dt)
        end
    end
end

-- Specific game object types
local Enemy = setmetatable({}, {__index = GameObject})
function Enemy:new(config)
    local enemy = GameObject.new(self, config)
    
    enemy.health = config.health or 100
    enemy.maxHealth = enemy.health
    enemy.damage = config.damage or 10
    enemy.speed = config.speed or 50
    enemy.aiType = config.aiType or "basic"
    enemy.dropTable = config.dropTable or {}
    
    -- Add AI component based on type
    if enemy.aiType == "basic" then
        enemy:addComponent(BasicAI:new())
    elseif enemy.aiType == "aggressive" then
        enemy:addComponent(AggressiveAI:new())
    elseif enemy.aiType == "defensive" then
        enemy:addComponent(DefensiveAI:new())
    end
    
    -- Add other components
    enemy:addComponent(HealthComponent:new(enemy.health))
    enemy:addComponent(MovementComponent:new(enemy.speed))
    
    return enemy
end

local Projectile = setmetatable({}, {__index = GameObject})
function Projectile:new(config)
    local projectile = GameObject.new(self, config)
    
    projectile.damage = config.damage or 25
    projectile.speed = config.speed or 200
    projectile.lifetime = config.lifetime or 5.0
    projectile.piercing = config.piercing or false
    projectile.owner = config.owner
    
    -- Set velocity based on angle
    local angle = config.angle or 0
    projectile.vx = math.cos(angle) * projectile.speed
    projectile.vy = math.sin(angle) * projectile.speed
    
    projectile:addComponent(ProjectileComponent:new(projectile.lifetime))
    projectile:addComponent(DamageComponent:new(projectile.damage, projectile.piercing))
    
    return projectile
end

local Pickup = setmetatable({}, {__index = GameObject})
function Pickup:new(config)
    local pickup = GameObject.new(self, config)
    
    pickup.pickupType = config.pickupType or "coin"
    pickup.value = config.value or 1
    pickup.magnetRadius = config.magnetRadius or 0
    pickup.lifetime = config.lifetime or 30
    
    pickup:addComponent(PickupComponent:new(pickup.pickupType, pickup.value))
    if pickup.magnetRadius > 0 then
        pickup:addComponent(MagnetComponent:new(pickup.magnetRadius))
    end
    
    return pickup
end

-- Factory setup with templates
local function setupGameObjectFactory()
    local factory = GameObjectFactory:new()
    
    -- Register constructors
    factory:registerConstructor("enemy", function(config) return Enemy:new(config) end)
    factory:registerConstructor("projectile", function(config) return Projectile:new(config) end)
    factory:registerConstructor("pickup", function(config) return Pickup:new(config) end)
    
    -- Register enemy templates
    factory:registerTemplate("goblin", {
        type = "enemy",
        health = 50,
        damage = 8,
        speed = 40,
        aiType = "basic",
        width = 24,
        height = 24,
        dropTable = {
            {item = "coin", chance = 0.8, quantity = {1, 3}},
            {item = "health_potion", chance = 0.1, quantity = 1}
        }
    })
    
    factory:registerTemplate("orc", {
        type = "enemy",
        health = 100,
        damage = 15,
        speed = 30,
        aiType = "aggressive",
        width = 32,
        height = 32,
        dropTable = {
            {item = "coin", chance = 0.9, quantity = {3, 8}},
            {item = "weapon_upgrade", chance = 0.05, quantity = 1}
        }
    })
    
    factory:registerTemplate("skeleton_archer", {
        type = "enemy",
        health = 75,
        damage = 12,
        speed = 25,
        aiType = "ranged",
        range = 150,
        attackRate = 2.0,
        width = 28,
        height = 28
    })
    
    -- Register projectile templates
    factory:registerTemplate("arrow", {
        type = "projectile",
        damage = 20,
        speed = 300,
        lifetime = 3.0,
        piercing = false,
        width = 8,
        height = 8
    })
    
    factory:registerTemplate("fireball", {
        type = "projectile",
        damage = 40,
        speed = 150,
        lifetime = 4.0,
        piercing = true,
        explosionRadius = 50,
        width = 16,
        height = 16
    })
    
    -- Register pickup templates
    factory:registerTemplate("health_potion", {
        type = "pickup",
        pickupType = "health",
        value = 25,
        magnetRadius = 30,
        width = 16,
        height = 16
    })
    
    factory:registerTemplate("coin", {
        type = "pickup",
        pickupType = "currency",
        value = 1,
        magnetRadius = 40,
        lifetime = 60,
        width = 12,
        height = 12
    })
    
    return factory
end

-- Usage examples
local function spawnEnemyWave(factory, waveConfig)
    local enemies = {}
    
    for _, enemyConfig in ipairs(waveConfig.enemies) do
        for i = 1, enemyConfig.count do
            local enemy = factory:create(enemyConfig.type, {
                x = enemyConfig.spawnArea.x + math.random() * enemyConfig.spawnArea.width,
                y = enemyConfig.spawnArea.y + math.random() * enemyConfig.spawnArea.height
            })
            table.insert(enemies, enemy)
        end
    end
    
    return enemies
end

-- Example wave configuration
local wave1 = {
    enemies = {
        {type = "goblin", count = 5, spawnArea = {x = 100, y = 100, width = 200, height = 100}},
        {type = "orc", count = 2, spawnArea = {x = 150, y = 50, width = 100, height = 200}}
    }
}

local factory = setupGameObjectFactory()
local enemies = spawnEnemyWave(factory, wave1)

Pattern 6: Object Pool Pattern

Object pooling is crucial for performance in games, especially for frequently created and destroyed objects like bullets, particles, and enemies.

Generic Object Pool

-- Generic Object Pool
local ObjectPool = {}
ObjectPool.__index = ObjectPool

function ObjectPool:new(createFunc, resetFunc, initialSize)
    local pool = setmetatable({
        createFunc = createFunc,
        resetFunc = resetFunc,
        available = {},
        active = {},
        totalCreated = 0,
        peakActive = 0
    }, ObjectPool)
    
    -- Pre-populate pool
    initialSize = initialSize or 10
    for i = 1, initialSize do
        local obj = createFunc()
        obj._pooled = true
        table.insert(pool.available, obj)
        pool.totalCreated = pool.totalCreated + 1
    end
    
    return pool
end

function ObjectPool:acquire()
    local obj
    
    if #self.available > 0 then
        obj = table.remove(self.available)
    else
        obj = self.createFunc()
        obj._pooled = true
        self.totalCreated = self.totalCreated + 1
    end
    
    self.active[obj] = true
    
    local activeCount = 0
    for _ in pairs(self.active) do activeCount = activeCount + 1 end
    self.peakActive = math.max(self.peakActive, activeCount)
    
    return obj
end

function ObjectPool:release(obj)
    if not obj._pooled or not self.active[obj] then
        return -- Object not from this pool or already released
    end
    
    self.active[obj] = nil
    
    if self.resetFunc then
        self.resetFunc(obj)
    end
    
    table.insert(self.available, obj)
end

function ObjectPool:getStats()
    local activeCount = 0
    for _ in pairs(self.active) do activeCount = activeCount + 1 end
    
    return {
        totalCreated = self.totalCreated,
        available = #self.available,
        active = activeCount,
        peakActive = self.peakActive
    }
end

-- Pool Manager for multiple object types
local PoolManager = {}
PoolManager.__index = PoolManager

function PoolManager:new()
    return setmetatable({
        pools = {}
    }, PoolManager)
end

function PoolManager:createPool(name, createFunc, resetFunc, initialSize)
    self.pools[name] = ObjectPool:new(createFunc, resetFunc, initialSize)
end

function PoolManager:acquire(poolName)
    local pool = self.pools[poolName]
    if not pool then
        error("Pool not found: " .. poolName)
    end
    return pool:acquire()
end

function PoolManager:release(poolName, obj)
    local pool = self.pools[poolName]
    if pool then
        pool:release(obj)
    end
end

function PoolManager:printStats()
    print("=== Pool Manager Stats ===")
    for name, pool in pairs(self.pools) do
        local stats = pool:getStats()
        print(string.format("%s: %d total, %d active, %d available, peak %d",
              name, stats.totalCreated, stats.active, stats.available, stats.peakActive))
    end
end

-- Example: Bullet Pool
local Bullet = {}
Bullet.__index = Bullet

function Bullet:new()
    return setmetatable({
        x = 0, y = 0,
        vx = 0, vy = 0,
        damage = 0,
        lifetime = 0,
        maxLifetime = 0,
        active = false,
        owner = nil
    }, Bullet)
end

function Bullet:init(x, y, angle, speed, damage, lifetime, owner)
    self.x = x
    self.y = y
    self.vx = math.cos(angle) * speed
    self.vy = math.sin(angle) * speed
    self.damage = damage
    self.lifetime = 0
    self.maxLifetime = lifetime
    self.active = true
    self.owner = owner
end

function Bullet:update(dt)
    if not self.active then return end
    
    self.x = self.x + self.vx * dt
    self.y = self.y + self.vy * dt
    self.lifetime = self.lifetime + dt
    
    -- Check if bullet should expire
    if self.lifetime >= self.maxLifetime then
        self:deactivate()
    end
    
    -- Check bounds (assuming screen bounds)
    if self.x < -50 or self.x > screenWidth + 50 or
       self.y < -50 or self.y > screenHeight + 50 then
        self:deactivate()
    end
end

function Bullet:deactivate()
    self.active = false
    -- Release back to pool
    if self._pooled then
        poolManager:release("bullets", self)
    end
end

function Bullet:reset()
    self.x = 0
    self.y = 0
    self.vx = 0
    self.vy = 0
    self.damage = 0
    self.lifetime = 0
    self.maxLifetime = 0
    self.active = false
    self.owner = nil
end

-- Particle System with Pooling
local Particle = {}
Particle.__index = Particle

function Particle:new()
    return setmetatable({
        x = 0, y = 0,
        vx = 0, vy = 0,
        ax = 0, ay = 0,
        color = {255, 255, 255, 255},
        size = 1,
        lifetime = 0,
        maxLifetime = 1,
        active = false
    }, Particle)
end

function Particle:init(config)
    self.x = config.x or 0
    self.y = config.y or 0
    self.vx = config.vx or 0
    self.vy = config.vy or 0
    self.ax = config.ax or 0
    self.ay = config.ay or 0
    self.color = config.color or {255, 255, 255, 255}
    self.size = config.size or 1
    self.lifetime = 0
    self.maxLifetime = config.maxLifetime or 1
    self.active = true
end

function Particle:update(dt)
    if not self.active then return end
    
    self.vx = self.vx + self.ax * dt
    self.vy = self.vy + self.ay * dt
    self.x = self.x + self.vx * dt
    self.y = self.y + self.vy * dt
    
    self.lifetime = self.lifetime + dt
    
    -- Fade out over time
    local fadeProgress = self.lifetime / self.maxLifetime
    self.color[4] = math.max(0, 255 * (1 - fadeProgress))
    
    if self.lifetime >= self.maxLifetime then
        self:deactivate()
    end
end

function Particle:deactivate()
    self.active = false
    if self._pooled then
        poolManager:release("particles", self)
    end
end

function Particle:reset()
    self.x = 0
    self.y = 0
    self.vx = 0
    self.vy = 0
    self.ax = 0
    self.ay = 0
    self.color = {255, 255, 255, 255}
    self.size = 1
    self.lifetime = 0
    self.maxLifetime = 1
    self.active = false
end

-- Setup pools
local function setupPools()
    local poolManager = PoolManager:new()
    
    -- Bullet pool
    poolManager:createPool("bullets", 
        function() return Bullet:new() end,
        function(bullet) bullet:reset() end,
        50  -- Initial size
    )
    
    -- Particle pool
    poolManager:createPool("particles", 
        function() return Particle:new() end,
        function(particle) particle:reset() end,
        200  -- Initial size
    )
    
    -- Enemy pool (for spawning systems)
    poolManager:createPool("enemies", 
        function() return Enemy:new({}) end,
        function(enemy) enemy:reset() end,
        20
    )
    
    return poolManager
end

-- Usage in game systems
local WeaponSystem = {}
function WeaponSystem:new(poolManager)
    return setmetatable({
        poolManager = poolManager
    }, WeaponSystem)
end

function WeaponSystem:fire(weapon, x, y, angle)
    local bullet = self.poolManager:acquire("bullets")
    bullet:init(x, y, angle, weapon.bulletSpeed, weapon.damage, weapon.range, weapon.owner)
    
    -- Spawn muzzle flash particles
    for i = 1, 5 do
        local particle = self.poolManager:acquire("particles")
        particle:init({
            x = x + math.random(-5, 5),
            y = y + math.random(-5, 5),
            vx = math.random(-50, 50),
            vy = math.random(-50, 50),
            color = {255, 200, 100, 255},
            size = math.random(2, 4),
            maxLifetime = 0.2
        })
    end
    
    return bullet
end

Bringing It All Together: Complete Game Architecture

Let's see how all these patterns work together in a complete game architecture. This example demonstrates how the patterns complement each other to create a maintainable and scalable codebase.

-- Complete Game Architecture Example
local Game = {}
Game.__index = Game

function Game:new()
    return setmetatable({
        ecs = AdvancedECS:new(),
        eventManager = EventManager:new(),
        poolManager = PoolManager:new(),
        factory = GameObjectFactory:new(),
        commandManager = CommandManager:new(),
        inputManager = nil,
        
        -- Game state
        gameState = "menu", -- menu, playing, paused, gameover
        score = 0,
        level = 1,
        
        -- Systems
        systems = {},
        
        -- Performance tracking
        frameTime = 0,
        updateTime = 0,
        renderTime = 0
    }, Game)
end

function Game:initialize()
    -- Setup pools
    self:setupPools()
    
    -- Setup factory
    self:setupFactory()
    
    -- Setup input
    self.inputManager = InputManager:new(self.commandManager)
    self:setupInput()
    
    -- Setup systems
    self:setupSystems()
    
    -- Setup event listeners
    self:setupEventListeners()
    
    print("Game initialized successfully")
end

function Game:setupPools()
    self.poolManager:createPool("bullets", 
        function() return Bullet:new() end,
        function(b) b:reset() end, 100)
    
    self.poolManager:createPool("particles", 
        function() return Particle:new() end,
        function(p) p:reset() end, 500)
    
    self.poolManager:createPool("enemies", 
        function() return Enemy:new({}) end,
        function(e) e:reset() end, 50)
end

function Game:setupFactory()
    -- Register all game object templates
    setupGameObjectFactory(self.factory)
end

function Game:setupSystems()
    -- Add ECS systems with priorities
    self.ecs:addSystem(InputSystem:new(self.inputManager), 100)
    self.ecs:addSystem(MovementSystem:new(), 90)
    self.ecs:addSystem(CollisionSystem:new(), 80)
    self.ecs:addSystem(WeaponSystem:new(self.poolManager), 70)
    self.ecs:addSystem(AISystem:new(), 60)
    self.ecs:addSystem(HealthSystem:new(), 50)
    self.ecs:addSystem(RenderSystem:new(), 10)
    
    -- Add game-specific systems
    table.insert(self.systems, ScoreSystem:new())
    table.insert(self.systems, UISystem:new())
    table.insert(self.systems, SoundSystem:new())
    table.insert(self.systems, AchievementSystem:new())
end

function Game:setupInput()
    -- Basic movement
    self.inputManager:bindKey("w", function()
        return MoveCommand:new(self:getPlayer(), "up", 100)
    end)
    
    -- More input bindings...
end

function Game:setupEventListeners()
    self.eventManager:subscribe("enemy_killed", self, function(self, data)
        self.score = self.score + data.points
        
        -- Chance to spawn pickup
        if math.random() < 0.3 then
            local pickup = self.factory:create("coin", {
                x = data.enemy.x,
                y = data.enemy.y
            })
            self:addGameObject(pickup)
        end
    end)
    
    self.eventManager:subscribe("player_death", self, function(self, data)
        self:changeState("gameover")
    end)
end

function Game:update(dt)
    local startTime = os.clock()
    
    -- Update command manager
    self.commandManager:executeQueue()
    
    -- Update ECS
    self.ecs:update(dt)
    
    -- Update other systems
    for _, system in ipairs(self.systems) do
        if system.update then
            system:update(dt)
        end
    end
    
    -- Process events
    self.eventManager:processQueue()
    
    -- Update pools (cleanup inactive objects)
    self:updatePools(dt)
    
    self.updateTime = os.clock() - startTime
end

function Game:render()
    local startTime = os.clock()
    
    -- ECS rendering is handled by RenderSystem
    -- Additional UI rendering here
    self:renderUI()
    
    self.renderTime = os.clock() - startTime
end

function Game:updatePools(dt)
    -- Update all active bullets
    for bullet in pairs(self.poolManager.pools.bullets.active) do
        bullet:update(dt)
    end
    
    -- Update all active particles
    for particle in pairs(self.poolManager.pools.particles.active) do
        particle:update(dt)
    end
end

function Game:addGameObject(obj)
    if obj.type == "player" then
        local entity = self.ecs:createEntity()
        self.ecs:addComponent(entity, "Position", {x = obj.x, y = obj.y})
        self.ecs:addComponent(entity, "Sprite", obj.sprite)
        self.ecs:addComponent(entity, "Health", {current = obj.health, max = obj.health})
        self.ecs:addComponent(entity, "Input", {})
        self.ecs:addComponent(entity, "Player", {})
        
        obj.entityId = entity
    elseif obj.type == "enemy" then
        local entity = self.ecs:createEntity()
        self.ecs:addComponent(entity, "Position", {x = obj.x, y = obj.y})
        self.ecs:addComponent(entity, "Sprite", obj.sprite)
        self.ecs:addComponent(entity, "Health", {current = obj.health, max = obj.health})
        self.ecs:addComponent(entity, "AI", {type = obj.aiType})
        self.ecs:addComponent(entity, "Enemy", {damage = obj.damage})
        
        obj.entityId = entity
    end
    
    return obj
end

function Game:changeState(newState)
    local oldState = self.gameState
    self.gameState = newState
    
    self.eventManager:emit("state_changed", {
        from = oldState,
        to = newState
    })
    
    -- State-specific logic
    if newState == "playing" then
        self:startLevel(self.level)
    elseif newState == "gameover" then
        self:saveHighScore()
    end
end

function Game:getPlayer()
    local players = self.ecs:queryEntities("Player")
    if #players > 0 then
        return players[1]
    end
    return nil
end

-- Main game loop
function Game:run()
    local lastTime = os.clock()
    
    while self.gameState ~= "quit" do
        local currentTime = os.clock()
        local dt = currentTime - lastTime
        lastTime = currentTime
        
        self.frameTime = dt
        
        -- Handle input
        handleSystemInput(self.inputManager)
        
        -- Update game
        if self.gameState == "playing" then
            self:update(dt)
        end
        
        -- Render
        self:render()
        
        -- Cap framerate
        local targetFrameTime = 1/60
        local actualFrameTime = os.clock() - currentTime
        if actualFrameTime < targetFrameTime then
            sleep(targetFrameTime - actualFrameTime)
        end
    end
end

-- Usage
local game = Game:new()
game:initialize()
game:run()

Performance Considerations

When implementing these architectural patterns, keep performance in mind. Here are key optimization strategies:

Performance Best Practices

  • Cache frequently accessed components: Store references instead of repeated queries
  • Use spatial partitioning: Don't check collisions between distant objects
  • Limit event processing: Queue events and process in batches
  • Profile regularly: Measure actual performance, not assumptions
  • Pool everything: Reuse objects to reduce garbage collection pressure
  • Batch similar operations: Group rendering calls, audio updates, etc.

Conclusion

Good game architecture is like a well-planned city - it provides structure that enables growth while maintaining efficiency and livability. The patterns we've explored aren't just theoretical concepts; they're battle-tested solutions that have powered countless successful games.

Remember that architecture is not about following patterns blindly, but about choosing the right tools for your specific problems. Start simple, measure performance, and refactor when complexity demands it. A small indie game might only need basic ECS and event systems, while a large MMO might require all the patterns discussed and more.

The key to success is understanding these patterns deeply enough to adapt them to your needs. Lua's flexibility makes it an excellent language for implementing these patterns, but that same flexibility requires discipline to maintain clean, organized code.

As you build your games, remember that great architecture is invisible to players but essential for developers. Invest the time to build solid foundations, and your future self will thank you when you're adding that amazing new feature instead of fighting with tangled code.

Now go build something amazing! 🎮

About the Author

MT

Michael Thompson

Senior Game Architect & Technical Director

Michael has designed and implemented game architectures for over 15 years, working on everything from mobile games to AAA console titles. He's passionate about sharing knowledge and helping developers build better, more maintainable game systems.