using UnityEngine; using Mirror; using System.Collections.Generic; using System.Collections; using CustomExtensions; public class Player : NetworkBehaviour { private float timer; private int currentTick; private float minTimeBetweenTicks; private const float SERVER_TICK_RATE = 30f; private const int BUFFER_SIZE = 1024; // Client specific private StatePayload[] client_stateBuffer; private InputPayload[] inputBuffer; private StatePayload latestServerState; private StatePayload lastProcessedState; private float horizontalInput; private float verticalInput; private StatePayload[] stateBuffer; private Queue inputQueue; void Start() { minTimeBetweenTicks = 1f / SERVER_TICK_RATE; stateBuffer = new StatePayload[BUFFER_SIZE]; inputBuffer = new InputPayload[BUFFER_SIZE]; inputQueue = new Queue(); } void Update() { horizontalInput = Input.GetAxis("Horizontal"); verticalInput = Input.GetAxis("Vertical"); timer += Time.deltaTime; while (timer >= minTimeBetweenTicks) { timer -= minTimeBetweenTicks; if(isServer){ HandleTick(); }else if(isLocalPlayer){ HandleTickClient(); } currentTick++; } } public void OnClientInput(InputPayload inputPayload){ if(isServer){ m_OnClientInput(inputPayload); }else{ CmdOnClientInput(inputPayload); } } [Command] public void CmdOnClientInput(InputPayload inputPayload){ m_OnClientInput(inputPayload); } public void m_OnClientInput(InputPayload inputPayload) { inputQueue.Enqueue(inputPayload); } IEnumerator SendToClient(StatePayload statePayload) { yield return new WaitForSeconds(0.02f); OnServerMovementState(statePayload); } void HandleTick() { // Process the input queue int bufferIndex = -1; while(inputQueue.Count > 0) { InputPayload inputPayload = inputQueue.Dequeue(); bufferIndex = inputPayload.tick % BUFFER_SIZE; StatePayload statePayload = ProcessMovement(inputPayload); stateBuffer[bufferIndex] = statePayload; } if (bufferIndex != -1) { StartCoroutine(SendToClient(stateBuffer[bufferIndex])); } } void HandleTickClient() { if (!latestServerState.Equals(default(StatePayload)) && (lastProcessedState.Equals(default(StatePayload)) || !latestServerState.Equals(lastProcessedState))) { HandleServerReconciliation(); } int bufferIndex = currentTick % BUFFER_SIZE; // Add payload to inputBuffer InputPayload inputPayload = new InputPayload(); inputPayload.tick = currentTick; inputPayload.inputVector = new Vector3(horizontalInput, 0, verticalInput); inputBuffer[bufferIndex] = inputPayload; // Add payload to stateBuffer stateBuffer[bufferIndex] = ProcessMovement(inputPayload); // Send input to server StartCoroutine(SendToServer(inputPayload)); } void HandleServerReconciliation() { lastProcessedState = latestServerState; int serverStateBufferIndex = latestServerState.tick % BUFFER_SIZE; float positionError = Vector3.Distance(latestServerState.position, stateBuffer[serverStateBufferIndex].position); float rotationError = CustomExtensions.QuaternionExtensions.Difference(latestServerState.rotation, stateBuffer[serverStateBufferIndex].rotation); if (positionError > 0.001f || rotationError > 0.001f) { Debug.Log($"We have to reconcile bro, Errors\npos:{positionError}, rot:{rotationError}"); // Rewind & Replay transform.position = latestServerState.position; transform.rotation = latestServerState.rotation; // Update buffer at index of latest server state stateBuffer[serverStateBufferIndex] = latestServerState; // Now re-simulate the rest of the ticks up to the current tick on the client int tickToProcess = latestServerState.tick + 1; while (tickToProcess < currentTick) { int bufferIndex = tickToProcess % BUFFER_SIZE; // Process new movement with reconciled state StatePayload statePayload = ProcessMovement(inputBuffer[bufferIndex]); // Update buffer with recalculated state stateBuffer[bufferIndex] = statePayload; tickToProcess++; } } } public void m_OnServerMovementState(StatePayload serverState) { latestServerState = serverState; } public void OnServerMovementState(StatePayload serverState){ if(isServer){ RpcOnServerMovementState(serverState); }else{ m_OnServerMovementState(serverState); // CmdOnServerMovementState(serverState); } } [ClientRpc] public void RpcOnServerMovementState(StatePayload serverState){ m_OnServerMovementState(serverState); Debug.Log(serverState.position + ":" + transform.position); } IEnumerator SendToServer(InputPayload inputPayload) { yield return new WaitForSeconds(0.02f); OnClientInput(inputPayload); } StatePayload ProcessMovement(InputPayload input) { // Should always be in sync with same function on Client transform.position += input.inputVector * 5f * minTimeBetweenTicks; transform.Rotate(input.inputVector); return new StatePayload() { tick = input.tick, position = transform.position, rotation = transform.rotation }; } } public struct InputPayload { public int tick; public Vector3 inputVector; public override string ToString() { return JsonUtility.ToJson(this); } public static InputPayload Parse(string json){ return JsonUtility.FromJson(json); } } public struct StatePayload { public int tick; public Vector3 position; public Quaternion rotation; public override string ToString() { return JsonUtility.ToJson(this); } public static StatePayload Parse(string json){ return JsonUtility.FromJson(json); } }