Advanced Motor Mechanics

This work flow is specific to Prediction v1. Version 2 will be significantly easier to use. Version 2 is nearing completion and a new guide will be made upon release.

For the most part you now have an understanding of using CSP within Fish-Networking. There are always going to be some areas in-between where things are a little more complicated, and for those times you're encouraged to ask for help in our server. This section may also help you resolve some of those scenarios on your own.

It seems unlikely that your entire logic is going to consist entirely of teleporting your character upward when jumping, so let's cover a few more things such as movement, gravity, and creating consistency.

To start we're going to add more values to the MoveData.

public struct MoveData
{
    public bool Jump;
    public float Horizontal;
    public float Forward;
}

We are now prepared for basic movement. Now the BuildAction method must be changed to assign the Horizontal and Forward value.

private void BuildActions(out MoveData moveData)
{
    moveData = default;
    moveData.Jump = _jumpQueued;
    moveData.Horizontal = Input.GetAxisRaw("Horizontal");
    moveData.Forward = Input.GetAxisRaw("Vertical");

    //Unset queued values.
    _jumpQueued = false;
}

There is nothing out of the ordinary with these changes. You're gathering input normally and assigning the values to your MoveData. Now these inputs must be processed within the Replicate method, so let's make changes to that now.

[Replicate]
private void Move(MoveData moveData, bool asServer, bool replaying = false)
{
    float delta = (float)base.TimeManager.TickDelta;
    Vector3 movement = new Vector3(moveData.Horizontal, 0f, moveData.Forward).normalized;

    //If jumping move the character up one unit.
    if (moveData.Jump && _characterController.isGrounded)
    {
        movement += new Vector3(0f, 1f, 0f);
        if (!asServer && !replaying)
            _jumpAudio.Play();
    }
    
    _characterController.Move(movement * delta);
}

A new Vector3 named movement is built using the Horizontal and Forward fields. Also notice that these values are being normalized. If they were not normalized the client could potentially send large values to increase their move rate.

Another very important inclusion is the delta as base.TimeManager.TickDelta, not Time.deltaTime nor Time.fixedDeltaTime. Remembering that this logic is being run OnTick, therefor the TickDelta should be used.

If you were to use a lower Tick Rate this would cause larger jumps between ticks, but the PredictedObject will automatically smooth those changes. Having this feature is great for running low tick rates on casual games while having a fluid server authoritative experience.

With movement now available, let's make the jumping a little more practical.

[SerializeField]
private float _moveSpeed = 5f;

private float _verticalVelocity;

[Replicate]
private void Move(MoveData moveData, bool asServer, bool replaying = false)
{
    float delta = (float)base.TimeManager.TickDelta;
    Vector3 movement = new Vector3(moveData.Horizontal, 0f, moveData.Forward).normalized;
    //Add moveSpeed onto movement.
    movement *= _moveSpeed;

    //If jumping move the character up one unit.
    if (moveData.Jump && _characterController.isGrounded)
    {
        //7f is our jump velocity.
        _verticalVelocity = 7f;
        if (!asServer && !replaying)
            _jumpAudio.Play();
    }

    //Subtract gravity from the vertical velocity.
    _verticalVelocity += (Physics.gravity.y * delta);
    //Perhaps prevent the value from getting too low.
    _verticalVelocity = Mathf.Max(-20f, _verticalVelocity);

    //Add vertical velocity to the movement after movement is normalized.
    //You don't want to normalize the vertical velocity.
    movement += new Vector3(0f, _verticalVelocity, 0f);

    //Move your character!
    _characterController.Move(movement * delta);
}

A few parts of the logic have changed. Even so, at this time we are essentially coding as though it were a single player or client-authoritative game.

A _moveSpeed variable has been added so that you may adjust the characters movement rate. Also, _verticalVelocity to control the characters jumping and gravity.

The movement Vector3 is now multiplied by the move speed. Rather than teleport the object on jump, verticalVelocity is set. VerticalVelocity is reduced every call to replicate, this simulates gravity. Lastly, the verticalVelocity is added onto the movement, and CharacterController.Move is called with the delta.

CharacterController will produce unreliable results when called multiple times per frame. It's not advised to call CharacterController's move methods multiple times in a your replicate method. This is a Unity bug and will occur even outside prediction.

There is one final change to make this code perform correctly. It's been said many times before, and it shall be said again, that it's very important to remember you reconcile any value that may affect movement. The _moveSpeed does not change, so we do not need to reconcile it. However, the vertical velocity does and may cause a de-synchronization be it one side could jump but the other couldn't.

To adjust for these new changes the ReconcileData must be updated to include VerticalVelocity.

public struct ReconcileData
{
    public Vector3 Position;
    public float VerticalVelocity;
}

And the value must also be set before the server sends the reconcile.

private void TimeManager_OnTick()
{
    if (base.IsOwner)
    {
        Reconcile(default, false);
        BuildActions(out MoveData md);
        Move(md, false);
    }
    if (base.IsServer)
    {
        Move(default, true);
        ReconcileData rd = new ReconcileData()
        {
            Position = transform.position,
            VerticalVelocity = _verticalVelocity
        };
        Reconcile(rd, true);
    }
}

[Reconcile]
private void Reconcile(ReconcileData recData, bool asServer)
{
    //Reset the client to the received position. It's okay to do this
    //even if there is no de-synchronization.
    transform.position = recData.Position;
    _verticalVelocity = recData.VerticalVelocity;
}

Your character now has movement, a better jump, and gravity; all of which are fully server authoritative.

Last updated