World Origin Shifting

Creating massive multiplayer worlds

Unity has a well-known limitation of offering high precision positioning only within a few kilometers from the center of the world. A common technique to get around this limitation is to move the whole world underneath the player. This is called floating origin. Here's how you can use floating origin with coherence.

Unity uses 32-bit floating-point numbers to represent the world position of game objects in memory. While this format can represent numbers up to 3.4028235×10383.4028235 × 10^{38}, its precision decreases as the number gets larger. You can use this site to see that already around the distance of 10610^6 meters the precision of a 32-bit float is 1 meter, which means that the position can only be represented in steps of one meter or more. So if your Game Object moves away from the origin by 1000 kilometers it can be only positioned with the accuracy of 1000km and one meter, or at 1000km and two meters, but not in between. As a result, usable virtual worlds can be limited to a range of as little as 5km, depending on how precisely GameObjects like bullets need to be tracked.

Floating origin in coherence

Having a single floating world origin as used in single player games is not sufficient for multiplayer games since each player can be located in different parts of the virtual world. For that reason, in coherence, all positions on the Replication Server are stored in absolute coordinates, while each Client has its own floating origin position, to which all of their game object positions are relative.

Limitations

To represent the absolute position of Game Objects on the Replication Server, we use the 64-bit floating-point format. This format allows for sub-1mm precision out to distances of 5 billion kilometers. To keep the implementation simple, floating origin position and any Game Object's absolute position is limited to the 32-bit float range, but because of the Floating Origin, it will have precision of a 64-bit float when networked with other Clients.

Quick start guide

Here is a simple example how the floating origin could be used. We will create a script that is attached to the player Prefab and is active only on the Client with authority.

using Coherence.Toolkit;
using UnityEngine;

[RequireComponent(typeof(CoherenceSync))]
public class PlayerShifter : MonoBehaviour
{
    private CoherenceSync sync;

    private void Awake()
    {
        sync = GetComponent<CoherenceSync>();
    }

    private void Update()
    {
        // We don't want to shift floating origin based on other player's position.
        // The same can be achieved via components configuration in the CoherenceSync.
        if (!sync.HasStateAuthority)
        {
            return;
        }
            
        // If player moves farther than 100 meters from the current floating origin
        if (transform.position.magnitude > 100)
        {
            // Translate the floating origin so the player is back at (0, 0, 0)
            sync.CoherenceBridge.TranslateFloatingOrigin(transform.position);
        }
    }
}

Calling the CoherenceBridge.TranslateFloatingOrigin will shift all CoherenceSync objects by the translated vector, but you have to shift other non-networked objects by yourself. We will create another script which takes care of this.

using Coherence;
using Coherence.Toolkit;
using UnityEngine;

public class WorldShifter : MonoBehaviour
{
    public CoherenceBridge bridge;
    public Transform[] nonNetworkedObjectsToShift;

    private void Start()
    {
        bridge.OnFloatingOriginShifted += OnFloatingOriginShifted;
    }

    private void OnFloatingOriginShifted(FloatingOriginShiftArgs args)
    {
        foreach (var nonNetworkedObj in nonNetworkedObjectsToShift)
        {
            nonNetworkedObj.position -= args.Delta.ToUnityVector3();
        }
    }
}

When your floating origin changes, the CoherenceBridge.OnFloatingOriginShifted event is invoked. It contains arguments such as the last floating origin, the new one, and the delta between them. We use the delta to shift back all non-networked game objects ourselves. Since the floating origin is Vector3 of doubles we need to use ToUnityVector3 method to convert it to Vector3 of floats.

Advanced settings

To control what happens to your entities when you change your floating origin, you can use CoherenceSync's floatingOriginMode and floatingOriginParentedMode fields. Both are accessible from the inspector under Advanced Settings.

Available options for both fields are:

  • MoveWithFloatingOrigin - when you change your floating origin, the Entity is moved with it, so its relative position is the same and absolute position is shifted.

  • DontMoveWithFloatingOrigin - when you change your floating origin, the Entity is left behind, so its absolute position is the same and relative position is shifted.

Floating Origin Mode dictates what happens to the Entity when it is a Root Object in the scene hierarchy, and Floating Origin Parented Mode dictates what happens to it when its parented under another non-synced Game Object.

If the Entity is parented under another CoherenceSync Object (even using CoherenceNode), its local position will never be changed, since it will always be relative to the parent.

If you are using Cinemachine for your cameras, you'll need to call OnTargetObjectWarped to notify them that the camera target has moved when you shift the floating origin.