This article is one of the deep-dive installments of the How to Build an Open-World Game series (the series overview, *A Systems Overview*, is here—reading it first is recommended for the big picture), focused on a single point deep enough to deserve its own piece: when a street has to keep hundreds of NPCs and a hundred-plus driver-occupied vehicles alive at the same time, how does the system actually hold up—and how do you carry it over to UE5 without it coming apart? The material comes from a long-running, source-level reverse-engineering read of the engine and client code of a mature commercial open-world game (roughly 700+ source-level notes). Every class name, function name, and phase split mentioned here has been verified against the source, line by line; per series convention it is anonymized for publication—presented as a generic “open-world population system,” naming no specific game or middleware.
The pedestrian topic is split into two halves by layer: this half (Part 1) covers scheduling and decision—up to the point where a task slot is produced: who owns all the objects, in what order a frame advances them, how population is spawned and culled, who decides how much detail an object gets, and how a single NPC goes from perception to “deciding what to do.” Part 2 covers behavior, presentation, and physics—executing the task: how task slots become walk/flee/ambient behaviors, scenario-point-driven “doing the right thing in the right place,” and where locomotion animation and ragdoll physics land.
Unlike a piece that only discusses the original design, the UE5 side of this article is not a paper exercise: there is a UE5 prototype project under active development (hereafter VirtualWorld / VW), which deliberately does not use Mass or any off-the-shelf crowd/entity framework, but instead builds from scratch a runtime of “world-owned entities advanced by phases,” preserving the original’s update timing, function granularity, and ownership boundaries as closely as possible. Why not Mass? Not because it wasn’t considered—it is a conclusion reached and then set aside after weighing the trade-offs. Each chapter spells out the specifics: to keep the original’s semantics of “layered ownership, interruptible behavior, cost encoded into state,” bolting on a data-oriented ECS framework designed for “homogeneous, parallel batch processing” points in the opposite direction.
Structurally, every subsystem follows the same three-beat thread: first how the original designed it (source-level, down to functions), then why off-the-shelf options (Mass in particular) don’t fit these semantics, and finally how the prototype implements or plans it—what is finished, what is still a skeleton, and what is a known gap, all stated plainly, never passing off “skeleton in place” as “done.”
An honest boundary: what I read through is “how this population layer of the runtime is organized”—the scheduling skeleton and ownership boundaries of spawning, culling, LOD, and task dispatch. What I did not read through are the algorithmic black boxes underneath (navmesh construction, the pathfinding core, animation blend-tree internals, the task-tree scheduling algorithm). Wherever these come up, I mark them explicitly as blind spots. Read this far, told this far—that line is held throughout.
Hands-on UE5 implementation belongs to the companion landing articles: this article (like every design piece in the series) is positioned at design and migration decisions—why the original is designed this way, what to build versus reuse, where to draw the seams. Each design piece has a matching landing piece covering how it actually runs in the UE5 project—code walkthroughs, concrete ways of wiring engine subsystems, pitfalls and verification—added over time.
Introduction: The System Cost of a Multi-Entity Population Runtime
Start with a thought experiment. You have an engine with rendering, animation, and physics ready to go, and an art team has handed you a well-made pedestrian character. Put one such NPC into the world and have it walk around, check its phone, and flee when startled—not hard. In UE5, a Character plus a behavior tree gets you a demo quickly.
The hard part is multiplying that number by several hundred.
The moment a street needs hundreds of pedestrians and a hundred-plus driver-occupied vehicles at once, every assumption that was harmless at the scale of “one NPC” fails simultaneously:
- Who decides, this frame, who gets spawned and who gets deleted? You can’t let each NPC decide “should I exist”—then no one manages the global budget, and the count spirals once it grows.
- Those two hundred people a kilometer from the camera—why would they automatically “compute a little less” instead of draining the frame budget? You can’t expect an “optimizing compiler” to make distant objects cheap for you; this has to be designed in explicitly.
- A freshly created pedestrian—who installs its first behavior? The factory that spawned it, or someone else? Draw this boundary wrong, and every hard-to-trace “NPC frozen at birth” bug downstream comes from here.
- A vehicle driven by an NPC—who owns the driving AI? The car, or the driver? Fail to think this ownership question through, and both networking and LOD fall into disorder with it.
None of these questions can be answered inside a single Actor’s Tick()—because none of them is “an object’s own business.” They are all the world’s unified scheduling over every object.
That is this article’s through-line. The real difficulty of a massive-character system is not “making one NPC feel alive,” but feeding, on a fixed per-frame budget, a heterogeneous crowd whose count keeps changing, whose distances from the player vary wildly, and whose ownership differs object by object. Unpacked, that sentence becomes the topics of this piece:
- World foundation layer: who owns all objects, in what order a frame advances them, and how additions and removals stay safe.
- Entity representation: how “full instance vs. lightweight proxy” is encoded into an object’s own state, and how it switches seamlessly between the two.
- Population spawning: the world-side policy for “how many people should exist right now, and where they appear.”
- Population culling and budget: who may be deleted, who goes into the recycle pool, and how much output is still allowed this frame.
- Behavior-level LOD and proxy switching: how far an object is from the player decides whether this frame it “computes everything, computes some, or computes almost nothing.”
- The perception–decision–task pipeline: the real mechanism by which a single NPC goes from “knowing what happened around it” to “deciding what to do.”
And what comes after “deciding what to do”—how tasks are executed into walk/flee/ambient behaviors, where the smokers and bench-sitters come from, how animation and ragdoll are positioned—belongs to Part 2.
One discipline runs through the whole article and deserves stating up front: this system never compresses “should this object update” into a single lonely boolean. It is always tiered, always phased, always ownership-split—objects near the player compute everything, farther ones take turns on timeslices, and the farthest drop to a proxy that costs almost no CPU; “when to compute” is driven by a world-level scheduler on fixed phases, not ticked out by each object on its own; and “who owns this state” versus “who merely requests or observes” is divided cleanly. This discipline—cost is distance, scheduling is phases, responsibility is ownership—is the foundation on which a massive-character system stands.

Foundation Responsibilities and Mass Fit
First, a positioning note to prevent misreading the article’s theme: this is not a “case against Mass.” Mass is only the representative example in this article’s migration decisions; the real question is more general—whether a framework matches the semantic shape of the target system. Mass appears throughout because it is the UE community’s first instinct for crowds today, and explaining exactly why that instinct doesn’t hold here happens to throw this system’s semantic shape into relief.
Before going further, the foundational decision of the whole piece needs to be spelled out: the VirtualWorld prototype does not derive entities from AActor, does not rely on Tick(), and does not adopt Mass or any data-oriented ECS framework. Instead, a world runtime (FVWWorldRuntime) owns every entity and advances them by phases.
This “no Mass” decision deserves its own explanation, because it runs exactly opposite to the current UE community instinct of “crowds? use Mass.”
What Mass excels at is “tens of thousands of homogeneous, light-state, parallel-batchable entities”—a field of swaying grass, a background crowd with no individual differences, a particle storm. Its power comes from laying out same-kind entity data in contiguous memory (Fragments) and sweeping it with Processors in batch, cache-friendly passes. If your problem fits that shape, Mass performance is superb.
But a massive-character system needs exactly the opposite shape:
- Every NPC has layered ownership—who owns its tasks, who merely requested its spawn, who is only observing from the side. These are *relationships*, not data rows.
- Every NPC has an interruptible everyday state—it is sitting, but a gunshot can interrupt it at any moment. That is a *prioritized state machine*, not a single-pass pure function.
- Every object carries a “how much detail this frame” cost tier—and the tier itself is a state machine with hysteresis.
- Every NPC has a causal chain from event to task—”heard a gunshot, therefore produced a flee task.” That causality must be traceable, adjudicable, and legally cancellable.
Put these into ECS Fragments and Processors and you hit an immediate dilemma: either write Processors as interdependent logic riddled with per-entity branches—at which point the contiguous-memory batching that ECS lives on evaporates, and you pay the architectural cost without collecting the performance dividend—or flatten the original’s ownership and causality into plain data to appease the framework—and that ownership and causality is precisely the entire difference between “a living city” and “a painted backdrop.”
The prototype’s choice is therefore clear-cut: build the foundation layer yourself and carry the original’s semantics over intact, rather than bending to a framework designed for a different problem. This is not “distrusting the engine”; it is “recognizing the shape of the problem”—heterogeneous, stateful, all about ownership and causality, better carried by a hand-written runtime. The chapters below return to this repeatedly: because the foundation is self-built, every layer of semantics has its native shape to settle into.
To be equally clear on the other side: this is an engineering trade-off, not a rejection of Mass—still less of the ECS paradigm itself. The only thing rejected here is letting a mismatched framework define the system’s semantics. Mass remains an effective, worthwhile choice for homogeneous batch scenarios (fields of vegetation, undifferentiated far-crowd extras, particle-grade agents)—where data-oriented batching belongs to ECS, this prototype won’t force object-orientation onto it either. It is only that in the “massive characters” problem, the semantic constraints (ownership, interruptibility, causality, cost tiers) outrank batch throughput, which is what tips the scale toward building. Change the shape of the problem, and the conclusion could flip entirely. Every later chapter’s “why not Mass” section speaks to the mismatch in this specific problem, not a universal verdict.

(One more boundary, stated in passing: “no Mass” ≠ “no off-the-shelf facilities at all.” Part 2 will show that pure data/query-layer facilities like “world-authored interaction points” are planned for direct reuse; components unrelated to population semantics, like vehicle rigid-body physics, should likewise reuse what the engine ships. “Self-built” targets the runtime scheduling and ownership foundation—not everything.)
Finally, the reporting convention used throughout: when describing how the prototype lands something, its status is stated in three explicit tiers—implemented (has an implementation, function bodies, or test coverage), skeleton (types, states, and transitions in place, behavior still empty), and not started (a roadmap entry, no code yet). Every chapter’s third section marks which tier it sits in, so “skeleton in place” is never read as “done.”
The chapters follow.
Chapter 1 · The Foundation: World-Owned Entities and Phase Scheduling
The problem this chapter solves: who owns all the objects? In what order does a frame advance them? And when an object is spawned or deleted mid-traversal, how does that not crash?
This chapter covers the lowest slice. If you view the city as a running system, this chapter answers the “operating-system-level” questions: who owns the objects, when things get scheduled, and when the world’s state may be mutated. It looks the farthest removed from “massive characters,” yet it is the base on which every higher-level policy in this article either stands or wobbles—make the foundation “per-object Tick” and LOD, tasks, and population all inherit the constraint; hand the foundation to Mass and ownership and causality must bend to the framework. So it gets told thoroughly, first.
① Original mechanism
The original’s entity is not an “Actor.” It is a record owned by the world layer. That difference is the starting point for understanding everything else.
In the original, once an entity is created, its lifetime, its registration across subsystems (spatial partition, world, rendering, interiors, physics), and whether it updates each frame are none of its own business—they belong to a world layer (hereafter GameWorld). The entity itself only exposes a set of “phase hooks”—”at the control phase, call my ProcessControl“; “at the pre-physics phase, call my ProcessPrePhysics“—but when to call, whether to call, and whether to remove it from a list afterward are the world layer’s decisions.
Why insist on this? Because updates in an open world carry heavy ordering constraints, and a single object’s Tick simply cannot express them:
- AI may only decide after the full event context is in—if an NPC decides inside its own Tick, the “what happened around me” it depends on may not be fully collected this frame.
- Physics solving must happen in one concentrated stage close to the simulation clock, not scattered across object Ticks where it can be interrupted arbitrarily.
- Post-physics flags like “did a collision happen this frame” must be cleared by a single unified stage after physics has run; clear them inside individual objects and the ordering inevitably scrambles.
- Inverse kinematics (feet on ground, hands on walls) needs the final skeletal matrices, so it must come after both animation and physics have settled.
What these constraints share: they care about “which stage of the whole frame,” not “inside which object.” An object’s Tick slices by object; these constraints slice by stage—two orthogonal cuts, and expressing the latter through the former is inevitably awkward. The original’s answer is to cut a frame into ordered phases, each sweeping the relevant objects and doing only that phase’s work: control → pre-physics → physics → post-physics → pre-render → post-pre-render.
One more point that is critical and extremely easy to get wrong: mutation safety. In an open world, an object may be marked for deletion inside the control phase (it just died, or left view), or spawned inside some phase (an explosion produced debris). If you actually destroy an object mid-traversal of an update list, the traversal walks into a dangling pointer. The original’s approach: deletion requests are queued first; real destruction is deferred until all of this frame’s phases have run. Mid-traversal, phases only *mark*—they never *destroy*.
A fine detail that shows the original’s restraint: an entity’s ProcessControl returning false does not mean “destroy it”—it only means “remove this entity from the control phase’s update list” (it may simply no longer need per-frame control updates, while continuing to exist as a world entity). Keeping “no longer updated” and “to be destroyed” apart is one of this scheduler’s invisible disciplines.
Why this chapter matters: an open world’s foundation is not “a faster Tick.” It is “cut the frame into phases, concentrate mutation safety at the frame boundary, and take entity lifetime out of the entity’s own hands into the world layer.” Get these three right, and everything above finally has solid ground to stand on.

② Mass fit boundary
By this point, the first and most fundamental reason for “why not Mass” can already be stated: Mass would decide the shape of this foundation for you—and this is precisely the layer that most needs to be designed to the original’s semantics.
Use Mass, and the answers to “who owns entities, how a frame advances, how mutations are scheduled” become “Mass’s Processor execution graph plus Fragment lifetimes.” Mass’s scheduling is designed for “batch, parallel, cache-friendly.” It runs, of course—but its phase model is not the same thing as the original’s semantic phases of control → pre-physics → physics → post-physics → pre-render. Either you accept Mass’s model (and lose the original’s carefully designed ordering semantics), or you stack your own phase layer on top of Mass (and Mass becomes dead weight at this layer).
Moreover, Mass’s entities are “flat data rows,” and the framework has no natural affection for per-object, semantic lifetime decisions like “an entity returns false to remove itself from one list without being destroyed”—in ECS such logic becomes either an extra tag Fragment plus a dedicated Processor, or a fight against the framework’s grain. The original’s safe point of “queue deletions, flush at end of frame” is a natural design in a self-built runtime; in Mass it must be aligned, gingerly, with the deferred-command system.
In one sentence: at the foundation layer, the “convenience” Mass offers has a precondition—that your entities really are flat data rows and your updates really are batch Processors. Massive-character entities don’t meet that precondition, so using Mass here trades short-term convenience for a long-term adaptation burden.
③ Prototype status and plan
Implemented: this foundation is in place in the prototype, stable, and covered by 30 automated tests named after the world runtime. It is the most fully landed layer in this article.
FVWWorldRuntime owns all entities via TUniquePtr, and every entity gets a stable handle, FVWEntityHandle. Note the choice: entities are passed between systems by handle, not by raw pointer—scripts, networking, and debugging will all need to reference entities later, and a handle unifies the “is this reference still valid” check, eliminating dangling pointers. This matches the original’s stance that “an entity is a world-owned record, not a raw object any system may hold.”
It does not rely on AActor::Tick. Each frame advances on fixed phases, with names and call timing deliberately aligned to the original:
RunControlPhase -> ProcessControl on each registered entity
RunPrePhysicsPhase -> ProcessPrePhysics
RunPhysicsPhase -> ProcessPhysics
RunPostPhysicsPhase -> ProcessPostPhysics (UE-side extension point, see below)
RunPreRenderPhase -> PreRender
RunPostPreRenderPhase -> PostPreRender
PostPhysics is a UE-side extension phase added relative to the original—reserved for future reconciliation with Chaos or Actor transforms, kept out of the original-style physics hooks rather than forced into them. It is one example of “preserve the original’s semantics, but extend honestly where extension belongs.”
Three key safety semantics are pinned down by tests. One by one:
First, entities don’t manage global registration themselves; additions and removals go through the world side, with deferred destruction. Deletion enqueues via RequestRemoveEntity; actual destruction waits until the frame’s phases have finished—implemented as a PendingRemoval flag plus ProcessingDepth (how many phase traversals deep we currently are) plus FlushPendingRemovals (uniform cleanup at the safe point). Request deletion mid-traversal and only the mark is set; the entity truly disappears at the flush point. This mirrors the original’s “deletion is coordinated by the world side; never destroy mid-traversal.” One dedicated test verifies that “an entity requesting self-removal during the physics phase skips its subsequent phases”—evidence that “marked means no further phases, but the object survives until end-of-frame reclamation” genuinely holds and is genuinely tested.
Second, ProcessControl returning false only removes the entity from the control phase—it does not delete it. This semantic aligns with the original word for word, and one test is literally named ProcessControlFalseDisablesControl. The prototype even splits the distinction finer: one test verifies “leave the control phase and continue” still counts frames as usual, another verifies that “leave the control phase and simultaneously request deletion” lets deletion win—covering both combinations of “no longer updated” and “to be destroyed.”
Third, selective per-phase registration. EVWEntityUpdateFlags is a set of bit flags (Control / PrePhysics / Physics / PostPhysics / PreRender / PostPreRender); an entity registers only into the phase buckets it actually needs (ControlUpdateBucket / PrePhysicsUpdateBucket / …). Distant or purely decorative objects can sit in very few buckets, or none. This is “slice by phase, not full-suite Tick per object” made concrete—and it is the physical hook where LOD later attaches: the “downshift” of Chapter 5 largely lands as “remove this entity from certain phase buckets.” The prototype adds defenses: phase flags are strictly validated at the request boundary, unknown bits never enter the command buffer; an entity already pending removal refuses further phase enable/disable requests.
And one design that is easy to overlook but speaks to “the foundation must be stable”—spawning entities mid-traversal. If an entity creates another entity during the control phase, the new one must not be inserted into the bucket currently being traversed (it would scramble the traversal), nor immediately run the later phases (it missed this frame’s control phase). The prototype’s answer is PendingAddEntities: the new entity binds immediately (findable via FindEntity, can request phase changes, can request self-removal), but only registers into phase buckets at a safe point, and never back-fills phases it already missed within the same frame. A dedicated test verifies that “an entity added during the control phase has its bucket registration deferred to the next frame.” This whole set—pending add / pending remove / command buffer / safe-point flush—is the original’s “world-side list mutation boundary” faithfully landed in UE C++.
Honest boundary: command execution at this layer is still single-threaded—it is a safety boundary, not a TaskGraph parallel system. The prototype explicitly labels it “Layer 1 observational scheduling statistics”: physics-step results like NeedsAnotherStep / Skipped are currently only recorded into stats, with no real extra substeps run or queues skipped. In other words, the foundation’s correctness and safety are established; parallel performance comes later—and that ordering is itself deliberate.
The way this layer is accepted also set a methodological template for the whole series: test names are the spec list. Sweep the prototype’s test file by its registration macro and you can count 769 automated tests today; exactly 30 carry the world-runtime prefix, and their names map one-to-one onto the safety semantics above—ProcessControlFalse (false only exits the control phase), DeferredAdd and PendingAddPhaseCommand (mid-traversal additions go through the pending queue; the new entity’s phase commands defer likewise), PhaseMutationDuringControl and DuplicatePhaseMutation (mutating phases mid-traversal; duplicate phase-change submissions), ShutdownHandleReuse and DestructorShutdown (handles must not be reused after shutdown; the destructor path shuts down as a backstop), ImmediateRemovalRetiresEntity and FlushRetiredEntities (even immediate removal only retires; retired entities are reclaimed together at the safe point). The list reads almost as this chapter’s table of contents: to audit whether the foundation does something, don’t dig through the implementation—read the test names first. One more ratio worth noting: the foundation holds only about one twenty-fifth of all tests; the remaining seven hundred plus sit on the later vehicle and traffic subsystems. That distribution is itself evidence of “the foundation stabilized early and was rarely revisited”—once the bottom held, every new capability added tests above it, and these 30 have barely moved since.
Chapter 2 · Entity Representation: Full Instance or Proxy
The problem this chapter solves: why do distant objects become a “lightweight proxy”? And how is the full-instance ⇄ proxy switch made into a gated, traceable verdict instead of a bare toggle?
The previous chapter established who owns objects and how they are scheduled. This chapter moves one layer up: how the same conceptual object is carried by two representations of very different weight, and how it switches between them safely. This is one of the core mechanisms that keeps a massive object count from blowing up—and the spot most easily built as “just set a bool”—which makes the original’s restraint here especially worth studying.
① Original mechanism
For interactable props, the original separates full instances (real) from placeholder proxies (dummy). The distant props you will never interact with—trash cans, benches, railings—have no business existing as full instances (they cost memory and register into physics and rendering). They normally sit as featherweight dummies; when the player approaches, or a script needs one, or it enters the potentially visible set, it is “realized” into a full object. When the player leaves, it drops back to a dummy.
The key: this conversion is not an action; it is a verdict that must pass a long chain of gates. When the original’s object population system (ObjectPopulation) decides to “realize this dummy” or “demote this real back to a dummy,” it weighs a long list of reasons to reject or defer:
- Is the object held by a script? By a cutscene? By the procedural generation system?—if held, no casual conversion.
- Is the player carrying it? Is it visible to some remote player?—if so, no demotion.
- Is it in the potentially visible set? Needed by a sniper scope or first-person view?—then it must stay full.
- Are its model assets resident? Would converting trigger a streaming load, or an entity lifetime change?—if so, possibly defer.
- Is it a visible, already-damaged object?—damage state must survive; conversion must not erase the scars.
Every “don’t convert” reason is a distinct, distinguishable state—not a vague “conversion failed.” This matters enormously: when you are debugging “why wasn’t this object reclaimed,” *because a script holds it* and *because a remote player can see it* are entirely different investigation paths. Compressing rejection reasons into one bool throws the debugging information away.
The original also separates the verdict from the scheduling of conversion: the verdict is object-local (can this object convert right now); the scheduling is world-side (how many conversions this frame’s budget allows, and which get priority). That split returns in Chapter 5, on LOD budgets.
Why this chapter matters: “full instance or proxy” is the core switch that lets a massive object count scale, but that switch must never be bare—it has to be a verdict passing a long chain of gates, every rejection reason leaving a trace, with the verdict (can it convert) split from the scheduling (whether and which to convert this frame).
② Mass fit boundary
This is the layer where Mass’s “flat data rows” model fits worst.
“Full instance ⇄ proxy” is, at bottom, a switch between two representations of the same conceptual object with wildly different weights, guarded by a long chain of business-semantic gates. In ECS you immediately face an awkward modeling question: are real and dummy two Fragment sets on one entity, or two different entities?
- Same entity, swapped Fragments—then “realization” is a structural change (an archetype migration), which in Mass is relatively expensive and delicate, while you may be processing hundreds or thousands of conversion candidates per frame.
- Two entities—then you maintain the “which real does this dummy correspond to” mapping yourself, plus the identity transfer during conversion, and ECS gives you no help at all.
Not to mention the gate chain itself (script holds, player carrying, potentially visible set, damage state…)—per-object verdicts full of branches, exactly the shape Processor batching is worst at. Write that verdict chain into a Processor and it degenerates into a loop with twenty ifs; the point of ECS evaporates with it.
Same conclusion as the last chapter: the core here is “semantic representation switching plus per-object gated verdicts,” not “batch processing of homogeneous data”—Mass is the wrong tool for this layer.
③ Prototype status and plan
The prototype has this layer’s skeleton in place, and it is a high-quality skeleton—it turns the original’s discipline of “conversion is a gated verdict” into an explicit request–policy–resolution protocol. To be precise about status: what has landed is the single-object conversion protocol and its verdict gates; what has not is the world-side batch scan and budget scheduling. Taking them separately:
Landed (the single-object conversion protocol): the prototype has FVWObject, and makes “is this object currently a full instance or a proxy” first-class state: EVWObjectRepresentationState { None, Dummy, Real }. Conversion is not a bare setter but three steps:
- Request:
RequestRealizeFromDummy(...)/RequestConvertToDummy(...)submit a conversion request carrying the requester handle, archetype name, model hash, whether it is a script request, and a serial number (for the later “is this still the request I originally filed” check). - Policy verdict:
FVWObjectConversionPolicy::EvaluateAndApply(...)is the policy gate. Its header comment is written with remarkable restraint—”explicit Object-local request policy only; it does not scan, resolve, stream, create, destroy, or auto-run from phases.” It answers only “should this convert”; it never scans the world, never streams, never creates or destroys. That boundary matches the original’s “verdict split from scheduling” seamlessly. - Resolution: an accepted request is recorded as a resolution, carrying the before/after representation states and the serial number, for downstream consumption.
The genuinely striking part is the gate ordering inside the policy. Read the implementation and you see a strictly ordered verdict chain: validate the action (neither realize nor convert-to-dummy → reject outright) → representation must not be None (else invalid object) → check for an existing pending request (if present, keep the original, don’t overwrite) → check current representation against the target action (already the target / representation mismatch are two distinct returns) → then run the long chain of “input blocker” checks.
That input-blocker chain is nearly a one-for-one port of the original’s rejection list, evaluated in priority order: missing archetype or model metadata → would trigger an entity lifetime change → would trigger streaming → model assets not resident → unsafe for a real object → no associated dummy → invalid IPL/interior → pinned “never dummy” → in the potentially visible set → needed by sniper scope or first person → a visible damaged object → needed by script → held by script/procedural/cutscene so demotion refused → network control blocked → replay hold deferred → held by the player → near or visible to a remote player → temp/fragment lifetime deferred → deferred by policy. Every hit returns its own status enum value—NeverDummyPinned, FoundInPvs, RequiredBySniperOrFirstPerson, HeldByPlayer, NearOrVisibleToRemotePlayer… more than twenty in all.
This is the original’s discipline faithfully landed: conversion passes a long chain of gates, and every “don’t convert” reason is a distinguishable, unit-testable state, not a vague false. And the whole policy is pure-functional, object-local, world-untouched—you can throw batteries of unit tests at it, feed input combinations, and assert exactly which rejection reason comes back.

Not landed (the world-side scan and budget scheduling): the migration roadmap lists “Object and Dummy/Real Population” as its own layer, currently not started. The world-side scheduler—”each frame, scan a batch of candidates, apply a budget (how many reals and dummies may convert this frame), prioritize by focus/camera/player distance, then call the policy gate once per candidate”—exists only as a design entry, with no implementation. The prototype has done the hardest, most error-prone part (the verdict gates) thoroughly and left the comparatively mechanical part (batch scheduling) for later—once again, “establish correctness first, add throughput later.”
Honest boundary: the comment on EVWObjectRepresentationState is equally explicit—it is “Object-local semantic representation only; it does not prove model residency, drawable existence, world membership.” That is: this state says “semantically, this object currently counts as real or dummy”; it does not guarantee the model behind it is resident, a drawable exists, or the object has entered the world. The work of “actually loading the model, actually registering into rendering” belongs to the future UE presentation bridge (Actor/Component/Chaos)—for now this layer only pins down the semantic state machine.
One last boundary that is easy to blur: dummy is not LOD. This chapter answers “in what form does the object exist” (full instance or lightweight proxy—a representation question); Chapter 5 answers “how much compute budget does it get this frame” (everything, timesliced, or almost nothing—a cost-scheduling question). The two are related—switching representation is one way to cut the budget—but they are not the same question: a real object can still be updated at reduced frequency, and a dummy still occupies its own phase registrations. Carry this distinction into Chapter 5.
Chapter 3 · Population Spawning: Control Points, the Keyhole, Bounded Cycles
The problem this chapter solves: on the world side, who decides how many people the street should have right now, where new ones appear, and how to guarantee they never pop into view near the player?
The first two chapters established how objects are owned and scheduled, and how one object switches between two weights. From here on we enter *population* proper—how many people the street should hold, and where they come from.
① Original mechanism
The original hands this to a world-level population manager (hereafter PedPopulation) that runs Process() once per frame—yet executes not a single NPC’s AI itself. It is a “spatial population strategist centered on the player’s current position.” Its Process() splits into four major stages, and the ordering is deliberate.
Stage 1: frame gating and control-point construction. The function opens with early-outs: player not yet located → return; game paused → return; population disabled → return; replay mode → update only the recycle pool and return. It also maintains a “consecutive fade-out frames” counter for opening/transition special cases. These gates intercept every “should not spawn” situation before any spawning begins—a textbook case of “first decide when not to act, then act.”
Past the gates, it computes the population control point (hereafter PopCtrl): a pure data carrier holding the current population center, conversion center, facing, field of view (including the tangent of the half-angle), interior category (outdoors / shallow interior / deep interior), center velocity, and whether the center is in a vehicle. Where does it come from? Center and velocity come from the “focus entity manager” (usually the player); facing and FOV come from the currently dominant rendering or gameplay camera. Note the split between “population center” and “conversion center”—script can drive an independent conversion center (say, a cutscene camera flies elsewhere while population should still form around the player).
Stage 2: range and density computation. With the control point in hand, it derives this frame’s spawn/delete policy. The inputs are many: opening mode vs. normal, scene range-extension policy, player/control-point speed, global LOD scale, cutscene clamps, interior depth, player vehicle type (aircraft in particular widen the range), and per-frame script multipliers. From these it computes: in-view spawn range, out-of-view spawn range, extra frustum spawn range, deletion range scale, deletion rate scale, whether to run in-frustum tests, and whether the control point is indoors. The results go into a “current-frame computed parameters” record, and it constructs the keyhole shape used for spawning.
The keyhole shape is the piece of the original most worth telling and least widely known. A newcomer assumes “scatter random points around the player” will do—but that spawns NPCs into view near the player: an immersion break. The keyhole instead makes the spawn-candidate region an oriented, hollowed compound shape: it has a center, a normalized 2D facing, the cosine of a half-angle, inner and outer ring radii, and a sidewall thickness. It offers two core predicates—CategorisePoint (which category a point falls into) and CategoriseLink (which category a road-network link falls into). The categories include: usable in view, outside the inner ring (near, spawnable out of view), the outer ring, and a special “usable only when occluded” category. The semantics: directly ahead, spawn only at mid-to-far range (the player sees clearly up close—nothing may pop in); out of view, spawning may come near (nobody’s looking); occluded points are demoted to “use only while something blocks it.”
An exquisitely fine point: pedestrians and vehicles each hold their own keyhole. The pedestrian keyhole initializes from the current ped-spawn range plus the camera FOV, then hands a copy to the background pathfinding thread to enumerate candidate navmesh polygons—the pathfinding thread uses the keyhole’s bounding box to bound its iteration and CategorisePoint to judge each candidate. The vehicle keyhole initializes from the vehicle-spawn range plus camera facing plus look-ahead speed plus sidewall settings, caching road-link categories to select which spawn links to activate. The background pathfinding thread is only a consumer of this geometry, never a second owner—a clean ownership boundary.

Stage 3: culling and recycling, before spawning. Note the order—cull before spawn. Before any new person is created: mark/delete by visual importance and distance, delete by deletion priority, (if enabled) delete toward the preferred police count, clean up excess corpses, refresh the recycle pool. Deletion pressure is established first, recycling bookkeeping refreshed first, and only then does spawning get its turn. The order is not incidental: free the space and the budget first, and spawning has meaning.
Stage 4: bounded, cycle-based spawning. Spawning is not a “fill until full” loop. It pushes parameters to the background pathfinding spawn task, consumes pre-computed candidate coordinates, handles wildlife, seeds the per-cycle create/destroy counts, then enters a timeslice-driven population cycle loop. Each cycle runs a fixed order: process the deletion queue (if nothing was destroyed this cycle yet) → spawn at scenario points → spawn ambient pedestrians → one round-robin pass of scheduled spawning (scheduled peds in vehicles, scheduled scenario peds, scheduled ambient peds). Two key controls are visible here: spawn volume on the normal path has an upper bound, and creation overflow carries across cycles—preventing one frame’s spawn excess from bursting the frame budget.
There is also a dedicated “instant fill” fast path (ProcessInstantFill) for openings or forced fill: it repeatedly pushes pathfinding to spawn, refreshes candidate coordinates, handles wildlife, and adds ambient peds up to a per-frame attempt cap. Far more aggressive than the normal bounded cycle loop—the original treats “fill at opening” as a separate population mode, not “normal spawning but faster.”
Even at the function’s tail it is still doing world-side maintenance: updating performance counters, recomputing “over the ambient-ped cap,” updating in-vehicle ambient exception counts, updating bodyguard spawning, resetting temporary population-control overrides, decaying instant-fill state, updating multiplayer visibility-failure timers, re-initializing the ped animation catalog in the background, and forcing objects inside script population spheres to real. From head to tail, it is the owner of the population system—never the AI executor of any NPC.
One more easily missed design: spawn budgets and delete budgets come as a pair. Functions like “how many more scenario-point NPCs / ambient NPCs should spawn” are not generators but budget gates—they weigh current headcount, target headcount, the memory budget multiplier, the population-zone multiplier, and interior/opening state, and output “remaining allowed production.” This explains why spawning and culling must be read together: production is bounded by one world-side budget model, and the same model shortly decides who gets deleted and who enters the recycle pool (the next chapter unfolds the cull side).

Why this chapter matters: population density is not a per-NPC “should I exist” property; it is the world side continuously regulating a global budget. And the seemingly mundane matter of “where to spawn without breaking immersion” is encoded by the original as a reusable piece of keyhole geometry consumable by a background thread—a deft move that turns an experience constraint (“nothing pops in near the player”) into a computable geometric predicate. This chapter in one sentence: spawning is never “randomly creating objects”; it is selection under the triple constraint of spatial rules (the keyhole), navigation candidates (legal footholds enumerated by the background thread), and budget gates.
② Mass fit boundary
MassSpawner triggers the first reaction: “isn’t this exactly mass entity spawning—perfect fit.” Put the original’s population logic on top of it and the direction is wrong:
- Oriented “keyhole” anti-popping spawn—Mass provides area volumes / density volumes / environment queries, but the semantics of “mid-to-far ahead, near allowed out of view, occluded points demoted, one keyhole each for peds and vehicles, a copy handed to the pathfinding thread” are not the framework’s business; you implement all of it. And that still isn’t the biggest problem.
- Layered culling and recycling is not batch-shaped—the original’s “hard pool pressure / visual importance / deletion priority / no-delete zones / scenario-point membership / interior exceptions” is per-object, branch-heavy verdict logic. Fill an ECS Processor with per-entity branches like that and the contiguous-memory batching it lives on becomes a formality—you pay ECS’s architectural cost without collecting its performance dividend.
- Paired, cross-frame spawn/delete budgets are stateful world-side policy—they naturally belong to a world-side owner, not to a Processor that runs once per entity. Force them into Mass and you either bypass ECS with a global Subsystem (Mass shrinks to a spawner shell) or scatter budget state into Fragments (harder still to maintain).
- The throttling itself—bounded cycles with overflow carried across frames—is likewise a stateful, cross-frame world-side loop, not the Processor model of “stateless, sweep everything each frame.”
In one sentence: Mass optimizes for “many, homogeneous, parallel”; the population system’s hard core is “heterogeneous, ownership-laden, per-object verdicts, stateful throttling”—the shapes don’t match. With Mass you would be perpetually weighing “bend to the framework” against “bypass the framework.” Building this is not needless duplication; it is refusing to buy a whole mismatched architecture just to save one spawner.
③ Prototype status and plan
Status first: the population spatial policy is currently a gap in the prototype—it is sequenced after the entity foundation, physics, and the Ped/Vehicle/Object skeletons. But the foundation has already prepared the key attachment points, and the plan is clear.
Foundation support already in place: the previous chapter’s “deferred removal + phase safe points” naturally carries the population layer’s “cull before spawn” execution order—a cull pass and a spawn pass, ordered by the world runtime’s phase sequence, with all mid-flight additions and removals going through pending queues and flushing at end of frame, immune to the destroy-mid-traversal trap. The population layer will not need to invent its own mutation-safety mechanism; it builds directly on the foundation.
Planned landing:
- The population control point becomes a piece of the world runtime (matching PopCtrl): computed once per frame from player/camera position, facing, speed, and interior state, producing the frame’s parameters for all spawn/cull logic to consume. A pure data carrier plus a per-frame refresh point—it drops neatly into one of the world runtime’s phases.
- The keyhole geometry will be built as an independent value type, porting the original’s semantics verbatim: center, normalized facing, cosine half-angle, inner/outer radii, sidewall thickness, with
CategorisePoint/CategoriseLink. It is the piece of the original most worth replicating as-is—pure geometry, zero external dependencies, trivially unit-testable. The peds-and-vehicles-each-hold-one split is ported too. - Bounded cycle spawning + paired budget gates become policy functions of the population layer, running in the world runtime’s spawn phase, with budget state held as one shared world-side block (not scattered into entities).
Honest boundary (two items): first, none of the above is written yet—these are roadmap entries plus design direction, not implemented code; this chapter’s ③ has one of the lowest “implemented” ratios in the article. Second, a blind spot: keyhole spawning depends on the background thread enumerating navmesh candidates (“can someone stand here”). On the prototype side this maps to UE Navigation System queries, but the navmesh construction algorithm itself has not been read line by line; only the attachment point—”candidates must pass a standability check”—is described here, and construction internals are treated as a blind spot.
Chapter 4 · Population Culling, Recycling, and Budget
The problem this chapter solves: who may be deleted? Does deletion destroy, or park for reuse? And how much new output is still allowed this frame?
The previous chapter covered the “add” half of “cull before spawn.” This chapter supplies the “cull” half—the other half of the same population owner, not an afterthought of cleanup. It gets its own chapter because culling is every bit as complex as spawning, and it is where the “budget economics” actually lands.
① How the original designed it
The original puts large-scale ped culling, post-cull recycling, the deletion queue, and the spawn budget all inside the same population owner—parallel to the spawn path. Not a coincidence: deletion is spawning’s “other half of the balance,” and one budget model decides both production and reclamation.
Culling is layered, not a distance test. At minimum these layers, judged in order:
- Hard pool-pressure cleanup: when the ped pool is exhausted, force cleanup—the final protective backstop.
- Visual importance + distance culling: judge by “how much this ped matters to the current shot” combined with “how far away,” then mark or delete.
- Deletion-priority culling: when deletion demand is high, delete by priority.
- No-delete zones and facing-the-player constraints: peds in certain zones may not be deleted; peds facing the player may not be deleted casually.
- Interior and scenario-user exceptions: peds indoors, or currently using a scenario point, get special treatment.
And the cull range is computed, not a constant. The original has a dedicated function for “current cull geometry,” whose inputs include: in-view cull range, out-of-view cull range, interior awareness, player visibility and frustum rules, no-delete gating, ped state, and scenario participation. Which says: the cull layer is a persistent world-runtime policy, not ad-hoc cleanup scattered around.
Deletion does not equal destruction—there is a recycle pool. A ped can leave active simulation by “queued deletion” or by “entering the recycle pool,” rather than being destroyed outright. Dedicated functions process the deletion queue, update the pool, and add peds to it. The ownership implications are explicit:
- Task trees do not own final world deletion.
- The world layer owns generic entity add/remove registration.
- The population owner owns the population-specific decision of “does this ped qualify for deletion/recycling, and when.”
That is: an NPC’s task may flag it “worth keeping—don’t delete” or “deletable,” but the broad population-budget enforcement power stays with the population owner—the actor side only supplies input; final authority sits on the world side.
Budgets are paired and memory-aware. The “how many more should spawn” budget gates of the previous chapter and the culling here are two faces of one thing. They weigh current headcount, scenario/ambient targets, the memory budget multiplier, the population-zone multiplier, and interior/opening state. The memory budget multiplier input is the critical one—it means the population target is not a fixed number but scales dynamically with available memory. In a massive-object system, “how many is many” is itself a variable.
One-sentence close: culling is not “spawning’s cleanup”; it is the equal other half of population economics. Pull “who may be deleted, delete or recycle, how much may still be produced this frame” into one world-side owner, and let it scale with memory—that is the real reason “objects change constantly, yet the frame budget and memory budget always hold.”

② Why not Mass or another off-the-shelf option
This chapter reinforces Chapter 3’s ② by another notch. Mass LOD can indeed “deactivate distant entities,” which looks like culling. But the original’s culling is nothing so simple as deactivation:
- It is layered, prioritized verdict logic (hard pool pressure / visual importance / deletion priority / no-delete zones / interior exceptions), every layer per-object and branch-heavy—landing once more on Processor batching’s weak spot.
- It distinguishes deletion vs. recycling, which needs a stateful recycle pool and clear ownership of “who holds final deletion power”—in ECS, entity destruction/reclamation belongs to the framework, and stacking “don’t actually delete yet, enter my pool” on top runs against the framework’s grain.
- Its budget scales dynamically with memory—a cross-frame, world-side, stateful policy, not expressible as “run once per entity per frame.”
- Most fundamentally: the original’s code structure directly reads out “tasks may voice an opinion (don’t-delete / deletable), but final authority rests with the world-side population owner”—the deletion wish is a task-side flag; every deletion call site lives in the population manager. This input/authority-separation ownership contract has no natural home in ECS’s flat model—who is the “world-side adjudicator”? A global Subsystem? Then you’ve bypassed ECS again.
Same conclusion: the hard core is heterogeneous, ownership-laden, per-object verdicts and stateful throttling—none of Mass’s strengths apply here, and all of its weaknesses are hit.
③ How the prototype implements/plans it
Status mirrors the previous chapter: the cull policy itself is not written (population layer, not started), but the foundation support and the ownership contract’s landing spots are clear.
Foundation support already in place:
- “Queue deletions first, destroy together at end of frame” is an implemented foundation capability (Chapter 1)—the cull layer will not need to invent its own deletion-safety mechanism.
- The recycle pool is conceptually kin to the foundation’s “retired entity” mechanism (removed entities are retained until next frame or shutdown): the prototype already has the semantics of “an entity retires on removal, its handle is never reused, and flushing is explicit when needed.” Extending that into “culled peds enter the recycle pool first; the next spawn draws from the pool” grows naturally out of the foundation rather than starting a second system.
Planned landing, and the ownership contract: the thing most worth porting from the original here is not any block of code but that input/authority-separation ownership contract—an NPC’s task may write “I’m currently interesting / deletable” into its own state, but whether deletion actually happens is the world-side population owner’s call, under budget. The prototype’s entities can already carry such “actor-side input” state fields; the future population owner, as a world-side block, reads those inputs plus the global budget and makes the final ruling. Settle this contract once, and “who adjudicates, tasks or population” never becomes a conflict.
Honest boundary: the concrete layered-cull verdicts, the concrete recycle-pool policy, and the memory-scaled budget gates are all unimplemented in the prototype—they are population-layer roadmap entries. What this ③ offers is the fact that “the foundation has already reserved deletion safe points and retire/recycle semantics for culling, and the ownership contract’s landing spot is clear,” plus an explicit plan—not finished code.
Chapter 5 · Behavior-Level LOD and Proxy Switching
The problem this chapter solves: the same “driver-occupied vehicle” or “pedestrian”—why does the one 20 meters from the player run full physics and AI while the one 300 meters away costs almost no CPU? Who decides that downgrade, how is it seamless, and how does it not flicker?
Chapter 2 covered “how one object switches between full-instance and proxy representations”—the representation layer. This chapter covers the cost-scheduling layer: who decides, every frame, which of this large crowd should upshift and which should downshift, and how that decision stays stable. The two cooperate—representation supplies “how to change”; scheduling supplies “who changes, and how many this frame.”
① How the original designed it
The original has an exquisitely precise, deeply counter-intuitive split here, shown most clearly on the vehicle side (the ped side is isomorphic), worth telling in full. It splits “a vehicle” into three ownership tiers:
- The population owner (VehiclePopulation)—owns only the vehicle’s lifetime: spawn, delete, recycle, emergency-vehicle (police/ambulance) spawning. It does not care how much detail the vehicle gets this frame.
- The LOD manager (VehicleAILodManager)—globally unique, the actual decision-maker for “how much detail this vehicle gets this frame.”
- Per-vehicle LOD state (VehicleAILod)—storage only: current LOD flags, blocked flags, forced flags, whether this frame’s timeslice reached it. The header even states it outright: only the manager may modify these flags; the manager is its friend class.
The LOD manager’s per-frame Update() shows the division of labor best. It: picks the population control center → recomputes the “worst timeslice timestep” → updates LOD distances and range scaling → re-sorts all vehicles by effective distance → traverses in batches (never all in one frame) → uses hysteresis plus the N-LOD budget to decide each vehicle’s target detail tier → executes proxy conversions (TryToMakeIntoDummy / TryToMakeFromDummy) → toggles timeslice updating → manages “pretend occupant” real/fake conversion → refreshes the global timesliced-vehicle count and the next update window.
Three tiers:
- real (full instance)—full physics, full AI, a genuine driving loop.
- dummy (proxy)—no full rigid-body physics; simplified movement along the road network.
- superdummy (lighter proxy)—even the simplified movement is compressed further.
(How a dummy vehicle concretely “moves simplified along the road network”—the kinematic-preview advance formula—has real code to consult in the vehicle topic’s Part 2; here we cover only the scheduling semantics.)
Points especially worth learning:
Hysteresis. Upshift and downshift do not share one threshold—a dead band sits between them. Why? An object oscillating near a single threshold would flip between tiers repeatedly: visually flickering, and worse for performance (each switch has its own cost). Hysteresis is a *requirement* for stable tier changes, not an optional optimization.
Batching + timeslices. The LOD manager does not process every vehicle in one frame; it batches, and maintains a “worst timeslice timestep.” Distant vehicles do not need per-frame updates—they take turns by timeslice, where “the timeslice” means “which batch of distant objects gets updated this frame.” This is “prioritize by effective distance; near updates often, far updates rarely” made concrete.
Pretend occupants. A distant vehicle does not need a genuinely full-AI driver inside—too costly. The LOD manager maintains a budget: “how many vehicles this frame are allowed occupants who are ‘real people.'” Beyond the budget, occupants are “pretend” (a placeholder consuming no AI); when the player approaches and the vehicle enters the budget, the pretend occupant converts into a real NPC. The owner of that conversion budget is the LOD manager, not the population owner—because it governs “how expensive these occupants are allowed to be,” a runtime matter, not “when this vehicle spawns,” a lifetime matter. The ownership cut is particularly clean: population owns spawning and lifetime; the LOD manager owns the runtime budget of “how expensive these occupants may be.”
The ped side is isomorphic: every frame, the AI-LOD manager first computes a fullUpdate boolean for each NPC—should it run a full update this frame. And one nearly invisible detail: the outer fullUpdate and the fullUpdate fed to the intelligence/task step can differ—a tunable lets the outer layer process at full-frame cadence while the intelligence step still receives the raw AI-LOD decision. That is, “how much detail the object gets overall” and “how much detail its AI gets” are two independently adjustable knobs. The detail resurfaces in Chapter 6 (the bFullUpdate parameter of ProcessIntelligence is exactly it).
Why this chapter matters: a massive object count survives not because of “good optimization” but because “how much to compute” is explicitly encoded into each object’s own state and adjudicated centrally by a manager that holds the global budget, sorts by distance, and switches with hysteresis. Tiers, hysteresis, budget, timeslices—drop any one and you get flicker or a blown budget.

② Why not Mass or another off-the-shelf option
This layer is where Mass LOD misleads most easily: it ships “distance bucketing + timeslice down-rating,” which looks like exactly what the original needs. But it supplies only the single dimension of “how often to compute,” while the original’s proxy switching is “what to compute as”—a semantic transformation with a state machine. The two are far apart:
- What each of the three tiers runs, and how switches stay seamless—Mass LOD tells you “this LOD bucket should down-rate to such-and-such frequency,” but the per-tier behavior (“real runs full physics, dummy moves simplified along the road network, superdummy compresses further”) and the state migration on seamless tier switches are entirely outside the framework’s scope.
- Hysteresis—without it, objects flip tiers repeatedly near thresholds: flicker, and worse performance. It is part of the transition state machine, inexpressible as “bucketing.” You can hand-roll hysteresis at Mass LOD’s bucket edges, but that is already patching a state machine outside the framework.
- “Pretend occupants”—converting sub-objects between real/fake under a budget—”an entity’s sub-object (the occupant) converting between two representations under a global budget” is especially awkward in ECS’s flat-data-row model: is the occupant a Fragment of the parent entity or an independent entity? Both collide with Mass’s batching model (the former makes structural changes expensive; the latter leaves identity mapping to you).
- Two independently adjustable knobs (whole-object LOD ≠ AI LOD)—one Mass LOD bucket typically drives everything; splitting into two levels means designing around the framework itself.
Mass LOD solves “down-rating”; proxy switching demands “transformation”—use a down-rating framework for transformation, and you are forever patching state machines outside the framework. So the VirtualWorld prototype makes “tier, switch, budget” first-class entity state and world-side policy, landing squarely on its “world-owned + phase-scheduled” foundation—the conversion protocol is a first-class citizen, not a patch.
③ How the prototype implements/plans it
The “representation switching” skeleton of this layer already landed in Chapter 2’s FVWObject conversion protocol—the request–policy–resolution machinery of EVWObjectRepresentationState plus FVWObjectConversionPolicy is exactly the first-class implementation of “how to transform.” What this chapter’s ③ adds is “who schedules these transformations”—and that part is not started. Separately:
Landed (the transformation mechanism): how an object turns from dummy to real, which gates the change passes, and what every “cannot change” reason is—Chapter 2 covered these thoroughly, as real, unit-testable code. It corresponds to the conversion verdicts behind the original’s TryToMakeFromDummy / TryToMakeIntoDummy, and the prototype’s gates are far more complete than a bare conversion function (twenty-plus precise rejection reasons).
Attachment point reserved (where downshift physically lands): Chapter 1’s EVWEntityUpdateFlags plus phase buckets are behavior-level LOD’s physical hook. “Downshifting an object” is in large part “removing it from certain phase buckets”—a vehicle dropped to dummy no longer registers into the full physics phase; a downshifted NPC no longer runs the control phase every frame. The foundation already provides this selective per-phase registration; a future LOD scheduler only needs to operate those flags.
Not started (the LOD scheduler and budgets): the global LOD manager—”pick the control center each frame, re-sort by effective distance, traverse in batches, decide each vehicle’s target tier by hysteresis and the N-LOD budget, manage timeslice windows”—and the pretend-occupant budget are roadmap entries of the Vehicle/Object Population layer, unimplemented. The prototype has “how to transform” and “how downshift lands in phase buckets,” but not yet the decision core of “who upshifts, who downshifts, how many this frame.”
One explicit trade-off: for vehicle physics, the prototype plans not to build its own rigid-body proxy, but to use the engine’s vehicle physics plus physics LOD. The original built simplified movement because no off-the-shelf vehicle physics LOD existed in its era; today it does, and rebuilding is uneconomical. What the prototype ports here is the scheduling semantics of “three tiers + hysteresis + budget + timeslices,” not the original’s physics implementation—port the semantics, not the implementation. Which again shows “no Mass” never meant “write everything yourself”: off-the-shelf components unrelated to population semantics get reused.
Chapter 6 · The Perception–Decision–Task Pipeline: Up to “Producing a Task Slot”
The problem this chapter solves: how does a single NPC manage to “seem alive”—how does it know what happened around it, decide whether to react, and turn that decision into a pending task? And how is all of this still expressible when the NPC “legally does not react”?
The previous five chapters covered how a large crowd is owned, switched, spawned, culled, and scheduled—the collective view. This chapter switches to the individual view: what the pipeline inside one specific NPC looks like. This is the real mechanism of “seeming alive,” and the layer with the most complete structural skeleton in the prototype. The chapter runs up to “an event is adjudicated and becomes a pending task hung on a task slot”—executing tasks into walk/flee/ambient behavior is Part 2’s subject.
① How the original designed it
When the world’s scene update reaches a living NPC, its ProcessControl() runs layer after layer of per-frame wrapping: animation state sync → outer full-update maintenance → data/reset/pre-AI inventory → intelligence → post-AI inventory → animation/graphics/physics wrap-up. The core step is intelligence—that is where “seeming alive” happens.
The intelligence step is a clean pipeline, in order:
- Maintain the nearby-entities list—”who is around me.”
- Variable and state updates.
- Scan for events—a dedicated scanner runs here, a pure producer: it scans nearby entities, the world, and other NPCs, producing and enqueuing “event” objects (heard a gunshot, saw a corpse, car about to hit me…). It only produces events; it never decides reactions.
- Decision-maker, target selection, cover, enclosed-area updates.
- Handle events—the event handler consumes the queue and adjudicates.
- Process tasks—the task trees actually advance at this step of the same frame.
- Post-task state updates.
The key to this chain is that responsibilities are cut cleanly: the scanner is responsible only for “discovering and producing events” (production layer); the intelligence pipeline is the real gateway (consume the scan, hand events to the handler, then run tasks); the response factory sits further downstream, responsible only for “mapping an event-response type to a concrete task instance” (construction layer). The full causal chain: scanner produces events → the intelligence pipeline processes the frame and hands events to the handler → the response factory routes the selected response into a concrete task → the task trees execute within the same intelligence pass.
Tasks themselves are not one tree but several parallel trees: primary, secondary, movement, motion—one NPC can simultaneously “walk (movement) + make a phone call (secondary) + have its primary task interrupted by a higher-priority event.” These parallel trees are the structural basis of “one person doing several non-conflicting things at once, yet interruptible by higher-priority events.”
And here sits a deeply counter-intuitive design that is essential to “seeming alive”: a legal event response may deliberately do nothing (a no-op). Deciding is not “an event arrived, so react”; it is the output of the decision-maker’s weighting across the NPC’s personality, relationship group, and alertness—and it may perfectly well rule “this pedestrian’s reasonable response to that gunshot is to keep walking.” Precisely this “legally not reacting” is what makes a crowd look varied and characterful, instead of everyone answering the same stimulus with the same synchronized move.
One-sentence close: “seeming alive” comes not from more complex individual behaviors but from a pipeline that fully layers perception, adjudication, and execution—and licenses the adjudication layer to suppress reactions legally. Separate who produces, who adjudicates, who executes, and the crowd’s “personality” has room to grow.

② Why not Mass or another off-the-shelf option
This layer explains “why not Mass” better than any other. Mass’s charter is homogeneous, parallel batch processing—while everything “alive” flows from heterogeneity and causality: each NPC has its own personality weights, its own parallel task trees, its own verdict of “I legally ignore this event.” Per-object, branchy, stateful causal logic is exactly the shape ECS Processors handle worst.
What about UE’s perception component plus StateTree/behavior trees? They cover the two ends—perception and execution—reasonably well. But the middle layer—“events enqueue first, a weighted decision-maker adjudicates centrally, and a legal no-op is allowed”—does not exist in the off-the-shelf stack. Perception components typically pass stimuli straight to behavior logic, missing an arbitration layer that weights by personality/relationship/alertness and may rule “no reaction”; without it, crowds slide into “same stimulus, same response” uniformity. Nor will any off-the-shelf option establish the fixed frame order of “maintain perception → adjudicate → run tasks,” or the structure of several parallel task trees, on your behalf. The value of this pipeline lies entirely in its structure and ownership boundaries, not in any reusable algorithm block—which makes it a natural fit for self-building and a poor fit for framework adoption.
③ How the prototype implements/plans it
This pipeline is the prototype’s most structurally complete layer, and it has real function bodies. FVWPed builds out the whole chain’s states and steps, and VWPed.cpp contains genuine implementations (not bare declarations). Segment by segment, down to real code:
Several parallel task trees. EVWPedTaskTree { Primary, Secondary, Movement, Motion } map directly onto the original’s primary/secondary/movement/motion. Every task slot FVWPedTaskSlotState carries tree, priority, task type, lifecycle state (Pending / Active / Completed / Aborted)—and remembers which event triggered it: the slot holds SourceEventType / SourceEventPriority / SourceEventHandle / SourceEventTag. This “a task knows which event it came from” provenance is the original’s causal chain, landed.
Events are candidates, not commands. FVWPedEventState is the “event candidate” list (the scanner’s product); AddIntelligenceEvent(...) appends, FindHighestPriorityEvent() selects the highest priority—matching “scanner produces, priority adjudicates.” Events arrive as *candidates*, not *commands*; that word choice is the original’s “an event does not compel a reaction,” landed.
Event→task routing is an explicit layer. FVWPedEventTaskResponseState is the response table—”this event type maps to which tree, what priority, what task”—and ProcessEventTaskBridge() is the bridge, matching the original’s response factory. Read the bridge’s real implementation and the care shows: it looks up the response table by the current event type; on a hit, it dedups first—if the target slot already holds the identical task (same type, same source, same script parameters), it skips rather than re-applying; if the target slot is Active and running, it also skips (never interrupts a running same-position task); only when genuinely needed does it write the response as a Pending task slot, stamping the current event’s provenance (type/priority/handle/tag) into the new slot—the landing point of “a task knows which event it came from.” It also counts “bridge passes this frame vs. applications actually made” (EventTaskBridgeProcessCount / EventTaskBridgeAppliedCount), making “events really became tasks, exactly once” verifiable.
The pipeline’s entrance is ProcessIntelligence(Context, bFullUpdate). Note the bFullUpdate parameter—the AI-LOD knob from Chapter 5: it accepts an external decision of “full update this frame or not.” Read the implementation: it accumulates “time since last AI update” and increments the intelligence-pass counter; only when bFullUpdate is true does it refresh the “current event” (selecting from candidates) and run the event→task bridge; then it advances task processing. On non-full frames, this NPC’s intelligence deliberately “runs shallow”—behavior-level LOD expressed inside the individual pipeline.
The point that best embodies the original’s discipline—the legal no-op—is in, and doubly so. FVWPedState (precisely, its Intelligence) has bTasksSuppressed and SetTasksSuppressed(...). Read ProcessIntelligence and the switch gates two places at once: the event→task bridge (runs only when bFullUpdate && !bTasksSuppressed) and task-processing advancement (only when !bTasksSuppressed). Under suppression, events keep arriving and candidates keep queuing (perception never stops), but “turn events into tasks” and “advance tasks” are both explicitly skipped. This is the switch by which “the adjudication layer may legally suppress reactions”—not an “on error, skip” patch but a first-class, queryable state, with its gating points plainly visible in the code.
Honest boundary: all of this is currently skeleton-level semantic control—the headers repeat “skeleton-level semantic controls only; they do not execute subtasks, movement, animation, or vehicle entry.” That is: the task slots, event candidates, response routing, and suppression switch—the states and transitions—genuinely exist, are unit-testable, and have function bodies; but the actual behaviors underneath (walking, fleeing, playing animation) are not wired yet—what ProcessIntelligence advances today are counters and states, not animation and movement. And that ordering is deliberate: first fix the skeleton and ownership boundaries of “who produces, who adjudicates, who executes, who may legally do nothing,” then fill behavior into each slot—so behavior logic cannot balloon early and erode the architecture. It is the prototype’s consistent rhythm: correctness and boundaries first, concrete behavior after. The “fill the slots with behavior” half is exactly Part 2’s subject.
Blind spot: the task trees’ internal priority contention and concurrent scheduling algorithm (how multiple trees arbitrate; how high priority preempts low within one tree)—in the original I read only the structural layer, not the scheduling core line by line; the prototype likewise has no concrete arbitration rules yet (currently a simple “locate a slot by tree + priority” model). Here we cover only “responses route into task slots; slots carry priority and lifecycle”—the tree core is a blind spot.
Chapter 7 · Migration Trade-Offs: The Landing Checklist for Pedestrians, Part 1
Pulling six chapters together into one table. The three-tier criteria are the honest reporting convention for this prototype: ① implemented (in the prototype with tests/function bodies), ② skeleton (types/states/transitions built, behavior still empty), ③ planned/not started (roadmap entry, no code yet).
| Layer | Key mechanism | Prototype status | Landing spot |
|---|---|---|---|
| World foundation | World-owned entities, handles | ① implemented | FVWWorldRuntime + FVWEntityHandle |
| World foundation | Phase scheduling (not per-object Tick) | ① implemented | RunControlPhase / RunPrePhysicsPhase / … |
| World foundation | Deferred removal, phase safe points | ① implemented | PendingRemoval + ProcessingDepth + FlushPendingRemovals |
| World foundation | Safe mid-traversal add/remove | ① implemented | PendingAddEntities + command buffer |
| World foundation | Selective per-phase registration | ① implemented | EVWEntityUpdateFlags + phase buckets |
| World foundation | Return false = leave phase ≠ destroy | ① implemented | ProcessControl semantics (dedicated tests) |
| Entity representation | real/dummy representation state | ② skeleton | EVWObjectRepresentationState |
| Entity representation | Request–policy–resolution conversion protocol | ② skeleton | FVWObjectConversionPolicy (rejection reasons enumerated) |
| Entity representation | Verdict split from scheduling | ② skeleton | Policy “no scan / no stream / no create” |
| Population spawn | Population control point, frame parameters | ③ not started | Planned as a world-runtime block |
| Population spawn | Keyhole anti-popping spawn geometry | ③ not started | Planned self-built value type, original semantics ported |
| Population spawn | Bounded cycles + overflow carry | ③ not started | Planned as the population spawn phase |
| Population cull | Layered deletion priority | ③ not started | Foundation (deferred removal) ready |
| Population cull | Recycle pool | ③ not started | Kin to foundation retired-entity semantics |
| Population cull | Input/authority-separation ownership contract | ③ not started | Landing spot clear, unimplemented |
| Spawn/delete budget | Paired budgets, memory-scaled | ③ not started | Planned as shared world-side state |
| Proxy switching | Transformation mechanism (how to change) | ② skeleton | Reuses the representation layer’s conversion protocol |
| Proxy switching | Downshift landing in phase buckets (hook) | ① implemented | EVWEntityUpdateFlags (hook ready) |
| Proxy switching | Three tiers + hysteresis + global budget scheduling | ③ not started | Roadmap Vehicle/Object Population layer |
| Proxy switching | Pretend-occupant real/fake conversion budget | ③ not started | Planned as part of LOD scheduling |
| Vehicle physics LOD | Simplified movement | trade-off | Not self-built; planned on engine vehicle physics (see the vehicle topic) |
| Perception pipeline | Several parallel task trees | ② skeleton | EVWPedTaskTree + task slots |
| Perception pipeline | Task provenance to triggering event | ② skeleton | Slot SourceEvent* fields (written by the bridge) |
| Perception pipeline | Event candidates → response routing (with dedup) | ② skeleton | ProcessEventTaskBridge (has function body) |
| Perception pipeline | Legal no-op (suppressed reactions) | ② skeleton | bTasksSuppressed (double gating) |
| Perception pipeline | Full-update knob | ② skeleton | ProcessIntelligence(Context, bFullUpdate) |
The table in one sentence: this prototype has concentrated nearly all of its investment in the foundation—world ownership, phase scheduling, deferred removal, safe mid-traversal mutation, per-phase registration—now stable and covered by 30 world-runtime-named tests. One level up, entity representation and the perception pipeline have their skeletons in place (representation states, the conversion protocol’s twenty-plus rejection reasons, task trees, routing dedup and provenance, the doubly gated legal no-op—all real, unit-testable code with function bodies). Meanwhile the population spatial policy (keyhole, budgets, culling) has not broken ground. The task execution side (walk/flee/ambient behavior, the scenario system, animation and ragdoll) is Part 2’s subject.
The Build Order—and Why It Rules Out Mass
The table actually draws a clear build order, and that order is itself a lesson from reverse-engineering the original:
Fix the foundation first—who owns objects, when things compute, how mutation stays safe. Then raise the skeletons of representation and the individual pipeline—how an object switches between two weights; how an NPC perceives, adjudicates, and produces tasks. Only last fill in the top-layer policies—how many people the city should have, where they spawn, who stands at the curb. Do it the other way, and one tremor in the foundation forces every upper layer to be rebuilt.
That build order also explains why, in semantics-driven systems like this one, the foundation should not be handed to a batch-processing runtime like Mass—and note this is a statement about *applicability*, not “Mass is bad.”
Mass is a batch-oriented entity-runtime framework. Hand it the foundation, and the say over the bottom-most layer—who owns what, when things compute, how mutation stays safe—passes to the framework’s Fragment/Processor model. Yet reading these chapters shows that layer is precisely the part this article keeps stressing must be designed to the original’s semantics: world ownership rather than self-owning objects; semantic phases rather than batch Processors; returning false to leave a phase rather than to be destroyed; deletions queued and flushed at frame end; selective per-phase registration (which doubles as LOD’s hook)… None of this is “batch processing of homogeneous data”; all of it is semantic, per-object scheduling about ownership and ordering.
Once the foundation bends to the framework’s shape, the ownership layers, interruptible behavior, cost tiers, and causal chains above must all bend with it—you would swing forever between “bend to the framework” and “bypass the framework,” ending with a compromise that “nominally uses Mass while fighting it everywhere.” In this problem, using Mass is less “saving yourself a foundation” than outsourcing the foundation to a framework designed for a different class of problem. Once more for emphasis: switch to a homogeneous, parallel problem (fields of vegetation, undifferentiated far crowds) and the scale tips the other way.
The heuristic in one line: a semantics-driven foundation like this is better self-built (rather than adopting Mass, or using stock Tick as the master scheduling model); the semantic skeleton should be ported from the original (“roughly similar” is not equivalence); and only genuinely semantics-neutral off-the-shelf components—vehicle rigid-body physics, world-authored interaction points, pure data/query layers—should be reused from the engine. Knowing which layer to build and which to reuse matters more than either blanket “all in-house” or blanket “all engine.”
AI Collaboration Retro
This article draws on two lines of material: a set of source-level reverse-engineering notes on a mature open-world engine, and a UE5 prototype under active development. Aligning the two is itself an exercise in human–AI collaboration, worth recording honestly.
Where AI helped. On the reverse-engineering side, the six chapters’ mechanisms were scattered across hundreds of notes; AI’s value was re-seating the ownership boundaries in parallel—”the population owner acts only world-side and executes no AI,” “the LOD manager decides, per-vehicle LOD state only stores,” “the scanner produces, the intelligence pipeline is the gateway,” “population owns lifetime, the LOD manager owns the pretend-occupant budget”—distilled from file-by-file notes and aligned into the single through-line of “cost is distance, scheduling is phases, responsibility is ownership.” On the prototype side, AI helped map “those enums and state fields in the headers” back to “which mechanism of the original”—recognizing bTasksSuppressed as the landing of the original’s legal no-op, the FVWObjectConversionPolicy rejection enums as the original’s conversion gates ported item by item, the task slots’ SourceEvent* fields as the causal chain’s write point—and then went on to read VWPed.cpp / VWObjectConversionPolicy.cpp to confirm these are implementations with real function bodies, not empty declarations.
Where it nearly went wrong. In the first draft, AI wrote the UE5 landing defaulting to “build the crowd on Mass” (data-oriented entities + MassSpawner + Mass LOD), presenting these stock facilities as “equivalent replacements” for the original’s mechanisms. That route was flatly contradicted by the prototype’s actual choice—the prototype weighed Mass and explicitly set it aside, for exactly the shape-mismatch reasons each chapter lays out: Mass optimizes for homogeneous parallelism; the population system needs heterogeneity, ownership, per-object verdicts, stateful throttling. Chase the specifics one by one (Mass LOD gives bucketed down-rating—where’s the hysteresis? the three-tier transformation? pretend occupants? the legal no-op? the twenty-plus rejection reasons?) and it becomes plain that “a similar facility exists” and “it can carry the full semantics” are different things entirely. The lesson: before writing the UE5 landing, read the real prototype project and its technical decisions—don’t extrapolate from “the UE community currently uses Mass for crowds.” The popular practice and this project’s real, reasoned choice may point in exactly opposite directions.
How blind spots were handled. Every algorithmic black box the source notes never penetrated (navmesh construction, pathfinding core, task-tree scheduling internals), and every part the prototype has not written (population spatial policy, culling), is explicitly labeled “blind spot” or “not started”—never padded with plausible-sounding generic prose, and never passing “skeleton in place” off as “done.” This article, especially, must hold that line—its ③ sections span all three tiers of implemented/skeleton/not-started, and any vagueness would leave readers unable to tell running code from design intent. Read this far, told this far; built this far, claimed this far—that discipline is the credibility floor for any article that pairs reverse engineering with an in-progress prototype.
*This is a deep-dive installment of the “How to Build an Open-World Game” series. The pedestrian topic splits into two halves by layer: this half (Part 1) covers scheduling and decision—the world foundation, entity representation, population spawning and culling, behavior-level LOD, and the perception–decision–task pipeline (up to producing a task slot); Part 2 covers behavior, presentation, and physics—task execution, scenario-point-driven ambient behavior, and the positioning of locomotion animation and ragdoll. The prototype currently has the foundation (world runtime + phase scheduling + mutation safety) stable, entity representation and the perception pipeline built as skeletons, and the population spatial policy and culling not yet started. Every class name and source path in this article was verified against long-term reverse reading and the in-progress project, and is anonymized for publication per series convention.*