Repository navigation
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 says so explicitly with the new
NullViewCascadeSink.Instance. Releasing without a sink stranded every live view standing on the
released keys, and that hazard is now refused at construction instead of discovered at the first
unload.Runtime.DIand the HybridViews sample already passed a sink; only tests constructed
without one.PooledViewAssetProvidertracks identity per instance, not by name. A lease table maps every
instance the provider created to its key, prefab-registration generation and acquired/pooled
state.GameObject.nameis still set for the hierarchy view but nothing reads it back, so
renaming an instance no longer loses its pool. Return policies are now explicit: a duplicate
return is ignored (DuplicateReleaseCount) and can never enqueue the same object twice; a
foreign instance — one the provider did not create — is left untouched (ForeignReleaseCount)
where it used to be destroyed; an externally destroyed instance drops its lease
(ExternallyDestroyedCount).PooledViewAssetProvider.Disposehonours root ownership. A caller-suppliedpoolRootis
emptied of the provider's instances but never destroyed; only a root the provider created
(poolRoot == null, seeOwnsPoolRoot) goes with it. Outstanding acquired instances are
reclaimed per the newOutstandingLeasePolicy(defaultDestroy;Detachtransfers ownership
to the holder) and their count is logged as a warning — no silent tracking loss. Dispose is
idempotent;RegisterPrefab/PrewarmAsync/Acquire/AcquireAsyncthrow
ObjectDisposedExceptionafterwards, whileReleaseInstance/Releaseare no-ops so a despawn
system that tears down after the pool does not throw.PooledViewAssetProvider.RegisterPrefabwith a different prefab replaces the key. Pooled
instances of the old prefab are destroyed and the key is un-warmed; acquired instances keep
running and are destroyed rather than pooled when returned. Re-registering the same prefab is a
no-op (previously every call silently overwrote).PooledViewAssetProviderhonoursCancellationToken.PrewarmAsyncandAcquireAsync
return a cancelled task before doing any work when the token is already cancelled, and prewarm
checks the token between instantiations; what was already created is tracked and pooled, so a
mid-loop cancel leaks nothing and leaves the key un-warm.ChunkViewProvisioner.ReleaseAllgained anincludeSessionparameter (defaultfalse).ChunkStatedocuments its legal transitions;Pendingis documented as reserved and never
emitted.
Added
ChunkViewProvisionerepochs: everyPrewarmChunkAsyncstamps the chunk with a fresh epoch
before awaiting, and the completion marks the chunk warm only if that epoch is still current. A
release or re-warm that lands mid-load makes the older completion a no-op — noChunkWarmed, no
state change, no resurrected chunk.ChunkViewProvisionerfailure and retry: a prewarm that faults or is cancelled while its
epoch is current rolls the chunk back through the ordinary release path (cascade included), marks
the keys it tried to warm not-warm so the next requester re-issues the load, transitions the
chunk to the newChunkState.Failedand removes it, then rethrows to the awaiting caller.
Retrying is a plainPrewarmChunkAsyncwith the same id. Counters reconcile on every path.ChunkViewProvisionermain-thread affinity: every public member throws
InvalidOperationExceptionwhen called off the constructing thread, including the post-await
continuation, so a provider completing on a worker thread without Unity's synchronization
context fails loudly instead of mutating the pool off-thread.ChunkViewProvisionersession-owned assets:PinSessionKeysAsync/ReleaseSessionKeys/
IsSessionPinnedhold references under the reserved idChunkViewProvisioner.SessionIdso no
chunk release can drop the last reference to a session-wide key;ChunkCountexcludes the pin
andReleaseAll()leaves it alone unlessincludeSession: true.NullViewCascadeSink— the explicit, greppable "no view layer exists" sink.IPooledViewLifecycle— narrow host hook (OnAcquiredafter activation,OnReleasedbefore
deactivation) for resetting animator/particle/subscription state; optional constructor argument
onPooledViewAssetProvider.PooledViewAssetProvideradmission budget:maxActivePerKeyconstructor argument (0 =
unlimited).maxPoolSizebounds only inactive instances and never bounded the total; the
budget is the knob that does, and a refusedAcquirereturnsnulland increments
AdmissionRejectedCount.PooledViewAssetProviderdiagnostics:TotalInstanceCount,IsDisposed,OwnsPoolRoot,
PoolRoot,MaxPoolSize,MaxActivePerKey,IsOwned,TryGetKey,IsAcquired,
SweepDestroyed().PooledViewAssetProvider.IsRegistered(key)/RegisteredKeys— read-only registration query
for aViewConfigValidatorprefabExistscallback; independent of warmth or instance counts.- Tests:
PooledViewAssetProviderOwnershipTests(22) andChunkProvisioningEpochTests(17) with a
ManualViewAssetProviderfake whose loads stay pending until the test settles them. Documentation~/VIEW-PROVISIONING.md: ownership/disposal/cancellation contracts, pool cap
versus admission budget, chunk state machine and failure semantics.
Network lifecycle events are now published (D06)
NetworkEntitySpawned and NetworkEntityDespawned were added in 0.27.0 as two structs and nothing
in the package ever published them. They are now produced by the netcode adapter's drain system —
the one place that holds the id → entity map and can therefore promise exactly one spawn and one
despawn per life of a replicated id. Contract: Documentation~/NETWORK-LIFECYCLE.md.
- Network presence is not visual presence. These events say a mirror entity exists / stopped
existing.ViewSpawned/ViewDespawnedremain the visual lifecycle and are unchanged. An
area-of-interest exit is not a death — the wire does not distinguish exit from removal, and the new
NetworkDespawnReason(Despawned,ExternalDestruction,Teardown) reports only what the
adapter knows. - Payload. Both structs gain
Entity(index and version) so a despawn can be matched to its
spawn after the entity is gone;NetworkEntityDespawnedgainsIsLocalandReason. The 0.27.x
constructors still compile and carryEntity.Null. NetworkEntityLifecycle(Runtime.Netcode), reached throughDotsEntityView.Lifecycleor
passed to the view's new optionallifecycleconstructor parameter. Synchronous delivery on the
drain's thread in command order; handlers in subscription order; a throwing handler is logged with
Debug.LogExceptionand isolates nothing else; late subscription is not retroactive; subscribing
or disposing during dispatch takes effect from the next event; publishing is skipped entirely
whileHasObserversis false, so an unobserved session allocates nothing. This is deliberately
narrower than a bus — two event types, one owner, no queue — and the 0.5.0 rule that the core
names no MessagePipe type still holds.- Timing.
Spawnedfires after every adapter-owned component is on the entity.
Despawned(Despawned|Teardown)fires beforeDestroyEntity, so a handler can read the last
transform for an effect.Despawned(ExternalDestruction)fires on the next command for an id whose
mirror something else destroyed; the entity no longer exists then, and the eventual wire despawn is
silent — one report per life. DotsNetcodeBootstrap.Uninstall(world, destroyMirrors = false). The default is 0.27.1's
behaviour byte for byte.destroyMirrors: truepublishes oneDespawned(Teardown)per id the
drain still holds, destroys the mirror entities and empties the map; aDespawnstill queued in the
view is never applied afterwards, so there is no duplicate.- "Destroyed" means
Exists && HasComponent<NetworkEntity>is false, not!Exists. A mirror
with a view carriesEntityViewLinkCleanup, so an externalDestroyEntityleaves a shell that
stillExistsuntil presentation strips the cleanup; the drain runs before that and previously
mistook the shell for a live mirror (found by the lead's EditMode run of the two
external-destruction tests). - The drain now recovers from an externally destroyed mirror on the next
Spawnfor that id,
closing the first life withExternalDestructionbefore opening the second. Before, such a spawn
was dropped and the id stayed invisible until it left and re-entered the area of interest. - DI wiring, optional:
RegisterDotsNetworkLifecycle()inCuvara.DOTS.DI(gated on
CUVARA_NETCODEas well as VContainer). With MessagePipe it registers publisher/subscriber
adapters for both events and a hub that forwards into them; without, the hub itself is the
IDotsSubscriber<>.Cuvara.DOTS.DInow referencesCuvara.DOTS.Netcode, ignored when that
assembly is compiled out — the same arrangement it already has withCuvara.DOTS.GameLogic. - Consumer sample.
Samples~/NetworkedPrediction/NetworkLifecycleLog.cssubscribes both events,
counts presence, prints the last six with their reason in the overlay and Console, and the sample's
OnDestroytears down withdestroyMirrors: true. - Tests.
Tests/Editor.Netcode/NetworkLifecycleEventTests.cs(15) drives the public groups with
the scripted sequences the contract lists — keyframe, delta, repeated keyframe, AOI exit/re-entry,
session reset, reconnect, external destruction, teardown with and without a queued despawn, throwing
subscriber, late subscriber, disposal, no-observer path.NetworkEntityLifecycleTests.cs(9)
covers the hub alone: dispatch order, forwarding, re-entrancy, error isolation.
Accurate feature and support matrix (D01)
Documentation~/SUPPORT-MATRIX.mdclassifies every feature as implemented / integrated in
client / sample-only / data-contract-only / planned, records per module the dependencies,
installation API, update groups, singleton requirements, ownership and teardown, lists the three
CI configurations with their floors, the platforms (Editor/Mono only; Android and WebGL
unverified), and states that no measured performance figures exist in this repository.README.mdandROADMAP.mdrewritten from the tree. The README's install URL had pointed at
com.cuvara.dots.git#v0.6.2for twenty releases and said the package was "not yet compiled
against a Unity Editor"; it now nameshttps://github.com/Cuvara/UnityDots.git#v0.27.1and the CI
rows. The ROADMAP had not been updated since 0.7.0 and still listed the netcode adapter as planned.- Corrections, recorded in
SUPPORT-MATRIX.md § 7: the 0.26.0 entry below lists a
CollisionEventSystemthat does not exist in the source —EntityCollisionand
EntityTriggerEventhave no producer;MinimapBuffer's comment named aMinimapDataSystemthat
does not exist (comment corrected, type marked data-contract-only);VIEW-PROVISIONING.mdplaced
PrimitiveViewAssetProviderinRuntime/(it is sample-only) and describedViewSortingKeyas
applied (it is carried, not applied); the 0.27.0 "lifecycle events" entry added the structs only. CameraFollowSystemandPhysicsMovementBridgeare documented as having no installer —
both are[DisableAutoCreation]and no bootstrap creates them. Installers are in progress on
feat/bootstrap-config(D04).
Changed
com.cuvara.netcodefloor moves0.19.0→0.31.0in theversionDefinesof
Cuvara.DOTS.Netcode,Cuvara.DOTS.Netcode.Prediction, both test assemblies, the
NetworkedPredictionsample, and the new entry inCuvara.DOTS.DI. 0.31.0 is the netcode release
the client is adopting; with an older netcode the adapter is absent rather than broken, as before.- CI pins move with it, as the workflow header requires:
com.cuvara.netcode
v0.19.0→v0.31.0andcom.rpgmmo.shared-gamelogicsgl-v0.2.2→sgl-v0.3.0(netcode
0.31.0's declared manual dependency). MessagePipeVContainer.RegisterMessage<T>isinternalrather thanprivate, so the lifecycle
registration reuses it.NetworkViewCommandSystem's map value is now aMirror { Entity, Type, IsLocal }rather than a
bareEntity, so a despawn event can be built after the entity is gone.
Added
- Module lifecycle (D04).
Cuvara.DOTS.Modules:DotsModulesrecords every installed module
in the world (DotsModuleRecordentity,DotsModuleScope.Root/Session, uninstaller,
install count) — no static table, so a disposed world leaves no stale reference.
UninstallScope/UninstallAlltear modules down in reverse install order; two worlds keep
separate records.RequireSingleton/RequireSystem/RequireFinite/RequireAtLeastproduce
actionable precondition errors. SystemOrderVerifierwalks the three Unity root groups recursively and reports every
[UpdateInGroup]/[UpdateAfter]/[UpdateBefore]the actual master update list violates
(wrong group, unsorted group, brokenOrderFirst/OrderLast).MembersInUpdateOrderand
Containsfor tests.CameraFollowBootstrap— install/uninstall for the camera module; validates
CameraFollowConfig(finite offsets,SmoothTime >= 0,MaxSpeed > 0) before anything is
created;IsInstalled; replacing the config swaps the referenced instance.PhysicsMovementBootstrap(Runtime.Physics) — install/uninstall for
PhysicsMovementBridge; warns, or throws withrequirePhysicsPipeline: true, when the world
has noPhysicsSystemGroup.DotsSimulationBootstrap.Uninstall/IsInstalled, and ascopeparameter on install.DotsViewBootstrap.IsInstalled/InstalledRegistry, and ascopeparameter onInstall.- Configuration validation (D05).
ViewConfigValidator(ValidateLibrary,ValidateConfig,
ValidateMappings,ValidatePreset),ViewConfigIssuewith stable codes,
ViewConfigValidationReport,ViewConfigValidationException. Catches empty/duplicate/overlong
keys (61 UTF-8 bytes), missing prefabs via a caller-supplied lookup, non-finite values, unknown
entity-type mappings and invalid preset values at runtime, before gameplay. ViewConfigCatalog.TryBuild/BuildOrThrowgate a build on the report; a refused build
leaves the previous table installed.Version,CreateRef/TryCreateRef,Uninstall,
IsInstalled,InstalledWorldCount,ViewKeys.ViewConfigRef.Version/ViewConfigTable.Version. The spawn path refuses a ref whose
version differs from the installed table's (including unstampednew ViewConfigRef— version 0)
and falls back to the request key with a warning naming the rebuild. An old index can no longer
resolve to a different view.NetworkViewCommand.ConfigVersion;DotsEntityViewstamps it and drops its name→index cache
when the catalog version changes.Documentation~/MODULE-LIFECYCLE.md,Documentation~/CONFIG-VALIDATION.md(incl. the prefab
replacement contract for providers).- Verification matrix (D13). CI grows from three Unity rows to six: physics present
(com.unity.physics1.4.7;Tests.Physics ≥ 35), full stack (netcode v0.31.0 +
sgl-v0.3.0 + VContainer 1.16.9 + MessagePipe 1.8.1 + UniTask 2.5.10;Cuvara.DOTS.DIand the
newTests.DI ≥ 5must be present) and GameFoundation present (UniT pooling/resources;
Cuvara.DOTS.GameFoundationmust compile). Every row runs
.github/scripts/inventory_assemblies.py, which lists each package assembly as present/absent
against the row's expectation (log, step summary,artifacts/assemblies.md) — an absent
assembly is a stated fact in every row.assert_test_floors.pynow names the test assemblies
absent from the results (compiled out by design vs. missing) and writes a step-summary table.
Existing rows assertTests.Physics == 0/Tests.DI == 0where those packages are absent;
the Editor floor rises 30 → 100.SUPPORT-MATRIX.md §4mirrors the rows and states each test
assembly's mode (EditMode/PlayMode), gate and coverage; §5 states what an Android IL2CPP or
WebGL row would need and that neither exists. Cuvara.DOTS.Tests.DI(new, gated onCUVARA_DOTS_VCONTAINER):RegisterDotsViews
resolution (registry, provisioner, cascade, publishers), root-scope ownership, scope disposal
uninstalls the view module, world-before-container disposal, no-world warning.DotsViewsLifetime(Runtime.DI): registered byRegisterDotsViews; disposing the
VContainer scope now callsDotsViewBootstrap.Uninstallon the world it installed into.Documentation~/RELEASE.md(D14): branch → PR → tag withpackage.jsonbumped in the
tagged commit; consumer bumps manifest andpackages-lock.jsontogether; clean-consumer
check; rollback = restore the known manifest + lock pair, neverLibrary/PackageCache;
.meta/asmdef-gate/migration-note/changelog expectations; the compatibility-evidence record
format. READMEReleasingpoints at it.- Physics integration (D07,
Runtime.Physics).PhysicsEventCollectorSystemreads
Unity.Physics' collision and trigger streams (ICollisionEventsJob/ITriggerEventsJob,
CollisionEvent.CalculateDetails) afterPhysicsSimulationGroupand resolves them through
PhysicsContactTrackerinto enter/stay/exit per entity pair: identity is the fullEntity
(index and version — a recycled index is a new pair), pairs are canonically ordered
(PhysicsPairKey, normal from A toward B), several Unity events for one pair fold into one
(ContactCount, summed impulse, mean normal/position), exits are flagged
AnyEntityDestroyedwhen an entity is gone, output order is exits → enters → stays sorted by
pair. Results land in thePhysicsEventBuffersingleton and optionalIDotsPublishers;
PhysicsEventsBootstrapinstalls/uninstalls throughDotsModules. ColliderLibrary— explicit ownership of collider blobs: one blob per (shape, size,
filter, material), counted leases,Releasefrees at zero,Disposefrees all.
PhysicsBodyFactorygains library overloads (shared, library-owned) and explicit-blob overloads
(caller-owned); the overloads that silently allocated an unowned blob per body are gone.PhysicsBodyValidation— shape dimensions (incl. capsule height ≥ 2·radius), finite
positive mass, non-empty collision filter,AssertSingleIntegrator.PhysicsBodyFactory
validates through it and addsPhysicsWorldIndex(previously missing — bodies were invisible
toBuildPhysicsWorld) andTriggerMaterial()/CollisionEventMaterial()helpers.- One-integrator rule. New core tag
PhysicsDrivenMovement:MoveBounceSystemand
MoveTowardSystemexclude it,PhysicsMovementBridgerequires it,PhysicsBodyFactoryadds
it to dynamic and kinematic bodies. An entity is moved by the direct movers or by Unity.Physics,
never both.Documentation~/PHYSICS.md(semantics, fixed-step timing, prediction interaction,
client-only scope). - Tests (
Tests/Editor.Physics):PhysicsContactTrackerTests(11),ColliderLibraryTests(6),
PhysicsBodyFactoryTests(10, incl. the one-integrator test throughSimulationSystemGroup),
PhysicsEventsBootstrapTests(8, incl. Unity.Physics stepped in a test world: trigger
enter/stay/exit, destroyed entity + reused index, aggregated collision with A→B normal,
install/step/uninstall cycles returning collider leases to zero). - Camera behaviour and lifecycle (D09).
CameraFollowMath— the follow step as a pure
function: afloat3port ofVector3.SmoothDamp(held to 1e-4 against Unity's in tests) plus
a hardMaxSpeed * dtper-frame clamp soMaxSpeedmeans what it says;dt <= 0holds
(paused frames never jump); non-finite targets hold, a non-finite camera recovers by snapping.
CameraFollowConfiggainsCamera(supplied camera; null =Camera.main),
MultipleTargets(HoldAndReport|FollowLowestIndex),TargetSwitch(Snap|Smooth)
andTeleportDistance(snap beyond it; 0 disables).CameraFollowSystemnever calls
GetSingletonon the target query; first frame and target switches snap under the default
policy;ResetSmoothing()/CameraFollowBootstrap.ResetSmoothing(world)for reconnect;
Velocity/CurrentTargetdiagnostics; a destroyed target or supplied camera idles the system
with finite values.Documentation~/CAMERA-FOLLOW.md. - Tests:
CameraFollowMathTests(Unity parity, MaxSpeed at extreme distance, zero/negative/NaN
dt, zero SmoothTime, force snap, teleport radius, no overshoot, 30/60/144 Hz, random deltas,
non-finite inputs);CameraFollowBootstrapTestsextended with a real edit-mode camera
(supplied camera + first-frame snap, both multi-target policies, both switch policies,
teleport, reset, zero dt, destroyed target, destroyed camera, order after interpolation/sync). - Tests:
DotsModulesTests,SystemOrderVerifierTests,DotsViewBootstrapLifecycleTests,
CameraFollowBootstrapTests,DotsSimulationBootstrapTests,ViewConfigValidatorTests,
ViewConfigCatalogVersionTests, extendedArchetypeFactoryTests; new
Tests/Editor.Physicsassembly withPhysicsMovementBootstrapTests(gated on
com.unity.physics).
Changed
DotsViewBootstrap.Uninstallnow hands every linked entity itsEntityViewRequestback
(and stripsEntityViewLink/ViewTransformOffset/ViewSortingKey/EntityViewLinkCleanup)
so a reinstall respawns them;Installwith a different registry does the same before
swapping, so no entity keeps a handle into a replaced registry and no view exists twice.ViewConfigCatalog.Buildre-publishes the new blob into every world the catalog is
installed in, so no singleton points at the freed previous blob;Disposeremoves the
singleton from those worlds.RegisterDotsMessagingwith MessagePipe installed but no broker registered for a package
message now resolves the no-op publisher and warns once per type, instead of failing the
container build withNo such registration of type: IPublisher<T>. Subscribers still require
the broker.- Every bootstrap that installs into
SimulationSystemGroup(DotsViewBootstrap.InstallSystems,
DotsSimulationBootstrap,PhysicsMovementBootstrap) now createsTransformSystemGroupthere
when it is absent, soGameplaySystemGroup's[UpdateBefore(TransformSystemGroup)]is applied
in hand-built worlds instead of being dropped with a warning. CameraFollowBootstrap.InstallSystemsinstalls the full view group tree
(DotsViewBootstrap.InstallSystems) rather than only the two groups the camera orders against;
a partial tree made Entities dropViewTransformSyncGroup's ordering with a warning.CameraFollowSystemreports (once) and skips when the number ofCameraFollowTargetentities
is not exactly one, instead of throwing fromGetSingletonevery frame.MaxSpeedsemantics
changed from Unity's spring-distance clamp to a hard per-frame speed limit (see D09 above); a
camera tuned against the old behaviour may need a higherMaxSpeed.ViewOverlaySystem.OnDestroycompletes its dependency before disposing the overlay list and
nulls the list on the buffer so disposal is observable.ArchetypeFactory.Create/CreateBatchthrow on a null preset or uncreated containers.- Breaking (physics, pre-release):
EntityCollision/EntityTriggerEventnow carryEntity
(EntityA/EntityB),Phase, and for collisionsContactCount; the int-only constructors are
gone (EntityIndexA/Bremain as properties,EntityTriggerEvent.Enteredas a view ofPhase).
PhysicsBodyFactory.Add*Body(em, entity, shape, size[, mass])replaced by library / explicit-blob
overloads;CreateCollidergains aMaterialparameter and validates.PhysicsMovementBridge
only drives entities taggedPhysicsDrivenMovement. - README: view-configuration snippet uses
catalog.CreateRef; system-group tree lists
ViewOverlaySystemandCameraFollowSystem; new "Modules" section.
Migration
- Replace
new ViewConfigRef { Index = i }withcatalog.CreateRef(i)(orTryCreateRef(name)).
Unstamped refs are refused and fall back to the request's own key, with a warning. - Optional: install the camera through
CameraFollowBootstrap.Installinstead of creating
CameraFollowSystemand its singleton by hand.