Skip to content

Releases: Cuvara/UnityDots

v0.31.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 14:08
c3be200

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)
    and DotsPredictionBootstrap.TryGetClockSteering(world, out steering).
    Install creates a
    netcode PredictionClockSteering for the predictor (kept across a re-install with the same
    predictor). Report every input send with NoteInputSent beside RecordInput so the
    acknowledgement floor is measured; it stamps the DOTS world's Time.ElapsedTime, the clock the
    system measures snapshots and advances the predictor with.

Fixed

  • LocalPredictionSystem reconciles 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.Tick advances) it now feeds SetServerSpeed, then
    PredictionClockSteering.SampleTickRate + OnSnapshot (seed, measure, feed a corroborated rate
    forward, steer - the code WorldViewBinder steers with), then
    Reconcile(position, ack, worldTick, WorldState.AckAppliedTick) (the Vec3 overload, with z
    and vel_z, under the 3D motor). Position, tick, ack and ack_applied_tick all come from the
    snapshot's own WorldState entry, 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
    when WorldState.Tick goes 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).
    LocalPredictionSystem reconciled only the ground plane, so a predictor running netcode's
    3D CharacterMotor never 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's z and vel_z through 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.0 and sgl-v0.7.0 (wire
    protocol 3, and 0.47.0 for ack_applied_tick and PredictionClockSteering). Both tags are
    created by a human; CI fails until they exist.

  • Cuvara.DOTS.Netcode.Prediction (and its test assembly) now needs com.cuvara.netcode >= 0.47.0
    (versionDefines), the first version with the APIs above. Cuvara.DOTS.Netcode keeps >= 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.netcode v0.41.0 -> v0.45.0 and com.rpgmmo.shared-gamelogic
    sgl-v0.5.0 -> sgl-v0.6.0, the pins in IndieRPGMMOAdventure's packages-lock.json. The pins are
    part of the configuration under test (see the comment above them in ci.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
    positional EntitySnapshotData constructor, rpg-mmo-server #388) has no caller here; netcode's only
    call site passes actionSeq:/changedFields: by name. The compatibility tables in README.md and
    Documentation~/SUPPORT-MATRIX.md record 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.0 and sgl-v0.6.0 (all six rows green on #33, full-stack run 36724841010
    attempt 2). Documentation~/SUPPORT-MATRIX.md is a dated snapshot of the source and is left as is.

  • CI pins moved together: com.cuvara.netcode v0.31.0 → v0.41.0 and
    com.rpgmmo.shared-gamelogic sgl-v0.3.0 → sgl-v0.5.0
    (five pins across three
    generated manifests). EntityPoseAndEventTests uses ResolvedGameEvent and friends,
    which do not exist before netcode v0.41.0 — every test row failed with no
    ScriptAssemblies directory 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 on sgl-v0.1.6 while the feature it needed
    shipped in sgl-v0.1.8, and the failure surfaced inside com.cuvara.netcode rather
    than pointing at the pin. netcode v0.41.0 requires sgl-v0.5.0.

Fixed

  • Samples~/AnimationAndEvents looked 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.CreatePrimitive assigns 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 in Sprites/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 no Style element at all);
      this is the same failure reached from the other side, and the comment now says so.

v0.29.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 14:55
def0926

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 in package.json as 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 + one PanelSettings), and each
    scene is a camera, a light and one bootstrap MonoBehaviour with a UIDocument.
    • Scenes/PoolAndChunks.unity (D02/D03) — PooledViewAssetProvider and ChunkViewProvisioner
      with every accounting path on a button and every counter on screen: duplicate release, foreign
      release, external destruction plus SweepDestroyed, maxActivePerKey rejection, both
      OutstandingLeasePolicy values, 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, SystemOrderVerifier output, a
      ViewConfigValidator report over a deliberately broken library beside a valid one, and a
      catalog rebuild bumping Version so a held ViewConfigRef is refused.
    • Scenes/LifecycleEventsAndMinimap.unity (D06/D08/D09) — a scripted wire sequence through
      DotsEntityView (full → delta → AOI exit → re-entry → BeginGeneration reset → external
      destruction → teardown) against the NetworkEntityLifecycle log, with the MinimapBootstrap
      buffer drawn as a UI Toolkit minimap, ViewOverlayReconciler name plates, and camera-follow
      buttons for no target, two targets, switching, teleport and ResetSmoothing.
    • Scenes/PhysicsEvents.unity (D07) — ColliderLibrary bodies, PhysicsEventsBootstrap
      installed through DotsModules, 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|FAIL and 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 — only PrewarmAsync
      creates a key's pool queue, and ReleaseInstance parks 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 through Object.Destroy,
      which a player defers to end of frame, so the Destroy policy looked identical to Detach; 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 for Views as Session-scoped, which throws
      in the Default World whenever the host already installed it as Root — Views is Root-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
      ApplySpawn and ApplyState check IsLiveMirror when 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 and ExternalDestruction was 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 Start now fails instead of hanging. Setup is wrapped, the component
      disables itself rather than dereferencing nulls every frame, and under -showcaseAutorun it
      reports [PhaseB] <Scene> step=start ... FAIL plus [PhaseB] <Scene>: 0 passed, 1 failed and
      quits 1, so a broken scene fails fast in CI rather than running to the outer timeout.
    • Gating matches the runtime assemblies exactly: …PhaseBShowcase.Netcode carries the
      com.cuvara.netcode versionDefine with a CUVARA_NETCODE constraint and
      …PhaseBShowcase.Physics the com.unity.physics / CUVARA_DOTS_PHYSICS pair, so the sample
      imports inert in a project without either package.

v0.28.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 07:11

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, ViewTransformSyncGroup after the transform sync) rebuilds
    MinimapBuffer.Entries every frame from every marked entity with a LocalToWorld — a view is not
    required, so an entity whose key is still warming is on the map at its true position with
    ViewId = 0. MinimapEntry now carries Entity (index + version) as the stable identity and
    Category (was EntityTypeIndex); MinimapBuffer gains Plane (MinimapPlane.XZ/XY, chosen
    at install to match the world's SnapshotSpaceMapping) and Version.
    MinimapBootstrap.Install(world, plane, capacity, scope) (module Minimap, Session by default)
    allocates the native list and creates the system; Uninstall completes the producer's dependency,
    releases the list, destroys singleton and system; MinimapDataSystem.OnDestroy releases the list
    if the world is disposed first. Both go through one idempotent ReleaseEntries.
  • The map shows only what the server replicated. Nothing in the package marks an entity on its
    own. On the netcode path DotsEntityView takes an optional IMinimapCategoryResolver
    (TypeMinimapCategoryResolver keyed 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.
  • ViewOverlaySystem never produced an entry, in any version since 0.26.0. Its query was
    EntityViewLink + ViewOverlayAnchor while the collect job also reads LocalToWorld; Entities
    refuses to schedule an IJobEntity over a custom query narrower than the job and throws
    InvalidOperationException from inside the group update — logged, swallowed by the group, buffer
    left empty. No test exercised the system until this branch's ViewOverlayConsumerTests, which
    found it in the first EditMode run. The query now names LocalToWorld.
  • No stale markers, either feed. MinimapDataSystem requires only its buffer singleton, never a
    non-empty query, so the frame the last marked entity disappears is the frame the buffer reads
    zero. ViewOverlaySystem had 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
    in ViewOverlayBuffer for as long as the world lived. It now requires only the registry singleton,
    clears on the empty frame and bumps a new ViewOverlayBuffer.Version.
  • Overlay consumer contract, specified and implemented. ViewOverlayData gains Entity.
    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 ≤ 0 disables the filter; off-screen-edge is still visible so plates
    slide rather than pop; no camera reports NoCamera. ViewOverlayReconciler<TElement> over an
    IViewOverlayPresenter<TElement> (Acquire / Place / Hide / Release, any UI type) keeps one element
    per entity — not per ViewId, 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 on Version so calling it from OnGUI costs 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.md and MINIMAP-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).
    ViewSystemGroupLayoutTests roster gains ViewOverlaySystem and MinimapDataSystem.

Changed

  • DotsEntityView enforces its single-producer rule (D10). The first IEntityView call after
    construction or after BeginGeneration latches the calling thread; a call from any other thread
    throws InvalidOperationException instead of racing the unsynchronised _live/config caches
    that the ConcurrentQueue never protected. ProducerThreadId exposes the latch.
  • ReconciliationAnchor gained Sequence and Tick. Sequence increments on every
    authoritative state the drain writes (0 = spawn placeholder); Tick is the caller-stated server
    tick from SetStateAtTick, 0 when unstated — never invented. Both are written only from the
    command, so nothing predicted or rendered can reach the anchor.
  • LocalPredictionSystem reconciles 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
    when DotsEntityView.Generation changes, so reconciliation is not silently disabled after a
    reconnect to a server whose ticks are lower.
  • NetworkViewCommandSystem refuses the second transform writer through the other method. An
    untimed SetState for an entity that already holds buffered samples (it is owned by
    RemoteInterpolationSystem) writes the anchor and hp but leaves LocalTransform alone, counted
    in Metrics.MixedPathStates. Previously it teleported the entity and fought the job.
  • NetworkViewCommandSystem.Teardown takes a NetworkDespawnReason (default Teardown).
  • DotsEntityView constructor gained backlogWarningThreshold (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
    Generation and enqueues a Reset command; 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 new NetworkDespawnReason.SessionReset before 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. BeginGeneration re-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.Drain profiler 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

  • ChunkViewProvisioner requires a cascade sink (breaking). The cascadeSink constructor
    parameter is no longer optional; passing null throws ArgumentNullException. A streaming world
    passes EntityViewCascade; a context with no view layer s...
Read more

v0.27.1

Choose a tag to compare

@github-actions github-actions released this 05 Sep 20:05

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

Choose a tag to compare

@github-actions github-actions released this 05 Sep 18:56

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

Choose a tag to compare

@github-actions github-actions released this 05 Sep 18:13

Added

  • Runtime.Physics assembly now included (was missing from v0.26.0 release).
  • ViewOverlayAnchorTests � 5 tests for overlay data structures.

v0.26.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 18:01

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 — ChunkState enum (Pending/Warming/Warm/Released),
    ChunkStates dictionary, WarmChunkCount/PendingChunkCount, OnChunkStateChanged
    event.
  • View overlay anchors — ViewOverlayAnchor component, ViewOverlayBuffer
    singleton, ViewOverlayCollectJob (Burst), ViewOverlaySystem for 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
    on EntityViewRegistry.

Fixed

  • EntityViewDespawnSystem now sweeps externally-destroyed GameObjects every frame.

v0.25.0 — StressBenchmark sample

Choose a tag to compare

@dyCuong03 dyCuong03 released this 04 Sep 17:25
  • StressBenchmark sample (pure DOTS + hybrid, 100→100M entities, Unity.Physics)
  • Simulation system improvements
  • package.json bumped to 0.25.0

v0.24.0

Choose a tag to compare

@github-actions github-actions released this 22 Aug 14:11

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, inside ViewSystemGroup and UpdateBefore(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, and SimulationSystemGroup would
    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:
    ViewLifecycleGroup places a newly spawned view from LocalToWorld and ViewTransformSyncGroup
    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 by DotsViewBootstrap even without netcode, like every other
    group here, so a consumer's [UpdateAfter] resolves today and does not change meaning later.
    Because LocalTransform is now written in presentation, after TransformSystemGroup has already
    finished for the frame, the job composes LocalToWorld itself — exactly as ApplySpawn and
    ApplyState already do, for exactly the same reason.
  • SnapshotSample, an [InternalBufferCapacity(8)] IBufferElementData wrapping netcode's
    InterpolationSample, and InterpolationState, recording what was last drawn and at which
    render tick. Both are added in ApplySpawn alongside ReconciliationAnchor, 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 that InterpolationConfig.RingCapacity defaults to, and that constant's own documentation
    names this attribute.
  • InterpolationSettings and InterpolationTimeline, two blittable singletons: netcode's
    InterpolationConfig plus the SnapshotSpaceMapping, and the world's InterpolationClock.
    Components rather than a ScriptableObject and 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 as ReconciliationAnchor.ServerPosition is, and
    projected once after evaluation. Seed the tuning with
    DotsNetcodeBootstrap.Install(world, view, interpolation); default means netcode's defaults,
    every non-positive field filled in by Normalized().
  • 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 than OrderFirst, because Entities sorts
    OrderFirst members into a separate batch and then drops ordering relations between that batch and
    ordinary members, with a warning — the trap MovementSystemGroup already documents. The timeline
    has two writers at two declared points in the frame and that is deliberate: the drain calls
    NoteSnapshot on arrival in initialization, this calls Advance once 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 is ScheduleParallel with no ToEntityArray, no
    Complete() and no main-thread walk, and the ISampleBuffer wrapper is a struct passed to a
    where TBuffer : struct generic 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 with SystemAPI.Time stamped 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. NetcodeSystemLayoutTests gains three more asserting the group
    containment, the UpdateBefore relations 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 versionDefines floor for CUVARA_NETCODE moves 0.4.0 -> 0.19.0, in
    Cuvara.DOTS.Netcode, Cuvara.DOTS.Netcode.Prediction, both of their test assemblies and the
    NetworkedPrediction sample. It had to. Cuvara.Netcode.Interpolation does not exist before...
Read more

v0.23.1

Choose a tag to compare

@github-actions github-actions released this 20 Aug 03:29
37f7ef4

Fixed

  • Two folder .meta files were truncated and had been repaired only in the client's vendored
    copy.
    Runtime.Netcode.Prediction.meta and Tests/Editor.Prediction.meta carried a guid and
    then stopped — no trailing newline, no folderAsset: yes, no DefaultImporter block — 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 in IndieRPGMMOAdventure'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.