10,000 Trees and Bullets Without Blowing Up RAM
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.
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.
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.
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.
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.
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.
The product ticket
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.
- •
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.
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.
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.
#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;
}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.
Iterating through an array of bloated 1 KB+ Tree structs blows L1/L2 data cache lines during frustum culling and physics collision scans.
Updating the Oak mesh requires mutating 3,333 individual tree instances in memory instead of updating a single shared asset descriptor.
Step 2 — The fix: 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.
TreeModel — encapsulates immutable shared graphics data: species name, texture identifier, and the heavy mesh vertex buffer.
TreeModelFactory — pools and caches TreeModel instances by species key, ensuring identical assets are loaded only once.
TreeInstance — holds instance-specific transform coordinates (x, y, scale) and references the shared TreeModel via std::shared_ptr.
ForestMap — manages the collection of 10,000 TreeInstances and coordinates scene rendering.
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.