This is the second piece in the “How to Build an Open-World Game” series. The open world we took apart last time had its engineering center of gravity in “making the world move”—how to keep hundreds of NPCs and a hundred-plus vehicles alive at once, and moving in order. This time we switch engines, and switch game genres too: an open-world urban RPG. On the surface it, too, is a sprawling city you can roam freely—but glance into the engineering for even a moment and you find the center of gravity has shifted.
This article is distilled from a source-level reverse-engineering read of this game’s engine and client code, covering everything from the low-level engine up to the game-side systems—gameplay, narrative, networking, save. All architecture and naming have been generalized; it describes “the typical design of this *kind* of RPG open world,” not any one specific product. Going deep where I read deep, marking blind spots where I didn’t—that’s the discipline of the writing.
Introduction: When the “Open World” Is an RPG
Let me put the whole article’s verdict up front: though both are called “open world,” the engineering center of gravity of the previous piece and of this RPG are utterly different. Last time’s open world had its hard part in “making the world move”—keeping hundreds of NPCs and a hundred-plus vehicles running in order, all at once. This time’s open-world urban RPG moves the hard part to the other end—making a city able to tell stories, and a character able to be built up. This isn’t a difference of genre label, but of engineering center of gravity: the former pours its heaviest budget into scheduling, physics, and AI; the latter pours it into data-driven configuration and narrative orchestration. This article is about the latter.

Why dare to make this call? Start with an observation.
If you open this game’s engine code and sort the modules by size, you’d probably expect rendering, physics, and the “hardest-core”-looking low-level systems to top the list. But in fact, the single largest module in this engine is the narrative system responsible for “how to stage a piece of story”—its source file count approaches nine hundred, thicker than the renderer. Among the large modules right behind it is a data-driven configuration library, dedicated to describing “how a character can remake itself, what attributes a weapon has, how much a skill bonus is”—and the data entries it reserves are counted in the millions.
These two numbers nearly say the whole of what this article is about.
Last time’s open world pressed its heaviest engineering budget into “making the world move”: frame scheduling, physics, the orderly advance of AI. This one, because it is an RPG, additionally shoulders two mountains the previous piece need not carry—narrative density and character depth.
- Narrative density: dozens of handcrafted quest lines that must advance at the same time, be saved and loaded at any moment, and influence one another (your choice on this line changes the course of that one). This isn’t something a few scripts solve; it’s a concurrent, recoverable cluster of state machines.
- Character depth: cyberware, skills, weapons, items, modifiers… an RPG’s “depth” shows up in the player’s ability to remake their character without limit, and that means a content volume an order of magnitude above the previous piece’s kind, tunable for balance without taking the servers down. None of this can be hardcoded.
It’s worth stressing that these two mountains aren’t an art or design choice—they’re an engineering inevitability. Once a game decides its narrative must branch, run in parallel, and be saved and loaded, imperative scripting fails fast on concurrency and recoverability, and the narrative *has* to be refactored into a cluster of state machines—this step isn’t a question of whether you want to, it’s a result forced by scale. By the same token, once content volume crosses a certain order of magnitude, writing values and relationships into code turns every balance tweak into a release and pins content production to the programmer’s compile cycle, so data-driven design shifts from “one way to implement it” to “the only workable way.” This is also why these two mountains outweigh rendering and physics in source size: they aren’t features piled on extra, they *are* the engineering center of gravity of this genre.
So this piece walks the engineering stack of “an open-world urban RPG,” from the world’s foundation all the way up to the gameplay core, watching how these two mountains get caught by engineering. One through-line needs flagging first: whether it’s narrative concurrency, hot-reloaded data, or the frame-spreading of population and physics, their common precondition is an asynchronous bedrock of “issue-and-return, completion-by-signal”—a thread that starts with the job scheduler in Chapter 1, resurfaces in every chapter thereafter, and is tied off in Chapter 7.
The whole article runs on three through-lines that recur from start to finish:
- Content is data. Gameplay—character builds, item attributes, the behavior of world objects—isn’t hardcoded but described by a giant editable configuration library. That content can scale by orders of magnitude and balance can be tuned without server downtime all springs from this.
- Narrative is engineering, not scripting. The parallelism, save/load, and mutual influence of dozens of quest lines are built as a cluster of state machines running “graph execution + a facts bus.” This is the single heaviest investment in the whole engine.
- Tiered cost gives the city its life. A vertically dense metropolis, plus people and cars filling the streets, rides on “lightweight-representation-first, instantiate-by-budget”—the vast majority of objects exist in their cheapest form most of the time, and only the small handful near the player pays the full price.
A reminder on boundaries. What I can explain through is “how the runtime is organized”—the architectural “why” of world streaming, population, data-driven design, the narrative graph, state machines, network replication. What I didn’t read through is the kernel of the low-level algorithms: the collision solver, shader math, navmesh generation, the internals of pathfinding solving, audio DSP—for these I only reach the structural layer or the integration point, and I mark each one. There’s also a class of thing I deliberately don’t write: concrete numeric gameplay design (how a given weapon should be tuned, how a given build is assembled)—that’s the designer’s design, not engineering, my material doesn’t cover it, and I won’t make it up.
The article is long, so a brief reading guide: if you only want the core verdict of this genre, go straight to Chapter 4 (data-driven), Chapter 5 (narrative), Chapter 7 (the close on async discipline); if you care how the open world’s foundation is built, read Chapters 1–3 (foundation, streaming, population); if you care about the presentation layer and engineering boundaries, read Chapter 6. The chapters stand on their own yet echo one another—take what you need.
Below, chapter by chapter.

Chapter 1 · The Engine Foundation: How a Frame Advances, How the World Is Decoupled
The bedrock of an async engine isn’t having many threads—it’s being able to split “issue” from “complete.”
The problem this chapter solves: before unpacking those two RPG mountains, get the foundation straight first—how this system draws the line between “engine capability” and “game content,” and in what order a frame advances.
This layer is shared with most mature open-world engines, so I keep it brief. But it dictates how all the later RPG systems mount onto it: where the line is drawn, on what cadence a frame advances, and who executes the concrete tasks within each beat. Three things, top to bottom, make up the foundation underneath the whole article.
1.1 The Boundary Between Engine and Game
Any engine that intends to evolve over the long term must first answer one question: where do you draw the line between the engine layer and the game layer?
This system’s handling is of a piece with last time’s—the engine layer provides capability and cadence (when to initialize, when to advance a frame, when to shut down), the game layer provides content and rules (what this frame actually advances), and the two sides communicate through a narrow interface, neither needing to know the other’s internals. This is the consensus of mature engines, and I won’t expand on it. What’s worth pointing out is its significance for the whole article: those two RPG mountains to come—data-driven design and narrative—are in essence “the game layer’s content and rules,” and they mount on top of this boundary, driven by the frame cadence below. Put another way, only once the boundary is clean can the engine layer stay stable and the game layer iterate fast; the reason this engine can take narrative and character progression so deep is precisely that this boundary is clean enough to let the game layer grow on its own without disturbing the engine layer.
1.2 How a Frame Advances: Phases and Tiers
What’s truly worth unpacking in the foundation is frame advancement. This part follows the same kind of thinking as last time’s engine, so I’ll state its essentials briefly, with the focus on how it defines the cadence for the systems to come.
A frame is advanced not by “letting each object decide for itself when to update,” but by a world-level orchestrator that slices a frame into several ordered phases: first gather events, run AI decisions, advance the various state machines (settling every object’s “intent” for this frame), then apply movement, run physics, and finally—before rendering—complete the work that depends on “final results” (for example, computations that depend on the final skeletal matrices). This skeleton of “slice by stage, advance uniformly from the world” was argued out for its inevitability last time—the core being that the update constraints between objects hold “by stage,” not “by object,” and so a unified orchestrator is required to guarantee stage order. I won’t repeat the argument here.
Beyond phases, the engine’s per-frame update is also split into tiers. Not every frame and every scene needs to pay the cost of a full simulation: in certain states that need no full simulation (a loading or transitional state, say), the engine can take a stripped-down update tier and save the corresponding cost. Phases answer “the order of the various kinds of work within a frame”; tiers answer “how much to invest in this frame overall.” The two are orthogonal and together form the two dimensions of frame advancement.
These two—phase orchestration and tiered updates—are the “metronome” onto which all the later RPG systems mount. The narrative system’s advancement, the population system’s pooling, the state machines’ transitions—all hang on this cadence. It must be stressed: phases only dictate “which beat does which kind of work,” while who executes the thousands of concrete tasks within that beat, and on which thread, takes one more layer of digging—which is exactly the subject of the next section.
Takeaway: whether for action or for RPG, the moment world scale rises, “who computes first, who computes second” graduates from an implementation detail into a precondition for correctness. On this frame-advancement layer, if you’re also building an open world, I’d suggest looking back at last time’s treatment of “world-level phase scheduling”—it’s the section in the whole series with the greatest effect on architectural intuition. Here, all we need to establish is one point: phase orchestration is the cadence, and beneath the cadence there still needs to be an execution apparatus that actually dispatches each beat’s work onto multiple threads.
🔧 Design Retro · Frame Cadence
Why is it designed this way? That principle in the takeaway—”who computes first, who second, is a precondition for correctness”—roots, mechanically, in one fact: in an open world the update constraints between objects hold “by stage,” not “by object” (AI must first have the full events, physics must wait until all intents are settled, rendering must wait for the final matrices). This is what makes phase orchestration not an optimization but the bedrock of correctness.
What pitfalls were hit? The most common is picking the wrong phase when mounting a new system—reading another object’s state before intents are gathered, and getting last frame’s stale value; or placing a computation that should wait for a “final result” into an early phase, thereby depending on a matrix not yet computed. The value of phase orchestration lies exactly in forcing the developer to think through “in which beat should this logic run.” This is the same constraint, at a different granularity, as Chapter 1’s job scheduling rule that “a job may only execute once its dependencies reach zero.”
Where do traditional approaches fall short? Letting each object compute independently in its own Tick is no problem in a small scene, but at open-world scale it sends the cross-object ordering completely out of control—within one frame A reads B’s old value, B reads A’s new value, the result depends on who gets iterated first, and it’s extremely hard to reproduce. World-level phase scheduling isn’t for elegance; it’s the precondition for “correctness” under large-scale concurrent updates.
1.3 Job Scheduling: The Execution Apparatus Beneath the Phases
Phase orchestration dictates “how many beats a frame splits into and which kind of work each beat handles,” but the thousands of concrete tasks inside each beat—load a given sector, run a given AI, sample a given animation—who runs them, in what order, on which thread, the phases themselves don’t answer. What answers it is the engine’s whole job system. It is the execution layer of phase orchestration, and the shared low-level foundation of every system later in this article: streaming, population, narrative, and physics can all be made async precisely because there’s a scheduler underfoot orchestrating them uniformly.
The reason a dedicated scheduler is needed is that modern engines lean heavily on multithreading: within a single frame a vast amount of mutually dependent work must complete in parallel, and it must all converge inside a fixed frame budget. If each piece of code created its own thread and joined to wait, you fall into two classic traps—thread counts spiraling out of control, with context switches devouring performance, or threads waiting on each other into deadlock. What the scheduler takes over is exactly this: all async work is expressed uniformly as a job, submitted to it for central scheduling. There are three points in this scheduler’s design worth unpacking.
First, priority tiering. Jobs aren’t queued into a single uniform queue; they’re organized into four priority lanes—roughly: Immediate (must finish this frame), CriticalPath (on the critical path), RenderPath (the rendering path), and Latent (deferrable across frames). The scheduling thread pops jobs from high priority to low and hands them to Worker threads to execute; a consumer can even forcibly invert dependencies when necessary. This way, critical work isn’t blocked behind a mass of non-urgent tasks at the back of the queue, while work that can be amortized across frames (prefetching distant resources, say) sinks to the lowest priority and runs only when there’s slack. The reason a fixed frame budget can stay stable is precisely this tiering of “the urgent goes first, the non-urgent pads behind.” There’s one constraint here that must be noted: each priority queue’s capacity is fixed at initialization and does not grow dynamically; the regular submission path spin-retries when full, while the Worker-local path can do an emergency-push to a local buffer to lower deadlock risk—which means sustained overload turns into the friction of spin-retries, and scheduling scale must be planned against capacity in advance.
Second, dependencies are expressed by a counter plus a wait list, not by polling. A job often must wait for a batch of other jobs to finish before it can start. The naive implementation has it repeatedly check whether its prerequisites are ready—both burning CPU and hard to control in timing. The approach taken here: each job is associated with a counter; on issue it first checks whether the counter it depends on is zero—nonzero, and it’s hung on a wait list, occupying no thread; zero, and it’s queued directly. Each completed prerequisite decrements the counter; the instant the counter hits zero, the hung jobs are woken and queued. The whole flow is event-driven: no one spins waiting, everything is pushed by the single event of “the counter reached zero.” This is exactly trading “polling” for “notification”—rather than letting a thread keep asking “is it ready yet,” you let the fact of “completion” come find it.
Third, construction and synchronization are managed explicitly by Builder / Fence. Tasks are organized through job::Builder—which isn’t merely a tool for creating jobs but writes the wait, dispatch, and release boundaries of a group of async tasks into the flow itself: who waits, who releases, when the next round of synchronization may begin, all no longer rely on verbal agreement but are demarcated explicitly by a fence. The benefit is that ownership of async tasks becomes unambiguous; the cost is that the caller must strictly honor these boundaries, or you get the kind of hidden errors where the counter is released too early or the wait relationship becomes indeterminate. There’s a recurring tension here: the more you hand the details of synchronization to the caller’s control (in exchange for flexibility and performance), the less the mechanism itself can backstop you—it relies on the caller’s discipline rather than enforcing correctness from below.
Fourth, the boundaries of the lifecycle are the contract. This kind of scheduler also carries a scattering of implicit constraints: a parallel loop’s finalization logic runs on whichever work unit “happens to observe termination,” not on a fixed thread, so it can’t assume it runs on the main thread; the main thread’s flush interface refuses reentrancy, and a nested wait from inside a callback trips an assertion outright. Each looks trivial in isolation, but together they make up “the contract you must honor to use this scheduler”—pushing part of the responsibility for correctness down from the mechanism onto the caller’s discipline. This too is a common trait of such low-level facilities: the faster the fast path, the stricter the preconditions that ride along with it.
At this point the article’s core discipline surfaces at the very bottom, and its source is right here: RunJob returning does not mean that task has begun executing—it may be hung on the wait list, waiting for dependencies to reach zero; and even once queued, it doesn’t mean a Worker has taken it up to run. When the entire engine’s async execution is built on “issue a job, wait for its counter to reach zero,” it follows naturally that “issued” and “completed” are split into two independent, separately observable events. The “acceptance isn’t completion” that recurs throughout the article, wherever it shows up—in streaming, population, narrative, or physics—traces back to this basic semantic of the scheduler. It’s worth noting that this discipline isn’t this engine’s own invention—any serious engine that schedules a vast amount of async work within a fixed frame budget will sooner or later arrive at “separating acceptance from completion”; but this system carries it through with unusual thoroughness, so thoroughly that nearly every subsystem’s API honors the same boundary, and reading its code you meet it again and again across loading, spawning, action, narrative, and replication.

Takeaway: treat job scheduling as a first-class foundation—express urgency with priority lanes, and ordering with counter-dependencies plus a wait list. This is the precondition for a vast number of async tasks to neither deadlock nor spin under a fixed frame budget. As an engine scales, the naive “each creates its own thread and joins” can’t keep up; only once a unified scheduler stands firm can the upper systems safely “issue and return, finalize in a callback.” The reason the streaming, population, narrative, and physics later in this article can all be made fully async is precisely that this foundation was laid first.
Chapter 2 · Fitting a Three-Dimensional City: Streaming and Multi-Stage Readiness
“Loading” isn’t an atomic action—it’s a chain that spans time, space, and readiness state.
The problem this chapter solves: a vertically dense metropolis—upstairs and down, indoors and out, the signage up close and the skyline in the distance—is orders of magnitude larger than memory. How do you keep the near detailed and the far cheap, and produce no hitch at the moment of switching?
“An infinite world versus finite memory” is the contradiction every open world faces, discussed last time. But an urban RPG pushes that contradiction to a new level of difficulty: its density is three-dimensional—not just blocks laid flat on the ground, but enterable interiors, stacked floors, overpasses and ads hanging overhead. The same square kilometer of footprint needs far more content loaded than a stretch of countryside.
Its response, at the core, is to decompose “loading” into a multi-stage readiness chain, and then decide what exactly to load and unload each frame by mask-driven, on-the-spot arbitration. This chapter unfolds along three orthogonal dimensions: first “to what step is it loaded” (the readiness chain and its milestones), then “who gets loaded this frame” (mask-driven immediate decisions), and finally “how the world is spatially partitioned and culled” (scene organization). Only the three dimensions together make up complete streaming.
2.1 Two-Stage World Mount: Resources Ready ≠ World Ready
The first cognitive trap of streaming is equating “resource loading complete” directly with “world usable.”
This engine splits the world’s mount explicitly into two stages. The first stage is resources ready: asynchronously loading the world data in, and only once all loading jobs complete does it raise the “streaming world” flag (in the source, the setting of m_streamingWorld). Only the second stage is world ready: converting the sector descriptors into runtime sector-wrapper structures, building the bitmasks, assembling wrappers for the always-resident content, and only once this whole set of structures is in place does the world truly enter the Mounted state.
The reason for two stages is that between “data resident in memory” and “the world available to work on” lies a whole set of structure assembly. Loading data complete only means the raw material is in place; for the upper systems (navigation, rendering, AI) to be able to work in this world, the sectors must first be organized, the mesh of proxy nodes established, and the always-resident content mounted. Assembling this structure has its own cost, and splitting it from resource loading into two stages is exactly to give each its own clear completion signal.
Takeaway: set up a completion signal each for “resources available” and “world available,” rather than sharing one. This is streaming’s first lesson. Upper logic—especially the systems that “start working the moment the world is loaded”—if they can’t tell these two signals apart, will act while the world structure isn’t yet ready, and run into a classic kind of fault: the data is indeed loaded but the structure isn’t yet built, presenting as objects that exist in logic but can’t respond properly at runtime.
2.2 Mask-Driven Sector Streaming, Not a Global Priority Queue
Once the world mount completes, the real continuous work begins: every frame, the engine must adjudicate which sectors should stream in and which should stream out.
One intuitive approach is to maintain a global priority queue—compute a priority for each block awaiting load (by its distance from the player, say), put the high-priority ones at the front, and load in order. This streaming system did not take that route, for reasons given in the trade-off below.
What it uses is mask-driven plus per-frame immediate decisions. Each frame, the streaming grid (in the source, NodeStreamingGrid, whose core processing function is Process()) does several things in turn: gather the positions of all “observers” in the scene—note that observers aren’t limited to the player camera but include AI and other systems that need content present; then for each node, by its distance to the various observers, overlaid with multiple bitmasks (enabled, locked, prefetch, occlusion-cull, etc.), adjudicate on the spot whether it should stream in or out this frame. This adjudication happens inside ProcessSectorStreaming: it first computes the content observers’ positions, detects teleports by a distance threshold (a teleport being the case where an observer’s position jumps), then builds the sector mask and drives load and unload—the whole process under the protection of m_sectorsLock.
A few engineering details here are worth unpacking:
- The same node can be in different states for different observers. It may be under the gaze of the player camera (must load) while also falling within some AI’s prefetch range. A mask naturally expresses this “multi-subject, multi-state” situation, which a single priority value cannot carry.
- Batched with SOA (struct-of-arrays) plus SIMD. Sector bounding boxes are organized as a 16-byte-aligned struct-of-arrays, processed a batch at a time by SIMD instructions. Specifically,
ProcessSectorStreaming_CollectInRangeSectorsscans this SOA bounding-box buffer, performs 16-byte-aligned SIMD loads, then applies the enabled, locked, and prefetch masks to filter out the in-range sectors. It must be stressed that this alignment requirement is fatal-assert level—any change to sector volume allocation or counting must preserve the alignment assumption SIMD relies on, or it trips a fatal assertion outright. Facing a city’s thousands upon thousands of streaming objects, this kind of data-layout optimization isn’t icing on the cake but a necessity. - Budgeted, frame-spread advancement. Loading work has a per-frame time budget:
TimeLimitSectorLoadPerFramecaps the promotion rate of completed sectors,TimeLimitSectorUnloadPerFramecaps the unload rate. This way the engine won’t load a large region all at once in a single frame and cause a frame spike, but amortizes it across multiple frames.
But lurking in this design is a counterintuitive point that especially demands caution: streaming’s “completion mask” includes proxies that failed to load. That is, a node appearing in the “streaming complete” mask doesn’t mean it’s truly usable—it may have failed to load yet still be marked into the completion mask. Mask membership isn’t instance availability. If upper code infers from “this node is in the completion mask” that “this object is loaded and ready, operations may be issued on it,” it will operate on an object that actually failed to load; this kind of fault is often imperceptible on screen, surfacing only when some content that should be present quietly goes missing.

Takeaway: for a vast number of streaming objects, “a bitmask plus per-frame immediate evaluation” is both cheaper and better at dynamic re-evaluation than maintaining a global priority queue. A global queue must keep a sort, an O(n log n) ongoing cost, and a node can hold only a single priority, unable to express the “high priority for player A, prefetch for AI B” multi-subject case; mask plus immediate decision dissolves all of that. But always remember: a mask represents “streaming state,” not “available state”—don’t take “in the completion mask” as “safe to use.”
2.3 Streaming Is a Series of Milestones: a Sector from “Requested” to “Usable”
The last section covered streaming’s “immediate-decision face”—each frame computing on the spot which sectors should go in and out. But a sector decided “should load” goes through a series of separate milestones from “begin loading” to “actually usable,” each one a distinct signal you must wait on separately. This face you might call streaming’s “time face.”
Look first at the world level. Loading the entire streaming world (LoadStreamingWorld) is split into three segments: the async request kicks off (returns immediately), a token waits for the job to complete (raises the m_streamingWorld flag, at which point the data is in memory), then MountWorld mounts the world (raises Mounted). But even at Mounted, cached resources are still loading in the background against a counter—mounting only means the structure is built, not that all world runtime streaming is complete. There’s an early-return path here to be especially careful of: if the version mismatches, loading can return before the mount, leaving an intermediate state where “the scene is attached but the streaming-world flag is failure.”
Now the sector level. A single sector’s RequestLoad only kicks off the async request and returns immediately; actually registering it into the streaming grid waits until UpdateLoadingState checks the token as “complete/loaded,” after which it creates the proxy allocator and registers into NodeStreamingGrid. In other words, “issue the load” and “registration succeeds” are two distinct moments.
Finally the prefetch level, which has the strictest definition of “complete”: sector loading complete still isn’t enough. Prefetch goes in three stages—first wait for all prefetch sectors to finish loading, then build a prefetch snapshot of the streaming grid, and finally, only when the snapshot reports all nodes streamed in, notify the prefetch-complete callback (IStreamingPrefetchCallback). “Sector loading complete” is far from “prefetch complete.” There’s also an easily-overlooked callback semantic here: ClearActivePrefetch, when aborting or replacing a prefetch, still calls OnStreamingPrefetchCompleted before clearing the active state even if the callback was never formally notified—which means a caller on the abort path may observe a callback “of the same shape” as a normal completion. In other words, “received the completion callback” is itself insufficient to distinguish “truly prefetch complete” from “the cleanup after a prefetch was aborted,” and the caller must discern it against its own state.

Takeaway: every milestone of streaming should be a separately observable, separately awaitable signal—requested, loaded, mounted, node-ready, prefetch-complete are five different things. Fold them into one vague “loaded” and you’ll operate on a half-finished product at some layer: you think you can use the world once it’s mounted, but cached resources are still loading; you think prefetch is done once the sector is loaded, but the nodes aren’t all in place yet. This section is the other face of the last (immediate decisions)—the former governs “who gets loaded this frame,” the latter “to what step it’s loaded,” and only the two together make up complete streaming.
2.4 Scene Organization: How the World Is Spatially Partitioned and Culled
Streaming has a third face, orthogonal to the first two—the spatial face. The previous sections covered “when to load” and “to what step it’s loaded”; this one covers “how the world is spatially cut up, organized, and culled.”
The basic unit of partition is the sector. World resources are serialized into multiple sector descriptors, registered by category (exterior, interior, quest, navigation) as a SectorWrapper; each wrapper holds a loading token, a proxy allocator, and the compiled sector slices, and registers the slices into NodeStreamingGrid via CreateNodeProxies. The sector is streaming’s basic spatial unit, the node proxy the object under streaming management. There’s a design here that fits the city’s form: road-aware culling doesn’t use a uniform grid but, after StreamingQuery loads the road data, builds a quadtree so that culling unfolds along the road structure—city content is laid out along the streets, a quadtree fits its distribution better than a uniform grid, and it can cover the player-reachable range with fewer nodes when moving along a street.
And the part of spatial organization most worth unpacking is visibility. “Which content should be drawn” is itself a problem that must be carefully organized, and here it rests on occlusion culling, whose core is RuntimeSystemVisibility, managing the registration of occlusion-cull proxies, the render attach and detach queues, and the visibility state. An occluder goes from “loaded” to “actually taking part in culling” across three boundaries—and here that discipline running through the whole article takes a concrete form: a proxy being registered doesn’t mean the renderer has attached it into the scene:
- Initialization boundary:
OnInitializereturningtrueonly means the initialization work has been accepted; it does not mean the occlusion-cull proxy is registered, let alone attached to the render scene. - Registration boundary: only inside
OnAttachis theOccluderProxycreated and inserted intoRuntimeSystemVisibility‘s registry (entry pointAddOccluder). Static occluders register duringOnAttach; instanced occluders first generate a merged resource and then register inOnAttach. On load failure, an empty mesh is left and proxy creation is skipped. - Render-attach boundary: a visible proxy first queues into
m_occludersToAttach, and onlyPreRenderUpdate(or the queue accumulating to 64) issues the render command and truly attaches it—at which point it begins taking part in occlusion culling.
The most hidden spot is this: the code can observe “registered” (GetOccluders sees the proxy in the registry), but that doesn’t equal “the renderer has attached it.” Each of the three boundaries has its own “complete,” and conflating them leads you to believe some occluder is in effect when it’s actually still queued. Beyond that, instanced occluders hide a numeric boundary in the merge stage: when source vertex count times instance count exceeds 65535, the index-type conversion only asserts Uint32 is less than the vertex count, not that it’s less than UINT16_MAX—a hazard to watch at extreme instance scale.

Takeaway: “which objects should be rendered” is itself an async pipeline—don’t assume “a loaded occluder takes effect in culling at once.” The world is spatially cut into sectors and fed into the streaming grid, which shares a source with streaming; and visibility makes the “who to draw” judgment into an initialize → register → render-attach three-stage chain—each link completing only its own step. Spatial organization and streaming are two sides of the same thing: streaming decides “what’s in memory,” spatial organization decides “where these things are and whether they should be seen.”
2.5 Decoupling Story from Resources with a Queue
This section is a bridge between this chapter and the later “narrative” chapter.
In an RPG, the story often needs to directly change the world’s content: a quest segment requires a set of previously nonexistent prefabs to suddenly appear inside a building, or some region’s layout must swap to a different set once the quest advances. The question follows—story logic runs on its own cadence, world streaming runs on its own cadence, how do the two coordinate?
The least advisable approach is to let story logic directly and synchronously lock sectors and modify node state. Then every step the story advances must block waiting for the world to load, the two systems tightly coupled, each cadence constraining the other.
What this engine uses is decoupling via a queue. When the story requires a region’s prefab loading mode to change, the interface it calls (in the source, something like SetQuestPrefabLoadingMode) merely enqueues this “mode-change” request under the protection of a lock and returns immediately—the story logic doesn’t wait, it advances on. The real application happens in a post-processing stage: the engine batch-converts these queued requests into sectors’ stream-in flags, then a piece of pending-state polling logic checks whether the nodes are complete, and once complete, copies the listener list outside the lock and fires the callbacks.
That is, the story side “issues the request” and returns; the synchronization between world streaming and the story is gradually aligned by the queue in the background. This is the first time in the article we see “narrative directly driving the world”—a thread that will resurface in Chapter 5 when narrative is covered.
Takeaway: between game-logic requests (quests, story, UI) and resource streaming, go through a decoupled channel joined by a queue, rather than a direct synchronous call. Applying them in batch in a post-processing stage compresses the expensive sync point to a minimum. The moment story logic blocks directly on resource loading, the quest system gets pinned to the cadence of world streaming, and neither side can make progress.
2.6 Prefabs: How Scene Objects Are Organized Into Reusable Units
The “prefab” in the last section’s SetQuestPrefabLoadingMode deserves its own section—it answers a question no open world can avoid: are a city’s so many blocks, rooms, and street fixtures placed by hand, one by one, by artists? The answer is no. They’re assembled by reusing prefabs countless times in the world. This is the same logic as the material “make-many-from-few” in Chapter 4, only the object switches from “material appearance” to “scene object.”
One definition, citywide instances. A prefab is a resource definition—containing a set of nodes, supporting groups, and nestable (a prefab within a prefab). The same definition is wrapped into multiple instances (in the source, PrefabNodeInstance) scattered throughout the world: one on a corner, one in a plaza, one in an alley. Each instance independently runs async loading and carries its own instance data. Artists author once, the engine reuses it dozens or even hundreds of times—this is the first layer of a prefab’s “make-many-from-few.”
How does the same prefab produce variety? By two means. The first is transform override: each instance applies its own scale on instantiation, adjusted by proxy distance, so the same prefab is enlarged up close and shrunk in the distance—the node topology unchanged while the dimensions differ wildly. The second is variant switches: a variant manager (in the source, PrefabVariantsManager) maps a “prefab variant descriptor” into per-group show/hide toggles—a prefab’s damaged version and intact version, day and night, are in essence different group combinations of the same skeleton. So a handful of prefabs, through permutations of scale and variant, blanket the whole city’s blocks and rooms.
How does the story drive their appearance and disappearance? This loops back to the last section: when the story requires a region to “spawn” or “remove” a set of prefabs, the request is enqueued, batch-applied in post-processing, and the listeners are notified after polling that the nodes are ready—the story side issues and returns, the prefabs’ appearance and disappearance align asynchronously in the background. It, too, honors that familiar boundary: instantiation is staged, “load issued” doesn’t mean “the prefab is truly visible,” and the ready notification the listener receives must be discerned against actual state rather than taken at face value. In other words, the story’s rewrite of the world goes through the same “issue-and-return, completion-by-signal” decoupled channel as resource streaming.

Takeaway: making “scene objects” into reusable, parameterizable units that the story can schedule is an unavoidable piece of engineering for big-world content volume. Artists author one set of prefabs, the engine places it countless times, each instance scalable, variant-switchable, loadable or removable by the story—a city that couldn’t be done by hand, piece by piece, is thereby assembled. This belongs to the same “make-many-from-few” logic as materials and data-driven design: to scale content by orders of magnitude relies not on piling up more assets by hand, but on making assets into reusable, parameterizable units.
🔧 Design Retro · Multi-Stage Streaming
Why is it designed this way? Because in a vertically dense city, “loading” was never an atomic action but a chain spanning resource IO, structure assembly, proxy registration, and render mount. Different consumers care about different nodes on the chain—the navigation system only needs navigation data ready, the renderer needs the render proxy registered, the AI needs the entity queryable. Give a vague “loaded” signal, and instead no one dares trust it. Splitting readiness into multiple observable stages is exactly to let each system take what it needs.
What pitfalls were hit? The most hidden is the “false success in the completion mask”—a proxy that failed to load is marked into the completion mask all the same. Infer from mask membership that “the instance is loaded and usable,” and you’ll issue an operation on an object not yet truly in place. This kind of fault is often imperceptible on screen, surfacing only when content that should be present quietly goes missing, or an operation lands on an empty object.
Where do traditional approaches fall short? “The load-complete callback fires once, and the upper layer treats everything as ready” is no problem in a small scene, but in a streaming big world it mistakes “resources in place” for “world usable,” consuming a half-finished product as a finished one—the fault is in timing, not logic, and so is extremely hard to locate. Likewise, letting story logic directly and synchronously operate world loading runs fine in a linear level, but in an open-world RPG it deadlocks narrative against streaming. Multi-stage readiness plus queue decoupling isn’t for formal elegance—it’s a necessity under this scale and density.
Chapter 3 · The People and Life of the City: Population, Crowds, and Smart Objects
A city’s vitality comes not from “everyone fully simulated” but from “the right people paying the right cost at the right distance.”
The problem this chapter solves: an urban RPG must have “life”—pedestrians filling the streets, a stream of cars, people sitting in a bar, people mending things on the sidewalk—but obviously you can’t run full AI and physics for each one. How do you make the city look alive while not dragging the machine down?
Population is a topic last time touched on too. But in this piece I treat it as the engineering of “the city’s sense of life”—for an urban RPG, “whether there are pedestrians on the street, and whether those pedestrians look like they’re living their own lives” is the very root of immersion. The core idea is the home turf of the third through-line: lightweight-representation-first, instantiate-by-budget. This chapter unfolds along one thread: how a person exists in the cheapest form, how people near and far are tiered, and how those who are “doing something” are driven—then, at the chapter’s end, fills in the physics and navigation that support all of this in motion.
3.1 Stub-First: an NPC Is First a Record, and Only Becomes a Character on Demand
Start with the plainest question: to put a hundred pedestrians on a street, must you create a hundred complete character objects in memory?
If so, this city’s memory and CPU would buckle fast. A complete NPC character—with skeletal mesh, AI, physics, and assorted components—is quite heavy. A city has potential NPCs “present” in the thousands at any moment, and implementing them all as complete objects is an unbearable resource cost.
The answer is stub-first. An NPC, by default, is not a complete character but a lightweight stub (EntityStub)—essentially just a record, a bit of metadata, taking almost no resources. Only when it meets conditions (enters budget range, becomes visible, gets close enough) is it “resolved” (in the source, this action is Resolve())—promoted by the entity-stub system into a true entity.
This promotion path is itself staged. After the population system’s outward interface (such as PopulationSystem::AddEntity) is called, it has only enqueued and accepted the request, which doesn’t mean the NPC has appeared. The real processing is in the later pooling logic: the request is first registered, then a spawn record and template are computed, queued for spawning, and through the entity’s spawn token, attach scheduling, and event broadcast, an NPC truly appears in the world. And this spawn queue is throttled—simultaneous spawns are held to about ten, and lower-priority spawns are even cancelled when necessary to make room for the more urgent. Throttling is necessary because instantiation is itself a heavy operation; without a cap, one large visibility change (the player rounding a corner, say) could detonate hundreds or thousands of instantiations within a single frame and blow through the frame budget.
This points out an engineering reality recurring through the piece: these interfaces’ “return success” means “the request is accepted,” not “the work is done.” AddEntity returning doesn’t mean that NPC is standing on the street—it may still be queued, or partway through some link of the spawn-token chain. In the population system, this discipline takes the form: the request is received, which doesn’t mean the entity is available. It runs through nearly every subsystem in this system, and we’ll tie it off in the last chapter; its source is exactly that scheduler built around the job counter at the end of Chapter 1.
Takeaway: make “existence” the cheapest metadata state, and “instantiation” an on-demand upgrade. The reason a vast number of objects can “exist” without a memory blowup is that they’re, the vast majority of the time, just a record rather than a complete object. Promote only when needed, rather than complete-on-creation—this is the root of how a city can hold a city’s worth of population.
3.2 Three-Tier Crowds: Full NPC / Simplified Group / Distant Approximation
Stub-first solves “how potential NPCs exist cheaply,” but there’s still a problem: those already present shouldn’t all be uniformly complete characters either. An NPC standing in front of you talking, versus a figure walking across an overpass a kilometer away, should obviously pay wildly different costs.
This engine tiers “the people in the city” by cost and fidelity into three:
- Full NPC: a character with full AI, physics, and entity. This is the small handful near the player—interactable, with behavior, taking part in combat. It’s the most expensive, and therefore the fewest in number.
- Crowd stubs (CrowdStubs): simplified behavior plus visual representation. They derive position from a traffic lane, store a lightweight stub pointer, and are cleaned up by a callback on deletion. They make mid-distance streets “have people walking” without running full AI—a middle tier between “full entity” and “pure visual.”
- Distant crowd (DistantCrowd): pure visual approximation. This is the farthest, cheapest tier—essentially a multi-frame state machine that picks among lanes, maintains a buffer of “dots,” and emits render data. It can even, conversely, request spawning regular crowd via the dots on a lane, smoothly transitioning to a higher-fidelity tier as the player approaches.
There’s a counterintuitive point that must be made clear: a distant crowd being “render-valid” doesn’t mean “the game entity exists.” You stand on a height and watch a sparse stream of people move on a distant street—that’s very likely just a batch of dots used for rendering, with no real NPC entity behind them. They’re non-interactable, have no AI, take part in no game logic, purely to make the distance “look like it has people.” As you approach, these pure-visual dots get replaced on demand by real (if simplified) NPCs. In other words, the three tiers aren’t a static partition but a continuous ladder rising and falling dynamically with player distance.

Takeaway: a crowd is a “cost-fidelity ladder,” not a black-and-white “people / no people.” The farther out, the cheaper the approximation used—full character, simplified group, pure-visual dots, the three tiers each governing a stretch of distance. Pour all the budget saved into the small handful right in front of the player that truly needs high fidelity. This is exactly the key to “streets full of people” and “stable frame rate” holding at the same time.
3.3 Smart Objects and Workspots: Making the City’s People “Do Something”
Just “having people walking” still isn’t enough. In a believable city, people are doing things—leaning on a wall smoking, sitting on a bench, busy behind a stall, operating some device. This sense of “doing something” isn’t from hardcoding a script for each NPC, but from a system of smart objects plus workspots.
A smart object is one of those things in the world “usable by an NPC”—a chair, a leaning spot, an operable device. Its occupancy mechanism is rather telling: not a “first-come-first-served, exclusive-hold” lock, but a system of “demand plus reservation.” When an NPC wants to use a smart object, it registers a “demand” (in the source, something like AddSmartObjectDemand), so multiple NPCs can queue for the same object rather than fight and block each other. Further, these objects’ capacity management is budgeted per sector, rather than maintaining a global object pool—how many smart objects can be active at once within a region is bounded by a budget. This shares a source with streaming’s sector budget: the density of city life is likewise folded into the unified framework of “allocate budget by region.”
A workspot, then, is a “station” bound to an animation sequence—it delimits “which actions can play here.” Here the piece’s leitmotif surfaces again: command submission isn’t playback completion. When the system has an NPC begin an action at some workspot, the command-submission interface (SendCommand) merely enqueues the command, and its return value represents “whether an active instance received this command,” not “the action has finished playing.” Real completion waits for the later completion callback (OnCompleted). And if some action lacks the corresponding animation resource, the system doesn’t hard-fail but takes a timeout-fallback path—it would rather have the NPC gracefully exit this action and continue its behavior than have it stuck on a station that can never complete.
This means that “fixing the car” NPC and that “leaning on the railing looking at a phone” NPC on the sidewalk are each backed by a complete flow of “submit a workspot command at some smart object’s workspot, and wait for it to finish playing”—schedulable, queueable, interruptible, rather than a hardcoded looping animation. This “demand plus reservation” occupancy and “submit plus complete” dual signal make city life a uniformly schedulable system, rather than scattered, mutually unrelated scripted animations.

Takeaway: use “demand + reservation” for environment interaction rather than a preemptive lock, and a “command-submit / playback-complete” dual signal rather than a single boolean. The former lets multiple NPCs naturally queue for one chair instead of stepping on each other; the latter makes “start an action” and “the action truly completes” two separately observable signals, and lets it gracefully fall back when a resource is missing. This way, “people doing things” in the city becomes a schedulable system rather than hardcoded looping animation everywhere.
🔧 Design Retro · Giving the City Life
Why is it designed this way? The takeaway has already named the “cheap existence, on-demand upgrade” idea; I’ll add just one note on why it’s three tiers and not two: between the full NPC and the pure stub lies a mass of people “mid-distance, needing to look like they’re moving, yet not worth fully simulating”—without the “simplified group” middle tier, either the near high-fidelity budget gets thinned out or the mid-ground suddenly empties, both breaking the city’s continuity. The three-tier crowd, plus the smart-object scheduling needed for “doing something,” are at bottom different tiers of the same trade-off: between fidelity and scale, carve out several sweet spots by distance rather than pick one of two.
What pitfalls were hit? The easiest mistake is taking any spawn interface’s return as “the person is in place.”
AddEntityis only enqueue-and-accept; true availability waits for the whole spawn-token chain to finish, and operating on that not-yet-existing entity right after gets you a null. Another is the distant crowd’s “visual illusion”—mistake those pure-render dots for real NPCs and apply game logic, and you’ll find they have no AI and aren’t interactable. Both have the same root: mistaking “render-visible” or “interface-accepted” for “the game entity is ready.”Where do traditional approaches fall short? “Pre-place all NPCs in the scene” or “LOD the whole character by distance in real time” can hold in a small scene, but in a thousand-strong city the former exhausts memory outright and the latter blows the frame rate. Stub-first compresses the cost of “existence” to a record and makes “instantiation” an on-demand upgrade, then pulls fidelity tiers apart by distance with the three-tier crowd, to fit a city’s worth of people on one map—the cost being that the upper layer must accept “everything is delivered asynchronously” and can no longer assume “I request an NPC and it’s present at once.” This is exactly where the article’s main axis lands in the population system.
3.4 Physics: Plugging a Synchronous Physics Engine Into an Asynchronous World
For the city’s people and cars to “be alive,” AI alone isn’t enough—they have to stand on the ground, bump into walls, dodge obstacles, get hit by bullets. All of this needs physics. And physics has a peculiar engineering problem in this engine: underneath it uses a synchronous-model commercial physics engine (the PhysX kind), while the open world is asynchronous and frame-spread. How do you plug a synchronous middleware—”you call it, it computes for you at once”—into an asynchronous engine where “everything issues and goes, finalizes in a callback”? The algorithmic kernel (how collisions are solved) is a blind spot, but this architectural problem of “how to plug it in” is right in the range I can explain through.
The core idea: a synchronous API, asynchronous scene mutations. When you want to add a physics proxy to the scene (AttachProxy), it does not immediately go change PhysX’s scene—instead it makes this operation a PendingProxyAction and enqueues it. The real attach is deferred to the physics simulation’s processing stage (OnSimulate_ProcessPendingProxyActions), done in batch within an exclusive window. Why so roundabout? Because a single frame may have a large number of adds and removes, and concentrating them into an exclusive window for batch processing both avoids repeatedly firing callbacks and locks the mutations to PhysX’s scene to a controlled moment. The cost is that familiar discipline: AttachProxy returning doesn’t mean the proxy entered the scene—you must wait for the simulation to run (WaitSimulation) to be sure.
A frame’s physics simulation (KickSimulation) is orchestrated into a chain of jobs: first drain the pending proxy actions, pre-step, then run the PhysX/cloth simulation in parallel, then do the contact-callback fan-out, async queries, and finally push dirty transforms and flush. This decouples the physics frame from the game frame—the main thread can carry on with other work after issuing.
There are two especially watch-worthy pitfalls here. One is the async query’s “Success trap”: a raycast/sweep async query returning Success covers both “hit something” and “didn’t hit but did compute”—NoHit also maps to Success. So you can’t look only at the status; you must check the hit array to know whether there was actually a hit. The other is the geometry cache’s three states: physics geometry streams with sectors, its state transitioning among Pending/Loaded/Abandoned—if a sector is unloaded before its geometry finishes loading, the geometry turns Abandoned, and queries on it thereafter get no geometry and return Invalid.

Takeaway: plugging a synchronous middleware into an async engine hinges on giving it an “exclusive mutation window” + “deferred callback/release”—so the external synchronous calls land at an internally controlled moment. Physics is a classic example: the API layer takes your request (AttachProxy, issue a query), but the real scene mutation, callback firing, and memory release are all gathered into the simulation’s processing stage and done uniformly. This both preserves PhysX’s synchronous semantics and lets it run in parallel with other systems under the open world’s fixed frame budget. When you integrate any “synchronous-model” third-party library, this “synchronous shell + asynchronous kernel” pattern applies.
3.5 Navigation: the Pathfinding Mesh Streams With the World, Paths Ride “Token + Multi-Frame Assembly”
For people and cars to walk the city, they need pathfinding. But open-world pathfinding has a peculiar difficulty: the world is seamlessly streamed—the navmesh loads only where you’ve walked. This means pathfinding can’t assume “the whole map’s navigation data is in memory”; it must coordinate with streaming. The pathfinding algorithm’s kernel (the math of A\*, of Detour) is a blind spot, but this architectural problem of “how navigation coordinates with streaming, how a path is assembled across frames” can be told.
Here navigation is split into three layers: RuntimeSystemNavigation manages the streaming and caching of navmesh tiles; RuntimeSystemPathfinding manages multi-frame path assembly; RuntimeSystemTraffic manages global traffic (lanes, intersections, vehicle spawning). Pedestrians walk the navmesh, vehicles walk the lane network.
The navmesh loads with streaming, likewise a series of milestones—a token enqueued doesn’t mean the tile is queryable. RegisterTiles only enqueues a registration token and notifies listeners—the tile isn’t usable yet. The real Detour tile insertion happens in RegisterQueuedTiles, inserting one by one against a per-frame time budget, requeueing what doesn’t finish, until NotifyStreamedIn reports all tokens succeeded—only then is the navmesh truly queryable.
A path isn’t computed in one go—it’s assembled across multiple frames. StartPathfinding returns a token, not the path itself—it enqueues the query, issues the first query, and returns. Each frame thereafter Update advances active queries and writes results in batch; the caller polls TryToGetPath to fetch—returning only when there’s a result, continuing to wait otherwise. If the relevant region is still streaming during pathfinding, the query exposes a “partial result,” stays active, and retries every few seconds rather than failing outright.
There’s also a multi-path fallback layer that makes pathfinding more robust: GPSQueryBuilder picks a branch between navmesh and traffic lane, and uses a “fallback target set”—a single segment failing needn’t fail the whole path, and it’s only a true failure once all fallback targets have been tried. And precisely because of these fallbacks and “partial results,” navigation’s “success” signal demands special care: isReady, or a non-null result pointer, neither suffices to prove a true success—it may be an empty placeholder path or a partial result, and validity must be verified.

Takeaway: in a streaming big world, pathfinding must make both “mesh loading” and “path computation” async, cross-frame, and tolerant of momentarily-absent data. The navmesh streams with sectors (RegisterTiles only accepts, amortizes insertion, usable only when all tokens succeed), the path is made a token, assembled across frames, polled by the caller, plus multi-path fallback as a backstop—only thus can an NPC find paths in a world that “loads as it walks,” instead of getting stuck because the street underfoot was just streamed in. (Honest boundary: the pathfinding kernel, the frame-budget allocation for hundreds of concurrent NPCs, active replanning when a street re-streams, dynamic obstacle avoidance—these notes I didn’t read through, and this section doesn’t expand on them.)
Chapter 4 · The Core of the RPG: Data-Driven Character Progression
An RPG’s depth is, first of all, a data-engineering problem.
The problem this chapter solves: an RPG must let the player remake their character without limit—cyberware, skills, weapons, items, modifiers—a content volume an order of magnitude above the previous piece’s kind, tunable for balance without server downtime. How could this possibly be hardcoded? The answer is: it isn’t in the code at all.
This is the most fundamental engineering divide between this game and last time’s open world, and the chapter I think has the most to it. An RPG’s “depth”—what the player feels is “how I can shape my character, build my build”—comes down, in engineering, to a data-driven capability: moving gameplay out of the code and into an editable, hot-changeable data system that can hold a vast amount of content.
I unfold along one thread: how gameplay moves from code into data → how this database holds a million entries → how data is safely consumed at runtime → how it supports tuning without server downtime.
4.1 Gameplay as Data: One Giant Editable Configuration Library
Look first at the naive approach. How much damage a weapon does, what effect a piece of cyberware grants, how much a skill adds per level—the most intuitive implementation is to write these as constants in code, or stuff them into a handful of config files read in at runtime.
This is entirely sufficient when content volume is small. But an RPG’s content volume isn’t at that scale. It has hundreds or thousands of weapons, cyberware, skills, items, each in turn with dozens of attributes, modifiers, and relations; and they cross-reference one another (which category this weapon belongs to, what prerequisite this cyberware needs, which values this skill affects). Writing these into code amounts to fossilizing the whole flesh of gameplay into the program—changing one value requires a recompile, adding one piece of gear requires a programmer, a balance tweak requires shipping a new build. When content is counted in the thousands and adjustments in the days, the cost of this coupling is no longer “a hassle” but directly sets the ceiling on content throughput and iteration speed.
This engine’s approach is to build a unified, giant, editable configuration library (a data-driven database). Nearly all of the game’s gameplay values and relations are described with two basic units:
- Record: a structured object, like “a given weapon,” “a given piece of cyberware,” “a given skill.” It has its own type, its own set of fields.
- Flat: the concrete attribute values inside a record—damage numbers, bonus coefficients, the IDs of other records it references, and so on—stored flatly in one big table.
Every build a player assembles in-game is, in essence, dealing with the records in this library—equipping a piece of cyberware activates a corresponding record in the library; swapping a weapon switches to another record.
How big is this library? One number says a lot: its flat-value storage reserves a capacity of one million eight hundred thousand entries (that hardcoded reserve number in the source is 1,800,000). A system reserving its internal storage to the million scale is itself proof of the order of content scale—this is no small system of a few hundred config items, but a large database meant to hold the whole of an RPG’s gameplay data. Records and flats are protected at runtime by different locks: record and flat each hold their own lock, meaning access to structured objects and access to concrete values can happen in different critical sections without blocking each other—a design trade-off made for high-frequency concurrent queries.

Takeaway: an RPG’s “depth” is, first of all, a data-engineering problem. The player thinks they’re experiencing “a rich character-progression system,” while what’s built in engineering is “a database that can hold a vast amount of gameplay data and query it efficiently.” Moving gameplay out of code and into editable data is the precondition for content to scale by orders of magnitude and iteration to not depend on programmers. If you’re building a game with deep progression, this data-driven foundation is the first hard bone you can’t get around.
4.2 Holding a Million Entries: Warm-Up, Validation, and a Generative Schema
A database meant to hold a million entries—”able to hold” alone isn’t enough; it must also be able to load fast, validate integrity, and let upper code access it in a type-safe way. This system has its particulars on all three, which together form a kind of “layered notion of success.”
Fast loading rests on warm-up. Parsing a million entries one by one at startup would take unacceptably long. So it takes the “optimized binary blob warm-up” route (in the source, a LoadOptimized kind of path)—preprocessing the data into a compact binary block, pouring it in whole and quickly expanding it at load time, rather than parsing entry by entry. This moves the “parse cost” forward from runtime to the offline cook stage.
Integrity rests on validation. It runs a CRC check on the loaded data to confirm the file isn’t corrupted.
Type safety rests on a generative schema. This config library’s record types and field definitions are in large part auto-generated—of those nearly thousand source files, most are record headers and type definitions a tool generates from the data schema, wrapped on the outside by a hand-written flattening runtime. This way, each new record type needs no programmer hand-writing its memory layout and access code; the generator produces it, and the runtime reads and writes it by field meta-info via the engine’s type system. This is a clean three-tier division: the editor-side data files, the generative schema boundary, the hand-written runtime—each minding its own, none crossing the line.
But lurking here is an extremely important and extremely hidden counterintuitive point: scan-success, warm-up-success, CRC-success—none equals data-semantic validity. CRC can only show “the file bytes aren’t corrupt,” warm-up can only show “the data is expanded into memory”—they cannot show “the cyberware ID this weapon record references actually exists” or “this skill’s value is in a reasonable range.” All these outer checks pass, and the inner data semantics may still be missing, wrong, self-contradictory. This is exactly what “layered success” means: each layer’s check vouches only for the correctness of its own layer, never standing surety for a deeper one.

Takeaway: a vast data-driven system should be three-tiered—editor-side data files, a generative schema boundary, a hand-written runtime—and you must accept “graded success.” The outermost integrity checks (CRC, warm-up) passing never equals data-semantic validity. Conflating the two is the spot in a data-driven system where it’s easiest to let your guard down: you see the “load success” log and assume the data is fine, while in fact a record with a bad reference lies quietly in the library, waiting to surface in some player’s build. Integrity checks answer “are the bytes right,” semantic checks answer “is the content right,” and the latter must be borne by your own toolchain and process.
4.3 Safe Consumption: the Getter’s “Default Fallback” Is a Double-Edged Sword
How does upper code fetch a value from this library? “Get this weapon’s damage,” say.
Here a templated getter is provided (in the source, something like Val<T>). Its behavior includes a far-reaching design decision: when the data fetched doesn’t exist, it doesn’t error or throw an exception but automatically falls back to that type’s default value.
This design has its benefits—it spares the getter code from writing defensive null checks everywhere, and when some value is missing the system can carry on with the default rather than interrupt constantly. But it’s a double-edged sword, and for an RPG the other edge of this sword is especially sharp.
Picture this: some effect of a piece of cyberware, because some datum wasn’t configured, quietly falls back to a default (0, say) when fetched. The game doesn’t interrupt, the log may show no obvious error—but this cyberware, in the player’s hands, simply doesn’t work, or works incorrectly. For an RPG with values at the core of its gameplay, this “silent fallback” is one of the most dangerous defects: it throws no exception, it just silently goes wrong, until the player notices some piece of gear “seems useless” and goes back to investigate—by which time the data has long been live. Its hiddenness comes exactly from “looking robust”—an interface that never crashes turns “missing content,” a problem that should surface at once, into a silent error.
So this engine provides, at the interface level, an explicit existence check (HasEntry)—the caller should confirm the datum truly exists before fetching, rather than rely on the default fallback. In other words, “default fallback” is a backstop safety net, but it shouldn’t be used as the normal path.

Takeaway: a data-driven getter should either force an existence check or make a miss error explicitly—never paper over a content gap with a silent default. For a game where values *are* gameplay, “fall back to a default when you can’t fetch it” looks robust but in fact turns “a content configuration error,” a problem that should surface instantly, into a hazard that lurks until after launch. A safety net may exist, but the team shouldn’t form the habit of acting in reliance on it—or the safety net degrades from “backstop” into “blindfold.”
4.4 Tuning Without Server Downtime: Hot-Reload and Type Drift
Data-driven design’s biggest payoff shows up in iteration speed: a designer changes a value, and without waiting for a programmer to recompile or shipping a new build, sees the effect once the data is changed. This three-tier separation (editor-side files, generated schema, hand-written runtime) serves exactly this—it supports hot-reload: reloading config data at runtime so changes take effect immediately.
For an RPG that needs continuous balance polishing, this capability is nearly indispensable. Weapon strength, enemy health, skill bonuses, economy values… all must be tuned and playtested over and over, and if every value change required a recompile and relaunch, iteration efficiency would be badly dragged down. Hot-reload turns “changing a value” from an engineering operation into a near-instant adjustment.
But hot-reload has a cost, and the cost hides in an easily overlooked spot: at reload time, the validation of type changes is relaxed. When some field’s type in a reloaded record differs from before, the reload path here uses static_cast—which performs no strict type check, directly reinterpreting that memory as the new type. That is, hot-reload trades “relaxed type safety” for the ability to “swap data at runtime.” Ordinary value changes are no problem, but if you mistakenly change some field’s type during a hot-reload, it won’t error—it’ll just silently interpret the data the wrong way. This is the same hazard as the last section’s “silent default,” two faces of it: the former goes silent on “can’t fetch,” the latter on “wrong type”—both throw no exception, both just silently go wrong, and “silently going wrong” is exactly the hardest class of fault to locate.

Takeaway: the data-driven killer feature is hot-reload—letting designers change values without waiting on a compile—but you must think through the type/schema drift boundary at reload, lest “hot-changeable” become “changeable-to-broken.” Hot-reload relaxing type safety is a reasonable engineering trade-off (strict validation would make hot-reload slow or even unworkable), but this boundary must be clearly marked and constrained by tools or process, or one slip of a type change can plant a silent landmine at runtime.
🔧 Design Retro · Gameplay as Data
Why is it designed this way? The takeaway has made clear the cause-and-effect that “content volume forces data-driven design”; I’ll add an often-overlooked corollary: once content volume is large enough to require designers to iterate independently of programmers, the data must carry its own runtime validation, hot-reload, and type system, or “editable” backfires into “changeable-to-broken.” That reserve of one million eight hundred thousand entries proves not just the scale but the order of pressure this validation and hot-reload machinery must withstand—it moves the correctness guarantee originally borne by the compiler wholesale into the runtime. This is exactly the origin of the pitfalls below.
What pitfalls were hit? The most hidden is the “silent default”—an unfetchable config auto-falls-back to a seemingly reasonable default, so a poorly-configured piece of cyberware doesn’t interrupt but silently fails to take effect, lurking until a player notices after launch. Next is type drift at hot-reload: reload bypasses the type check with
static_cast, and a mistaken field-type change doesn’t error but silently interprets memory the wrong way. What these two share is—they throw no exception, they just silently go wrong, and “silently going wrong” is exactly the hardest class of fault to track down. They share a source with the foundation-layer “false success” described at the close of Chapter 7: each layer’s check vouches only for its own layer, and semantic correctness must be borne by the layer above.Where do traditional approaches fall short? Writing gameplay values as code constants or scattered config files suffices in a small game; at RPG content volume it turns every balance tweak into a release, pins content production to programmers, and makes designers wait a compile cycle for every value change. Data-driven design isn’t for elegance but a hard requirement forced by RPG content scale and iteration speed—its cost being that you must take on by hand the layer of “data correctness” the compiler used to provide for free. The type errors and reference errors the compiler caught for you must now be caught by your own toolchain, validation process, and discipline. It’s a clean trade: compile-time safety for runtime flexibility and iteration speed.
Chapter 5 · Narrative Is Engineering: Graph Orchestration and a Facts Bus for Handcrafted Quests
Narrative isn’t scripting—it’s a runtime system that can run in parallel, recover, and propagate influence.
The problem this chapter solves: a story-driven RPG, with dozens of quest lines that must advance in parallel, be saved and loaded at any moment, and influence one another—your choice on this line changes the course of that one. How do you make this narrative density into an engineerable system rather than a tangle of scripts?
I mentioned a number at the start: the largest module in this engine is the scene system responsible for narrative, with source files approaching nine hundred. Now we come to it formally. This fact is itself a forceful claim—for this game, narrative is not the job of writing a few scripts in the finishing stage; it’s first-class engineering, the single heaviest investment in the whole engine.
Why does narrative get this heavy? Because an open-world RPG’s narrative has three hard requirements a linear game never faces:
- Parallel: dozens of quest lines exist at once, the player can switch among them freely, each on its own progress.
- Recoverable: save at any moment, load at any moment, and after loading the whole narrative state must pick up exactly where it left off—not from the start, but from “that world halfway through.”
- Mutual influence: a line’s choices and outcomes must be seen by, and reacted to by, another line.
These three requirements, imperative scripting (“play this, then that, then check something”) simply can’t carry. Its answer is to make narrative a cluster of state machines running “graph execution + a facts bus”—the home turf of the second through-line.
I unfold along one thread: how a piece of story is orchestrated → how dozens of quests run in parallel → how quests influence one another → how all of this is saved and then resumed.
5.1 The Scene System: Orchestrating a Cutscene/Dialogue Into a State Machine
Look first at the smallest narrative unit—a cutscene, a dialogue.
The naive thought: a cutscene is “playing a string of things in order”—play this line, cut this shot, play that action. But an open-world RPG’s cutscene is far more complex: it may be interrupted by the player, may need to wait for a resource to load, may insert a node midway that requires a player choice, and some line in the dialogue may trigger another system. A linear “playlist” can’t express these.
Here a cutscene is made a tick-driven state machine. When a scene is requested, the request interface (DoQueueRequest) merely enqueues this control request and returns a request ID—the real work is split into several boundaries: enqueue (completes immediately), pre-process (build the internal stub on the next frame), execute (advance in the tick state machine). These three boundaries each stand alone, and that’s exactly the structural precondition for all the later flexibility.
Here that counterintuitive thread running through the whole article surfaces again: the request ID obtained is only a “receipt of acceptance,” not a “proof of completion.” Further—a scene’s resource handle can even exist while the resource hasn’t finished loading (it may be in a “fetching” or even “fetch-failed” state). The activation of a dialogue, the registration of a choice-hub, are likewise “scheduling boundaries”—they represent “this thing has been scheduled,” not “the audio has played” or “the player has chosen.” In other words, from request acceptance to the staging truly landing, several separately observable boundaries lie between, and the caller must not mistake any one of them for the finish line.
What this design buys is the flexibility of narrative staging: because a cutscene is a state machine and not a playlist, it can be interrupted (cut into a new state), wait for resources (rest at some waiting state), insert a choice (transition to a choice state and return). None of these can a linear script provide.
Takeaway: making “staging” a schedulable state machine rather than a linear playback script is the precondition for a cutscene to be interruptible, save/loadable, and able to insert interaction. What the player takes for granted—”a dialogue halfway through can be skipped,” “after a choice the story continues”—is backed by decomposing each piece of staging into states that can be entered, exited, interrupted. Write a cutscene as “play in order,” and none of these can be achieved.
5.2 The Quest Graph: Event-Driven + Indexed Continuation, Letting Dozens of Lines Run in Parallel
Above a single cutscene is the quest. An RPG has dozens of quest lines advancing at once—how do they not collide?
This engine makes a quest an event-driven graph. A quest is a graph whose nodes represent the quest’s various stages, with the flow between nodes driven by events—some condition met, some fact changed, the player did some action pushes the graph one step forward.
A quest’s startup is itself async: launching a quest (RunAsync) merely appends it to a to-run queue (m_questsToRun), and the real activation waits for the later tick logic (TickPendingQuests) to handle—taking it out under a lock, advancing the quest’s hash into the active list, then starting the graph’s root node. This means “launch a quest” and “the quest activates” are likewise two separate moments.
And the truly ingenious part lies in how it handles event continuation. When a batch of events must push multiple quests forward, a naive implementation would iterate holding the events and trigger nodes directly—but there’s a hazard here: in the process of triggering nodes, new events may be produced and need appending to the queue, and iterating and modifying the same container at once easily causes iterator-invalidation-class concurrency errors.
Its solution is indices in place of pointers: when processing events, first copy out the indices of the pending events, clear the shared queue, then resolve the corresponding node paths by these indices and continue the quests. This way, while “processing this batch of events,” newly produced events can safely enter the emptied queue and await the next round—the two batches not interfering. It further uses a skip-tracking scheme to prevent indices from being reused or misaligned across multi-frame processing. This “snapshot first, then process” pattern is just like Chapter 1’s job scheduling “copy the pending indices first, clear the queue, then continue”—the same concurrency discipline recurring at different layers.
Takeaway: an event-driven graph should use indexed continuation, not raw pointers. This is a seemingly tiny yet extremely crucial technique. It makes “receiving the next batch safely while processing this one” possible—which is exactly the key to dozens of quest lines advancing in parallel without falling into chaos from “triggering and adding at the same time.” Iterate and trigger with raw pointers directly, and sooner or later you’ll meet the extremely-hard-to-locate fault of “the container modified during iteration.”
5.3 The Facts Bus: a Global Facts Table That Lets Quests Influence One Another
Parallelism solved, there remains a last requirement—and the one most expressive of RPG narrative: how do quests influence one another?
Its answer is a global facts table (FactsDB). Quests, scenes, player state all read and write the same table—this quest line sets some “fact” true, and that quest line can read it and react. It’s in essence an “influence bus” across narrative lines.
This table has one design decision that gives pause at first glance yet has its own logic on reflection: when a fact is rewritten, it fires the callbacks directly inside the lock. Specifically, the fact-rewriting function (FactsTable::Do), after changing the value and before leaving the lock, calls FireCallbacks—so one fact write can immediately continue the downstream listeners along the dependency graph (scripts, entity events, other quests’ listeners), without passing through an extra layer of queue.
“Firing callbacks synchronously inside the lock” is a dangerous pattern in many contexts—it easily causes reentrancy and deadlock. But here this engine makes it a deliberate feature: what it pursues is exactly the immediate propagation of “the moment this line’s fact changes, that line knows.” If there were a queue layer in between, cross-narrative-line causality would lag a frame or several, and the “choices take effect instantly” narrative experience would be out of the question. The cost is that the callback chain must self-restrain—if a fact write’s triggered callback goes on to rewrite other facts and trigger other callbacks, the chain may recurse out of control. So the use of this mechanism requires the developer to audit the recursion depth of callbacks and ensure callbacks don’t write back to the facts table unboundedly. This is a classic trade of “the discipline of controllability” for “the gain of immediacy.”

Takeaway: using a global facts table plus synchronous callbacks as an “influence bus” across story lines lets state changes propagate immediately and spares the intermediate queue layer. This is the engineering means that makes “your choice in quest A instantly perceived by quest B” feel natural. But you must set a clear boundary on the callback chain and audit its recursion depth—the cost of synchronous firing is that one write may set off a long string of unforeseen chain reactions. Immediacy and controllability are, here, a contradiction to weigh carefully.
5.4 Three-Stage Save/Load: Letting a World Halfway Through Be Saved and Resumed
The last requirement: save/load. This is especially hard for an RPG, hard because—its world is alive.
Unlike a linear game’s “save the level progress,” at the instant an open-world RPG saves, dozens of quests are at their various stages, scene state, player state, objects in the world that have been changed… this whole snapshot of “the living cluster of state machines” must be saved, and on load resume exactly.
This system makes the save three-stage:
- pre-save: under the stage lock, serialize quests, quest instances, events, facts, prefab state, scene data, and player mappings all into a snapshot.
- load: hydrate this data back into the various subsystems.
- restored: note that loading isn’t finished once hydration is done. The restore stage only clears the wait flags—and the real “signal rebuild” is deferred to a later tick, where the related prefab resources are truly ready before those signals are rebuilt.
This “deferred rebuild” is key. Because at load time the world’s resources (prefabs, entities) are still loading asynchronously, and if you rush to rebuild quest signals before they’re ready, you’ll rebuild onto a heap of things that don’t yet exist. So restoration here is made staged—load complete ≠ quest playable, with a window in between of “hydrated, but signals not yet rebuilt,” which is only truly completed once resources are ready.

Takeaway: saving complex narrative state is three-stage—pre-save locks the snapshot, load hydrates, restored waits for dependencies to be ready before rebuilding signals. Don’t expect one load to return the world to a fully playable state. A living world’s restoration is necessarily staged and ordered: put the data back first, then wait for resources to arrive, and only last rebuild the signals that depend on resources. Mash these three into one step and you’ll load a half-finished world that “looks loaded but isn’t truly ready.”
🔧 Design Retro · Narrative Is Engineering
Why is it designed this way? Because a story-driven open-world RPG must let dozens of quest lines run in parallel, be saved and loaded at any moment, and influence one another—imperative scripting can’t carry this concurrency and recoverability. Splitting narrative into “an enqueued request plus a graph’s nodes plus a facts bus plus three-stage save” is what lets it run like a cluster of state machines, continuing frame by frame and recovering from a breakpoint. This is exactly why the scene system became the engine’s largest module—this game places its engineering investment in narrative at a level equal to, or higher than, rendering and physics.
What pitfalls were hit? Two, both related to “synchronous.” First, the facts bus fires callbacks synchronously inside a reentrant lock, and one fact write can set off a long chain of cascading callbacks along the dependency graph, recursing out of control. Second, mistaking load’s completion for quest-readiness, when the signals must actually be rebuilt in a tick after the prefab resources are ready, and operating on a quest within that window comes up empty. Both are not logic written wrong but a misjudgment of “the timing of completion”—just like the lesson of the presentation-layer chapter.
Where do traditional approaches fall short? Writing the story as one long sequence of scripts is entirely sufficient in linear narrative, but in an open-world RPG it fails immediately against the three hard requirements of “parallel plus recoverable plus mutual influence”—it can’t let multiple scripts safely interleave, can’t cleanly save a living world and resume it, can’t let one line’s choices cleanly influence another. A narrative engine is in essence graph execution and a facts bus, not scriptwriting. Understand this, and you understand why this game is willing to invest its largest block of code into “how to tell a story.”
Chapter 6 · Presentation and Control: from Action States to Render Submission
The presentation layer’s savings come from spreading expensive work across frames, and keeping untrustworthy transients out of consumers’ reach.
The problem this chapter solves: everything the player ultimately sees and operates—character animation, state switches in combat, driving, assorted VFX—must be computed in bulk every frame. How do you feed them out without dropping frames? And an RPG adds one more layer of difficulty: the complex poses, skills, and weapon-state switches in combat must be carried by state machines.
The presentation layer I only touch at the structural layer (the algorithmic kernel below it is a blind spot, marked later). In this chapter I want to single out the state machine—because in this engine the state machine carries a great deal of player and NPC combat and pose logic; it’s the skeleton behind “the feel of control.”
6.1 State Machines: Bounded Same-Frame Transitions + the Counterintuitive Order of the Switch Callback
Combat often needs “a chain of state switches within one frame”: in the same frame the character lands, it must switch from “falling” to “grounded,” then to “idle” or “getting up.” This chain of switches rides the state machine’s same-frame transition chain.
But same-frame transitions hide a risk: if state A transitions to B, B immediately meets a condition and transitions to C, C transitions back to A… it’ll transition without end within one frame, leaving the frame unable to end. The handling here is pragmatic—set a hard cap on the same-frame transition chain, about twenty (exceeding it trips a debug-assert alarm), plus the buffered command queue has a cap of about 64. This concrete number is itself telling: it’s an empirical value found in practice, allowing the graph-defined logic to converge within a frame (twenty steps suffice for most reasonable switch chains) while blocking a runaway infinite loop. An empirical cap plus an assert, and you hold “flexible” and “controllable” at once.
State switching also has a counterintuitive ordering. Intuitively, a state switch should be “exit the old state first, enter the new state, and only then is the switch complete.” But the order here is the reverse: it first replaces the current state with the new one, then calls the switch callback, and last runs the exit and enter handling. This means that when an observer (a script, say) queries the current state while the old state’s exit logic is still running, what it sees is already the new state.
Why design it this way? Because it ensures the observer always sees the terminal state, not some “mid-switch” intermediate state. This eliminates a class of extremely hidden fault—an observer reading a fleeting intermediate state and making a wrong judgment on it. The cost is that the exit logic itself must tolerate the fact that “the current state has already become the new state.” This is the same consideration as Chapter 5’s scene system “an observer should only see the determinate result of a scheduling boundary”: let external observation always land on a self-consistent state, not a half-completed transient.

Takeaway: same-frame state transitions should be bounded—give an empirical cap plus a debug assert, allowing within-frame logic to converge while blocking infinite transitions. And placing the switch callback after the state swap, so all observers uniformly see the terminal state, eliminates the “reading a transient” class of hidden bug. The two together hold the state machine’s “flexible” and “controllable” at once—naive recursive state switching often grabs only the flexible and drops the controllable.
6.2 Player Actions and Combat: Request Scheduling + an Object Pool
Each of the player’s actions—fire, reload, dodge, use a skill—is an “action” object in this engine. Their organization has two engineering points worth noting.
First, the player’s actions are wrappers. An action in the player state machine, on enter (OnEnter), pulls parameters, requests a concrete-typed underlying action, and calls Setup/Queue; its “is it done” (IsDone) is judged by merging the wrapper’s own state with the underlying action’s state. Here the leitmotif appears again: the wrapper’s “complete” is a signal of this action’s local lifecycle, not the completion of an external system. A fire action’s wrapper being “complete” doesn’t mean the network has confirmed this shot, doesn’t mean the hit is adjudicated, doesn’t mean the ammo is reconciled—those are further downstream matters.
Second, action allocation goes through an object pool. A game with frequent combat creates and destroys a large number of action objects per second, and if each went through regular memory allocation the cost would be considerable. This system uses an arena pool with a free-list—maintaining a free-list head per action type, preferring reuse on allocation, the arena sized in advance by “action count cap × single-action size × a multiplier” (in the source this arena size is ACTIONS_COUNT * sizeof(CAction) * 512).

Takeaway: use a per-type free-list object pool for high-frequency combat actions to lower allocation cost—but verify the arena capacity covers peak concurrency. At the same time, separate an action’s “request scheduling” from its “business completion”—don’t fold an action’s local state into “this attack is thoroughly complete at the game level.” In a combat system, “the action finished playing” and “damage settled, network confirmed” are different things, and mashing them together makes combat logic mismatch in all sorts of ways when networked.
6.3 Animation and Vehicles: Staged Streaming + a Smoothing Layer
Finally, briefly point out two presentation-layer savings, each demonstrating a general means.
On the animation side, the key is splitting “loading” into three mutually-non-assuming states: load-requested, buffer-ready, sample-safe. A piece of animation data reporting “loaded” doesn’t mean sampling it next frame is safe—between these three states lies the intermediate link of streaming; buffer-ready only means the data has entered the buffer, while sample-safe must still wait for the later steps of the streaming chain to complete. It further supports trigger-based loading and fast replay with a bit-packed ring buffer, and caps each frame’s preload requests to about ten, to guard against load peaks—the same peak-flattening means seen everywhere in streaming, population, and physics. The engineering significance of the three-state isolation is this: it fixes the discipline of “an intermediate state must not be taken as an available state” into the type and state machine themselves, rather than relying on each caller to judge consciously.
On the vehicle side, there’s a representative “smoothing layer” design. The wheel’s contact with the ground is derived from a physics query (trace), and physics queries carry jitter—missed on one frame, hit again the next. Pass this jitter directly to audio and VFX (tire sound, dust effects), and the presentation jumps and stutters. So here a layer of smoothing is inserted between physics contact and audio/visual: contact data is synthesized and smoothed, so even if a frame’s physics trace misses, the synthesized contact keeps decaying for a stretch rather than vanishing abruptly. Audio/visual consume this smoothed contact, so the sound and the look are both continuous. Beyond that, several vehicle states (radio station number, the summoned position) are stored in a persistence layer, decoupled from the runtime—which makes save/load and reload less worrisome. The general value of a smoothing layer: in any “physics-driven presentation” link, insert a smoothing or decay between the physics output and the final presentation, never letting the natural jitter of the low-level signal reach the player’s senses directly.

Takeaway: the presentation layer’s “savings” come from staging and tokenizing each thing, then flattening peaks with a smoothing layer and selective skipping. Physics-driven presentation (tire contact, hit feedback) especially needs a smoothing/decay layer—don’t let the natural jitter of a physics query pass straight to the player’s eyes and ears. Spread expensive work across frames, run jittery signals through a smoothing—these are the routine means by which the presentation layer survives within a fixed frame budget.
6.4 Materials: One Template, How It Sustains a Worldful of Appearance Variety
The presentation layer has one more unavoidable piece—materials. A cyber-metropolis has thousands upon thousands of material appearances: rusted steel, spray-painted metal, worn plastic, rain-soaked concrete… but underneath there obviously can’t be thousands of independent materials—VRAM and authoring load both couldn’t bear it. What it rides on is a move sharing a source with Chapter 4’s data-driven design—make-many-from-few.
The first layer is template → instance. A CMaterialTemplate is a master material defining a set of parameters (base color, metallic, roughness, normal, various profiles). The same template derives countless CMaterialInstances, each storing only its own overridden differing parameters: rusted steel changes the base color to rust-red and pushes roughness to 0.9; spray-painted metal sets metallic to full and lowers roughness. An instance tracks the base-material chain, registers into a dependency table, and the parameters are ultimately compiled by CompileDataBuffer into a buffer the shader can read by type. One template, a thousand sets of override values, is a thousand appearances—and since an instance stores only differences, VRAM doesn’t blow up.
The second layer, and the one that best “makes many,” is multilayer materials + masks. Multilayer_Setup blends several layer templates (a concrete layer, a paint layer, a rust layer, say) by a Multilayer_Mask: CreateRenderResource flattens the layer-template references into rend::MultilayerLayer data, applies each layer’s override parameters, and blends them into a single surface by the mask. The same few layer textures, paired with different masks, are a streetful of old walls, ground, and car paint in varied styles—exactly the source of the city’s grimy “every wall has a story” texture.
Sustaining this “make-many-from-few” are two more mechanisms. One is the shader cache: ShouldRecompile compares the material’s modification timestamp against the source file, and on a cache hit takes the compiled bytecode directly, letting the same template’s many variants reuse the compilation result without recompiling. The other is dependency propagation: change the base template, and ForceRecompilation holds the dependency lock to iterate all subordinate instances and update recursively—change one master material, and a worldful of instances using it update together. (There’s a pitfall here too: iterating holds the lock, subordinate updates call back into the renderer; an editor changing one texture can even trigger a global recompile.)

Takeaway: rich material appearance isn’t piling up countless materials but “a few templates × parameter overrides × multilayer masks × cache reuse.” This is the third appearance of the same logic as data-driven design and prefabs: to scale content by orders of magnitude relies on making assets into reusable, parameterizable units, not linearly piling up more assets. One master material defines the parameter pool, instances store only differences, multilayer plus masks stack a few textures into infinite variety—this is the engineering answer to a big world’s visual richness.
6.5 The Render Pipeline: How Game and Render Are Decoupled, in How Many Layers “Complete” Comes
Finally, down to the very end—rendering. First draw the boundary clearly: shader math, lighting algorithms, post-processing—these algorithmic kernels are blind spots, not covered. What can be covered is the structural layer—how data flows from the game to the GPU, who submits to whom, with what parallelism.
The most core structure is the double-buffered decoupling of the game thread and the render thread. The game thread, to change the scene (add or remove a render proxy), doesn’t directly touch the render scene—it calls AddPendingProxy, recording the operation into a pending queue. The render thread, at the frame boundary, atomically swaps this queue with GetPendingProxiesAndAdvanceFrame and advances a frame, then batch-sorts/merges, updates the BVH and the proxy array. The two threads collaborate via “a frame’s snapshot,” neither stalling the other. The cost is again that discipline: a proxy being “received” isn’t “applied,” and an add/remove/change may even be folded into another effective operation.
Draw submission is also a pipeline: the CPU-side Drawer::Finish fills vertices/indices into a DrawBuffer, SubmitDrawBuffer merely pushes it into a queue, and truly generating the GPU commands (MSAA resolve, mip, material merge) waits for a later render job. There’s an easy-to-trip contract detail here: the raw pointer returned by a DrawBuffer allocation is valid only until the next allocation of the same kind, so what you persist must be the offset, not the pointer.
The render graph’s node execution also has an interesting design: ExecuteParallel deliberately shuffles nodes into deterministic pseudo-random buckets and runs them in batch fence-free—this forces “node declaration order ≠ execution order,” compelling you to build any real ordering dependency explicitly into a render-graph dependency, or it randomly goes wrong. The cost is that the node implementation must itself uphold the rules of render-stream allocation and command-list ownership, with no fence as a backstop.
And what pushes the article’s main axis to its extreme is rendering’s “five layers of completion.” “Submit to render” is never more than acceptance; true completion must pass through five different signals: Attempt (accepted) → Queued (enqueued) → Job (dispatched, commands generated) → GPU Fence (the GPU actually finishes executing) → Visible (the player sees it only after present). Each layer completes only its own step. A screenshot is a good example: the command enqueues, FrameTick dispatches, a deferred job writes the file, the callback fires from the deferred job—the callback doesn’t come back from your original call site, and it reasonably skips frames when the thumbnail isn’t ready.

Takeaway: rendering is the longest chain of the whole article’s “acceptance ≠ completion” discipline—five layers of signals, each completing only its own step. The game thread records into a pending queue, the render thread atomically swaps and then batch-applies—the classic way to decouple game from render; while “submitted ≠ delivered to GPU ≠ visible” pushes async to the extreme. If you build a render layer, the first thing to internalize is: don’t assume “I submitted it” equals “it’s drawn”—the GPU’s completion waits for a fence, the player seeing it waits for present, and several stages you must treat separately lie between.
🔧 Design Retro · The Presentation Layer’s Savings
Why is it designed this way? Because the presentation layer must feed out hundreds or thousands of animation samples, physics queries, and effect computations every frame, and computing them synchronously within the same frame inevitably blows the frame budget. Splitting each thing into async parts of “request → intermediate state → complete,” and flattening peaks with ring buffers, object pools, smoothing layers, and per-frame caps, is the fundamental way the presentation layer holds up under a fixed frame budget. The state machine’s bounded transitions, the action object pool, the animation’s three-state isolation, the vehicle’s smoothing layer—all are different responses to the same pressure: they seem to belong to four subsystems but in fact share one design principle—spread expensive work across frames, keep untrustworthy transients out of consumers’ reach.
What pitfalls were hit? The most hidden is a “misalignment of status reports”—the animation buffer reports “loaded” while sampling isn’t yet safe; the action wrapper reports “complete” while the game-level settlement hasn’t happened. Use these intermediate states as terminal states, and on screen often nothing looks wrong, until some frame samples a dirty pose, or damage is settled against an action not yet truly complete. What these faults share: it’s in “the judgment of completion timing,” not the logic itself, and so is extremely hard to reproduce and locate.
Where do traditional approaches fall short? “Sample once loaded, treat the action as settled once it finishes playing, feed audio/visual directly once physics hits”—no problem in a low-density scene, but at city-level presentation density it instantly blows the frame budget and conducts the natural jitter of physics queries straight to the player’s sight and hearing. Staging, tokenizing, adding a smoothing layer—not for formal elegance, but a necessity forced by the frame budget. What the naive synchronous style saves is the developer’s mental cost, but it passes that cost on as the hitches and jumps the player sees.
Chapter 7 · Networking and the Foundation: Replication, Memory, and the Discipline Running Through It All
Acceptance isn’t completion—this is the low-level discipline of an async engine, running through it all from job scheduling to rendering.
The problem this chapter solves: all the systems above stand on the same foundation—networked sync, memory allocation. How does this foundation hold up? And the engineering discipline flashing repeatedly through the previous six chapters should, here, get its close.
This chapter is the tying-off. I’ll first touch on the two foundations of network replication and memory, then concentrate on a commonality scattered throughout the article—nearly every subsystem’s API return is only “acceptance,” and the real completion is delivered by later stages—make it clear, and own up to its origin.
7.1 Network Replication: receive → queue → apply, Three Stages
An open world that supports multiplayer must sync state on one machine to another. This engine’s replication system splits this strictly into three stages:
- receive: on receiving network data,
OnDataReceivedfirst enqueues the data, not applying it immediately. - apply: the real state application is in
ApplyStateUpdates—only here are objects created, state applied, callbacks fired. - send: and on the send side,
SendUpdatesis only a “send attempt”, not meaning the far end has received, let alone applied.
In this three-stage design hide several watch-worthy engineering realities. First, send is only an attempt—SendUpdates returning success doesn’t mean the far end got it. Second, the overflow handling of the reliable and unreliable channels is asymmetric: a reliable-queue overflow is a critical error; while an unreliable-queue overflow is silently dropped. This asymmetry is reasonable—the reliable channel carries the critical state that can’t be lost, and an overflow must alarm; the unreliable channel carries high-frequency state that can lose a few frames (position interpolation, say), and an overflow just drops. Third, freshness is judged by sequence-number wraparound—using half-interval comparison to tell which state update is newer and which is a stale out-of-order packet.

Takeaway: replication’s three stages (receive / enqueue / apply) each use an independent lock, which guarantees per-frame safety but provides no global atomicity. And the reliable and unreliable channels must be designed with separate overflow policies—critical, can’t-lose behavior goes through the reliable channel and alarms on overflow, high-frequency, droppable state goes through the unreliable channel and allows silent drops. Mix these two under one policy, and either critical state gets silently dropped or high-frequency state blows out the reliable queue.
7.2 Memory: a Three-Tier Pool + Explicit Thread Registration
Underpinning all of this at the very bottom is memory allocation. This system tiers by the scale of the allocation into three:
- Small allocations go through a per-thread lockless slab allocator—but with one precondition: the thread must be explicitly registered; an unregistered thread can’t use it and errors out (fatal) outright.
- Medium allocations go through TLSF (an allocator suited to real-time systems).
- Large allocations go straight through the system allocator.
Each tier has its own independent lock, avoiding a single global lock becoming the contention bottleneck for all allocation. A few engineering details worth a mention: TLSF’s in-place grow is disabled (a comment marks this path as having a bug), so realloc can only take “allocate new, copy, free old,” and pointers aren’t guaranteed stable; the FrameAllocator is a lockless pointer bump, supporting only LIFO-style rollback.
What’s most worth noting is still that explicit thread registration design. Using the fastest lockless slab has a threshold—your thread must register first. This is an opt-in rather than implicit contract: the engine won’t guess for you which thread should use the slab but requires you to register the thread explicitly, and only the registered have the right to take this lockless fast path—the unregistered fail outright, with no room to slip through.

Takeaway: tier memory by scale—small uses lockless slab, medium uses TLSF, large uses system—matching different load characteristics. And “trading explicit thread registration for the lockless fast path” is a good pattern, but make “unregistered means failure” an explicit contract rather than a quiet fallback to a slow path. An explicit failure beats an implicit downgrade—the former lets you discover at once that “this thread forgot to register,” the latter has you investigating for ages on some inexplicably slow path.
7.3 The Close: One Discipline Running Through It All—”Acceptance ≠ Completion”
Arriving here, every previous chapter has been attesting to the same thing over and over. Gather them now—this list is itself a panorama of the whole article’s index:
- Chapter 1, job scheduling’s
RunJobis only an issue; whether a job has begun executing depends on whether its counter has reached zero (it may still be on the wait list). - Chapter 2, a sector’s
RequestLoadis only a request issue; whether the sector is in place depends on later polling; an occluder’sOnInitializereturning true doesn’t mean it’s registered or attached either. - Chapter 3, an NPC’s
AddEntityis only enqueue-and-accept; whether the NPC appears waits for the whole spawn-token chain; a workspot’sSendCommandonly takes the command, and whether the action finishes playing waits for the completion callback. - Chapter 3, physics’s
AttachProxyonly records a pending operation; whether the proxy enters the scene waits for the simulation’s processing stage; an async query’sSuccessdoesn’t distinguish “hit” from “didn’t hit but did compute.” - Chapter 3, navigation’s
RegisterTilesis only an enqueue,StartPathfindingonly returns a token; whether the tile is usable and whether the path is ready both wait for later polling. - Chapter 5, a scene’s request ID is only a receipt of acceptance; whether the cutscene has played through depends on the tick state machine.
- Chapter 6, a render submission passes through “accepted → enqueued → job → GPU fence → visible,” five layers, each completing only its own step.
- Chapter 7, replication’s
SendUpdatesis only a send attempt; whether the far end received it cannot be guaranteed.
This is the same discipline projected repeatedly across different subsystems: nearly every subsystem’s API return represents only “the request is accepted,” not “the work is done.” The real completion is delivered by tokens that represent intermediate states, by multi-stage signal chains, by callbacks.
It must be stated honestly: this discipline isn’t this game’s own invention. Any serious async engine—whatever kind of game it is for, whether this one or another—so long as it must schedule a vast amount of async work within a fixed frame budget, will sooner or later arrive at “separating acceptance from completion.” It’s a commonality of modern async engines, not this game’s patent.
But this game carries it through with unusual thoroughness. So thoroughly that reading its code you meet this discipline repeatedly in nearly every subsystem—loading, spawning, action, narrative, replication, without exception. And as Chapter 1 revealed, this thoroughness is no accident: when the entire engine’s async execution is built on the job scheduler’s “issue-and-return, complete-only-at-zero” semantics, “separating acceptance from completion” is no longer each subsystem’s own agreement but the same structure conducted consistently up from the low-level scheduler, layer by layer. This thoroughness is itself a mark of engineering maturity: it nowhere assumes “call equals complete” to save trouble, but holds the boundary of “acceptance isn’t completion” from start to finish.

Takeaway: an async interface should make “acceptance” and “completion” two observable states—this is the universal discipline of all async engines, and the earlier internalized the better. Don’t let one interface’s return value carry both “I received the request” and “I’m done” at once. Splitting them, letting the caller observe and wait on each separately, is the root of an async system producing no “ghost bugs.” This discipline isn’t glamorous, but it’s the foundation for getting async right.
🔧 Design Retro · The Foundation’s Unified Discipline
Why is it designed this way? Because all the upper systems require “progressive loading, hot-reload, concurrency, networking,” and if the foundation provides synchronous, strongly-consistent, interrupt-on-failure interfaces, the upper async capability is out of the question. Carrying deferral (tokens, deferred buffers), decoupling (queues, multi-stage), and soft failure (default fallback, graded success) through to the very bottom layers of data, serialization, networking, and memory is exactly what lets the upper async contracts hold. Only once the foundation accepts “deferral and uncertainty” first can the upper capabilities have a base to rest on.
What pitfalls were hit? The most hidden, still, is the foundation-layer “false success”—an integrity check passes, but the data isn’t necessarily semantically valid; serialization succeeds, but the object isn’t necessarily ready; a send returns, but the far end hasn’t necessarily received. Any spot that mistakes “the outer success” for “the inner completion,” the fault floats up to surface at the upper layer yet is extremely hard to trace back to this foundation layer to locate.
Where do traditional approaches fall short? “Usable once loaded, complete once serialized, delivered once sent, allocatable from any thread”—these synchronous, strongly-consistent assumptions hold in a single-player small project, but in a streaming big world layered with multiplayer they fail one by one. The foundation must accept “deferral and uncertainty” first for the upper async contracts to have a footing. The real value of this discipline lies not in how ingenious it is but in being held consistently—and that, precisely, is the hardest part.
Conclusion: How a City That Can Tell Stories and Be Built Up Gets Caught by Engineering
We set out from one observation—this open world’s largest block of code is not in physics, not in rendering, but in “how to stage a piece of story”; its most complex set of data is for describing “how a character can remake itself”—and walked through seven layers: foundation, streaming, population, RPG core, narrative, presentation, networking. Looking back now, the weight of that opening observation is clear:
The engineering divide between the two “open worlds” is real. Last time’s open world pressed its heaviest budget into “making a vast number of objects move in order”; this one, because it is an RPG, pressed its heaviest budget into two other mountains—making a city able to tell stories, and a character able to be built up. The same mountain called “open world,” and because the game genre differs, engineering’s center of gravity presses on a different slope.
To tie the whole article into one sentence:
Building an open-world urban RPG, the hard part isn’t only “making a vast number of objects move,” but more so “making a city able to tell stories and a character able to be built up”—which requires moving gameplay into data (so content can scale and balance can be tuned), making narrative into graph execution plus a facts bus (so dozens of lines can run in parallel, save/load, influence one another), and then tiering cost down layer by layer (so the city can be held, and have life).
If this journey leaves three verdicts most worth remembering, they are:
Data-driven design is the engineering foundation of RPG depth.
Narrative is engineering, not scripting.
Acceptance isn’t completion—the low-level discipline of an async engine.
The first explains why content must leave the code and move into data; the second explains why the largest module in the whole engine is narrative; the third runs through every layer from job scheduling to rendering—they correspond respectively to this piece’s three through-lines, and can now be tied off one by one:
- Content is data—from Chapter 4’s data-driven library of one million eight hundred thousand entries, to cyberware, skills, weapons, items, and world-object behavior all described by data, “moving gameplay out of code and into editable data” is the same underlying logic by which RPG content can scale by orders of magnitude and balance can be tuned without server downtime.
- Narrative is engineering—from Chapter 5’s scene system, the largest in the whole engine, to the indexed-continuation quest graph, the in-lock synchronous-firing facts bus, the three-stage save/load, “narrative is a cluster of state machines, not a heap of scripts” is the same judgment this game invests in most heavily.
- Tiered cost gives the city life—from Chapter 2’s mask-driven sector streaming, to Chapter 3’s stub-first population and three-tier crowd, “let objects exist in their cheapest form first, and instantiate by budget” is the same logic by which a vertically dense metropolis can be held and can run.
If I were to condense this journey into a few takeaways you can put straight to use, I’d leave these eight:
- Job scheduling is a first-class foundation. Express urgency with priority lanes and dependency ordering with a counter plus a wait list, and a vast number of async tasks can neither deadlock nor spin under a fixed frame budget. The async capability of every upper system is built on this scheduler; its “issue-and-return, complete-only-at-zero” semantics is the very source of the article’s main axis.
- An RPG’s depth is, first of all, a data-engineering problem. Moving gameplay (values, relations, content) out of code and into an editable, hot-reloadable database is the precondition for content to scale and iteration to speed up. The cost is that you must shoulder “data correctness” by hand—the layer the compiler used to provide for free.
- Make narrative graph execution plus a facts bus, not sequential scripts. A quest is an event-driven graph, supporting parallelism with indexed continuation; cross-line influence rides a global facts table as a bus; save/load is split into pre-save, load, restored, so a living world can be saved exactly and resumed.
- A city’s life rides on tiered cost. An NPC is by default a cheap stub, instantiated only by budget, visibility, and distance; people near and far are tiered into full NPC, simplified group, and pure-visual approximation; environment interaction is made a schedulable system with “demand plus reservation” and a “submit plus complete” dual signal.
- Streaming’s “completion” is staged. Resources ready, world ready, proxy registered, render-mounted are different signals, not to be muddled through with one vague “loaded”; especially beware the “false success in the completion mask.” The same sector organization must also be understood from the spatial face (partition and visibility culling).
- Make-many-from-few is the unified answer to big-world content volume. Data-driven records plus flats, a prefab’s one definition plus many instances, materials’ templates plus instance overrides plus multilayer masks—all three belong to one logic: to scale content by orders of magnitude relies on making assets into reusable, parameterizable units, not piling them up linearly by hand.
- Plug synchronous middleware into an async engine with “an exclusive window plus deferred delivery.” Physics is the classic: gather scene mutations with a pending queue, execute them uniformly in the simulation’s processing stage, align timing with deferred callbacks and deferred release—both preserving the middleware’s synchronous semantics and letting it run in parallel with other systems under a fixed frame budget. Navigation’s “mesh streams, path rides a token” is the same idea in another domain.
- “Acceptance isn’t completion” is the universal discipline of an async engine. Make every async interface render “request received” and “work done” as two observable states. This one isn’t glamorous, but it’s the foundation for getting async right; rendering’s “accepted → enqueued → job → GPU fence → visible” five layers is its longest chain. This game’s maturity shows, in large part, in holding this discipline consistently.
Finally, the honest boundary must be stated clearly. What I can explain through is “how the runtime is organized”—the architectural “why” of world streaming, population, data-driven design, the narrative graph, state machines, network replication. What I didn’t read through is the kernel of the low-level algorithms: the collision solver, shader math, navmesh generation, the internals of pathfinding solving, audio DSP—for these I only reach the structural layer or the integration point, and I marked each one in the text. There’s also a class of thing I deliberately didn’t write: concrete numeric gameplay design—how a given weapon should be tuned, how a given build is assembled, how a given piece of cyberware is balanced—that’s the designer’s design, not the engine’s engineering, my material doesn’t cover it, and I won’t make it up. Explain through what I read through, mark clearly what I didn’t, and leave to design what isn’t engineering—this is the discipline this article wants to hold.
Read alongside the previous piece, it gets more interesting. Last time looked at that open world—its center of gravity in how to make a vast number of objects move in order, who owns state, how cost is encoded into objects; this time looks at an RPG open world—how to make the city narratable, the character buildable, the content moved into data. The two take apart the same “open world” mountain, but because the game genre differs, what they see are two utterly different paths up. Read them against each other, and you’ll have a fuller sense of the engineering diversity behind those words “open world”—it was never one standard answer, but a set of engineering trade-offs that shift with the game genre.
This too is only a beginning. This deep dive is one stop further along the series. Next, I’ll keep digging along several of the deep points in this piece—how exactly the data-driven library organizes those million records, how the quest graph plus facts bus weaves dozens of quest lines together, how stub-first population grows life out of an empty city—each told from “what it is” to “why it’s designed this way.” And the destination the whole series truly aims for has never changed: how, in today’s engines (UE, say), to rebuild these capabilities. The data-driven design, narrative orchestration, and population tiering an RPG open world needs—how to re-customize them in UE—this is where I want to walk, step by step.
The city is big and the stories are many, but the reasoning that builds them can be explained layer by layer. See you next time.
*This article is distilled from a source-level reverse-engineering read of the engine and client code of a mature, commercial open-world urban RPG, covering everything from the low-level engine all the way up to the game-side systems—gameplay, narrative, networking, save. All architecture and naming in the text have been generalized; it describes “the typical design of this kind of open-world RPG,” not any one specific product. Going deep where I read deep, marking blind spots where I didn’t, leaving to design what isn’t engineering—this is the discipline of the writing.*
1 thought on “How to Build an Open-World Game: A City That Can Tell Stories”