Marketplace

design-patterns

Game server design patterns including ECS, command pattern, and event sourcing

$ Instalar

git clone https://github.com/pluginagentmarketplace/custom-plugin-server-side-game-dev /tmp/custom-plugin-server-side-game-dev && cp -r /tmp/custom-plugin-server-side-game-dev/skills/design-patterns ~/.claude/skills/custom-plugin-server-side-game-dev

// tip: Run this command in your terminal to install the skill


name: design-patterns description: Game server design patterns including ECS, command pattern, and event sourcing sasmp_version: "1.3.0" version: "2.0.0" bonded_agent: 01-game-server-architect bond_type: SECONDARY_BOND

Parameters

parameters: required: - pattern_type optional: - storage_backend validation: pattern_type: type: string enum: [ecs, command, observer, state_machine, object_pool, event_sourcing] storage_backend: type: string enum: [memory, redis, postgresql] default: memory

Retry Configuration

retry_config: max_attempts: 1 fallback: none

Observability

observability: logging: level: debug fields: [pattern, entity_count, event_type] metrics: - name: ecs_entities_total type: gauge - name: command_execution_duration_us type: histogram - name: event_sourcing_replay_duration_ms type: histogram - name: object_pool_utilization type: gauge

Design Patterns for Game Servers

Apply proven design patterns for scalable, maintainable game server architecture.

Pattern Selection Guide

PatternPurposeUse Case
ECSData-oriented designEntity management
CommandAction encapsulationInput replay, undo
ObserverEvent notificationState changes
State MachineState transitionsPlayer states
Object PoolMemory efficiencyBullets, particles
Event SourcingAudit trailMatch replay

Entity Component System (ECS)

// Components - pure data, no logic
struct Position { float x, y, z; };
struct Velocity { float dx, dy, dz; };
struct Health { int current, max; };
struct NetworkSync { uint32_t last_sync_tick; };

// Entity is just an ID
using Entity = uint32_t;

// Component storage
template<typename T>
class ComponentArray {
    std::unordered_map<Entity, T> components;

public:
    void add(Entity e, T component) {
        components[e] = component;
    }

    T* get(Entity e) {
        auto it = components.find(e);
        return it != components.end() ? &it->second : nullptr;
    }

    void remove(Entity e) {
        components.erase(e);
    }
};

// System - logic that operates on components
class MovementSystem {
public:
    void update(float dt, World& world) {
        for (auto& [entity, pos] : world.query<Position, Velocity>()) {
            auto& vel = world.get<Velocity>(entity);
            pos.x += vel.dx * dt;
            pos.y += vel.dy * dt;
            pos.z += vel.dz * dt;
        }
    }
};

class DamageSystem {
public:
    void applyDamage(Entity target, int amount, World& world) {
        if (auto* health = world.get<Health>(target)) {
            health->current -= amount;
            if (health->current <= 0) {
                world.emit<EntityDied>(target);
            }
        }
    }
};

Command Pattern

struct GameCommand {
    uint64_t tick;
    uint32_t playerId;

    virtual ~GameCommand() = default;
    virtual void execute(GameState& state) = 0;
    virtual void undo(GameState& state) = 0;
    virtual std::unique_ptr<GameCommand> clone() const = 0;
};

struct MoveCommand : GameCommand {
    Vector3 direction;
    Vector3 previousPosition;  // For undo

    void execute(GameState& state) override {
        auto& player = state.players[playerId];
        previousPosition = player.position;
        player.velocity = direction * player.speed;
    }

    void undo(GameState& state) override {
        state.players[playerId].position = previousPosition;
    }

    std::unique_ptr<GameCommand> clone() const override {
        return std::make_unique<MoveCommand>(*this);
    }
};

// Command history for replay
class CommandHistory {
    std::vector<std::unique_ptr<GameCommand>> history;

public:
    void record(std::unique_ptr<GameCommand> cmd) {
        history.push_back(std::move(cmd));
    }

    GameState replay(const GameState& initial) {
        GameState state = initial;
        for (const auto& cmd : history) {
            cmd->execute(state);
        }
        return state;
    }

    void undoLast(GameState& state) {
        if (!history.empty()) {
            history.back()->undo(state);
            history.pop_back();
        }
    }
};

Event Sourcing

// Events are immutable facts
struct GameEvent {
    uint64_t timestamp;
    uint64_t sequence;

    virtual ~GameEvent() = default;
    virtual void apply(GameState& state) const = 0;
    virtual std::string serialize() const = 0;
};

struct PlayerDamaged : GameEvent {
    uint32_t playerId;
    uint32_t sourceId;
    int damage;
    DamageType type;

    void apply(GameState& state) const override {
        auto& player = state.players[playerId];
        player.health -= damage;
        player.lastDamageSource = sourceId;
    }

    std::string serialize() const override {
        return fmt::format("{{\"type\":\"damage\",\"player\":{},\"damage\":{}}}",
                          playerId, damage);
    }
};

class EventStore {
    std::vector<std::unique_ptr<GameEvent>> events;

public:
    void append(std::unique_ptr<GameEvent> event) {
        event->sequence = events.size();
        events.push_back(std::move(event));
    }

    GameState rebuildState() const {
        GameState state;
        for (const auto& event : events) {
            event->apply(state);
        }
        return state;
    }

    GameState rebuildAtTime(uint64_t timestamp) const {
        GameState state;
        for (const auto& event : events) {
            if (event->timestamp > timestamp) break;
            event->apply(state);
        }
        return state;
    }
};

Object Pool

template<typename T, size_t N>
class ObjectPool {
    struct PoolEntry {
        T object;
        bool in_use = false;
    };

    std::array<PoolEntry, N> pool;
    std::stack<size_t> free_indices;
    std::mutex mutex;

public:
    ObjectPool() {
        for (size_t i = 0; i < N; ++i) {
            free_indices.push(i);
        }
    }

    T* acquire() {
        std::lock_guard<std::mutex> lock(mutex);
        if (free_indices.empty()) return nullptr;

        size_t idx = free_indices.top();
        free_indices.pop();
        pool[idx].in_use = true;
        return &pool[idx].object;
    }

    void release(T* obj) {
        std::lock_guard<std::mutex> lock(mutex);
        size_t idx = obj - &pool[0].object;
        pool[idx].object.reset();
        pool[idx].in_use = false;
        free_indices.push(idx);
    }

    size_t available() const {
        return free_indices.size();
    }
};

// Usage
ObjectPool<Bullet, 1000> bulletPool;

void fireBullet(Player& player) {
    Bullet* bullet = bulletPool.acquire();
    if (bullet) {
        bullet->init(player.position, player.aimDirection);
        activeBullets.push_back(bullet);
    }
}

void onBulletHit(Bullet* bullet) {
    activeBullets.remove(bullet);
    bulletPool.release(bullet);
}

State Machine

enum class PlayerState {
    Idle, Running, Jumping, Attacking, Dead
};

class PlayerStateMachine {
    PlayerState current = PlayerState::Idle;
    std::unordered_map<PlayerState,
        std::unordered_set<PlayerState>> transitions;

public:
    PlayerStateMachine() {
        // Define valid transitions
        transitions[PlayerState::Idle] = {
            PlayerState::Running,
            PlayerState::Jumping,
            PlayerState::Attacking,
            PlayerState::Dead
        };
        transitions[PlayerState::Running] = {
            PlayerState::Idle,
            PlayerState::Jumping,
            PlayerState::Dead
        };
        transitions[PlayerState::Jumping] = {
            PlayerState::Idle,
            PlayerState::Dead
        };
        transitions[PlayerState::Attacking] = {
            PlayerState::Idle,
            PlayerState::Dead
        };
        // Dead is terminal - no transitions out
        transitions[PlayerState::Dead] = {};
    }

    bool canTransition(PlayerState newState) const {
        auto it = transitions.find(current);
        return it != transitions.end() &&
               it->second.count(newState) > 0;
    }

    bool transition(PlayerState newState) {
        if (!canTransition(newState)) return false;

        onExit(current);
        current = newState;
        onEnter(current);
        return true;
    }

private:
    void onEnter(PlayerState state) {
        switch (state) {
            case PlayerState::Jumping:
                player->velocity.y = JUMP_FORCE;
                break;
            case PlayerState::Dead:
                player->onDeath();
                break;
        }
    }

    void onExit(PlayerState state) {
        // Cleanup for previous state
    }
};

Troubleshooting

Common Failure Modes

PatternProblemRoot CauseSolution
ECSComponent fragmentationSparse storageArchetype-based storage
CommandMemory bloatUnbounded historyLimit history size
Event SourcingSlow replayToo many eventsPeriodic snapshots
Object PoolExhaustionLeaksTrack allocations
State MachineInvalid stateMissing transitionValidate all paths

Debug Checklist

// ECS diagnostics
void debugECS(World& world) {
    std::cout << "Entities: " << world.entityCount() << std::endl;
    std::cout << "Components per type:\n";
    world.forEachArchetype([](const Archetype& a) {
        std::cout << "  " << a.signature() << ": " << a.size() << "\n";
    });
}

// Object pool health
void debugPool(ObjectPool& pool) {
    std::cout << "Pool utilization: "
              << (pool.capacity() - pool.available())
              << "/" << pool.capacity() << std::endl;
}

// Event store stats
void debugEventStore(EventStore& store) {
    std::cout << "Events: " << store.size() << std::endl;
    std::cout << "Rebuild time: " << measureRebuildTime(store) << "ms\n";
}

Unit Test Template

#include <gtest/gtest.h>

TEST(ECS, CreatesAndQueriesEntities) {
    World world;

    Entity player = world.createEntity();
    world.add<Position>(player, {0, 0, 0});
    world.add<Velocity>(player, {1, 0, 0});

    auto entities = world.query<Position, Velocity>();
    EXPECT_EQ(entities.size(), 1);
}

TEST(Command, UndoRestoresState) {
    GameState state;
    state.players[1].position = {0, 0, 0};

    auto cmd = std::make_unique<MoveCommand>();
    cmd->playerId = 1;
    cmd->direction = {1, 0, 0};

    cmd->execute(state);
    EXPECT_NE(state.players[1].velocity.x, 0);

    cmd->undo(state);
    EXPECT_EQ(state.players[1].position.x, 0);
}

TEST(EventSourcing, RebuildMatchesLive) {
    EventStore store;
    GameState liveState;

    for (int i = 0; i < 100; ++i) {
        auto event = std::make_unique<PlayerDamaged>();
        event->playerId = 1;
        event->damage = 1;
        event->apply(liveState);
        store.append(std::move(event));
    }

    auto rebuiltState = store.rebuildState();
    EXPECT_EQ(rebuiltState.players[1].health, liveState.players[1].health);
}

TEST(ObjectPool, AcquireAndRelease) {
    ObjectPool<Bullet, 10> pool;

    std::vector<Bullet*> acquired;
    for (int i = 0; i < 10; ++i) {
        acquired.push_back(pool.acquire());
        EXPECT_NE(acquired.back(), nullptr);
    }

    // Pool exhausted
    EXPECT_EQ(pool.acquire(), nullptr);

    // Release one
    pool.release(acquired.back());
    acquired.pop_back();

    // Can acquire again
    EXPECT_NE(pool.acquire(), nullptr);
}

Resources

  • assets/ - Pattern implementations
  • references/ - Design pattern catalogs