Basic Player Movement covered using client side authority to move your players. This is ok, but opens your game up to cheating and other problems. The better way is to have your server be authoritative. This will cover that process.
Client Prediction with Server Authoritative allows the client to respond immediately to the input control, making the player more responsive feeling, but prevents cheating by the client and to some extent masks network lag because the magic under the covers in fishnet also smooths the 'compliance' with the server if you desire.
The following assumes you are using the same project/scene/files as you did for Basic Player Movement, we will be modifying / building on that and adding some stuff to it to change it into a the more advanced method. The different sections of the examples in the are organized by section order and name. After following this modification to the Basic Player Movement you should have essentially created the same thing as in the Example folder corresponding to this section.
Modify your NetworkManager
We need our own 'network' tick to keep the movement in sync.
Add a TimeManager script to your NetworkManager
Change the 'Physics Mode' in the TimeManager to'TimeManager'.
Save the scene
Modify your player prefab
The player prefab needs to tie into the FishNet client prediction system so edit the prefab and:
Add a PredictedObject to your Player Prefab
Wire in the graphical object that represents your player
Wire the network transform of your player.
Modify your PlayerInputDriver script
The client prediction relies on two annotated methods in your existing player input/movement script to do its magic. At a high level - one of them [Replicate] manages making sure the movement on both client and server happens correctly. The second one [Reconcile] manages making sure the the client conforms to position/state on the server ( server authoritative )
This means that it is input data that is sent across the network and the actual movement is called on each side. We need new data structures to transfer the [Replicate] and [Reconcile] data on the network
#region Types.
public struct MoveInputData
{
public Vector2 moveVector;
public bool jump;
public bool grounded;
}
public struct ReconcileData
{
public Vector3 Position;
public Quaternion Rotation;
public ReconcileData(Vector3 position, Quaternion rotation)
{
Position = position;
Rotation = rotation;
}
}
#endregion
Note that the move data is just input data, not the actual movement. We need to collect that input data and process it.
Add the following two methods to the script. These work with the methods above to get input from the client and drive it through [Replicate] and [Reconcile]
Note there is a method referring to TimeManager. This relates to the TimeManager component we added to our network manager and the switch of the physics method from unity to TimeManager. We need to subscribe to the 'ticks' of TimeManager and quit tying into the 'ticks' of Unity - in this case the 'Update' method we had in the Basic Player example.
Delete the Update() method in the PlayerInputDriver script - it s replaced by TimeManager_OnTick() above.
Add in a new line in Start() - we need to subscribe them to this tick.
InstanceFinder.TimeManager.OnTick += TimeManager_OnTick; // a line in Start()
unsubscribe in Destroy() because we are not monsters.
That's it. You should be able to test multiple clients and one sever or host and movement should work and feel good. The final script should look like this.
using FishNet;
using FishNet.Object;
using FishNet.Object.Prediction;
using UnityEngine;
using UnityEngine.InputSystem;
namespace FishNetQuckstart.Advanced
{
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(PlayerInput))]
public class PlayerInputDriver : NetworkBehaviour
{
#region Types.
public struct MoveInputData
{
public Vector2 moveVector;
public bool jump;
public bool grounded;
}
public struct ReconcileData
{
public Vector3 Position;
public Quaternion Rotation;
public ReconcileData(Vector3 position, Quaternion rotation)
{
Position = position;
Rotation = rotation;
}
}
#endregion
#region Fields
private CharacterController _characterController;
private Vector2 _moveInput;
private bool _jump;
[SerializeField] public float jumpSpeed = 6f;
[SerializeField] public float speed = 8f;
[SerializeField] public float gravity = -9.8f; // negative acceleration in y - remember physics?
#endregion
private void Start()
{
InstanceFinder.TimeManager.OnTick += TimeManager_OnTick; // Could also be in Awake
_characterController = GetComponent(typeof(CharacterController)) as CharacterController;
_jump = false;
}
public override void OnStartClient()
{
base.OnStartClient();
}
private void OnDestroy()
{
if (InstanceFinder.TimeManager != null)
InstanceFinder.TimeManager.OnTick -= TimeManager_OnTick;
}
#region Movement Processing
private void GetInputData(out MoveInputData moveData)
{
moveData = new MoveInputData
{
jump = _jump,
grounded = _characterController.isGrounded,
moveVector = _moveInput
};
}
private void TimeManager_OnTick()
{
if (base.IsOwner)
{
Reconciliation(default, false);
GetInputData(out MoveInputData md);
Move(md, false);
}
if (base.IsServer)
{
Move(default, true);
ReconcileData rd = new ReconcileData(transform.position, transform.rotation);
Reconciliation(rd, true);
}
}
#endregion
#region Prediction Callbacks
[Replicate]
private void Move(MoveInputData md, bool asServer, bool replaying = false)
{
Vector3 move = new Vector3();
if (md.grounded)
{
move.x = md.moveVector.x;
move.y = gravity;
move.z = md.moveVector.y;
if (md.jump)
{
move.y = jumpSpeed;
}
}
else
{
move.x = md.moveVector.x;
move.z = md.moveVector.y;
}
move.y += gravity * (float)base.TimeManager.TickDelta; // gravity is negative...
_characterController.Move(move * speed * (float)base.TimeManager.TickDelta);
}
[Reconcile]
private void Reconciliation(ReconcileData rd, bool asServer)
{
transform.position = rd.Position;
transform.rotation = rd.Rotation;
}
#endregion
#region UnityEventCallbacks
public void OnMovement(InputAction.CallbackContext context)
{
if (!base.IsOwner)
return;
_moveInput = context.ReadValue<Vector2>();
}
public void OnJump(InputAction.CallbackContext context)
{
if (!base.IsOwner)
return;
if (context.started || context.performed)
{
_jump = true;
}
else if (context.canceled )
{
_jump = false;
}
}
#endregion
}
}