Floating Origin in Unity

If you're creating an endless runner or a game with big/endless worlds, you will encounter an error after a certain distance from the game engine's origin. The error will mostly be noticeable in visual things, e.g. meshes have overlaps and maybe some stuff is not rendered in the place where you expect it. But it also can lead to calculation errors in your code.

The reason behind this is floating-point precision. This happens in all engines and is not just a Unity thing. Not going into much detail here, because Alan Wolfe has a great in-depth article about it, but due to the nature of floating-point numbers, the higher it gets the less precise it is. That means that all calculations using those numbers will have some errors. Most of the time (for example for idle games) this does not matter, e.g.if you have one billion coins in your idle game, you do not care if the pennies are not precise anymore. But visual things are pretty noticeable.

Floating Origin

A solution for that is a so-called Floating Origin. It is centered at the game engine's origin (likely at 0/0/0) and defines a boundary (e.g. a sphere in 3D or a box in 2D). Within the boundary, nothing will happen. But as soon as the player reaches the boundary, an origin shift will happen. That means that the player is teleported back to the engine's origin and the whole world is shifted the same negative amount of units.

For example, take a look at this little example video for a current game prototype:

0:00
/
Floating Origin example

You can see the little plane moving along the positive z-axis. As soon as it hits the boundary (50 units in this case), it will be reset to 0/0/0 and the whole world is shifted by -50 units on the z-axis. For the sake of completeness: the video shows an endless runner so it immediately removes unused tiles behind the player and will create new ones in front. Take a look at the yellow'ish polygon where you can see that it will shift back.

Coding a floating origin

There're two basic approaches to creating a little script in Unity for floating origin. The first is a typical one-size-fits-all. This script will when it recognizes an origin shift should happen gather all GameObjects, TrailRenderers, etc., and perform a shift. This approach is nice to make it going but from a usage and performance perspective, I don't like this approach (most likely you'll need to execute FindObjectOfType several times to gather all the objects you need). When a shift happens, I want to have more control over it.

The second basic approach is using events. You've got a simple script that recognizes that an origin shift should happen and then fires an event. Other scripts can pick up the event and do whatever is necessary. Let's take a look at how to do that!

FloatingOriginController

[RequireComponent(typeof(SphereCollider))]
[DefaultExecutionOrder(1000)]
public class FloatingOriginController : MonoBehaviour
{
  [SerializeField]
  private Vector3EventChannelSO OriginShiftEventChannel;

  [SerializeField]
  private Rigidbody PlayerRigidbody;

  private float _threshold;

  private void Awake()
  {
    var sphereCollider = GetComponent<SphereCollider>();
    _threshold = sphereCollider.radius;
  }

  private void FixedUpdate()
  {
    var referencePosition = PlayerRigidbody.position;

    if (referencePosition.magnitude >= _threshold)
    {
      OriginShiftEventChannel.Raise(-referencePosition);
    }
  }
}

First, we require that our script needs a SphereCollider. Then we use DefaultExecutionOrder to execute our script as late as possible. In my example, the player uses a Rigidbody, so I want all calculations for the player done before we check for hitting the boundary.

Then, the script uses a ScriptableObject-approach for an event system. The idea is from Ryan Hipple's talk about Game Architecture with Scriptable Objects. Blog post is here and the talk is here. If you don't know about this yet, just imagine it's a standard C# event.

In the Awake method, we read in the sphere's radius for our threshold. In FixedUpdate we're simply checking if the magnitude of our position vector is bigger or equal to the threshold. If that's the case, we raise the event with the inverted vector.

Now we need different scripts listening to the event and reacting to it, some examples will follow:

TransformOriginShiftController

A simple script to adjust any transform-based GameObject (basically everything but Rigidbodies):

public class OriginShiftController : MonoBehaviour
{
  [SerializeField]
  private Vector3EventChannelSO OriginShiftEventChannel;

  private void OnEnable()
  {
    OriginShiftEventChannel.Raised += OriginShift;
  }

  private void OnDisable()
  {
    OriginShiftEventChannel.Raised -= OriginShift;
  }

  private void OriginShift(Vector3 offset)
  {
    transform.position += offset;
  }
}

The workhorse is the method OriginShift. It will simply add the offset we got from our FloatingOriginController and move the transform.

RigidbodyOriginShiftController

Basically the same the TransformOriginShiftController script but better suitable for a Rigidbody:

[RequireComponent(typeof(Rigidbody))]
public class OriginShiftController : MonoBehaviour
{
  [SerializeField]
  private Vector3EventChannelSO OriginShiftEventChannel;
  
  private Rigidbody _rigidbody;
  
  private void Awake()
  {
    _rigidbody = GetComponent<Rigidbody>();
  }

  private void OnEnable()
  {
    OriginShiftEventChannel.Raised += OriginShift;
  }

  private void OnDisable()
  {
    OriginShiftEventChannel.Raised -= OriginShift;
  }

  private void OriginShift(Vector3 offset)
  {
    _rigidbody.position += offset;
  }
}

TrailRendererOriginShiftController

TrailRenderers need their own script for updating, only setting their transform will result in their trails going from the boundary back to the newly shifted origin. That looks super strange. :-)

public class TrailRendererOriginShiftController : MonoBehaviour
{
  [SerializeField]
  private Vector3EventChannelSO OriginShiftEventChannel;
    
  [SerializeField]
  private TrailRenderer[] TrailRenderers;

  private void OnEnable()
  {
    OriginShiftEventChannel.Raised += OriginShift;
  }

  private void OnDisable()
  {
    OriginShiftEventChannel.Raised -= OriginShift;
  }

  private void OriginShift(Vector3 offset)
  {
    foreach (var trailRenderer in TrailRenderers)
    {
      OriginShift(trailRenderer, offset);
    }
  }

  private void OriginShift(TrailRenderer trailRenderer, Vector3 offset)
  {
    var positions = new Vector3[trailRenderer.positionCount];
    trailRenderer.GetPositions(positions);

    for (var i = 0; i < positions.Length; i++)
    {
      positions[i] += offset;
    }
      
    trailRenderer.SetPositions(positions);
  }
}

In this script, we can have multiple TrailRenderers attached (this is because the script is from the game prototype, and the plane has two trails attached to its wing). When an origin shift happens, the script offsets the TrailRenderer's position, because those are in world space and not local space. With that, the trails will still look nice.

CinemachineOriginShiftController

If you're using Cinemachine (or your own camera following solution) you must inform Cinemachine that the target has been teleported. Otherwise, Cinemachine will make a smooth transition back to the player which, again, results in a strange visualization for the player.

[RequireComponent(typeof(CinemachineVirtualCamera))]
public class CinemachineOriginShiftController : MonoBehaviour
{
  [SerializeField]
  private Vector3EventChannelSO OriginShiftEventChannel;

  private CinemachineVirtualCamera _virtualCamera;

  private void Awake()
  {
    _virtualCamera = GetComponent<CinemachineVirtualCamera>();
  }

  private void OnEnable()
  {
    OriginShiftEventChannel.Raised += OriginShift;
  }

  private void OnDisable()
  {
    OriginShiftEventChannel.Raised -= OriginShift;
  }

  private void OriginShift(Vector3 offset)
  {
    _virtualCamera.OnTargetObjectWarped(_virtualCamera.Follow, offset);
  }
}

To teleport a virtual camera back to the player, we can use OnTargetObjectWarped. Cinemachine will then handle the teleport appropriately and no strange visualization will happen.

If you're using particle systems, you also need to move those as well, just like we did with the TrailRenderers.

Cheers!