Using Rewired with / instead of CoherenceInput

We’re using Rewired as our current input solution. It integrates well with the Nintendo Switch SDK, we get to re use code from previous projects, and it has allowed us to work around the new Unity Input system not recognising Siri remote swipe input.

We use a custom and deterministic physics engine (just rectangles). Contrary to the advice in Determinism, Prediction and Rollback, we tick the simulation from Update - this is because we implement our own fixed step so we can control and debug it. We tested the difference and confirmed that FixedUpdate() appears to be an accumulator solution stapled to the beginning of Update(). This is the code we use to determine the number of ticks at 30 FPS:

public static void FramesToAdvance(ref float accumulatedTime, out int frames)
{
    accumulatedTime += Time.deltaTime;
    frames = Mathf.FloorToInt(accumulatedTime * FrameratePhys);
    accumulatedTime -= frames * DelayFramePhys;
    if(frames > 5)
        frames = 5;
}

With all this in mind, what are our options if we want to sync our clients?

We have already tried an approach that didn’t work: Loading a player’s Rewired inputs into a byte at the beginning of every Update, and setting that as a property on a synced prefab. A client then used that data instead of a game pad to control the network-player. We stress-tested by running one client in the editor, one in a build, and connected over Cloud. Perhaps I set this up wrong, but it was more laggy and unreliable than expected, with the net-player ending up off-position easily. We should have buffered input, but honestly, we want to send a stream not sync a list.

We experimented before with Unity Transport, sending all inputs to a byte-buffer every update. This was much tighter / faster, but scaling it up in Unity Transport’s strict Server-Client model is a Herculean labour.

We’re not sure what to try next. Maybe there’s a lower level way of doing things so we can perform what CoherenceInput is doing, or there’s a means to plug into it.

Any advice is welcome.

Hey!

Syncing inputs through a field is indeed the slowest of all options, and due to the eventual consistency nature of state sync, you’re pretty much guaranteed to lose inputs.

The next best alternative would be commands:

private CoherenceSync sync;

[Command]
public void HandleInput(int tick, byte[] inputData)
{
}

public void SendInput(int tick, byte[] inputData)
{
    sync.SendCommand(HandleInput, MessageTarget.All, tick, inputData);
}

Using CoherenceInput would guarantee best user experience as inputs have a special fast-track on the Replication Server. There are two problems though:

  • CoherenceInput supports only specific data types (int, float, vec2, vec3, string, bool, quat) so you’ll have to map to/from those
  • It always uses the internal simulation frame

We could probably resolve the second problem (sim frame), so if you can deal with the first one, this would be the way to go. I’ll make some quick tests around injecting simulation frame and will get back to you.

I made some tests - you can modify the CoherenceInput to make it work with your tick/frame. If you got the SDK from the Asset Store you can make changes to the script at will. If it was imported as an npm package then you’ll need to turn it into embedded package, otherwise any changes to the code will be automatically reverted.

The CoherenceInput script resides at <packagePath>/Coherence.Toolkit/CoherenceInput.cs.

All you have to do is change this (~ line 106)

public long CurrentSimulationFrame => UseFixedSimulationFrames
            ? bridge.NetworkTime.ClientFixedSimulationFrame
            : bridge.NetworkTime.ClientSimulationFrame;

to

public long CurrentSimulationFrame { get; set; }

From now on, you will be able to pass your own tick/frame by assigning it to the CurrentSimulationFrame. This is the value that’s used for buffering inputs and that’s what will be sent to other clients. Keep in mind that the receiving client also has to keep updating this value, otherwise the input.GetButton("myButton") will fail as under the hood it will try to look for input from CurrentSimulationFrame, which without updates would be always 0. Alternatively you could use the input.GetButton("myButton", frame) version.

Another caveat with this modified approach is, the inputs will still be sent at the rate of FixedNetworkUpdate. The rate defaults to Unity’s FixedUpdate rate but it can be changed by setting the coherenceBridge.NetworkTime.FixedTimeStep variable.

Should using CoherenceInput that way satisfy your needs, we could make an official addition which would let you override the CurrentSimulationFrame without the need to modify the SDK.

Here’s the script that I’ve used for testing:

using Coherence.Toolkit;
using UnityEngine;

public class TestInput : MonoBehaviour
{
    private CoherenceSync sync;
    private CoherenceInput input;

    public float ReceivedTime;
    public long ReceivedFrame;

    void Start()
    {
        sync = GetComponent<CoherenceSync>();
        input = sync.Input;
    }

    void Update()
    {
        input.CurrentSimulationFrame = Time.frameCount;

        if (!sync.HasStateAuthority)
        {
            ReceivedTime = input.GetAxis("time");
            ReceivedFrame = input.LastReceivedFrame;
        }
        else
        {
            input.SetAxis("time", Time.time);
        }
    }
}

once running, I could observe the frame and the input (“time”) being updated in the inspector

Hope this helps!

Thanks.

I’ll work on a CoherenceInput based solution, and I promise to report back with results so you know if this is a feature requirement.

I tried both methods.

SendCommand was fairly unreliable unless I buffered input, then it would spool out the actions too late to be usable. I would have to use SendOrderedCommand for the buffer to work at all.

CoherenceInput.CurrentSimulationFrame would similarly spool out actions late. It felt like I couldn’t match the step on either machine because I can’t get the ticks to match. I’m pausing both games until a SendCommand to both, but it seems like the local Client gets the message first when what I really want is a go signal purely from the server.

I moved the code from our own fixed step into FixedUpdate() and removed the CurrentSimulationFrame hack, this finally got some results with minimal lag. It feels like Coherence is already doing too much under the hood with FixedUpdate and buffering when you compare the difference to setting the simulation frame yourself.

Feeding Rewired’s results into CoherenceInput seems fine otherwise, though there is the issue that we often lose a frame and end up off position due to processing local inputs first. But that’s another topic. Thanks for the help.