using System; using System.Collections.Generic; using UnityEngine; namespace Mirror.Examples.BilliardsPredicted { // example input for this exact demo. // not general purpose yet. public struct PlayerInput { public double timestamp; public Vector3 force; public PlayerInput(double timestamp, Vector3 force) { this.timestamp = timestamp; this.force = force; } } public class PlayerPredicted : NetworkBehaviour { // white ball component WhiteBallPredicted whiteBall; // keep a history of inputs with timestamp public int inputHistorySize = 64; readonly SortedList inputs = new SortedList(); void Awake() { // find the white ball once #if UNITY_2022_2_OR_NEWER whiteBall = FindAnyObjectByType(); #else // Deprecated in Unity 2023.1 whiteBall = FindObjectOfType(); #endif } // apply force to white ball. // common function to ensure we apply it the same way on server & client! void ApplyForceToWhite(Vector3 force) { // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Rigidbody.AddForce.html // this is buffered until the next FixedUpdate. // AddForce has different force modes, see this excellent diagram: // https://www.reddit.com/r/Unity3D/comments/psukm1/know_the_difference_between_forcemodes_a_little/ // for prediction it's extremely important(!) to apply the correct mode: // 'Force' makes server & client drift significantly here // 'Impulse' is correct usage with significantly less drift whiteBall.GetComponent().AddForce(force, ForceMode.Impulse); } // called when the local player dragged the white ball. // we reuse the white ball's OnMouseDrag and forward the event to here. public void OnDraggedBall(Vector3 force) { // record the input for reconciliation if needed if (inputs.Count >= inputHistorySize) inputs.RemoveAt(0); inputs.Add(NetworkTime.time, new PlayerInput(NetworkTime.time, force)); Debug.Log($"Inputs.Count={inputs.Count}"); // apply force locally immediately ApplyForceToWhite(force); // apply on server as well. // not necessary in host mode, otherwise we would apply it twice. if (!isServer) CmdApplyForce(force, NetworkTime.predictedTime); } // while prediction is applied on clients immediately, // we still want to validate every input on the server and reject it if necessary. // this way we can latency free yet cheat safe movement. // this should include a certain tolerance so players aren't hard corrected // for their local movement all the time. // TODO this should be on some kind of base class for reuse, but perhaps independent of parameters? bool IsValidMove(Vector3 force) => true; // TODO send over unreliable with ack, notify, etc. later [Command] void CmdApplyForce(Vector3 force, double predictedTime) { if (!IsValidMove(force)) { Debug.Log($"Server rejected move: {force}"); return; } // client is on a predicted timeline. // double check the prediction - it should arrive at server time. // // there are multiple reasons why this may be off: // - time prediction may still be adjusting itself // - time prediction may have an issue // - server or client may be lagging or under heavy load temporarily // - unreliable vs. reliable channel latencies are signifcantly different // for example, if latency simulation is only applied to one channel! double delta = NetworkTime.time - predictedTime; if (delta < -0.010) { Debug.LogWarning($"Cmd predictedTime was {(delta*1000):F0}ms behind the server time. This could occasionally happen if the time prediction is off. If it happens consistently, check that unreliable NetworkTime and reliable [Command]s have the same latency. If they are off, this will cause heavy jitter."); } else if (delta > 0.010) { // TODO consider buffering inputs which are ahead, apply next frame Debug.LogWarning($"Cmd predictedTime was {(delta*1000):F0}ms ahead of the server time. This could occasionally happen if the time prediction is off. If it happens consistently, check that unreliable NetworkTime and reliable [Command]s have the same latency. If they are off, this will cause heavy jitter. If reliable & unreliable latency are similar and this still happens a lot, consider buffering inputs for the next frame."); } else { Debug.Log($"Cmd predictedTime was {(delta*1000):F0}ms close to the server time."); } // apply force ApplyForceToWhite(force); } } }