using System.Collections; using System.Collections.Generic; using UnityEngine; using Mirror; using System.Linq; public class SpaceshipController : NetworkBehaviour { public Transform body; public float movingSpeed = 0.1f; public float turningSmoothFactor = 0.1f; public Joystick joystick; public Vector2 joyInput; [Header("Client Prediction")] public SortedDictionary serverStateBuffer = new SortedDictionary(); public bool showDebugHUD = false; void Start() { if (isLocalPlayer) { if (joystick == null) { joystick = FindObjectOfType(); } FindObjectOfType().SetTarget(transform); GetComponent().enabled = false; } if (isServer) { GetComponent().enabled = true; } } // Update is called once per frame int timeInMillis => (int)(NetworkTime.time * 1000); void FixedUpdate() { if (isLocalPlayer) { joyInput = joystick.input; } //Simulate on both client and server if (isLocalPlayer || isServer) { body.Translate(new Vector3(0, movingSpeed), body); if (joyInput != Vector2.zero) { //Turn var angle1 = Mathf.Atan2(-joyInput.y, -joyInput.x) * Mathf.Rad2Deg; body.rotation = Quaternion.Lerp(body.rotation, Quaternion.AngleAxis(angle1 + 90, Vector3.forward), turningSmoothFactor * joyInput.magnitude); } } if(isLocalPlayer){ CmdUpdateJoyInput(joyInput); } //Fill input and position buffer for client predictions int lastUpdatedTime = 0; if (timeInMillis % 32 == 0 && lastUpdatedTime != timeInMillis) { lastUpdatedTime = timeInMillis; StatePayload currentState = new StatePayload(timeInMillis, transform.position, transform.rotation, joyInput); if (isLocalPlayer) { CmdValidateMovement(timeInMillis, currentState.Position, currentState.Rotation, currentState.Input); } if (isServer) { if (!serverStateBuffer.ContainsKey(timeInMillis)) { serverStateBuffer.Add(timeInMillis, currentState); } } } } [Command] void CmdUpdateJoyInput(Vector2 input){ joyInput = input; } [Command] void CmdValidateMovement(int time, Vector3 position, Quaternion rotation, Vector2 input) { StatePayload payload = new StatePayload(time, position, rotation, input); joyInput = payload.Input; if(!serverStateBuffer.ContainsKey(time)){Debug.Log("Server doesn't have that data yet");return;} if (serverStateBuffer[time] == payload) { //Validated, move on } else { RpcRubberBand(serverStateBuffer.Values.Last().Position,serverStateBuffer.Values.Last().Rotation, NetworkTime.time); } } void ValidateCSBuffer() { } [ClientRpc] void RpcRubberBand(Vector3 position, Quaternion rotation, double sentTime) { if (isLocalPlayer) { //Lag comprehension double delay = NetworkTime.time - sentTime; transform.position = position; transform.Translate(new Vector3(0, movingSpeed * ((float)delay / 0.02f))); transform.rotation = rotation; } } void OnGUI() { if (showDebugHUD) { GUI.Label(new Rect(Screen.width - 120, 10, 100, 20), transform.position.ToString()); GUI.Label(new Rect(Screen.width - 120, 30, 100, 20), timeInMillis.ToString()); } } public float Angle(Vector2 vector2) { return 360 - (Mathf.Atan2(vector2.x, vector2.y) * Mathf.Rad2Deg * Mathf.Sign(vector2.x)); } //Auto assign default variables [Editor only] void OnValidate() { if (body == null) { body = transform; } } } public class StatePayload { public int Time; public Vector3 Position; public Quaternion Rotation; public Vector2 Input; public StatePayload(int time, Vector3 position, Quaternion rotation, Vector2 input) { Time = time; Position = position; Rotation = rotation; Input = input; } }