2Brute2Best
🔥0
C6

Entities × Renderers (OpenGL, Vulkan, Headless)

Pro OnlyBridge
⚡ 60-Second Architecture SummaryC6 · Bridge Pattern
Naive Bottleneck→Bridge Pattern Cure
The Naive Bottleneck: Combinatorial subclass explosion (N × M)

Multiplying $M$ entity types by $N$ rendering APIs (`VulkanHero`, `Dx12Hero`, `VulkanMonster`, `HeadlessHero`...) creates an $M \times N$ class grid. High-level sprite state (HP bar formulas, animation timers) is copy-pasted into every backend subclass, and players cannot switch graphics APIs in settings at runtime.

3 entities times 3 renderers creates 9 separate classes. Adding 2 new entities and 2 new renderers blows the class count up to 25 classes.

The Pattern Cure: Bridge Pattern

Decouple an abstraction from its implementation so that the two can vary independently, connecting them via composition instead of inheritance.

We separate the system into two independent class hierarchies. The Implementor hierarchy (RenderBackend) defines primitive, low-level drawing contracts (drawQuad, drawText). Concrete renderers (OpenGlRenderer, VulkanRenderer, HeadlessTestRenderer) implement these primitives. The Abstraction hierarchy (SceneEntity) holds a reference/pointer to RenderBackend. Refined entities (HeroSprite, BossSprite, ParticleBanner) build high-level visuals out of primitive backend operations. Instead of N × M subclasses, we have N + M classes.

Sprint 6 of 8 · Decoupling Entity Logic From Graphics Backends (Vulkan / DX12 / Headless)
Story C — 2D RPG Game Engine — Progressive System Build
1. Where our codebase left off
← C5

In Sprint 5 (C5 · Flyweight), we decoupled intrinsic mesh data from extrinsic coordinates, allowing our engine to render 50,000 trees, ground tiles, and particle sprites within strict RAM budgets. But all drawing code was hardwired to a single platform.

2. What we are building now

We need our game to render across multiple graphics backends—Vulkan, DirectX 12, OpenGL, and a Headless Server simulator for CI test runs—without coupling high-level entity logic (HeroSprite, BossSprite, ParticleBanner) to concrete graphics APIs.

3. The pain point we hit

Multiplying M entity types by N rendering APIs (VulkanHero, Dx12Hero, VulkanMonster, HeadlessHero...) creates an M × N class grid. High-level sprite state (HP bar formulas, animation timers) is copy-pasted into every backend subclass, and players cannot switch graphics APIs in settings at runtime.

4. What this unlocks next
Next: C7 →

The Bridge Pattern separates the SceneEntity abstraction from the RenderBackend implementor via composition, collapsing complexity from M × N to M + N and enabling runtime backend toggling. Next in Sprint 7 (C7 · Visitor), where analytics, save-export, and minimap teams need to run new passes over our closed set of world entities (Hero, Monster, Trap, LootChest) every sprint without recompiling entity classes.

01

The product ticket

Ticket · C6RPG-106 · Cross-Platform Rendering: Entities × Graphics Backends

Our RPG engine runs across desktop PCs, mobile devices, and automated headless CI test servers. We have distinct visual entities (HeroSprite, BossSprite, ParticleBanner) that must be rendered across 3 graphics backends: OpenGlRenderer, VulkanRenderer, and HeadlessTestRenderer (which logs draw commands for CI validation without opening a window).

Requirements
  • •

    Render diverse entity types (HeroSprite, BossSprite, ParticleBanner) with specialized drawing logic like health bars and particle auroras.

  • •

    Support runtime backend selection between OpenGlRenderer, VulkanRenderer, and HeadlessTestRenderer.

  • •

    Allow new entity types and new rendering backends to be developed and compiled independently without combinatorial subclass explosion.

The next requirement that breaks naive code

Next sprint, the engine team is adding a DirectX12Renderer and gameplay designers are introducing NpcSprite. In the naive inheritance model, adding one backend requires writing N new subclasses for every single existing entity.

02

Step 1 — The naive code & why it breaks

The naive codebase combines every entity with every graphics API using deep inheritance, producing classes like OpenGlHeroSprite, VulkanHeroSprite, and HeadlessHeroSprite.

Naive design (what not to ship) · naive.cpp
#include <print>
#include <string>

// Naive design: combines every entity type with every graphics API via inheritance.
// Result: N entities * M renderers = N * M classes!

class NaiveEntity {
public:
    virtual ~NaiveEntity() = default;
    virtual void render() = 0;
};

// 1. HeroSprite variations
class OpenGlHeroSprite : public NaiveEntity {
    std::string name_;
    float x_, y_;
public:
    OpenGlHeroSprite(std::string name, float x, float y)
        : name_(std::move(name)), x_(x), y_(y) {}
    void render() override {
        std::println("[OpenGL] glBind(hero.png) -> glDrawQuad at ({:.1f}, {:.1f}) for Hero '{}'", x_, y_, name_);
    }
};

class VulkanHeroSprite : public NaiveEntity {
    std::string name_;
    float x_, y_;
public:
    VulkanHeroSprite(std::string name, float x, float y)
        : name_(std::move(name)), x_(x), y_(y) {}
    void render() override {
        std::println("[Vulkan] vkCmdBindPipeline -> vkCmdDrawQuad at ({:.1f}, {:.1f}) for Hero '{}'", x_, y_, name_);
    }
};

class HeadlessHeroSprite : public NaiveEntity {
    std::string name_;
    float x_, y_;
public:
    HeadlessHeroSprite(std::string name, float x, float y)
        : name_(std::move(name)), x_(x), y_(y) {}
    void render() override {
        std::println("[Headless] Record draw: Hero '{}' at ({:.1f}, {:.1f})", name_, x_, y_);
    }
};

// 2. BossSprite variations (duplicated for each backend)
class OpenGlBossSprite : public NaiveEntity {
    std::string name_;
public:
    explicit OpenGlBossSprite(std::string name) : name_(std::move(name)) {}
    void render() override {
        std::println("[OpenGL] glDrawBoss '{}' with rage aura", name_);
    }
};

class VulkanBossSprite : public NaiveEntity {
    std::string name_;
public:
    explicit VulkanBossSprite(std::string name) : name_(std::move(name)) {}
    void render() override {
        std::println("[Vulkan] vkDrawBoss '{}' with rage aura", name_);
    }
};

class HeadlessBossSprite : public NaiveEntity {
    std::string name_;
public:
    explicit HeadlessBossSprite(std::string name) : name_(std::move(name)) {}
    void render() override {
        std::println("[Headless] Record draw: Boss '{}'", name_);
    }
};

// 3. ParticleBanner variations (3 more classes...)
class OpenGlParticleBanner : public NaiveEntity {
public:
    void render() override { std::println("[OpenGL] glDrawParticles"); }
};
class VulkanParticleBanner : public NaiveEntity {
public:
    void render() override { std::println("[Vulkan] vkDrawParticles"); }
};
class HeadlessParticleBanner : public NaiveEntity {
public:
    void render() override { std::println("[Headless] Record draw: Particles"); }
};

int main() {
    // 3 entities * 3 renderers = 9 subclasses already!
    // Adding DirectX12 requires writing 3 new classes.
    // Adding NpcSprite requires writing 4 new classes (one per backend).
    OpenGlHeroSprite heroGl("Valeria", 10.0f, 20.0f);
    VulkanBossSprite bossVk("Dragon Lord");
    HeadlessParticleBanner bannerHl;

    heroGl.render();
    bossVk.render();
    bannerHl.render();

    return 0;
}
Combinatorial subclass explosion (N × M)

3 entities times 3 renderers creates 9 separate classes. Adding 2 new entities and 2 new renderers blows the class count up to 25 classes.

High-level game logic tangled with low-level graphics APIs

Sprite state (HP bar formulas, animation frame timers) is copy-pasted and intertwined with OpenGL glDrawQuad and Vulkan pipeline calls.

Impossible to switch graphics APIs at runtime

Because rendering API is baked into the class type (OpenGlHeroSprite), a player cannot toggle from OpenGL to Vulkan in settings without destroying and recreating the entire scene entity hierarchy.

03

Step 2 — The fix: Bridge Pattern

Core idea in one sentence

Decouple an abstraction from its implementation so that the two can vary independently, connecting them via composition instead of inheritance.

We separate the system into two independent class hierarchies. The Implementor hierarchy (RenderBackend) defines primitive, low-level drawing contracts (drawQuad, drawText). Concrete renderers (OpenGlRenderer, VulkanRenderer, HeadlessTestRenderer) implement these primitives. The Abstraction hierarchy (SceneEntity) holds a reference/pointer to RenderBackend. Refined entities (HeroSprite, BossSprite, ParticleBanner) build high-level visuals out of primitive backend operations. Instead of N × M subclasses, we have N + M classes.

Pattern roles in our story
Abstraction

SceneEntity — defines high-level entity operations and maintains a bridge reference (std::shared_ptr<RenderBackend>).

Refined Abstraction

HeroSprite, BossSprite, ParticleBanner — extend SceneEntity to compose high-level visual features using primitive backend calls.

Implementor (Interface)

RenderBackend — declares the primitive drawing interface (drawQuad, drawText, name).

Concrete Implementors

OpenGlRenderer, VulkanRenderer, HeadlessTestRenderer — provide platform-specific implementations of the primitive drawing methods.

Brute2Best Pro Content

Unlock Full UML Diagram & C++23 Solution

The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers