Chunk Observers & Chunk Loading
Chunk Observers & Chunk Loading
How 7DTD decides which chunks to load, mesh, and display — and how a mod can make terrain load around something that isn't the player (camera drone, remote viewpoint, teleport preload).
The one API that matters
ChunkManager.ChunkObserver obs = world.m_ChunkManager.AddChunkObserver(
worldPos, // absolute world coords (NOT Origin-shifted)
_bBuildVisualMeshAround: true, // true = also mesh/display; false = data only
viewDim, // radius in chunks; see caps below
-1); // entityId to stream chunks to; -1 = local/none
obs.SetPosition(newWorldPos); // every frame; cheap (just stores the vector)
world.m_ChunkManager.RemoveChunkObserver(obs);
GameManager.Instance.AddChunkObserver(...) / RemoveChunkObserver(...) are
thin public wrappers over the same calls.
- Vanilla creates the SP local-player observer exactly like this:
AddChunkObserver(pos, true, GameUtils.GetViewDistance(), -1). - For remote clients the server uses
(pos, false, viewDim, player.entityId)—bBuildVisualMeshAround: falsebecause the client builds its own meshes. - On a client in MP, a locally added observer is useless for data: chunk
loading is gated by
ConnectionManager.IsServer— only server-side observers cause chunks to load. Works fine in SP (integrated server).
viewDim semantics and caps
viewDimis in chunks; the observer covers rings0 .. viewDim+2— a(2*(viewDim+2)+1)²-ish square of data chunks (rectanglesAroundPlayers).- Hard engine cap:
rectanglesAroundPlayershas 15 rings, soviewDim + 2 <= 15→ viewDim ≤ 13. Vanilla clamps client observers to 12. Stay ≤ 12. GameUtils.GetViewDistance()=GameStats AllowedViewDistance, which in SP is set from the video optionOptionsGfxViewDistance(GameModeAbstract.Init). Typically 6–12.- The outermost ~2 rings of an observer never get a visual mesh: mesh regeneration requires the chunk to be lit + decorated, which requires all neighbors present. Effective displayed radius ≈ viewDim rings.
The pipeline is 100% observer-driven
Every stage keys off the observer lists — there is no "near the player" or camera-frustum gating anywhere in ChunkManager:
DetermineChunksToLoad— per observer, recomputed only when it crosses a chunk boundary; merges all observers intom_AllChunkPositions(data) andm_ViewingChunkPositions(observers withbBuildVisualMeshAround).GetNextChunkToProvide— serves all observer positions ring-by-ring (near rings of every observer first, so a second observer doesn't starve).- Lighting queue =
m_AllChunkPositions; decoration needs neighbors loaded. RegenerateNextChunkmeshes chunks inm_ViewingChunkPositionsonce lit + decorated;doCopyChunksToUnitydisplays them. The ChunkGameObject pool grows on demand (new GameObject("Chunk (new)")) — no fixed budget.
Practical consequence: a remote observer does load and display terrain in SP, but the generate→light→decorate→mesh→copy latency is seconds per area. A fast-moving observer (FPV drone at 25+ m/s) outruns it — the detail bubble trails the flight. Fix: lead the observer along the velocity vector (FPV mod leads by 2 s of horizontal velocity, capped at 64 m) and consider +2 viewDim over the video option since the whole bubble is in frame from altitude.
Working ground-vehicle implementation: Supra src/SupraChunkPreloader.cs —
extra observer while the local player drives (2 s lead capped at 96 m = 6
chunks for the ~69 m/s top speed), viewDim = Utils.FastMin(12, GameUtils.GetViewDistance()) (vanilla's own local-player clamp), removed on
dismount, rebuilt if the video option changes, skipped when not
ConnectionManager.IsServer. Motivation: road-decoration wrecks only appear
once their chunk is decorated + meshed, so at speed they pop in too close to
dodge; vanilla's only knob is View Distance (Options → Video), which just
widens the player-centered bubble.
Debugging chunk loading
- Vanilla console command
chunkobserver(aliasco):co add <x> <z> [radiusChunks],co remove <x> <z>,co list. Places data-only observers (bBuildVisualMeshAround: false) — good for testing the load half, not the display half. - The periodic
Time:log line decodes as:Chunks:= chunks in cache (includes chunks held by DynamicMesh etc., not just observers),CGO:= displayed ChunkGameObjects,CO:= observer count (sleeper volumes and wandering-horde spawners add temporary ones, so it fluctuates without mods). - Per-chunk ground truth from a mod:
world.ChunkCache.GetChunkSync( WorldChunkCache.MakeChunkKey(cx, cz))→ null = not cached;.IsDisplayed= has a live visual mesh. All public in the shipped (publicized) assembly.
Related
- Decompiling for reference: see [[Vehicles]] §"Decompiling vanilla source" —
ilspycmd -t ChunkManager "<GameDir>/7DaysToDie_Data/Managed/Assembly-CSharp.dll". - On this machine the game is at
D:\SteamLibrary\steamapps\common\7 Days To Die(NOT theC:\Program Files (x86)default in some csproj files — pass-p:GameDir=...when building outside modman).