Server-authoritative setup

What is CoherenceInput?

CoherenceInput is a component that enables a Simulator to take control of the simulation of another Client's objects based on the Client's inputs.

When to use CoherenceInput?

  • In situations where you want a centralized simulation of all inputs. Many game genres use client inputs and centralized simulation to guarantee the fairness of actions or the stability of physics simulations.

  • In situations where Clients have low processing power. If the Clients don't have sufficient processing power to simulate the World it makes sense to send inputs and just display the replicated results on the Clients.

  • In situations where determinism is important. RTS and fighting games will use CoherenceInput and rollback to process input events in a shared (not centralized) and deterministic way so that all Clients simulate the same conditions and produce the same results.

coherence currently only supports using CoherenceInput in a centralized way where a single Simulator is setup to process all inputs and replicate the results to all Clients.

Setup with CoherenceSync and CoherenceInput

Setting up an object for server-side simulation using CoherenceInput and CoherenceSync is done in three steps:

1. Preparing the CoherenceSync component on the object Prefab

The simulation type of the CoherenceSync component is set to Server Side With Client Input

Setting the simulation type to this mode instructs the Client to automatically transfer State Authority for this object to the Simulator that is in charge of simulating inputs on all objects.

2. Declaring Inputs for the simulated object

Each simulated CoherenceSync component is able to define its own, unique set of inputs for simulating that object. An input can be one of:

  • Button. A button input is tracked with just a binary on/off state.

  • Button Range. A button range input is tracked with a float value from 0 to 1.

  • Axis. An axis input is tracked as two floats from -1 to 1 in both the X and Y axis.

  • String. A string value representing custom input state. (max length of 63 characters)

To declare the inputs used by the CoherenceSync component, the CoherenceInput component is added to the object. The input is named and the fields are defined.

In this example, the input block is named Player Movement and the inputs are WASD and mouse for the XY mouse position.

3. Bake the CoherenceSync object

In order for the inputs to be simulated on CoherenceSync objects, they must be optimized through baking.

If the CoherenceInput fields or name is changed, then the CoherenceSync object must be re-baked to reflect the new fields/values.

Using CoherenceInput

When a Simulator is running it will find objects that are set up using CoherenceInput components and will automatically assume authority and perform simulations. Both the Client and Simulator need to access the inputs of the CoherenceInput of the replicated object. The Client uses the Set* methods and the Simulator uses the Get* methods to access the state of the inputs of the object. In all of these methods, the name parameter is the same as the Name field in the CoherenceInput component.

Check the CoherenceInput API for a complete list of the available methods.

For example, the mouse click position can be passed from the Client to the Simulator via the "mouse" field in the setup example.

public void SendMousePosition()
{
    var coherenceInput = GetComponent<CoherenceInput>();
    var mousePos = Input.mousePosition;
    coherenceInput.SetAxisState("mouse", new Vector2(mousePos.x, mousePos.y));
}

The Simulator can access the state of the input to perform simulations on the object which are then reflected back to the Client just as any replicated object is.

public void ProcessMousePosition()
{
    var coherenceInput = GetComponent<CoherenceInput>();
    var mousePos = coherenceInput.GetAxisState("mouse");
    
    //Move object
}

Input Authority

Each object only accepts inputs from one specific Client, called the object's Input Authority.

When a Client spawns an object it automatically becomes the Input Authority for that object. The object's creator will retain control over the object even after state authority has been transferred to the Simulator.

If an object is spawned directly by the Simulator, you will need to assign the Input Authority manually. Use the TransferAuthority method on the CoherenceSync component to assign or re-assign a Client that will take control of the object:

public void AssignNewInputAuthority(CoherenceClientConnection newInputOwner)
{
    var coherenceSync = GetComponent<CoherenceSync>();
    coherenceSync.TransferAuthority(newInputOwner.ClientId, AuthorityType.Input);
}

The ClientId used to specify Input Authority can currently only be accessed from the ClientConnection class. For detailed information about setting up the ClientConnection Prefab, see the Client connections page.

Use the OnInputAuthority and OnInputRemote events on the CoherenceSync component to be notified whenever an object changes input authority.

Only the object's current State Authority is allowed to transfer Input Authority.

In order to get notified when the Simulator (or host) takes state authority of the input you can use the OnInputSimulatorConnected event from the CoherenceSync component.

The OnInputSimulatorConnected event can also be raised on the Simulator or host if they have both input and state authority over an entity. This allows the session host to use inputs just like any other client but might be undesirable if input entities are created on the host and then have their input authority transferred to the clients.

To solve this you can check the CoherenceSync.IsSimulatorOrHost flag in the callback:

coherenceSync.OnInputSimulatorConnected.AddListener(() =>
{
    if (coherenceSync.Bridge.IsSimulatorOrHost)
    {
        // Ignore for Simulators and hosts.
        return;
    }

    // Insert your game logic here
    Debug.Log("Input ready for use!");
});

Server-authoritative network visibility

The CoherenceLiveQuery component can be used to limit the visible portion of the Game World that a player is allowed to see. The Replication Server filters out networked objects that are outside the range of the LiveQuery so that players can't cheat by inspecting the incoming network traffic.

When a query component is placed on a Game Object that is set to Server Side With Client Inputs the query visibility will be applied to the Game Object's Input Authority (i.e., the player) while the component remains in control of the State Authority (i.e. the Simulator). This prevents players from viewing other parts of the map by simply manipulating the radius or position of the query component.

See Area of interest for more information on how to use queries.

Client-side prediction

Using Server-side simulation takes a significantly longer period of time from the Client providing input until the game state is updated, compared to just using Client-side simulation. That's because of the time required for the input to be sent to the Simulator, processed, and then the updates to the object returned across the network. This round-trip time results in an input lag that can make controls feel awkward and slow to respond.

If you want to use a Server-authoritative setup without sacrificing input responsiveness, you need to use Client-side prediction. With Client-side prediction enabled, incoming network data is ignored for one or more bindings, allowing the Client to predict those values locally. Usually, position and rotation are predicted for the local player, but you can toggle Client-side prediction for any binding in the Configuration window.

By processing inputs both on the Client and on the Server, the Client can make a prediction of where the player is heading without having to wait for the authoritative Server response. This provides immediate input feedback and a more responsive playing experience.

Note that inputs should not be processed for Clients that neither have State Authority nor Input Authority. That's because we can only predict the local player; remote players and other networked objects are synced just as normal.

public void Update()
{
    if (coherenceSync.HasStateAuthority || coherenceSync.HasInputAuthority)
    {
        ProcessMousePosition();
    }

    if (coherenceSync.HasInputAuthority)
    {
        SendMousePosition();
    }
}

Misprediction and Server Reconciliation

With Client-side prediction enabled, the predicted Client state will sometimes diverge from the Server state. This is called misprediction. When misprediction occurs, you will need to adjust the Client state to match the Server state in one way or another. This is called Server Reconciliation.

There are many possible approaches to Server Reconciliation and coherence doesn't favor one over another. The simplest method is to snap the Client state to the Server state once a misprediction is detected. Another method is to continuously blend from Client state to Server state.

Misprediction detection and reconciliation can be implemented in a binding's OnNetworkSampleReceived event callback. This event is called every time new network data arrives, so we can test the incoming data to see if it matches with our local Client state.

private void Awake()
{
    var positionBinding = GetComponent<CoherenceSync>().Bindings.FirstOrDefault(c => c.Name == "position");
    positionBinding.OnNetworkSampleReceived += DetectMisprediction;
}

private void DetectMisprediction(object sampleData, long simulationFrame)
{
    const float MispredictionThreshold = 3;

    var networkPosition = (Vector3)sampleData;
    var distance = (networkPosition - transform.position).magnitude;

    if (distance > MispredictionThreshold)
    {
        transform.position = networkPosition;
    }
}

The misprediction threshold is a measure of how far the prediction is allowed to drift from the Server state. Its value will depend on how fast your player is moving and how much divergence is acceptable in your particular game.

Remember that incoming sample data is delayed by the round-trip time to the Server, so it will trail the currently predicted state by at least a few frames, depending on network latency. The simulationFrame parameter tells you the exact frame at which the sample was produced on the authoritative Server.

For better accuracy, incoming network samples should be compared to the predicted state at the corresponding simulation frame. This requires keeping a history buffer of predicted states in memory.

Client as a host

This feature is in the experimental phase.

A client-hosted session is an alternative way to use CoherenceInput in Server Side With Client Input mode that doesn't require a Simulator.

A Client that created a Room can join as a Host of this Room. Just like a Simulator, the Host will take over the State Authority of the CoherenceInput objects while leaving the Input Authority in the hands of the Client that created those objects.

The difference between a Host and a Simulator is that the Host is still a standard client connection, which means it counts towards the Room's client limit and will show up as a client connection in the connection list.

Usage

To connect as a Host all we have to do is call CoherenceBridge.ConnectAsHost:

public CoherenceBridge bridge;

public async Task CreateRoomAndJoinAsHost(string region)
{
    CloudService cloudService = bridge.CloudService;
    
    // Wait for connection to be established with the coherence Cloud, authenticated as a Guest User.
    await cloudService.WaitForCloudServiceReadyAsync(1000);

    var roomServiceForRegion = cloudService.Rooms.GetRoomServiceForRegion(region);
    
    RoomData roomData = await roomServiceForRegion.CreateRoomAsync(RoomCreationOptions.Default);
    (EndpointData roomEndpoint, bool isEndpointValid, string validationErrorMessage) = RoomData.GetRoomEndpointData(roomData);

    if (!isEndpointValid)
    {
        throw new Exception($"Invalid room endpoint: {validationErrorMessage}");
    }

    bridge.onConnected.AddListener(OnConnected);
    bridge.ConnectAsHost(roomEndpoint);
}

public void OnConnected(CoherenceBridge bridge)
{
    Debug.Log($"Connected! IsHost: {bridge.IsSimulatorOrHost}");
}