Shifting Origin
NWH.Common.ShiftingOrigin.ShiftingOrigin reduces floating-point precision problems by moving
loaded scene content back toward the origin when the main camera travels too far away.
Setup
- Add one
ShiftingOrigincomponent to an active GameObject. - Tag the camera that represents the player's position as MainCamera. The component follows
Camera.main; there is no target-camera field in the inspector. - Set
distanceThresholdin meters. The default is 500. - Register optional callbacks in
onBeforeJumpandonAfterJump.
Only one active instance is supported. ShiftingOrigin.Instance is assigned during Awake and an
assert is raised if another instance is already present.
Trigger Condition
During LateUpdate, the component shifts when:
Camera.main.transform.position.magnitude > distanceThreshold
The comparison uses the camera's full three-dimensional distance from (0, 0, 0), not its
distance from the ShiftingOrigin GameObject. Camera.main is checked again every 60 frames so a
newly selected main camera can become the target.
What Moves
Every root Transform in the game is translated by the negative of the main camera's position.
Children follow their root normally, so relative positions are preserved. The roots are collected
from an object query rather than from SceneManager, so inactive objects and DontDestroyOnLoad
objects move with everything else. Leaving those behind would let the world run away from them.
The component also:
- adjusts particles in particle systems that use world simulation space;
- sets cached Rigidbody sleep thresholds to zero and interpolation to
Nonebefore the move, then restores both afterwards so no body sleeps through the shift and no interpolated pose streaks across it; - calls
Physics.SyncTransforms()after the move; and - adds the shift vector to the read-only
TotalOffsetproperty.
Particle systems in local or custom simulation space are not rewritten because their particles already follow their transform as appropriate.
Runtime-Created Objects
Rigidbodies and particle systems are re-scanned at the start of every shift, so objects spawned
since the last one are included automatically. RefreshCaches() is public and can be called
directly, but no normal setup needs it.
The scan uses FindObjectsByType over the whole scene and is not cheap; it runs at most once per
distanceThreshold of travel.
There is no public method that forces a shift. A shift is initiated only by the threshold check.
Absolute Coordinates
TotalOffset is the cumulative translation removed from the scenes. Add it to a shifted local
world position when an external system needs the corresponding unshifted coordinate:
using NWH.Common.ShiftingOrigin;
using UnityEngine;
public static class AbsolutePosition
{
public static Vector3 FromShifted(Vector3 shiftedPosition)
{
ShiftingOrigin origin = ShiftingOrigin.Instance;
return origin == null
? shiftedPosition
: shiftedPosition + origin.TotalOffset;
}
}
Events
onBeforeJumpis invoked immediately before root transforms and particles move. Its built-in listener zeroes Rigidbody sleep thresholds and disables interpolation.onAfterJumpis invoked after the move. Its built-in listener restores the sleep thresholds, callsPhysics.SyncTransforms(), then restores interpolation.OnShiftis a staticAction<Vector3>raised last, carrying the offset that was applied to every object. Subscribe from components that cache world positions across frames; being static, it can be reached without a reference to the component.
Use these to resynchronize systems that store absolute coordinates outside the moved hierarchy. Static event subscriptions are cleared at the start of each play session.
Troubleshooting
The origin never shifts
- Verify that an enabled camera is tagged MainCamera and therefore returned by
Camera.main. - Check the camera's world-position magnitude. The trigger uses
>; a camera exactly atdistanceThresholddoes not shift. - Lower
distanceThresholdto make the shift happen sooner.
An object did not move
Roots are collected from an object query, so anything with a Transform is included. Check whether a
separate world-coordinate system restores the object's position after onAfterJump, or whether the
object caches an absolute position that needs correcting from OnShift.