Repository navigation
Releases: Cuvara/UnityDots
Release list
v0.31.0
Ships with com.cuvara.netcode v0.47.0 (prediction fix + KCP/UDP-only gameplay) and
sgl-v0.7.0. Includes the never-tagged 0.30.0 below.
Added
LocalPredictionReference.Steering,DotsPredictionBootstrap.NoteInputSent(world, inputTick)
andDotsPredictionBootstrap.TryGetClockSteering(world, out steering).Installcreates a
netcodePredictionClockSteeringfor the predictor (kept across a re-install with the same
predictor). Report every input send withNoteInputSentbesideRecordInputso the
acknowledgement floor is measured; it stamps the DOTS world'sTime.ElapsedTime, the clock the
system measures snapshots and advances the predictor with.
Fixed
-
LocalPredictionSystemreconciles against the predictor's history at the right tick and steers
its clock - the path the game runs (MainScene). It called the two-argument
Reconcile(position, ack): no snapshot tick, so the history was never used, and nothing steered
the predictor's clock, which in a deterministic harness against a server 0.7% slow ran from 0 to
22-23 ticks ahead of the server's tick line over 55 s. Once per merged snapshot (whenever
WorldState.Tickadvances) it now feedsSetServerSpeed, then
PredictionClockSteering.SampleTickRate+OnSnapshot(seed, measure, feed a corroborated rate
forward, steer - the codeWorldViewBindersteers with), then
Reconcile(position, ack, worldTick, WorldState.AckAppliedTick)(theVec3overload, withz
andvel_z, under the 3D motor). Position, tick, ack andack_applied_tickall come from the
snapshot's ownWorldStateentry, so they cannot describe two different snapshots; the old
anchor-plus-new-ack rule remains only as the fallback for a local entity that is not in
WorldState. The system's memory and the clock measurements reset on a new view generation or
whenWorldState.Tickgoes backwards. Tests:LocalPredictionClockAndOffsetTests(new),
PredictionOwnershipTests.AMergedSnapshot_IsReconciledOnce_FromWorldState_SoItCannotPairWithAStaleAnchor
(replaces the anchor-wait test, whose pairing problem no longer exists). -
The local player's height is reconciled on a protocol 3 server (ADR-28).
LocalPredictionSystemreconciled only the ground plane, so a predictor running netcode's
3DCharacterMotornever had its height or vertical velocity corrected: a missed landing or a
server-side step-up stayed wrong for the session. When the predictor uses the motor, the
system now reconciles with the snapshot'szandvel_zthrough netcode's
LocalMovePredictor.Reconcile(Vec3, float, long). Protocol 2 servers take the unchanged
two-dimensional path.
Changed
-
CI and README peer pins move to
com.cuvara.netcode#v0.47.0andsgl-v0.7.0(wire
protocol 3, and 0.47.0 forack_applied_tickandPredictionClockSteering). Both tags are
created by a human; CI fails until they exist. -
Cuvara.DOTS.Netcode.Prediction(and its test assembly) now needscom.cuvara.netcode>= 0.47.0
(versionDefines), the first version with the APIs above.Cuvara.DOTS.Netcodekeeps >= 0.40.0. -
Version 0.31.0 (
package.json): new public API above. -
CI: the netcode-present, full-stack and netcode-absent rows now test against the peers the game
ships -com.cuvara.netcodev0.41.0->v0.45.0andcom.rpgmmo.shared-gamelogic
sgl-v0.5.0->sgl-v0.6.0, the pins inIndieRPGMMOAdventure'spackages-lock.json. The pins are
part of the configuration under test (see the comment above them inci.yml); stale ones validated
a combination nobody uses. Compatibility checked before the bump: netcode 0.41 -> 0.45 has no
breaking change ("nothing removed"), and sgl-v0.6.0's one breaking change (the ten-argument
positionalEntitySnapshotDataconstructor, rpg-mmo-server #388) has no caller here; netcode's only
call site passesactionSeq:/changedFields:by name. The compatibility tables inREADME.mdand
Documentation~/SUPPORT-MATRIX.mdrecord verified runs and are updated after this CI run is green. -
README "Tested configurations" and the optional-package install line now name the pins CI verifies:
com.cuvara.netcode#v0.45.0andsgl-v0.6.0(all six rows green on #33, full-stack run 36724841010
attempt 2).Documentation~/SUPPORT-MATRIX.mdis a dated snapshot of the source and is left as is. -
CI pins moved together:
com.cuvara.netcodev0.31.0 → v0.41.0 and
com.rpgmmo.shared-gamelogicsgl-v0.3.0 → sgl-v0.5.0 (five pins across three
generated manifests).EntityPoseAndEventTestsusesResolvedGameEventand friends,
which do not exist before netcode v0.41.0 — every test row failed with no
ScriptAssembliesdirectory at all, which reads as a broken harness rather than a
missing dependency.Both pins move in the same commit deliberately. This file's own header comments record
the last time they did not: a manifest onsgl-v0.1.6while the feature it needed
shipped insgl-v0.1.8, and the failure surfaced insidecom.cuvara.netcoderather
than pointing at the pin. netcode v0.41.0 requires sgl-v0.5.0.
Fixed
-
Samples~/AnimationAndEventslooked broken in a BUILD while working in the Editor. Two
defects, both found by looking at a screenshot of the built player rather than at a test
result — neither throws, so nothing in the suite could have caught either.- Magenta capsules.
GameObject.CreatePrimitiveassigns the built-in default material,
whose shader is not included in a URP player. The provider now builds its own material from
a fallback chain ending inSprites/Default, which Unity always includes. - Unstyled UI. The UXML linked its stylesheet correctly and then used class names from a
different one, so every element rendered with no styling. The sibling showcase files carry a
comment warning about the near-miss version of this (a UXML with noStyleelement at all);
this is the same failure reached from the other side, and the comment now says so.
- Magenta capsules.
v0.29.0
Added
Samples~/PhaseBShowcase— four scenes for the D01–D14 improvement plan. v0.28.0 shipped the
whole plan with unit tests and code-only samples but no scene, so none of it could be watched
working. Registered inpackage.jsonas Phase B Showcase. Every scene runs offline: no
backend, no gateway, no netcode connection — the snapshot, chunk and collision sequences are all
scripted local calls. UI is UI Toolkit throughout (UXML + USS + onePanelSettings), and each
scene is a camera, a light and one bootstrapMonoBehaviourwith aUIDocument.Scenes/PoolAndChunks.unity(D02/D03) —PooledViewAssetProviderandChunkViewProvisioner
with every accounting path on a button and every counter on screen: duplicate release, foreign
release, external destruction plusSweepDestroyed,maxActivePerKeyrejection, both
OutstandingLeasePolicyvalues, chunk warm/release, release-while-warming, two chunks sharing a
key, and repeated cycles returning to baseline.Scenes/ModulesAndConfig.unity(D04/D05) — install and uninstall twice, two Worlds with
independent registries, the scope-change refusal,SystemOrderVerifieroutput, a
ViewConfigValidatorreport over a deliberately broken library beside a valid one, and a
catalog rebuild bumpingVersionso a heldViewConfigRefis refused.Scenes/LifecycleEventsAndMinimap.unity(D06/D08/D09) — a scripted wire sequence through
DotsEntityView(full → delta → AOI exit → re-entry →BeginGenerationreset → external
destruction → teardown) against theNetworkEntityLifecyclelog, with theMinimapBootstrap
buffer drawn as a UI Toolkit minimap,ViewOverlayReconcilername plates, and camera-follow
buttons for no target, two targets, switching, teleport andResetSmoothing.Scenes/PhysicsEvents.unity(D07) —ColliderLibrarybodies,PhysicsEventsBootstrap
installed throughDotsModules, the Enter/Stay/Exit stream with canonical pair ordering and
aggregated contact counts, destruction mid-contact showing entity index reuse, install/uninstall
cycles returning collider blobs and leases to zero, and the one-integrator guard tripped and
restored.- Headless self-test.
-showcaseAutorun(with optional-showcaseAutorunDelay <seconds>,
default 1) makes each scene drive its own buttons in a scripted order, assert the outcomes its
README documents, and quit with 0 when every assertion held and 1 otherwise — a headless run
can build a scene and read its panel, but it cannot click, so the scenarios behind the buttons
were the part still unproven. Steps log
[PhaseB] <Scene> step=<step> expected=<label>:<value> actual=<value> PASS|FAILand each scene
ends with[PhaseB] <Scene>: N passed, M failed; these strings are a CI contract and are fixed.
Waits that depend on the physics pipeline are bounded, so a world that never steps fails the run
instead of hanging it. Without the flag the scenes behave exactly as they do interactively, and
neither mode needs a backend. - Fixes found by running the autorun headlessly, all sample-side; no package change was
needed. The pool scene registered its prefabs but never prewarmed them — onlyPrewarmAsync
creates a key's pool queue, andReleaseInstanceparks an instance only when a queue exists for
its key, so every release destroyed the instance and dropped its lease; a second release of the
same instance was then counted as a foreign release rather than a duplicate one. The dispose
demo read its verdict on the same frame, but the provider destroys throughObject.Destroy,
which a player defers to end of frame, so theDestroypolicy looked identical toDetach; the
verdict is now read a frame later. The two-World scene compared world B's module count against
world A's, which is not a valid baseline because A also carries the Simulation module; each
world now has its own. The lifecycle scene asked forViewsasSession-scoped, which throws
in the Default World whenever the host already installed it asRoot—ViewsisRoot-scoped
by design, being a service that outlives a scene, so the scene now takes the package default and
tears down by named module instead of by scope. The physics scene's teardown is likewise named
rather than a scope sweep, since the Default World is shared with the host. - The external-destruction step now sends a command after destroying the mirror. Detection is
command-driven by design — the drain has no callback for an entity disappearing, so
ApplySpawnandApplyStatecheckIsLiveMirrorwhen the next command for the id arrives, and
teardown checks it as well. The scene destroyed a mirror and then went quiet about that id, so
nothing ever reached a detection site andExternalDestructionwas never raised. It now sends
one more state for the id, which is also what a real server does, having no idea the client
destroyed anything. The package behaviour was correct and is unchanged. - A scene that throws in
Startnow fails instead of hanging. Setup is wrapped, the component
disables itself rather than dereferencing nulls every frame, and under-showcaseAutorunit
reports[PhaseB] <Scene> step=start ... FAILplus[PhaseB] <Scene>: 0 passed, 1 failedand
quits 1, so a broken scene fails fast in CI rather than running to the outer timeout. - Gating matches the runtime assemblies exactly:
…PhaseBShowcase.Netcodecarries the
com.cuvara.netcodeversionDefinewith aCUVARA_NETCODEconstraint and
…PhaseBShowcase.Physicsthecom.unity.physics/CUVARA_DOTS_PHYSICSpair, so the sample
imports inert in a project without either package.
v0.28.0
Minimap producer, overlay consumer contract, 2D sorting decision (D08)
The three features SUPPORT-MATRIX.md classified as data-contract-only in 0.27.1, resolved.
Contract document: Documentation~/MINIMAP-OVERLAY.md.
- Minimap is a module.
MinimapMarker { Category, IsLocal }is the per-entity opt-in;
MinimapDataSystem(internal,ViewTransformSyncGroupafter the transform sync) rebuilds
MinimapBuffer.Entriesevery frame from every marked entity with aLocalToWorld— a view is not
required, so an entity whose key is still warming is on the map at its true position with
ViewId = 0.MinimapEntrynow carriesEntity(index + version) as the stable identity and
Category(wasEntityTypeIndex);MinimapBuffergainsPlane(MinimapPlane.XZ/XY, chosen
at install to match the world'sSnapshotSpaceMapping) andVersion.
MinimapBootstrap.Install(world, plane, capacity, scope)(moduleMinimap, Session by default)
allocates the native list and creates the system;Uninstallcompletes the producer's dependency,
releases the list, destroys singleton and system;MinimapDataSystem.OnDestroyreleases the list
if the world is disposed first. Both go through one idempotentReleaseEntries. - The map shows only what the server replicated. Nothing in the package marks an entity on its
own. On the netcode pathDotsEntityViewtakes an optionalIMinimapCategoryResolver
(TypeMinimapCategoryResolverkeyed on the wire kind with a local-player override) and the drain
marks the mirror at spawn. A mirror exists exactly while the server lists the id, so an entity the
area of interest omitted has no marker and no entry, and no host code can widen that through the
interface. ViewOverlaySystemnever produced an entry, in any version since 0.26.0. Its query was
EntityViewLink + ViewOverlayAnchorwhile the collect job also readsLocalToWorld; Entities
refuses to schedule anIJobEntityover a custom query narrower than the job and throws
InvalidOperationExceptionfrom inside the group update — logged, swallowed by the group, buffer
left empty. No test exercised the system until this branch'sViewOverlayConsumerTests, which
found it in the first EditMode run. The query now namesLocalToWorld.- No stale markers, either feed.
MinimapDataSystemrequires only its buffer singleton, never a
non-empty query, so the frame the last marked entity disappears is the frame the buffer reads
zero.ViewOverlaySystemhad exactly the stale-entry bug this rule prevents: it required at
least one anchored entity, stopped updating when the last one despawned, and left its final entries
inViewOverlayBufferfor as long as the world lived. It now requires only the registry singleton,
clears on the empty frame and bumps a newViewOverlayBuffer.Version. - Overlay consumer contract, specified and implemented.
ViewOverlayDatagainsEntity.
ViewOverlayProjection.Project(camera, world, maxDistance)is the single world-to-screen rule:
the host owns the camera; behind-camera anchors are hidden, never mirrored; distance is measured
from the camera position and≤ 0disables the filter; off-screen-edge is still visible so plates
slide rather than pop; no camera reportsNoCamera.ViewOverlayReconciler<TElement>over an
IViewOverlayPresenter<TElement>(Acquire / Place / Hide / Release, any UI type) keeps one element
per entity — not perViewId, which changes when a view is recycled — acquires on first sight,
hides while behind/too far, releases the frame the entity leaves the buffer or the buffer is
released, and is gated onVersionso calling it fromOnGUIcosts one comparison.Runtime
references no UI package. - 2D sorting stays unsupported, on purpose.
ViewSortingKey/ViewConfig.Sorting*remain
carried and unapplied: no consumer of the package renders sprites, a correct implementation needs a
per-view renderer lookup and a root/child/sorting-group decision only a real 2D prefab can answer,
and the value is a one-line write at spawn once one exists. Recorded in the component's remarks,
SUPPORT-MATRIX.md,VIEW-PROVISIONING.mdandMINIMAP-OVERLAY.md. - Sample.
Samples~/HybridViews/HudOverlaysSample.cs(add to the sample GameObject) anchors and
marks the sample's entities, installs the minimap module, and draws IMGUI labels through a
ViewOverlayReconciler<Label>plus a corner minimap with per-category colours; the header counts
acquires/releases so recycling is visible, and both feeds read zero after the last release step. - Tests.
Tests/Editor/MinimapModuleTests.cs(19: install twice, uninstall twice, capacity
validation, reinstall, world disposal without uninstall,UninstallAll+ dispose leaves no native
container, two worlds, scope conflict, spawn/move/despawn consistency, last-entity-gone clears
that frame, empty world, marker removed, XY plane, health fraction incl. zero max and clamp, growth
past capacity, independence from the view module, layout).
Tests/Editor/ViewOverlayConsumerTests.cs(13: producer spawn/move/despawn/empty incl. the stale
regression, view recycle keeps entity, projection in-front/behind/too-far/edge/no-camera,
reconciler acquire-once/place/release, version gating, hide-keeps-element, keyed by entity,
released buffer releases all, no camera).Tests/Editor.Netcode/NetworkMinimapTests.cs(4:
resolver marks only named kinds with category + locality, no resolver marks nothing, server
position + AOI exit removes the entry that frame, never-replicated cannot appear).
ViewSystemGroupLayoutTestsroster gainsViewOverlaySystemandMinimapDataSystem.
Changed
DotsEntityViewenforces its single-producer rule (D10). The firstIEntityViewcall after
construction or afterBeginGenerationlatches the calling thread; a call from any other thread
throwsInvalidOperationExceptioninstead of racing the unsynchronised_live/config caches
that theConcurrentQueuenever protected.ProducerThreadIdexposes the latch.ReconciliationAnchorgainedSequenceandTick.Sequenceincrements on every
authoritative state the drain writes (0 = spawn placeholder);Tickis the caller-stated server
tick fromSetStateAtTick, 0 when unstated — never invented. Both are written only from the
command, so nothing predicted or rendered can reach the anchor.LocalPredictionSystemreconciles only on a new ack paired with a fresh anchor (Sequence
changed since the last reconcile and non-zero). An ack that advanced while the drain has not yet
refreshed the anchor waits one frame rather than pairing the new tick with the previous
snapshot's position, and the spawn placeholder is never rewound to. Its ack/anchor memory resets
whenDotsEntityView.Generationchanges, so reconciliation is not silently disabled after a
reconnect to a server whose ticks are lower.NetworkViewCommandSystemrefuses the second transform writer through the other method. An
untimedSetStatefor an entity that already holds buffered samples (it is owned by
RemoteInterpolationSystem) writes the anchor and hp but leavesLocalTransformalone, counted
inMetrics.MixedPathStates. Previously it teleported the entity and fought the job.NetworkViewCommandSystem.Teardowntakes aNetworkDespawnReason(defaultTeardown).DotsEntityViewconstructor gainedbacklogWarningThreshold(default 4096): one warning per
generation when the pending count reaches it. Diagnostics only; nothing is dropped or capped.
Added
- Session generations.
DotsEntityView.BeginGeneration()forgets every live id, bumps
Generationand enqueues aResetcommand; every command carries the generation it was
enqueued under. The drain drops commands older than the view's current generation
(Metrics.StaleCommandsDropped) and, on the reset, tears down every mirror of the previous
generation with the newNetworkDespawnReason.SessionResetbefore applying the new session.
Old-session data still queued — or enqueued later by a stale producer — cannot respawn an old
entity, not even for a frame.BeginGenerationre-opens the producer latch so a new session may
drive the view from a new thread. NetworkIngestionMetrics(DotsEntityView.Metrics):Enqueued,Drained,
PendingHighWatermark,Drains,LastDrainCount,LastDrainSeconds,MaxDrainSeconds,
LastOldestCommandAgeSeconds,MaxOldestCommandAgeSeconds,StaleCommandsDropped,
RejectedSamples,MixedPathStates,GenerationResets. The drain runs under the
Cuvara.DOTS.NetworkViewCommandSystem.Drainprofiler marker. Measured before any bounding of the
queue is considered; the queue stays unbounded and fully drained per frame.- Tests:
SnapshotIngestionTests(13) — duplicate and reordered ticks refused without a direct
write, mixed-path guard, generation reset with queued stale commands, late old-session data,
two resets before a drain, AOI re-entry with a fresh ring, drain metrics, 3000-command burst
drained in one frame, backlog warning, producer-thread latch and re-latch.
PredictionOwnershipTests(6) — reconnect re-enables reconciliation, ack ahead of the anchor
waits, placeholder never reconciled, timed states on the predicted entity never rendered by
interpolation, toggling prediction leaves no frame without a writer, anchor never fed from the
predicted position. Documentation~/NETCODE-INTEGRATION.md: thread affinity, generations, ingestion metrics,
transform ownership and reconciliation inputs.
Changed
ChunkViewProvisionerrequires a cascade sink (breaking). ThecascadeSinkconstructor
parameter is no longer optional; passingnullthrowsArgumentNullException. A streaming world
passesEntityViewCascade; a context with no view layer s...
v0.27.1
Fixed
- StressBenchmark asmdef: Unity.Physics moved to optional versionDefine
- Removed ProductionHardening sample (API mismatches in CI)
- PooledViewAssetProvider: EditMode-safe DestroyImmediate
- ArchetypeFactory: correct LocalToWorld + TimeToLive field names
v0.27.0
Added
- PooledViewAssetProvider � SetActive pooling for views (12 tests)
- EntityArchetypePreset + ArchetypeFactory � reusable component presets (8 tests)
- CameraFollowSystem � smooth damp camera follow
- NetworkEntitySpawned/Despawned lifecycle events
- MinimapEntry + MinimapBuffer for minimap rendering
v0.26.1
Added
- Runtime.Physics assembly now included (was missing from v0.26.0 release).
- ViewOverlayAnchorTests � 5 tests for overlay data structures.
v0.26.0
Added
- Runtime.Physics optional assembly (
Cuvara.DOTS.Physics) — gated by
com.unity.physics >= 1.0.0. PhysicsBodyFactory (dynamic/static/kinematic),
CollisionEventSystem, SpatialQuery (overlap sphere, raycast, closest body),
PhysicsMovementBridge (MoveData → PhysicsVelocity sync). - Robust despawn —
EntityViewRegistry.SweepDestroyed()detects views whose
GameObject was destroyed externally and cleans up stale entries. - Chunk provisioner metrics —
ChunkStateenum (Pending/Warming/Warm/Released),
ChunkStatesdictionary,WarmChunkCount/PendingChunkCount,OnChunkStateChanged
event. - View overlay anchors —
ViewOverlayAnchorcomponent,ViewOverlayBuffer
singleton,ViewOverlayCollectJob(Burst),ViewOverlaySystemfor world-space
health bars and name plates. - Editor debug window — Window > Cuvara > DOTS View Debug: live view counts,
key states, deferred spawn tracking. - ProductionHardening sample — scripted 7-step demo of all new features.
- Documentation —
Documentation~/OVERVIEW.md,VIEW-PROVISIONING.md,
NETCODE-INTEGRATION.md. - Diagnostic APIs —
TotalViews,TotalKeys,LiveCountsByKey,DeferralsByKey
onEntityViewRegistry.
Fixed
EntityViewDespawnSystemnow sweeps externally-destroyed GameObjects every frame.
v0.25.0 — StressBenchmark sample
- StressBenchmark sample (pure DOTS + hybrid, 100→100M entities, Unity.Physics)
- Simulation system improvements
- package.json bumped to 0.25.0
v0.24.0
Remote entities are now interpolated in ECS, by netcode's core rather than by a second copy of it
Until this release the DOTS path had exactly one way to place a replicated entity: whatever position
reached IEntityView.SetState was written straight to LocalTransform. That was correct and it was
never the whole story — the smoothing was happening one layer up, inside WorldViewBinder, and this
package was consuming its output. It worked, and it fixed nothing that netcode's own interpolation
could not fix, but it meant the ECS path could never render a remote entity from anything richer
than a single position, and it meant an ECS client had no way to use the buffered, tick-bracketed,
clock-driven core that netcode 0.18.0 extracted precisely so a Burst job could call it.
It can now. RemoteInterpolationSystem is a [BurstCompile] ISystem scheduling an IJobEntity
over (DynamicBuffer<SnapshotSample>, ref LocalTransform, ref LocalToWorld, ref InterpolationState),
and the body of that job is one call to
Cuvara.Netcode.Interpolation.SnapshotInterpolation.Evaluate — the identical method the GameObject
path calls, over a SnapshotSampleBuffer instead of over a pooled array. There is no interpolation
arithmetic anywhere in this package. That is the whole design constraint and not a stylistic one:
the day two implementations disagree, the symptom is a remote avatar drawn in the wrong place with
both copies passing their own tests, and the only defence is that the second copy does not exist.
What a player gets. A remote avatar that moves at frame rate along the path the server actually
described, rather than one that holds still for 66 ms and jumps. It is drawn about 100 ms behind the
newest received tick — netcode's TargetDelay, one and a half snapshot intervals at the 15 Hz world
rate — and that delay is what buys the smoothness: an early snapshot waits in the buffer instead of
displacing the segment being drawn, a late one is covered by the margin instead of stalling, and a
dropped one interpolates across two ticks' worth of distance in two ticks' worth of time instead of
sprinting and freezing. The local player pays none of it. A predicted entity carries
PredictedTransform, the job excludes it with WithNone, and the one entity whose response delay
the player is holding a key to feel keeps its zero.
It is opt-in, because the server tick is. IEntityView.SetState carries (id, x, y, hp, maxHp)
and no tick, and a sample without a tick cannot be placed on a timeline — the same wall
ReconciliationAnchor documents about the anchor's own tick. So the adapter gained its own entry
point, DotsEntityView.SetStateAtTick(id, x, y, hp, maxHp, tick, receiveTimeSeconds), for a caller
that has the tick in its hand: a snapshot handler reading WorldState.Tick. Nothing else changes.
A consumer that keeps calling SetState gets 0.23.1's behaviour byte for byte.
And the two paths are mutually exclusive per entity, enforced rather than documented.
WorldViewBinder.Tick interpolates on the netcode side and hands the result to SetState, so a
view driven by the binder is already receiving a rendered position. Buffering those and evaluating
them again would stack a second TargetDelay on top of the first: every remote entity twice as far
behind the server, moving perfectly smoothly, with no error, no exception and nothing in any log to
notice — the failure shape this workspace keeps paying for. The drain therefore decides per state:
a tick means "buffer it, the transform belongs to interpolation", no tick means "write it, the buffer
stays empty and the job passes over this entity". A refused sample — a duplicate or reordered tick,
rejected by netcode's shared InterpolationRing.Accepts — deliberately does not fall back to a
direct write, because the entity is still owned by interpolation and its superseded state is not
worth rendering.
Added
ViewInterpolationGroup, insideViewSystemGroupandUpdateBefore(ViewLifecycleGroup).
Presentation, not simulation, and that is the load-bearing choice: interpolation answers "where is
this drawn on this frame", which is asked once per drawn frame, andSimulationSystemGroupwould
tie it to fixed-step semantics it must not have — a rendered position evaluated at 60 Hz fixed
steps while the client draws at 144 Hz is the stutter this exists to remove, reintroduced one layer
up. Before the lifecycle group because both of the other view groups read the transform:
ViewLifecycleGroupplaces a newly spawned view fromLocalToWorldandViewTransformSyncGroup
copies it onto every live GameObject, so running later would show this frame's views at last
frame's position — a constant one-frame lag on every remote entity, visible as softness and blamed
on the render delay. Created empty byDotsViewBootstrapeven without netcode, like every other
group here, so a consumer's[UpdateAfter]resolves today and does not change meaning later.
BecauseLocalTransformis now written in presentation, afterTransformSystemGrouphas already
finished for the frame, the job composesLocalToWorlditself — exactly asApplySpawnand
ApplyStatealready do, for exactly the same reason.SnapshotSample, an[InternalBufferCapacity(8)]IBufferElementDatawrapping netcode's
InterpolationSample, andInterpolationState, recording what was last drawn and at which
render tick. Both are added inApplySpawnalongsideReconciliationAnchor, for the reason that
component already states: a component set that changed on the first state would change archetype at
snapshot rate, and every query over mirrors would then iterate two chunk sets. The capacity is 8
because 8 x 24 B = 192 B is what keeps the buffer inline in the chunk; a ninth sample moves the
whole thing to a per-entity heap allocation that area-of-interest churn would repeat. It is the
same 8 thatInterpolationConfig.RingCapacitydefaults to, and that constant's own documentation
names this attribute.InterpolationSettingsandInterpolationTimeline, two blittable singletons: netcode's
InterpolationConfigplus theSnapshotSpaceMapping, and the world'sInterpolationClock.
Components rather than aScriptableObjectand rather than a managed reference, because the
consumer is a Bursted job and a Bursted job cannot follow either. The mapping is seeded from the
view's own, so the space the samples were produced in is the space they are rendered in — samples
are stored in server coordinates verbatim, exactly asReconciliationAnchor.ServerPositionis, and
projected once after evaluation. Seed the tuning with
DotsNetcodeBootstrap.Install(world, view, interpolation);defaultmeans netcode's defaults,
every non-positive field filled in byNormalized().InterpolationClockSystem, advancing the render clock once per frame from
SystemAPI.Time.DeltaTime— a system of its own rather than the first lines of the evaluation,
so that the frame's advance has one owner no matter how many things come to read the timeline.
Ordered by an explicit[UpdateBefore]rather thanOrderFirst, because Entities sorts
OrderFirstmembers into a separate batch and then drops ordering relations between that batch and
ordinary members, with a warning — the trapMovementSystemGroupalready documents. The timeline
has two writers at two declared points in the frame and that is deliberate: the drain calls
NoteSnapshoton arrival in initialization, this callsAdvanceonce per frame in presentation,
and an arrival is not a frame, so they cannot be collapsed into one.- Zero per-frame allocation on the whole path. The samples live in chunk memory, the singletons
are copied by value into job fields, the query isScheduleParallelwith noToEntityArray, no
Complete()and no main-thread walk, and theISampleBufferwrapper is a struct passed to a
where TBuffer : structgeneric so the indexer calls are constrained calls Burst specialises
rather than interface dispatch that boxes. The only copy is the front-shift when a full buffer
admits a new sample — at most seven 24-byte elements, inside the chunk, on snapshot arrivals and
never on the frame path. - Eight tests in
Tests/Editor.Netcode/RemoteInterpolationTests.cs, driven through the public
groups and withSystemAPI.Timestamped explicitly, because a world updated group by group never
advances it and every assertion about motion would otherwise be an assertion about nothing: a
ticked state is buffered and drawn from the buffer rather than at the newest state; the rendered
position never steps backwards across twenty frames and never runs past what the server sent; a
predicted entity is not interpolated and keeps accumulating samples so that releasing the tag
hands interpolation a history rather than a cold buffer; an unticked state is still written
straight to the transform; an empty buffer and a single-sample buffer are both legal and neither
throws; a duplicate tick is refused without falling back to a direct write; the clock does not
advance before the first snapshot.NetcodeSystemLayoutTestsgains three more asserting the group
containment, theUpdateBeforerelations in both directions, and that both new systems are
internal and not auto-created.ViewSystemGroupLayoutTests' hand-maintained group roster gains
ViewInterpolationGroup— that list is what the "no package group is auto-created" and "every
group is public" checks iterate, so a group missing from it is a group nothing checks.
Changed
- The
versionDefinesfloor forCUVARA_NETCODEmoves0.4.0->0.19.0, in
Cuvara.DOTS.Netcode,Cuvara.DOTS.Netcode.Prediction, both of their test assemblies and the
NetworkedPredictionsample. It had to.Cuvara.Netcode.Interpolationdoes not exist before...
v0.23.1
Fixed
-
Two folder
.metafiles were truncated and had been repaired only in the client's vendored
copy.Runtime.Netcode.Prediction.metaandTests/Editor.Prediction.metacarried aguidand
then stopped — no trailing newline, nofolderAsset: yes, noDefaultImporterblock — which is
the shape of a meta written by hand, not one Unity generated. Every sibling folder meta in the
package has the full body.Unity tolerates this: it fills the missing fields in on import, which is why nothing ever failed
and why the defect survived to be noticed only when the two copies were compared byte for byte.
The repaired versions have been sitting inIndieRPGMMOAdventure's vendored copy — same GUIDs,
full body — since the package was first vendored, and never came back upstream.The GUIDs are unchanged, so no asset reference moves. What this buys is not a behaviour fix but a
comparison that means something: with these repaired, upstream and the vendored copy are
byte-identical, and the client's drift check can assert plain equality instead of carrying an
allowlist entry for a difference nobody could explain.