Final Script

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.

using FishNet.Object;
using FishNet.Object.Prediction;
using UnityEngine;

public struct MoveData : IReplicateData
{
    public bool Jump;
    public float Horizontal;
    public float Forward;

    /* Everything below this is required for
    * the interface. You do not need to implement
    * Dispose, it is there if you want to clean up anything
    * that may allocate when this structure is discarded. */
    private uint _tick;
    public void Dispose() { }
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}


public struct ReconcileData : IReconcileData
{
    public Vector3 Position;
    public float VerticalVelocity;

    /* Everything below this is required for
    * the interface. You do not need to implement
    * Dispose, it is there if you want to clean up anything
    * that may allocate when this structure is discarded. */
    private uint _tick;
    public void Dispose() { }
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}


public class CSPMotor : NetworkBehaviour
{
    /// <summary>
    /// Audio to play when jumping.
    /// </summary>
    [SerializeField]
    private AudioSource _jumpAudio;
    /// <summary>
    /// How fast to move.
    /// </summary>
    [SerializeField]
    private float _moveSpeed = 5f;

    /// <summary>
    /// CharacterController on the object.
    /// </summary>
    private CharacterController _characterController;
    /// <summary>
    /// True if a jump was queued on client-side.
    /// </summary>
    private bool _jumpQueued;
    /// <summary>
    /// Velocity of the character, synchronized.
    /// </summary>
    private float _verticalVelocity;

    private void Awake()
    {
        _characterController = GetComponent<CharacterController>();
    }

    public override void OnStartNetwork()
    {
        base.OnStartNetwork();
        base.TimeManager.OnTick += TimeManager_OnTick;
    }

    public override void OnStopNetwork()
    {
        base.OnStopNetwork();
        if (base.TimeManager != null)
            base.TimeManager.OnTick -= TimeManager_OnTick;
    }

    private void Update()
    {
        if (!base.IsOwner)
            return;
        //Check if the owner intends to jump.
        _jumpQueued |= Input.GetKeyDown(KeyCode.Space);
    }

    /// <summary>
    /// Called every time the TimeManager ticks.
    /// This will occur at your TickDelta, generated from the configured TickRate.
    /// </summary>
    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);
        }
    }

    /// <summary>
    /// Build MoveData that both the client and server will use in Replicate.
    /// </summary>
    /// <param name="moveData"></param>
    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;
    }

    /// <summary>
    /// Runs MoveData on the client and server.
    /// </summary>
    /// <param name="asServer">True if the method is running on the server side. False if on the client side.</param>
    /// <param name="replaying">True if logic is being replayed from cached inputs. This only executes as true on the client.</param>
    [Replicate]
    private void Move(MoveData moveData, bool asServer, Channel channel = Channel.Unreliable, 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);
    }

    /// <summary>
    /// Resets the client to ReconcileData.
    /// </summary>
    [Reconcile]
    private void Reconcile(ReconcileData recData, bool asServer, Channel channel = Channel.Unreliable)
    {
        /* 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;
    }

}

Last updated