2Brute2Best
🔥0
C5

10,000 Trees and Bullets Without Blowing Up RAM

Pro OnlyFlyweight std::span (C++20)
⚡ 60-Second Architecture SummaryC5 · Flyweight Pattern
Naive Bottleneck→Flyweight Pattern Cure
The Naive Bottleneck: Duplicate heavy buffers cause memory explosion

Duplicating heavy read-only mesh and sprite data inside every spawned object exhausts memory and CPU cache. When each of 50,000 entities owns its vertex buffer and texture metadata, RAM usage surges past 40+ MB, blowing L1/L2 cache lines and crashing budget hardware with out-of-memory errors.

10,000 trees each holding a 256-float vertex buffer and texture strings consume over 40 MB of RAM just to display three visual tree variations.

The Pattern Cure: Flyweight Pattern

Separate intrinsic state (shared, immutable) from extrinsic state (unique to each instance), sharing the intrinsic objects across thousands of instances via a factory pool.

We separate tree data into two parts. Intrinsic state (species, texture ID, vertex mesh buffer) is immutable and moved into TreeModel. A TreeModelFactory caches and returns a std::shared_ptr<const TreeModel>. Extrinsic state (world position x, y and scale) stays inside TreeInstance, which holds only primitive floats and the shared pointer. 10,000 trees now consume under 520 KB of RAM while sharing exactly three heavy model objects.

Sprint 5 of 8 · Rendering 50,000 World Tiles & Particles Within RAM Budgets
Story C — 2D RPG Game Engine — Progressive System Build
1. Where our codebase left off
← C4

In Sprint 4 (C4 · Prototype), we implemented polymorphic monster cloning via EnemyPrototype::clone(), allowing our wave spawners to generate dense horde encounters in O(1) time without disk-parsing latency. However, expanding our battlefields into massive biomes reveals a critical hardware limit.

2. What we are building now

We need to render expansive world biomes and battle arenas packed with environmental scenery—such as 10,000 to 50,000 forest trees (Oak, Pine, CherryBlossom), dungeon floor tiles, and particle effects—where each instance has its own world coordinates (x, y) and scaling.

3. The pain point we hit

Duplicating heavy read-only mesh and sprite data inside every spawned object exhausts memory and CPU cache. When each of 50,000 entities owns its vertex buffer and texture metadata, RAM usage surges past 40+ MB, blowing L1/L2 cache lines and crashing budget hardware with out-of-memory errors.

4. What this unlocks next
Next: C6 →

The Flyweight Pattern splits shared intrinsic state (TreeModel / TileType cached in a factory pool) from per-instance extrinsic coordinates (x, y) and scale, dropping memory consumption to under 520 KB. Next in Sprint 6 (C6 · Bridge), where our game must render those entities across Vulkan, DirectX 12, and a Headless Server simulator without coupling entity logic to graphics APIs.

01

The product ticket

Ticket · C5RPG-105 · Render 10,000 Forest Trees Without Exhausting RAM

Our 2D open-world RPG map features dense wilderness biomes. The art team created high-fidelity textures and vertex mesh arrays for Oak, Pine, and CherryBlossom trees. Each tree model contains a vertex buffer, texture IDs, and shading parameters.

Requirements
  • •

    Spawn and render up to 10,000 trees across the game world with distinct coordinates (x, y) and individual scale factors.

  • •

    Support runtime rendering and bounding box calculation for each tree instance.

  • •

    Restrict memory usage so 10,000 trees consume under 1 MB of RAM instead of 40+ MB.

The next requirement that breaks naive code

Next milestone adds 100,000 projectile bullets and grass tufts. If each entity continues to own its full mesh and sprite data, mobile devices and budget laptops crash with out-of-memory (OOM) errors during map load.

02

Step 1 — The naive code & why it breaks

The naive approach stores coordinates, scale, species, texture paths, and the complete vertex array directly inside every Tree instance.

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

// Naive Tree: every instance duplicates heavy texture and vertex buffer data.
class NaiveTree {
    float x_;
    float y_;
    float scale_;
    std::string species_;
    std::string textureId_;
    std::vector<float> meshVertices_; // 3D/2D vertex buffer (e.g., 256 floats per tree)

public:
    NaiveTree(float x, float y, float scale, std::string species, std::string textureId,
              std::vector<float> vertices)
        : x_(x), y_(y), scale_(scale), species_(std::move(species)),
          textureId_(std::move(textureId)), meshVertices_(std::move(vertices)) {}

    void render() const {
        std::println("Rendering {} at ({:.1f}, {:.1f}) scale {:.1f} with {} vertices [tex: {}]",
                     species_, x_, y_, scale_, meshVertices_.size(), textureId_);
    }

    [[nodiscard]] size_t bytesUsed() const {
        return sizeof(*this) + species_.capacity() + textureId_.capacity() +
               (meshVertices_.capacity() * sizeof(float));
    }
};

int main() {
    // A single tree mesh of 256 floats (~1 KB buffer)
    std::vector<float> sampleMesh(256, 1.0f);

    std::vector<NaiveTree> forest;
    forest.reserve(10000);

    // Spawning 10,000 trees copies the heavy vertex vector and strings 10,000 times!
    for (int i = 0; i < 10000; ++i) {
        std::string species = (i % 3 == 0) ? "Oak" : (i % 3 == 1) ? "Pine" : "CherryBlossom";
        std::string texture = "tex_" + species + "_diffuse.png";
        forest.emplace_back(static_cast<float>(i % 100), static_cast<float>(i / 100), 1.0f,
                            species, texture, sampleMesh);
    }

    size_t totalBytes = 0;
    for (const auto& tree : forest) {
        totalBytes += tree.bytesUsed();
    }

    std::println("Spawned {} naive trees.", forest.size());
    std::println("Total RAM used by naive trees: {:.2f} MB", static_cast<double>(totalBytes) / (1024.0 * 1024.0));
    std::println("Sample tree render:");
    forest[0].render();
    forest[1].render();

    return 0;
}
Duplicate heavy buffers cause memory explosion

10,000 trees each holding a 256-float vertex buffer and texture strings consume over 40 MB of RAM just to display three visual tree variations.

Destructive CPU cache locality

Iterating through an array of bloated 1 KB+ Tree structs blows L1/L2 data cache lines during frustum culling and physics collision scans.

Inability to hot-reload or patch assets

Updating the Oak mesh requires mutating 3,333 individual tree instances in memory instead of updating a single shared asset descriptor.

03

Step 2 — The fix: Flyweight Pattern

Core idea in one sentence

Separate intrinsic state (shared, immutable) from extrinsic state (unique to each instance), sharing the intrinsic objects across thousands of instances via a factory pool.

We separate tree data into two parts. Intrinsic state (species, texture ID, vertex mesh buffer) is immutable and moved into TreeModel. A TreeModelFactory caches and returns a std::shared_ptr<const TreeModel>. Extrinsic state (world position x, y and scale) stays inside TreeInstance, which holds only primitive floats and the shared pointer. 10,000 trees now consume under 520 KB of RAM while sharing exactly three heavy model objects.

Pattern roles in our story
Flyweight

TreeModel — encapsulates immutable shared graphics data: species name, texture identifier, and the heavy mesh vertex buffer.

Flyweight Factory

TreeModelFactory — pools and caches TreeModel instances by species key, ensuring identical assets are loaded only once.

Context (Extrinsic State Holder)

TreeInstance — holds instance-specific transform coordinates (x, y, scale) and references the shared TreeModel via std::shared_ptr.

Client

ForestMap — manages the collection of 10,000 TreeInstances and coordinates scene rendering.

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