Entities × Renderers (OpenGL, Vulkan, Headless)
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.
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.
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.
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.
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.
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.
The product ticket
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).
- •
Render diverse entity types (
HeroSprite,BossSprite,ParticleBanner) with specialized drawing logic like health bars and particle auroras. - •
Support runtime backend selection between
OpenGlRenderer,VulkanRenderer, andHeadlessTestRenderer. - •
Allow new entity types and new rendering backends to be developed and compiled independently without combinatorial subclass explosion.
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.
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.
#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;
}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.
Sprite state (HP bar formulas, animation frame timers) is copy-pasted and intertwined with OpenGL glDrawQuad and Vulkan pipeline calls.
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.
Step 2 — The fix: 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.
SceneEntity — defines high-level entity operations and maintains a bridge reference (std::shared_ptr<RenderBackend>).
HeroSprite, BossSprite, ParticleBanner — extend SceneEntity to compose high-level visual features using primitive backend calls.
RenderBackend — declares the primitive drawing interface (drawQuad, drawText, name).
OpenGlRenderer, VulkanRenderer, HeadlessTestRenderer — provide platform-specific implementations of the primitive drawing methods.
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.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.