123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- 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<double, PlayerInput> inputs = new SortedList<double, PlayerInput>();
- void Awake()
- {
- // find the white ball once
- #if UNITY_2021_3_OR_NEWER
- whiteBall = FindAnyObjectByType<WhiteBallPredicted>();
- #else
- // Deprecated in Unity 2023.1
- whiteBall = FindObjectOfType<WhiteBallPredicted>();
- #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<Rigidbody>().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);
- }
- }
- }
|