Egbe Companions · Architecture

UE5 Client Modules & Agent Embodiment

A focused page within the Egbe Companions Architecture documentation. The full map and every sibling page live in the Architecture hub.

7sections11 minread1diagram

On this page

V6 — Egbe — is a universe of LLM-driven autonomous beings, the Ori, and the product promise is that you can stand next to one. An Ori is, by design, split across three authorities: its decisions live in the Moirai cognition cluster, its biography lives in the Ori service, and neither of them is a body. The body is the UE5 client's job. The canonical Egbe client is a single UE5.5 LTS project at V6/ue/, carved into eighteen *.Build.cs modules, and its hardest problem is one V3's metaverse never faced at this scale: a single Orun ground routinely renders far more simultaneous embodied agents than a V3 venue ever did, and each of those agents is a mind that costs money to think. So embodiment in V6 is not just "spawn a skeletal mesh" — it is a coupled render-and-perception LOD pipeline that decides, per agent per frame, how much body it gets and how much cognition it is owed, so that fidelity and cost both scale with what the player can actually see.

This page is the client side of that wire. It walks the UE5 module split, then the embodiment stack proper — the near pawn, the avatar runtime (VRM and MetaHuman through the vendored VRM4U plugin and the reused V3 avatar pipeline), emotion-driven animation, the Mass Entity density LOD ladder, and the StateTree-driven autonomous behavior that runs at the body when no model call is affordable. V6 reuses V3's avatar/animation/audio substrate wholesale and adds the agent-density and Ori-binding layer on top; the orientation map for the whole system is ../V6_ARCHITECTURE.md.

What ships, honestly#

The embodiment logic is real C++ and is tested. The avatar runtime, the emotion→animation resolver, the agent density/perception LOD planner, the Ori-backed LOD-transition continuity check, the StateTree behavior selector, and the Sequencer cinematics director are all implemented under V6/ue/Source/ and covered by V6Tests automation specs that the V6 completion run built and ran on the on-box UnrealEngine-5.5.4 (the full V6 UE suite reported 30/30 green). The load-bearing specs for this page are real and named: V6.Avatar.Runtime.AppearanceSeedCostumeAndAging, V6.Animation.EmotionDrivenGaitPostureIdleAndFace, V6.Agent.NearPawnBindsFullEmbodiment / ...RejectsIncompleteEmbodiment, V6.Agent.DensityLOD.Holds150AgentDrawBudget, V6.Agent.PerceptionLOD.BoundsCognitionByVisibleAgents, V6.Agent.LODBoundary.PreservesOriStateAcrossRenderLODs, and V6.Agent.Behavior.SelectsPerCognitionTier / ...FailsLoudWithoutTree.

V6 ships zero binary UE content — by deliberate decision, not omission. There are no .uasset/.umap files in the project; V6/ue/Content/ is .gitkeep placeholders. The six Orun districts are authored as procedural C++ grounds (V6World/V6OrunDistricts.cpp), not authored levels, and the embodiment modules here are the runtime binding and resolution logic plus the data contracts that a skeletal mesh, an AnimBP, a MetaHuman master, or a VRM body would be driven through — not the art itself. This is the same honest line V3 draws between its real retarget tables and the avatars they gate (v3arch§"Avatar Pipeline"): the pipeline is code; the bodies are content the pipeline binds and validates.

VRM4U is vendored, not hard-linked. The avatar plugin is the third-party VRM4U importer/runtime loader, dropped in at V6/ue/Plugins/VRM4U/ as an untracked submodule with "EnabledByDefault": false. Notably, V6Avatar.Build.cs does not list VRM4U as a module dependency — it depends only on Core, CoreUObject, Engine, GameplayTags, and V6Core. V6 reaches VRM bodies through soft references (an FSoftObjectPath VrmAssetPath) and the named V3 retarget profile V3Avatar.Oshun60, so the V6 C++ stays buildable and testable headless without the plugin loaded. (On Linux the editor additionally needs VRM4U's Shaders/Private case-shim symlink before it will boot — a build-time shim, never committed.)

Four modules are honestly thin. The integration-boundary modules V6Net (32 lines), V6Gameplay (21), V6OnlineServices (31), and V6Telemetry (31) are currently module-registration contracts; their heavy lifting lives in the Rust services and the libs/v6/* packages (the wire protocol, for instance, is the 373-line proto3 schema in libs/v6/egbe-protocol, documented in the gateway page). The embodiment-critical modules — V6Avatar, V6Animation, V6Agent, V6Voice (1,585 lines), V6Audio (527), V6Cinematics, V6UI, V6World — carry real domain types.

The UE5 client module split#

C++ is the primary gameplay language; Blueprint is reserved for designers, data assets, and cinematic glue. The client is a high-fidelity client of authoritative Rust world-server state — it owns no frame-deterministic combat (that belongs to the destination Aye realm on incarnation), and all client-side prediction reconciles against the world server. The eighteen modules group into four bands:

  • FoundationV6Core (the AV6EgbeGameMode / AV6EgbePawn, engine subsystems, save-game and V1-account bridge, the UV6GameFeatureData registration that lets districts and shard modes hot-load as Game Feature Plugins) and V6World (the client-side replicated shard model UV6ClientWorldModel and the procedural district grounds).
  • EmbodimentV6Avatar, V6Animation, and V6Agent, the subject of the rest of this page.
  • Interaction & presenceV6Input (Enhanced Input plus an OpenXR backend matrix and VR stewardship interaction), V6Voice (the Vac mic / intent / squad client), V6Audio (MetaSound and conversation-audio types), V6UI (token-themed CommonUI roster / dossier / Chronicle / HUD widgets with first-class FV6AccessibilityDescriptor metadata), V6VFX, and V6Cinematics.
  • Integration & tooling — the four thin boundary modules above, plus V6Persistence, V6Editor (the Egbe Studio authoring / verification commandlets), and V6Tests (the automation harness and Gauntlet drivers).

Agent embodiment: the near pawn#

The richest form an Ori can take is a near pawn: AV6AgentPawn, an ACharacter that composes a UV6AgentComponent, a UAudioComponent for agent TTS, and a USphereComponent for the interaction radius. These are the Clotho candidates — co-present, on-screen, addressable, full collision and movement.

Binding one is a fail-loud contract, not a best-effort spawn. UV6AgentComponent::BindNearAgent() takes a FV6AgentNearEmbodimentSpec and the live skeletal-mesh, voice, interaction, movement, and collision components, and BuildNearReadinessReport() returns a FV6AgentNearReadinessReport whose EV6AgentNearBindingState is exactly one of Unbound, BoundIncomplete, or RenderReady. RenderReady requires all capabilities present — an Ori id, the V3 avatar pipeline binding (V3AvatarPipelineRef defaults to V3Avatar.VRMOrMetaHuman), a full skeletal mesh, individual animation, voice, expression channels, a renderable appearance seed, emotion animation, collision, movement, and interaction. Anything missing is listed by name in MissingCapabilities; the spec carries an EV6AgentBodySource of Vrm, MetaHuman, or Procedural. This is what V6.Agent.NearPawnBindsFullEmbodiment and its rejection twin guard — a half-bound agent reports BoundIncomplete rather than rendering a broken body.

Avatars: appearance seed, costume, and visible aging (V6Avatar)#

V6Avatar is the runtime that turns an Ori's FV6AvatarAppearanceSeed into a dressed, age-marked body. The seed names its EV6AvatarRuntimeSource (VRM or MetaHuman), carries the soft VrmAssetPath / MetaHumanBlueprintPath, the V3Avatar.Oshun60 retarget profile, a list of FV6AvatarCostumeSlotBinding, and a list of FV6AvatarAgingMark. The default wardrobe is three slots — body, outer (the steward-wrap), and accent (the "ori-thread") — each mapped to a named material vector+scalar parameter pair.

The component does real engine work, not bookkeeping. ApplyCostumeSlotsToMesh() calls USkeletalMeshComponent::SetVectorParameterValueOnMaterials() and SetScalarParameterValueOnMaterials() to tint each slot, and ApplyAgingMarksToMesh() calls SetMorphTarget() plus a material scalar to weight each aging mark. Visible aging — V6's signature addition over V3 — is driven from arc state: ResolveLifeStageFromArc(ArcProgress, bTranscendent) maps a normalized arc into NewlyKnown (<0.18), Young (<0.42), Adult (<0.78), Elder, or Ancestor (on transcendence), and the three default marks — adult-story-lines (weight 0.42), elder-silvering (0.72), and ancestor-lumens (1.0) — fade in by stage via FV6AvatarAgingMark::AppliesToStage(). BuildRuntimeBindingReport() is again fail-loud: it enumerates every missing capability and only an avatar with an Ori id, a runtime asset, the V3 retarget profile, required costume slots, and stage-appropriate aging marks reports IsRenderable(). (V6.Avatar.Runtime.AppearanceSeedCostumeAndAging.)

Emotion-driven animation (V6Animation)#

The second V6 addition is that mood is legible at a distance, before any dialogue. V6Animation maps eight EV6OriEmotionState values — Calm, Joy, Focus, Concern, Grief, Anger, Awe, Tired — to gait, posture, idle, and facial-curve profiles. BuildDefaultEmotionPoseMap() authors a full FV6EmotionAnimationPose per emotion (grief, for instance, is gait.grief.weighted + posture.closed.lowered, an 11° forward lean, and the V6Face_Grief / V6Face_LowerLids curves), and ResolveEmotionDrivenAnimation() takes the live FV6EmotionAnimationInput (valence, arousal, confidence from the Ori) and blends it: arousal nudges the gait-speed multiplier (clamped 0.25–1.75) and idle intensity, low confidence lengthens the blend time, and the resolver computes a MoodLegibilityDistanceMeters from posture lean, idle intensity, arousal, and facial richness. ValidateMoodLegibilityAtDistance() enforces a real floor — the blend must read at ≥ 12 m — so an agent's emotional state is silhouette-legible across a plaza. An unrecognized emotion falls back to the Calm pose rather than producing nothing. (V6.Animation.EmotionDrivenGaitPostureIdleAndFace.)

The density LOD pipeline (Mass Entity)#

This is the part of embodiment that is genuinely new at the V6 scale, and it is where V6Agent depends on the full UE Mass framework (MassEntity, MassLOD, MassRepresentation, MassActors, MassSpawner, …). Agents stream across four render tiers, and — the load-bearing idea — perception and cognition LOD are slaved to render LOD, so the world server only spends rich perception, and Moirai only spends a full model call, on agents the player can actually see.

flowchart TD subgraph PLAN["UV6AgentDensityLODLibrary (V6Agent)"] IN["Agents: distance + importance + visible"] --> SORT["sort: visible ▸ importance ▸ distance"] SORT --> RENDER{"PlanDensityLOD<br/>per-platform caps + draw budget"} end RENDER -->|"≤ NearRadius, cap MaxNearPawns"| NEAR["NearPawn · AV6AgentPawn<br/>full skeletal mesh + AnimBP"] RENDER -->|"≤ MidRadius, cap MaxMidMass"| MID["MidMassInstanced · Mass StaticMeshInstance<br/>shared animation"] RENDER -->|"≤ FarRadius"| FAR["FarSilhouette · billboard / crowd shader"] RENDER -->|"beyond / not visible"| CULL["Culled"] NEAR --> PRICH["Rich perception → Clotho"] MID --> PCOARSE["Coarse perception → Lachesis"] FAR --> PSUM["Summary perception → Atropos"] CULL --> PDORM["Dormant → no cognition"] PRICH --> STREAM["PlanAppearanceAssetStreaming<br/>FullBody 48MB / Mid 12MB / Sil 2MB / 0MB"] PCOARSE --> STREAM PSUM --> STREAM NEAR -. "cross-LOD" .-> CONT["ValidateLODTransitionContinuity<br/>Ori durable state: identity · activity · rels · transform"] MID -. "cross-LOD" .-> CONT CONT --> SEAM["bSeamless = no popping"]

UV6AgentDensityLODLibrary::PlanDensityLOD() sorts agents by visibility, then importance, then distance (with a lexical Ori-id tiebreak for determinism) and greedily assigns NearPawn within NearRadiusCm (default 2,500 cm) up to MaxNearPawns, MidMassInstanced within MidRadiusCm (9,000 cm) up to MaxMidMassAgents, FarSilhouette within FarRadiusCm (25,000 cm), and Culled beyond. Caps and the draw budget are per platform, read from the [V6.MassEntity] config section with code defaults: desktop allows 64 near pawns / 64 mid / a 340-draw-unit budget; mobile drops to 24 / 48 / 160; a Pixel Streaming worker to 48 near / 280; the Tier-2 fallback floor to 16 / 32 / 100. A near pawn costs 4 draw units, a Mass-instanced mid agent 1, and the entire far silhouette batch 1 — so the FV6AgentDensityLODSummary can prove bWithinDrawBudget (V6.Agent.DensityLOD.Holds150AgentDrawBudget, V6.Agent.DensityLOD.HoldsFestivalTierGatheringBudget).

PlanPerceptionLOD() then walks the render assignments and derives, per agent, the EV6AgentPerceptionLOD (Rich / Coarse / Summary / Dormant) and the EV6AgentCognitionTier (Clotho / Lachesis / Atropos / Dormant) directly from the render LOD — NearPawn→Rich→Clotho, Mid→Coarse→Lachesis, Far→Summary→Atropos. Because every cognition tier is anchored to a render tier, the summary can assert bWithinCognitionBudget and bRenderAndPerceptionLODsTrack: total cognition is bounded by the visible-agent count, which is the client-side half of V6's cost story (V6.Agent.PerceptionLOD.BoundsCognitionByVisibleAgents, V6.Agent.PerceptionLOD.TracksRenderLOD). PlanAppearanceAssetStreaming() closes the loop on memory: FullBody ≈ 48 MB resident, the shared mid proxy ≈ 12 MB, a silhouette ≈ 2 MB, dormant 0, each checked against the residency budget and flagged bCanShedDetailWithoutPop.

The Mass side is concrete: UV6AgentMassVisualizationTrait (a UMassMovableVisualizationTrait) carries the mid instanced mesh, the far silhouette billboard mesh, and the silhouette material, and RepresentationForRenderLOD() maps mid/far to EMassRepresentationType::StaticMeshInstance. Agent identity, Ori durable state, render LOD, and perception LOD all ride as FMassFragments (FV6AgentMassIdentityFragment, FV6AgentOriDurableStateFragment, …) so they survive the ECS.

Crossing a LOD boundary without popping#

The reason a player never feels an agent drop from a full pawn to a Mass instance is that durable agent state lives in the Ori, not the pawn. FV6AgentOriDurableState carries the Ori id, the current activity (id / tag / phase / progress), the relationship LOD set, and the authoritative transform, with explicit PreservesIdentityWith() / PreservesActivityWith() / PreservesRelationshipsWith() / PreservesTransformWith() comparators (the last with a 1 cm tolerance). ValidateLODTransitionContinuity() takes a from/to transition and returns a FV6AgentLODTransitionContinuityReport that is bSeamless only when the LOD actually changed and both sides are Ori-backed and identity, activity, relationships, and transform are all preserved; otherwise it lists the ContinuityBreaks by name. That guarantee is exactly what V6.Agent.LODBoundary.PreservesOriStateAcrossRenderLODs asserts.

Autonomous behavior at the body (V6Agent StateTree)#

A budget without a behavior is useless, so V6Agent carries a real AI path. Each frame the system has a cognition tier for an agent; UV6AgentBehaviorComponent::EvaluateBehavior(tier) writes that tier into an FV6AgentBehaviorBlackboard and runs an authored UStateTree through a real FStateTreeExecutionContext. The tree's states are gated by FV6AgentTierCondition (an enter-condition that passes when the blackboard tier matches the state's MatchTier), and the matching state's FV6AgentSetBehaviorTask writes the selected EV6AgentBehaviorFullDeliberation for Clotho, CoarsePlanning for Lachesis, AmbientRoutine for Atropos, Idle for Dormant. Crucially it fails loud: with no usable StateTree assigned, EvaluateBehavior returns EV6AgentBehavior::None rather than fabricating a behavior, which is what V6.Agent.Behavior.FailsLoudWithoutTree pins (and V6.Agent.Behavior.SelectsPerCognitionTier proves the real selection path, the spec compiling a StateTree in-process). This is the believability floor the cognition stack drops to when a model call is unavailable or over budget.

Presence, cinematics, and the client edge#

Two more client subsystems carry embodiment beyond the live pawn. V6Cinematics is the runtime Sequencer hook: UV6CinematicsDirector::PlayAuthoredSequence() instantiates a real ULevelSequencePlayer for an authored ULevelSequence, tracks it by tag, and tears it down on StopPlaybacksByTag() / StopAllPlaybacks() — this is the playback half of Clio Chronicle replays, incarnation ceremonies, and Yemaya memorial reels. It is honestly scoped: the header states that offline MovieRenderQueue render-to-file is not driven here, because it needs a live GPU render unavailable in a headless -nullrhi context; the director owns real-time playback and never fakes a render (V6.Cinematics.Director.PlaysAndManagesAuthoredSequences). VR presence is V6Input's VR stewardship interaction plus V6UI's VR comfort options, validated by V6.XR.OpenXRBackendMatrix and V6.XR.VRComfort.OptionsPersistenceReview, so a steward can be embodied alongside their company in VR with comfort options that persist.

Underneath all of it, the client is a peer on one wire. The near pawn, the Mass crowd, and the silhouette field are all rendered from interest-managed egbe-protocol agent state delivered through the Realtime Gateway; the density LOD planner's near/mid/far bands are the client-side consumer of the protocol's interest windows and visible-agent caps. Fidelity is the only thing that moves — identity, presence, and the authoritative world do not.

How it composes#

The client is the visible end of the mind/body/memory split. The body it renders is owned by the world server and shard continuum — agent transforms, perception, and the 20 Hz tick that feeds the density planner's distance / visibility inputs. The frames and the wire that carry that state to a native UE client, a Pixel Streaming worker, or the Tier-2 three.js fallback are the gateway, Pixel Streaming, and the web fallback, whose interest windows and per-surface agent caps the density LOD ladder mirrors one-for-one. And the player's voice into an agent's ear — mic capture in V6Voice, intent parsing, agent TTS played back on the pawn's UAudioComponent, and squad comms — is the Vac communication pipeline. Across all three, the rule holds: the UE5 client makes the Ori legible — its dress, its age, its mood, its attention — but it is never authoritative over who the Ori is.