├── CubeMove.cs ├── Interpolator.cs ├── LICENSE ├── PayLoad.cs ├── PayLoadBuffer.cs ├── PredictionManager.cs ├── PredictionObject.cs ├── PredictionTimer.cs └── README.md /CubeMove.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using Unity.Netcode; 3 | using System.Collections.Generic; 4 | 5 | // This example CubeMove file is slightly different from the readme file. 6 | // It gives a few more examples on how it can be modified to fit your needs 7 | 8 | [GenerateSerializationForTypeAttribute(typeof(BoxInputPayload))] 9 | [GenerateSerializationForTypeAttribute(typeof(BoxStatePayload))] 10 | public class CubeMove : PredictionObject 11 | { 12 | [SerializeField] Rigidbody _rigidbody; 13 | private int counter; 14 | 15 | public override BoxInputPayload GetInput() 16 | { 17 | float horizontalInput = Input.GetAxis("Horizontal"); 18 | float verticalInput = Input.GetAxis("Vertical"); 19 | 20 | BoxInputPayload inputPayload = new BoxInputPayload(); 21 | inputPayload.SetAxis(horizontalInput, verticalInput); 22 | 23 | inputPayload.spacePressed = Input.GetKey(KeyCode.Space); 24 | 25 | return inputPayload; 26 | } 27 | 28 | public override void SetInput(BoxInputPayload inputPayload) { 29 | if (IsOwner || IsServer) 30 | _rigidbody.AddForce(new Vector3(inputPayload.GetHorizontal(), 0f, inputPayload.GetVertical()) * 100f); 31 | 32 | if (inputPayload.spacePressed && !PredictionManager.Singleton.IsReconciling) { 33 | counter++; 34 | Debug.Log("Counter: " + counter); 35 | } 36 | } 37 | 38 | public override BoxStatePayload GetState() { 39 | Quaternion holderRotation = _rigidbody.rotation; 40 | 41 | return new BoxStatePayload() 42 | { 43 | position = _rigidbody.position, 44 | rotation = QuaternionCompressor.CompressQuaternion(ref holderRotation), 45 | velocity = _rigidbody.velocity, 46 | angularVelocity = _rigidbody.angularVelocity 47 | }; 48 | } 49 | 50 | public override void SetState(BoxStatePayload statePayload) { 51 | _rigidbody.position = statePayload.position; 52 | _rigidbody.rotation = statePayload.GetRot(); 53 | _rigidbody.velocity = statePayload.velocity; 54 | _rigidbody.angularVelocity = statePayload.angularVelocity; 55 | } 56 | 57 | public override bool ShouldReconcile(BoxStatePayload latestServerState, BoxStatePayload ClientState) { 58 | float positionError = Vector3.Distance(latestServerState.position, ClientState.position); 59 | float rotDif = 1f - Quaternion.Dot(latestServerState.GetRot(), ClientState.GetRot()); 60 | 61 | if (positionError > 0.01f || rotDif > 0.001f) { 62 | return true; 63 | } 64 | return false; 65 | } 66 | 67 | public override void OnPostSimulation(bool DidRunPhysics) 68 | { 69 | if (!PredictionManager.Singleton.IsReconciling && interpolator != null) { 70 | interpolator.AddPosition(GetStateAtTick(PredictionTimer.Singleton.tick).position, NeededToReconcile); 71 | interpolator.AddRotation(GetStateAtTick(PredictionTimer.Singleton.tick).GetRot()); 72 | } 73 | } 74 | 75 | public override void SendClientInputsToServer(List inputPayloads, Message sender) 76 | { 77 | PayLoadBuffer.Compress(inputPayloads); 78 | sender.inputs.AddRange(inputPayloads); 79 | } 80 | 81 | public override void ReceiveClientInputs(Message receiver) 82 | { 83 | List objectInputs = PayLoadBuffer.FindObjectItems(receiver.inputs, ObjectID); 84 | PayLoadBuffer.Decompress(objectInputs); 85 | AddClientInputs(objectInputs, receiver.tick); 86 | } 87 | 88 | public override void CompileServerState(BoxStatePayload statePayload, List inputPayloads, Message Compiler) { 89 | Compiler.states.Add(statePayload); 90 | Compiler.inputs.AddRange(inputPayloads); 91 | } 92 | 93 | public override void ReceiveServerState(Message receiver) { 94 | ApplyServerState(PayLoadBuffer.FindObjectItem(receiver.states, ObjectID), receiver.tick); 95 | ApplyServerInputs(PayLoadBuffer.FindObjectItems(receiver.inputs, ObjectID), receiver.tick); 96 | } 97 | 98 | public override void SortStateToSendToClient(Message defaultWorldState, Message sender) { 99 | for (int i = 0; i < defaultWorldState.states.Count; i++) 100 | { 101 | if (PayLoadBuffer.ContainsID(sender.states, defaultWorldState.states[i].ObjectID)) continue; 102 | 103 | if (defaultWorldState.states[i].ObjectID == ObjectID ) { 104 | sender.states.Add(defaultWorldState.states[i]); 105 | } 106 | else if (Vector3.Distance(defaultWorldState.states[i].position, GetState().position) < 1000f) { 107 | sender.states.Add(defaultWorldState.states[i]); 108 | sender.inputs.AddRange(PayLoadBuffer.FindObjectItems(defaultWorldState.inputs, defaultWorldState.states[i].ObjectID)); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Interpolator.cs: -------------------------------------------------------------------------------- 1 | using Unity.VisualScripting; 2 | using UnityEngine; 3 | 4 | public class Interpolator : MonoBehaviour 5 | { 6 | public float interpolationValue = 0.1f; 7 | private float timeToTarget = 0.1f; 8 | private float time; 9 | private Vector3 lastPos; 10 | private Quaternion lastRot; 11 | private Vector3 curPos; 12 | private Quaternion curRot; 13 | Vector3 nextPos; 14 | Quaternion nextRot; 15 | private bool DidReconcile; 16 | [HideInInspector] public bool IsOwner = false; 17 | [HideInInspector] public bool IsServer = false; 18 | 19 | void Start() 20 | { 21 | nextPos = transform.parent.position; 22 | nextRot = transform.parent.rotation; 23 | lastPos = Vector3.zero; 24 | lastRot = Quaternion.identity; 25 | timeToTarget = PredictionTimer.Singleton.minTimeBetweenTicks; 26 | time = PredictionTimer.Singleton.minTimeBetweenTicks; 27 | } 28 | 29 | void Update() { 30 | time += Time.deltaTime; 31 | if (time > timeToTarget) time = timeToTarget; 32 | 33 | if ((!IsOwner && !IsServer) || DidReconcile) { 34 | curPos = Vector3.Lerp(lastPos, nextPos, time/interpolationValue); 35 | curRot = Quaternion.Slerp(lastRot, nextRot, time/interpolationValue); 36 | } 37 | else { 38 | curPos = Vector3.Lerp(lastPos, nextPos, time/timeToTarget); 39 | curRot = Quaternion.Slerp(lastRot, nextRot, time/timeToTarget); 40 | } 41 | 42 | transform.position = curPos; 43 | transform.rotation = curRot; 44 | } 45 | 46 | public void AddPosition(Vector3 newPosition, bool DidReconcile = false) { 47 | if (this.DidReconcile && IsOwner) { 48 | timeToTarget = 0f; 49 | time = 0f; 50 | } 51 | if (IsOwner || IsServer) { 52 | timeToTarget += PredictionTimer.Singleton.minTimeBetweenTicks - time; 53 | } 54 | 55 | time = 0f; 56 | 57 | lastPos = curPos; 58 | nextPos = newPosition; 59 | 60 | this.DidReconcile = DidReconcile; 61 | } 62 | 63 | public void AddRotation(Quaternion newRotation) { 64 | lastRot = curRot; 65 | nextRot = newRotation; 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Apollo99-Games 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PayLoad.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using Unity.Netcode; 4 | using System; 5 | 6 | // This example Payload file is slightly different from the readme file. 7 | // It gives a few more examples on how the pay loads can be modified to fit your needs 8 | 9 | public struct BoxInputPayload: ICompressible 10 | { 11 | private int tick; 12 | private byte objectID; 13 | public sbyte vertical; 14 | public sbyte horizontal; 15 | private byte numberOfCopies; 16 | public bool spacePressed; 17 | 18 | public int Tick { get => tick; set => this.tick = value; } 19 | 20 | public byte ObjectID { get => objectID; set => this.objectID = value; } 21 | 22 | public byte NumberOfCopies { get => numberOfCopies; set => this.numberOfCopies = value; } 23 | 24 | public void SetAxis(float horizontal, float vertical) { 25 | this.vertical = (sbyte)(vertical * 100f); 26 | this.horizontal = (sbyte)(horizontal * 100f); 27 | } 28 | 29 | public float GetHorizontal() { 30 | return horizontal/100f; 31 | } 32 | 33 | public float GetVertical() { 34 | return vertical/100f; 35 | } 36 | 37 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 38 | serializer.SerializeValue(ref objectID); 39 | serializer.SerializeValue(ref vertical); 40 | serializer.SerializeValue(ref horizontal); 41 | serializer.SerializeValue(ref numberOfCopies); 42 | serializer.SerializeValue(ref spacePressed); 43 | } 44 | } 45 | 46 | public struct BoxStatePayload: IPayLoad 47 | { 48 | private int tick; 49 | private byte objectID; 50 | public Vector3 position; 51 | public uint rotation; 52 | public Vector3 velocity; 53 | public Vector3 angularVelocity; 54 | 55 | public int Tick { get => tick; set => this.tick = value; } 56 | 57 | public byte ObjectID { get => objectID; set => this.objectID = value; } 58 | 59 | public Quaternion GetRot () { 60 | Quaternion rot = Quaternion.identity; 61 | QuaternionCompressor.DecompressQuaternion(ref rot, rotation); 62 | return rot; 63 | } 64 | 65 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 66 | serializer.SerializeValue(ref objectID); 67 | serializer.SerializeValue(ref position); 68 | serializer.SerializeValue(ref rotation); 69 | serializer.SerializeValue(ref velocity); 70 | serializer.SerializeValue(ref angularVelocity); 71 | } 72 | } 73 | 74 | public struct WorldInputPayload: INetworkSerializable 75 | { 76 | public int tick; 77 | public BoxInputPayload[] inputs; 78 | public static WorldInputPayload Create(Message statemessage) { 79 | WorldInputPayload holder = new WorldInputPayload(); 80 | holder.inputs = statemessage.inputs.ToArray(); 81 | holder.tick = statemessage.tick; 82 | return holder; 83 | } 84 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 85 | serializer.SerializeValue(ref inputs); 86 | serializer.SerializeValue(ref tick); 87 | } 88 | } 89 | 90 | public struct WorldPayload: INetworkSerializable 91 | { 92 | public int tick; 93 | public BoxStatePayload[] states; 94 | public BoxInputPayload[] inputs; 95 | public static WorldPayload Create(Message statemessage) { 96 | WorldPayload holder = new WorldPayload(); 97 | holder.states = statemessage.states.ToArray(); 98 | holder.inputs = statemessage.inputs.ToArray(); 99 | holder.tick = statemessage.tick; 100 | return holder; 101 | } 102 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 103 | serializer.SerializeValue(ref states); 104 | serializer.SerializeValue(ref inputs); 105 | serializer.SerializeValue(ref tick); 106 | } 107 | } 108 | 109 | public class Message: EventArgs { 110 | public List states; 111 | public List inputs; 112 | public int tick; 113 | public ushort OwnerID; 114 | public ClientRpcParams sendParams; 115 | 116 | public Message() { 117 | states = new List(); 118 | inputs = new List(); 119 | tick = 0; 120 | OwnerID = 0; 121 | } 122 | 123 | public Message(WorldPayload worldPayload) { 124 | this.states = new List(worldPayload.states); 125 | this.inputs = new List(worldPayload.inputs); 126 | this.tick = worldPayload.tick; 127 | } 128 | 129 | public Message(WorldInputPayload worldPayload) { 130 | this.inputs = new List(worldPayload.inputs); 131 | this.tick = worldPayload.tick; 132 | } 133 | } 134 | 135 | public interface IPayLoad : INetworkSerializable 136 | { 137 | int Tick { get; set; } 138 | 139 | byte ObjectID { get; set; } 140 | } 141 | 142 | public interface ICompressible : IPayLoad 143 | { 144 | byte NumberOfCopies { get; set; } 145 | } -------------------------------------------------------------------------------- /PayLoadBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Unity.VisualScripting; 4 | 5 | /// 6 | /// A buffer built for the objects using the IPayLoad interface. 7 | /// Has methods to prevent the buffer from exceeding a specific size. 8 | /// Sorts the objects according to their tick. 9 | /// 10 | public class PayLoadBuffer where T : struct, IPayLoad { 11 | private List buffer; 12 | private List lastItems; 13 | private T lastItem; 14 | private int sizeToMaintain; 15 | private int sizeThreshold; 16 | private int maxEatCount; 17 | private int absoluteMaxSize; 18 | private bool Shrink; 19 | 20 | /// 21 | /// Creates a pay load buffer that tries to keep a certain max size. 22 | /// Note these constraints only apply to the EnqueueRedundentItems() and DequeueToMaintain() methods 23 | /// 24 | /// The max size the buffer should try to keep. 25 | /// The threshold the buffer is allowed to have. 26 | /// The maximum number of objects the buffer can Dequeue at once. 27 | /// If the buffer exceeds this value it will not except anymore inputs 28 | public PayLoadBuffer(int sizeToMaintain, int sizeThreshold, int maxEatCount = Int32.MaxValue, int absoluteMaxSize = Int32.MaxValue) { 29 | buffer = new List(); 30 | lastItems = new List(); 31 | this.sizeToMaintain = sizeToMaintain; 32 | this.sizeThreshold = sizeThreshold; 33 | Shrink = false; 34 | this.maxEatCount = maxEatCount; 35 | this.absoluteMaxSize = absoluteMaxSize; 36 | } 37 | 38 | /// 39 | /// Enqueues a list into the buffer. 40 | /// Ensures there are not duplicate ticks. 41 | /// The ticks of the objects are calculated by subtracting one from the provided tick. 42 | /// 43 | /// The list of items you want to add to the buffer 44 | /// The game tick of the items 45 | /// The maximum number of items inserted at this time 46 | /// bool - if the the buffer has room to add the object 47 | public bool EnqueueRedundentItems(List items, int tick, int max = -1) { 48 | if (buffer.Count + items.Count > absoluteMaxSize) return false; 49 | 50 | for (int i = 0; i < items.Count; i++) 51 | { 52 | // Ensure the objects do not exceed the max number 53 | if (max != -1 && i + 1 > max) break; 54 | 55 | // Calculate and set the tick 56 | T item = items[i]; 57 | item.Tick = tick - i; 58 | 59 | // If the 60 | if(!ContainsTick(tick - i)) { 61 | Enqueue(item); 62 | } 63 | } 64 | return true; 65 | } 66 | 67 | /// 68 | /// Enqueues a list into the and shorts them by tick 69 | /// 70 | /// The item to be want to added to the buffer 71 | /// void 72 | public void Enqueue(T item) { 73 | for (int i = buffer.Count - 1; i >= 0; i--) 74 | { 75 | if (buffer[i].Tick > item.Tick) { 76 | 77 | buffer.Insert(i + 1, item); 78 | return; 79 | } 80 | } 81 | buffer.Insert(0, item); 82 | } 83 | 84 | /// 85 | /// Dequeues the buffer in a way in which it tries to ensure the length stays below the max size 86 | /// 87 | /// List of Dequeued items 88 | public List DequeueToMaintain() { 89 | List BufferToFill = new List(); 90 | lastItems.Clear(); 91 | 92 | if (buffer.Count == 0) return BufferToFill; 93 | 94 | if (buffer.Count <= sizeToMaintain) Shrink = false; 95 | 96 | // If the buffer size is greater than the allowed threshold 97 | // Shrink it to the required size. 98 | // else just Dequeue as normal 99 | if (buffer.Count > sizeThreshold + sizeToMaintain || Shrink ) { 100 | Shrink = true; 101 | 102 | int EatCount = buffer.Count - sizeToMaintain; 103 | if (EatCount > maxEatCount) EatCount = maxEatCount; 104 | 105 | for (int i = 0; i < EatCount; i++) 106 | { 107 | T holder = InternalDequeue(); 108 | lastItems.Add(holder); 109 | BufferToFill.Add(holder); 110 | } 111 | } 112 | else { 113 | BufferToFill.Add(Dequeue()); 114 | } 115 | 116 | return BufferToFill; 117 | } 118 | 119 | /// 120 | /// Dequeues the buffer. 121 | /// 122 | /// the Dequeued item 123 | private T InternalDequeue() { 124 | T item = buffer[buffer.Count - 1]; 125 | buffer.RemoveAt(buffer.Count - 1); 126 | lastItem = item; 127 | return item; 128 | } 129 | 130 | /// 131 | /// Dequeues the buffer 132 | /// 133 | /// the Dequeued item 134 | public T Dequeue() { 135 | InternalDequeue(); 136 | lastItems.Clear(); 137 | lastItems.Add(lastItem); 138 | return lastItem; 139 | } 140 | 141 | /// 142 | /// Finds if the buffer already contains a tick. 143 | /// Will return false if the tick is smaller or equal to the last dequeued tick 144 | /// 145 | /// The tick to find 146 | /// boolean - if it contains or doesn't contain the tick 147 | public bool ContainsTick(int tick) { 148 | if (buffer.Count == 0) return false; 149 | if (!lastItem.Equals(default(T)) && tick <= lastItem.Tick) return true; 150 | foreach (var item in buffer) 151 | { 152 | if (item.Tick == tick) { 153 | return true; 154 | } 155 | } 156 | return false; 157 | } 158 | 159 | public int Size() { 160 | return buffer.Count; 161 | } 162 | 163 | /// 164 | /// Gets the last objects dequeued 165 | /// 166 | /// the List of objects 167 | public List GetLastItems() { 168 | return lastItems; 169 | } 170 | 171 | /// 172 | /// Gets the last object dequeued 173 | /// 174 | /// the object 175 | public T GetLastItem() { 176 | return lastItem; 177 | } 178 | 179 | /// 180 | /// Checks if a object list contains a specifc ID 181 | /// 182 | /// The list to search 183 | /// The object ID to search for 184 | /// boolean - if the ID was found 185 | public static bool ContainsID(List items, int ObjectID) { 186 | foreach (var item in items) 187 | { 188 | if(item.ObjectID == ObjectID) return true; 189 | } 190 | return false; 191 | } 192 | 193 | /// 194 | /// Finds all the objects with a specific ID 195 | /// 196 | /// The list to search 197 | /// The object ID to search for 198 | /// the List of objects 199 | public static List FindObjectItems(List items, int ObjectID) { 200 | List BufferToFill = new List(); 201 | for (int i = 0; i < items.Count; i++) 202 | { 203 | if (items[i].ObjectID == ObjectID) { 204 | BufferToFill.Add(items[i]); 205 | } 206 | } 207 | return BufferToFill; 208 | } 209 | 210 | /// 211 | /// Finds the first object with a specific ID in a list 212 | /// 213 | /// The list to search 214 | /// The object ID to search for 215 | /// the object found or the default value of the object if it is not found 216 | public static T FindObjectItem(List items, int ObjectID) { 217 | foreach (var item in items) 218 | { 219 | if (item.ObjectID == ObjectID) { 220 | return item; 221 | } 222 | } 223 | return default; 224 | } 225 | 226 | /// 227 | /// Compresses a list of objects with the ICompressible interface. Does this is deleting duplicate objects. 228 | /// The function will still store the number of duplicates so it can be Decompressed later. 229 | /// This will ignore the tick value 230 | /// 231 | /// The list to compress 232 | /// void - Modifies the list passed in 233 | // Example Decompressed: Object Value: 1, Object Value: 1, Object Value: 1, Object Value: 2, Object Value: 2 234 | // Example Compressed: Object Value: 1 (Duplicates: 2), Object Value: 2 (Duplicates: 1) 235 | public static void Compress(List items) where item: struct, ICompressible { 236 | if (items.Count < 2) return; 237 | 238 | item LastDiff = items[0]; 239 | LastDiff.Tick = 0; 240 | int LastDiffIndex = 0; 241 | int Duplicates = 0; 242 | for (int i = 1; i < items.Count; i++) 243 | { 244 | // Set the tick value to 0 as it doesn't matter 245 | item holder = items[i]; 246 | holder.Tick = 0; 247 | 248 | // if the last unique object was the same as the current one 249 | // the current one will be removed and will increment the counter to keep track of the number of duplicates 250 | if (LastDiff.Equals(holder)) { 251 | items.RemoveAt(i); 252 | i--; 253 | Duplicates++; 254 | } 255 | else { 256 | // if the current object is different than the last unique object. 257 | // Store number of duplicates in the last object 258 | // Now the current object unique object will become the last object 259 | LastDiff.NumberOfCopies = (byte)Duplicates; 260 | items[LastDiffIndex] = LastDiff; 261 | Duplicates = 0; 262 | 263 | LastDiff = items[i]; 264 | LastDiff.Tick = 0; 265 | LastDiffIndex = i; 266 | } 267 | } 268 | LastDiff.NumberOfCopies = (byte)Duplicates; 269 | items[LastDiffIndex] = LastDiff; 270 | } 271 | 272 | /// 273 | /// Decompress a list of objects with the ICompressible interface. Does this is by adding the duplicate objects in the list. 274 | /// 275 | /// The list to Decompress 276 | /// void - Modifies the list passed in 277 | public static void Decompress(List items) where item: struct, ICompressible { 278 | for (int i = 0; i < items.Count; i++) 279 | { 280 | // Makes duplicates of an object 281 | // the extact number of objects is stored in the GetNumberOfCopies() method. 282 | for (int indexCopy = 1; indexCopy <= items[i].NumberOfCopies; indexCopy++) 283 | { 284 | if (i + indexCopy < items.Count) { 285 | items.Insert(i + indexCopy, items[i]); 286 | } 287 | else { 288 | items.Add(items[i]); 289 | } 290 | } 291 | 292 | i += items[i].NumberOfCopies; 293 | } 294 | } 295 | } -------------------------------------------------------------------------------- /PredictionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.Netcode; 5 | 6 | public class PredictionManager : NetworkBehaviour 7 | { 8 | public const int BUFFER_SIZE = 512; 9 | [Tooltip("Over the network packets can be lost, so extra redundant packets are sent to compensate. This sets the maximum number of extra packets a prediction object can send")] 10 | public int MaxRedundentInputs = 10; 11 | 12 | [Tooltip("The maximum time in milliseconds the client can spend running the physics simulation when reconciling.")] 13 | [SerializeField] float maxPhysicsSimulationTime = 10f; 14 | 15 | [Tooltip("The maximum time in milliseconds the client can spend reconciling.")] 16 | [SerializeField] float maxReconciliationTime = 10f; 17 | /// 18 | /// Returns if the prediction manager is currently reconciling 19 | /// 20 | [HideInInspector] public bool IsReconciling { get; private set; } 21 | /// 22 | /// Returns if the prediction manager did reconcile sometime during this tick 23 | /// 24 | [HideInInspector] public bool DidReconcile { get; private set; } 25 | /// 26 | /// Returns if the prediction manager received an input packet this tick 27 | /// 28 | [HideInInspector] public bool DidReceiveInputPacket { get; private set; } 29 | /// 30 | /// Returns if the prediction manager received a state packet this tick 31 | /// 32 | [HideInInspector] public bool DidReceiveStatePacket { get; private set; } 33 | 34 | [Tooltip("If left checked, will reconcile all clients, which allows for better collisons and extrapolation at the cost of performance.")] 35 | public bool PredictAllClients = true; 36 | 37 | [HideInInspector] public static PredictionManager Singleton { get; private set; } 38 | 39 | // Inputs and states 40 | [HideInInspector] public event Action OnInputs; 41 | [HideInInspector] public event Action OnState; 42 | 43 | // Reconciliation 44 | [HideInInspector] public event Action OnReconcileInputs; 45 | [HideInInspector] public event Func OnShouldRecon; 46 | [HideInInspector] public event Action OnSetState; 47 | /// 48 | /// Runs before the Reconciliation process 49 | /// 50 | [HideInInspector] public event Action OnPreReconcile; 51 | /// 52 | /// Runs after the Reconciliation process 53 | /// 54 | [HideInInspector] public event Action OnPostReconcile; 55 | 56 | // Simulation 57 | 58 | /// 59 | /// Runs before the physics simulation 60 | /// 61 | /// A boolean value indicating if physics will run after this event 62 | [HideInInspector] public event Action OnPreSimulation; 63 | 64 | /// 65 | /// Runs after the physics simulation 66 | /// 67 | /// A boolean value indicating if physics ran before this event 68 | [HideInInspector] public event Action OnPostSimulation; 69 | 70 | // Sending and Receiving Packets 71 | [HideInInspector] public event Action OnComplieState; 72 | [HideInInspector] public event Action> OnSendState; 73 | [HideInInspector] public event Action OnSendInput; 74 | 75 | void Awake() { 76 | IsReconciling = false; 77 | DidReconcile = false; 78 | DidReceiveInputPacket = false; 79 | DidReceiveStatePacket = false; 80 | 81 | if (Singleton != null && Singleton != this) Destroy(this); 82 | else Singleton = this; 83 | } 84 | 85 | void Start() { 86 | PredictionTimer.Singleton.OnTick += RunPrediction; 87 | } 88 | 89 | public override void OnDestroy() { 90 | PredictionTimer.Singleton.OnTick -= RunPrediction; 91 | base.OnDestroy(); 92 | } 93 | 94 | /// 95 | /// Runs the main prediction loop at a fixed time step. Calls the: 96 | /// 1) Reconciliation functions, 97 | /// 2) Compiles and sends inputs/states, 98 | /// 3) Actions to get inputs, save states, 99 | /// 4) Runs Physics, 100 | /// 101 | /// void 102 | void RunPrediction() { 103 | DidReconcile = false; 104 | if (!IsServer && IsClient) { 105 | //Check if we need to Reconcile with the server 106 | int tickToProcess = DoReconcile(); 107 | ServerReconcile(tickToProcess); 108 | } 109 | //Get and Apply all player inputs 110 | if (OnInputs != null) OnInputs(); 111 | 112 | // Run the Physics simulation and store the client states 113 | if (OnPreSimulation != null) OnPreSimulation(true); 114 | Physics.Simulate(PredictionTimer.Singleton.minTimeBetweenTicks); 115 | 116 | if (OnState != null) OnState(PredictionTimer.Singleton.tick); 117 | if (OnPostSimulation != null) OnPostSimulation(true); 118 | 119 | // Compile All the States and send them to the clients for Reconciliation 120 | if (IsServer) CompileStates(); 121 | //Compile all the inputs and send them to the server 122 | else if (IsClient) CompileInputs(); 123 | 124 | DidReceiveStatePacket = false; 125 | DidReceiveInputPacket = false; 126 | } 127 | 128 | /// 129 | /// Reconciles the client state with the server states from a specified tick. 130 | /// 131 | /// The tick to reconcile from. 132 | /// void 133 | void ServerReconcile(int tickToProcess) { 134 | if(tickToProcess != -1) { 135 | 136 | if (OnPreReconcile != null) OnPreReconcile(); 137 | IsReconciling = true; 138 | float startTime = Time.realtimeSinceStartup; 139 | 140 | // Revert all players back to the latest received server state 141 | if (OnSetState != null) OnSetState(tickToProcess); 142 | tickToProcess += 1; 143 | 144 | // resimulate all clients back to one tick before the current tick 145 | // This is because the current tick's input will be known after Reconciliation 146 | while (tickToProcess < PredictionTimer.Singleton.tick) 147 | { 148 | // Apply the previous inputs 149 | if (OnReconcileInputs != null) OnReconcileInputs(tickToProcess); 150 | 151 | // Check to ensure if have not crossed our target reconcilation time to maintain performace 152 | // if so terminate the reconciliation 153 | if (Time.realtimeSinceStartup - startTime > maxReconciliationTime/1000f) { 154 | // Debug.Log("Recon fail"); 155 | break; 156 | } 157 | 158 | // Check to ensure if have not crossed our target simulation time to maintain performace 159 | bool CanRunPhysics = Time.realtimeSinceStartup - startTime <= maxPhysicsSimulationTime/1000f; 160 | 161 | // Run the simulation if we can. 162 | if (OnPreSimulation != null) OnPreSimulation(CanRunPhysics); 163 | if (CanRunPhysics) { 164 | Physics.Simulate(PredictionTimer.Singleton.minTimeBetweenTicks); 165 | } 166 | 167 | // Update with the new corrected states 168 | if (OnState != null) OnState(tickToProcess); 169 | 170 | if (OnPostSimulation != null) OnPostSimulation(CanRunPhysics); 171 | 172 | tickToProcess++; 173 | } 174 | 175 | // Debug.Log("Time: " + (Time.realtimeSinceStartup - startTime)); 176 | DidReconcile = true; 177 | IsReconciling = false; 178 | if (OnPostReconcile != null) OnPostReconcile(); 179 | } 180 | } 181 | 182 | /// 183 | /// Compiles All the inputs for each player 184 | /// 185 | /// void 186 | void CompileInputs() { 187 | // First we compile all the inputs of all the clients 188 | Message holder = new Message(); 189 | if (OnSendInput != null) OnSendInput(holder); 190 | // We set the current tick and the ping between client and server 191 | holder.tick = PredictionTimer.Singleton.tick; 192 | 193 | SendToServerRPC(WorldInputPayload.Create(holder)); 194 | } 195 | 196 | /// 197 | /// Compiles All the states into customized packets for each player 198 | /// 199 | /// void 200 | void CompileStates() { 201 | // First we compile all the states of all the clients 202 | Message defaultWorldState = new Message(); 203 | if (OnComplieState != null) OnComplieState(defaultWorldState); 204 | 205 | // We then sort to customize the packets for each client to minimize bandwidth 206 | List AllStates = new List(); 207 | if (OnSendState != null) OnSendState(AllStates); 208 | 209 | // iterate through all the packets to send them to the clients 210 | foreach (var state in AllStates) 211 | { 212 | SendToClientRPC(WorldPayload.Create(state), state.sendParams); 213 | } 214 | } 215 | 216 | /// 217 | /// Sends the states to the client 218 | /// 219 | /// The state payload to send to the client. 220 | /// The client Params - to limit which client you send the state to. 221 | /// void 222 | [ClientRpc] 223 | public void SendToClientRPC(WorldPayload worldStatePayLoad, ClientRpcParams clientRpcParams = default) 224 | { 225 | if (OnComplieState != null) OnComplieState(new Message(worldStatePayLoad)); 226 | DidReceiveStatePacket = true; 227 | } 228 | 229 | /// 230 | /// Sends the inputs to the server 231 | /// 232 | /// /// The input payload to send to the server. 233 | /// void 234 | [ServerRpc(RequireOwnership = false)] 235 | public void SendToServerRPC(WorldInputPayload worldInputPayload) 236 | { 237 | if (OnSendInput != null) OnSendInput(new Message(worldInputPayload)); 238 | DidReceiveInputPacket = true; 239 | } 240 | 241 | /// 242 | /// Checks to see if reconciliation is needed and returns the reconciled tick value. 243 | /// 244 | /// The reconciled tick value 245 | private int DoReconcile() { 246 | int rewindTick = -1; 247 | if (OnShouldRecon == null) return rewindTick; 248 | foreach (Func funcCall in OnShouldRecon.GetInvocationList()) 249 | { 250 | int tick = funcCall(); 251 | 252 | if (tick != -1) { 253 | rewindTick = tick; 254 | } 255 | } 256 | return rewindTick; 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /PredictionObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using Unity.Netcode; 4 | using System; 5 | 6 | public class PredictionObject: NetworkBehaviour where InputPayload : struct, IPayLoad where StatePayload : struct, IPayLoad 7 | { 8 | // Shared 9 | [HideInInspector] public byte ObjectID {get; private set;} = 0; 10 | [HideInInspector] public ushort OwnerID {get; private set;} = 0; 11 | private PayLoadBuffer inputQueue; 12 | private StatePayload[] stateBuffer; 13 | private InputPayload[] inputBuffer; 14 | 15 | //Client 16 | private StatePayload latestServerState; 17 | public StatePayload lastProcessedState; 18 | 19 | public bool NeededToReconcile {get; private set;} 20 | public bool DidReceiveState {get; private set;} 21 | 22 | //Server 23 | [HideInInspector] public bool DidReceiveInput {get; private set;} 24 | private Message currentDefaultWorldState; 25 | public ClientRpcParams sendParams {get; private set;} 26 | 27 | //Interpolation 28 | public Interpolator interpolator; 29 | 30 | 31 | // =========== Setup ============== 32 | 33 | public override void OnNetworkSpawn() 34 | { 35 | stateBuffer = new StatePayload[PredictionManager.BUFFER_SIZE]; 36 | inputBuffer = new InputPayload[PredictionManager.BUFFER_SIZE]; 37 | 38 | inputQueue = new PayLoadBuffer(25, 5, 3, 100); 39 | 40 | DidReceiveInput = false; 41 | NeededToReconcile = false; 42 | OwnerID = Convert.ToUInt16(OwnerClientId); 43 | ObjectID = (byte)NetworkObjectId; 44 | 45 | sendParams = new ClientRpcParams { 46 | Send = new ClientRpcSendParams 47 | { 48 | TargetClientIds = new ulong[]{OwnerID} 49 | } 50 | }; 51 | 52 | if(IsServer) { 53 | PredictionManager.Singleton.OnInputs += ServerHandleTick; 54 | PredictionManager.Singleton.OnSendState += OnSendState; 55 | } 56 | else { 57 | PredictionManager.Singleton.OnInputs += ClientHandleTick; 58 | PredictionManager.Singleton.OnReconcileInputs += ForceSetInput; 59 | PredictionManager.Singleton.OnSetState += SetToPast; 60 | PredictionManager.Singleton.OnShouldRecon += NeedToReconcile; 61 | } 62 | PredictionManager.Singleton.OnState += StoreClientState; 63 | PredictionManager.Singleton.OnComplieState += ReceiveCompileState; 64 | PredictionManager.Singleton.OnSendInput += SendReceiveInput; 65 | PredictionManager.Singleton.OnPreSimulation += OnPreSimulation; 66 | PredictionManager.Singleton.OnPostSimulation += OnPostSimulation; 67 | 68 | if (interpolator != null) { 69 | interpolator.IsOwner = IsOwner; 70 | interpolator.IsServer = IsServer; 71 | } 72 | 73 | OnPredictionSpawn(); 74 | base.OnNetworkSpawn(); 75 | } 76 | 77 | /// 78 | /// Runs when the object spawns 79 | /// 80 | public virtual void OnPredictionSpawn() {} 81 | 82 | /// 83 | /// Runs when the object is destroyed 84 | /// 85 | public virtual void OnPredictionDespawn() {} 86 | 87 | public override void OnNetworkDespawn() { 88 | if(IsServer) { 89 | PredictionManager.Singleton.OnInputs -= ServerHandleTick; 90 | PredictionManager.Singleton.OnSendState -= OnSendState; 91 | } 92 | else { 93 | PredictionManager.Singleton.OnInputs -= ClientHandleTick; 94 | PredictionManager.Singleton.OnReconcileInputs -= ForceSetInput; 95 | PredictionManager.Singleton.OnSetState -= SetToPast; 96 | PredictionManager.Singleton.OnShouldRecon -= NeedToReconcile; 97 | } 98 | PredictionManager.Singleton.OnState -= StoreClientState; 99 | PredictionManager.Singleton.OnComplieState -= ReceiveCompileState; 100 | PredictionManager.Singleton.OnSendInput -= SendReceiveInput; 101 | PredictionManager.Singleton.OnPreSimulation -= OnPreSimulation; 102 | PredictionManager.Singleton.OnPostSimulation -= OnPostSimulation; 103 | 104 | OnPredictionDespawn(); 105 | base.OnNetworkDespawn(); 106 | } 107 | 108 | // ============ Simulation Short Cuts ============= 109 | 110 | /// 111 | /// Runs before the physics simulation 112 | /// 113 | /// if the physics was able to run 114 | public virtual void OnPreSimulation(bool DidRunPhysics) {} 115 | 116 | /// 117 | /// Runs after the physics simulation 118 | /// 119 | /// if the physics was able to run 120 | public virtual void OnPostSimulation(bool DidRunPhysics) {} 121 | 122 | // ============ Client Logic ============= 123 | 124 | /// 125 | /// Gets the new client input, stores it and applies the input 126 | /// 127 | /// void 128 | void ClientHandleTick() 129 | { 130 | if (IsOwner) { 131 | int bufferIndex = PredictionTimer.Singleton.tick % PredictionManager.BUFFER_SIZE; 132 | DidReceiveInput = true; 133 | 134 | InputPayload newInput = GetClientInput(); 135 | 136 | inputBuffer[bufferIndex] = newInput; 137 | SetInput(newInput); 138 | } 139 | else { 140 | ClientHandleOtherClients(); 141 | } 142 | } 143 | 144 | /// 145 | /// Applies and stores all the other client inputs (the one that are owned by other clients) 146 | /// 147 | /// void 148 | void ClientHandleOtherClients() { 149 | while(inputQueue.Size() > 0) { 150 | if (inputQueue.Size() > 0) SetInput(inputQueue.Dequeue()); 151 | } 152 | inputBuffer[PredictionTimer.Singleton.tick % PredictionManager.BUFFER_SIZE] = inputQueue.GetLastItem(); 153 | } 154 | 155 | 156 | // =========== Client Logic Section 2: Reconcile Logic ============ 157 | 158 | /// 159 | /// Decides if this object needs to be reconciled 160 | /// If Reconciliation is needed returns the tick to rewind to otherwise returns -1. 161 | /// 162 | /// int - the tick to rewind to 163 | int NeedToReconcile() { 164 | NeededToReconcile = false; 165 | DidReceiveState = false; 166 | 167 | // Ensure that the new state is not equal to the default state and doesn't equal the last state 168 | if (!latestServerState.Equals(default(StatePayload)) && 169 | (lastProcessedState.Equals(default(StatePayload)) || 170 | !latestServerState.Equals(lastProcessedState))) 171 | { 172 | // Enure the new state is not in the past 173 | if (lastProcessedState.Tick > latestServerState.Tick) return -1; 174 | DidReceiveState = true; 175 | 176 | int serverStateBufferIndex = latestServerState.Tick % PredictionManager.BUFFER_SIZE; 177 | lastProcessedState = latestServerState; 178 | 179 | // Run only if prediction of all Clients is enabled (if this is enabled it is very expensive) 180 | if (!PredictionManager.Singleton.PredictAllClients && !IsOwner) { 181 | SetState(lastProcessedState); 182 | return -1; 183 | } 184 | 185 | // A user defined function sets the criteria of what defines an desync and when Reconciliation is needed 186 | bool isRecon = ShouldReconcile(lastProcessedState, stateBuffer[serverStateBufferIndex]); 187 | 188 | if (isRecon) { 189 | NeededToReconcile = true; 190 | return lastProcessedState.Tick; 191 | } 192 | } 193 | return -1; 194 | } 195 | 196 | /// 197 | /// A user defined method that determines the criteria for Reconciliation. 198 | /// This can be done by comparing the latestServerState with the ClientState at that time. 199 | /// 200 | /// the latest server state received from the server 201 | /// the client state that happened at a similar time to the received server state 202 | /// boolean - should the server Reconcile 203 | public virtual bool ShouldReconcile(StatePayload latestServerState, StatePayload ClientState) { 204 | return true; 205 | } 206 | 207 | /// 208 | /// Sets the latestServerState to the current state and saves it to the buffer at the specified tick 209 | /// 210 | /// the tick to save the latestServerState at the corresponding index in the buffer 211 | /// void 212 | public void SetToPast(int tick) 213 | { 214 | int bufferIndex = tick % PredictionManager.BUFFER_SIZE; 215 | 216 | if (!latestServerState.Equals(default(StatePayload)) && 217 | !lastProcessedState.Equals(default(StatePayload))) 218 | { 219 | stateBuffer[bufferIndex] = lastProcessedState; 220 | SetState(lastProcessedState); 221 | } 222 | if (IsServer) { 223 | SetState(stateBuffer[bufferIndex]); 224 | } 225 | } 226 | 227 | /// 228 | /// Forces setting the input to a past one by a specified tick. 229 | /// 230 | /// The tick for which to set the currnet input to. 231 | /// void 232 | void ForceSetInput(int tick) { 233 | if (IsOwner) { 234 | SetInput(inputBuffer[tick % PredictionManager.BUFFER_SIZE]); 235 | } 236 | else { 237 | ClientHandleOtherClients(); 238 | } 239 | } 240 | 241 | // ========== Server Logic ========== 242 | 243 | /// 244 | /// Run's the server logic every tick 245 | /// 246 | /// void 247 | void ServerHandleTick() 248 | { 249 | if (IsOwner) { 250 | HandleServerPlayer(); 251 | } 252 | else { 253 | HandleOtherClients(); 254 | } 255 | } 256 | 257 | /// 258 | /// Empties the buffer and Applies the inputs of all the clients (except the server's) in a way that stops 259 | /// the buffer from increasing which reduces latency. 260 | /// 261 | /// void 262 | void HandleOtherClients() { 263 | List newInputs = inputQueue.DequeueToMaintain(); 264 | DidReceiveInput = newInputs.Count > 0? true: false; 265 | 266 | for (int i = 0; i < newInputs.Count; i++) 267 | { 268 | SetInput(newInputs[i]); 269 | } 270 | inputBuffer[PredictionTimer.Singleton.tick % PredictionManager.BUFFER_SIZE] = inputQueue.GetLastItem(); 271 | } 272 | 273 | /// 274 | /// Gets the new input for the server player and applies the input 275 | /// 276 | /// void 277 | void HandleServerPlayer() { 278 | DidReceiveInput = true; 279 | InputPayload newInput = GetClientInput(); 280 | inputBuffer[PredictionTimer.Singleton.tick % PredictionManager.BUFFER_SIZE] = newInput; 281 | 282 | SetInput(newInput); 283 | 284 | inputQueue.Enqueue(newInput); 285 | inputQueue.Dequeue(); 286 | } 287 | 288 | 289 | // ========= Setting and Getting Inputs ========= 290 | 291 | /// 292 | /// Gets the player inputs. This will only run on the client that owns the object 293 | /// 294 | /// InputPayload 295 | public virtual InputPayload GetInput() { 296 | return new InputPayload(); 297 | } 298 | 299 | /// 300 | /// Gets the current client state. 301 | /// Guaranteed to set the tick and object ID 302 | /// 303 | /// StatePayload - the current state 304 | InputPayload GetClientInput() { 305 | InputPayload inputPayload = GetInput(); 306 | inputPayload.Tick = PredictionTimer.Singleton.tick; 307 | inputPayload.ObjectID = ObjectID; 308 | return inputPayload; 309 | } 310 | 311 | 312 | /// 313 | /// Sets the player inputs. This will run on all machines. 314 | /// NOTE: it is only guaranteed to run on the client that owns the object 315 | /// 316 | /// void 317 | public virtual void SetInput(InputPayload input) {} 318 | 319 | /// 320 | /// Gets the player input at a specified tick 321 | /// 322 | /// The tick to get the input at 323 | /// InputPayload 324 | public InputPayload GetInputAtTick(int tick) { 325 | return inputBuffer[tick % PredictionManager.BUFFER_SIZE]; 326 | } 327 | 328 | 329 | // ========= Sending and Receiving Inputs ========== 330 | 331 | /// 332 | /// If the functions is called on the server it will call a user defined function to apply the client inputs 333 | /// On the client it will get the current and previous inputs and add them to the input message through a user defined function 334 | /// 335 | /// The object calling the function (this can be null) 336 | /// Where the inputs are added and applied from 337 | /// void 338 | void SendReceiveInput(Message message) { 339 | if (IsServer) { 340 | ReceiveClientInputs(message); 341 | } 342 | else if (IsOwner) { 343 | // redundent inputs and from the past as because of packet loss the server may have not gotten a previous input 344 | // to save bandwidth we don't send redundent inputs confirmed by the server 345 | int RedundentInputsCount = PredictionTimer.Singleton.tick - latestServerState.Tick; 346 | 347 | // to save bandwidth we ensure that the redundent inputs don't surpass the MaxRedundentInputs 348 | if (RedundentInputsCount > PredictionManager.Singleton.MaxRedundentInputs) { 349 | RedundentInputsCount = PredictionManager.Singleton.MaxRedundentInputs; 350 | } 351 | 352 | // add all redundent inputs to a list then send them to a client defined functions to be added to the input message 353 | List RedundentInputs = new List(); 354 | for (int i = 0; i < RedundentInputsCount; i++) 355 | { 356 | RedundentInputs.Add(inputBuffer[(PredictionTimer.Singleton.tick - i) % PredictionManager.BUFFER_SIZE]); 357 | } 358 | 359 | SendClientInputsToServer(RedundentInputs, message); 360 | } 361 | } 362 | 363 | /// 364 | /// Adds the client inputs to a buffer where they can be applied over time 365 | /// 366 | /// The inputs to be added to the buffer 367 | /// the tick received from the client 368 | /// the ping between client and server in ticks 369 | /// void 370 | public void AddClientInputs(List inputs, int tick) { 371 | inputQueue.EnqueueRedundentItems(inputs, tick, PredictionManager.Singleton.MaxRedundentInputs); 372 | } 373 | 374 | /// 375 | /// Adds the client inputs to the input message 376 | /// 377 | /// The inputs to be added to the message 378 | /// the message that will be sent to the server 379 | /// void 380 | public virtual void SendClientInputsToServer(List inputs, Message sender) {} 381 | 382 | /// 383 | /// Applies the inputs using the AddClientInputs() method 384 | /// 385 | /// the input message received from the client 386 | /// void 387 | public virtual void ReceiveClientInputs(Message receiver) {} 388 | 389 | // ========= Setting and Getting States ========= 390 | 391 | /// 392 | /// Gets the state at a specified tick 393 | /// 394 | /// the input message received from the client 395 | /// StatePayload - The state at that tick 396 | public StatePayload GetStateAtTick(int tick) { 397 | return stateBuffer[tick % PredictionManager.BUFFER_SIZE]; 398 | } 399 | 400 | /// 401 | /// sets the current state to a previous state a specified tick 402 | /// Basically rewinds to a previous tick 403 | /// 404 | /// the tick to rewind to 405 | /// void 406 | void StoreClientState(int tick) { 407 | stateBuffer[tick % PredictionManager.BUFFER_SIZE] = GetState(); 408 | stateBuffer[tick % PredictionManager.BUFFER_SIZE].Tick = tick; 409 | stateBuffer[tick % PredictionManager.BUFFER_SIZE].ObjectID = ObjectID; 410 | SetState(stateBuffer[tick % PredictionManager.BUFFER_SIZE]); 411 | } 412 | 413 | /// 414 | /// Gets the current client state. 415 | /// Guaranteed to set the tick and object ID 416 | /// 417 | /// StatePayload - the current state 418 | StatePayload GetClientState() { 419 | StatePayload statePayload = GetState(); 420 | statePayload.Tick = inputQueue.GetLastItem().Tick; 421 | statePayload.ObjectID = ObjectID; 422 | return statePayload; 423 | } 424 | 425 | /// 426 | /// Gets the current client state 427 | /// 428 | /// StatePayload - the current state 429 | public virtual StatePayload GetState() { 430 | return new StatePayload(); 431 | } 432 | 433 | /// 434 | /// Sets the current state 435 | /// 436 | /// The state to set the current state to 437 | /// void 438 | public virtual void SetState(StatePayload statePayload) {} 439 | 440 | // ========= Sending and Receiving States ========== 441 | 442 | /// 443 | /// On the server the function will compile all the states and inputs into one state message 444 | /// On the client it will apply the states received to the ojects through a user defined function 445 | /// 446 | /// The object calling the function (this can be null) 447 | /// Where the states are compiled and applied from 448 | /// void 449 | void ReceiveCompileState(Message message) { 450 | if (IsServer) { 451 | CompileServerState(GetClientState(), inputQueue.GetLastItems(), message); 452 | currentDefaultWorldState = message; 453 | } 454 | else { 455 | ReceiveServerState(message); 456 | } 457 | } 458 | 459 | /// 460 | /// Creates customized messages defined by the user that are sent to the client. 461 | /// This helps save bandwidth each client gets only what they need 462 | /// 463 | /// The object calling the function (this can be null) 464 | /// Customized state Messages for each client 465 | /// void 466 | void OnSendState(List messages) { 467 | // Loop through the current state message list to see if this object's Client ID is in it. 468 | // If it is call the user defined function SortState() to customize the medthod to lower bandwidth 469 | for (int i = 0; i < messages.Count; i++) 470 | { 471 | if (messages[i].OwnerID == OwnerID) { 472 | SortStateToSendToClient(currentDefaultWorldState, messages[i]); 473 | return; 474 | } 475 | } 476 | 477 | // We the state message does nor exist for this object's Client, we create it and add it to the list 478 | Message holder = new Message 479 | { 480 | OwnerID = OwnerID, 481 | tick = inputQueue.GetLastItem().Tick, 482 | sendParams = sendParams 483 | }; 484 | SortStateToSendToClient(currentDefaultWorldState, holder); 485 | messages.Add(holder); 486 | } 487 | 488 | /// 489 | /// Adds only relevant information from the defaultWorldState to the sender to save bandwidth. 490 | /// For example is if a prediction object is out of sight of a client, don't send state information for that object. 491 | /// 492 | /// The defualt state containing all the states/inputs of every prediction object 493 | /// The customized state to send to the client to save bandwidth 494 | /// void 495 | public virtual void SortStateToSendToClient(Message defaultWorldState, Message sender) {} 496 | 497 | /// 498 | /// Adds the client inputs to the state or/and inputs to the state message 499 | /// 500 | /// The state to be added to the message 501 | /// The inputs to be added to the message 502 | /// the message that will be sent to the clients 503 | /// void 504 | public virtual void CompileServerState(StatePayload statePayload, List inputPayloads, Message sender) {} 505 | 506 | /// 507 | /// Applies the states using the ApplyServerState() method 508 | /// Applies the inputs using the ApplyServerInputs() method 509 | /// 510 | /// the state message received from the client 511 | /// void 512 | public virtual void ReceiveServerState(Message receiver) {} 513 | 514 | /// 515 | /// Applies the new server state if the client has desynced 516 | /// 517 | /// the state to apply 518 | /// the server tick 519 | /// void 520 | public void ApplyServerState(StatePayload state, int tick) { 521 | if (!state.Equals(default(StatePayload))) { 522 | state.Tick = tick; 523 | latestServerState = state; 524 | } 525 | } 526 | 527 | /// 528 | /// Applies the new server input from the server for objects that this client doesn't own 529 | /// 530 | /// the input to apply 531 | /// the server tick 532 | /// void 533 | public void ApplyServerInputs(List inputPayloads, int tick) { 534 | if (IsServer || IsOwner) return; 535 | 536 | for (int i = 0; i < inputPayloads.Count; i++) 537 | { 538 | InputPayload newInput = inputPayloads[i]; 539 | newInput.Tick = tick; 540 | inputQueue.Enqueue(newInput); 541 | } 542 | } 543 | } -------------------------------------------------------------------------------- /PredictionTimer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System; 3 | using Unity.Netcode; 4 | 5 | public class PredictionTimer : NetworkBehaviour 6 | { 7 | private float timer; 8 | [HideInInspector] public int tick {get; private set; } 9 | [HideInInspector] public float minTimeBetweenTicks {get; private set; } 10 | public int serverTickRate = 30; 11 | public Action OnTick; 12 | [HideInInspector] public static PredictionTimer Singleton { get; private set; } 13 | 14 | void Awake() { 15 | tick = 0; 16 | minTimeBetweenTicks = 1f / (float)serverTickRate; 17 | 18 | if (Singleton != null && Singleton != this) Destroy(this); 19 | else Singleton = this; 20 | } 21 | 22 | void Start() 23 | { 24 | Physics.simulationMode = SimulationMode.Script; 25 | } 26 | // Update is called once per frame 27 | void Update() 28 | { 29 | // Check to ensure the server or client has started 30 | if (!NetworkManager.Singleton.IsClient && !NetworkManager.Singleton.IsServer) return; 31 | // This loop ensures that each device has the same fixed time step. 32 | timer += Time.deltaTime; 33 | while (timer >= minTimeBetweenTicks) 34 | { 35 | timer -= minTimeBetweenTicks; 36 | if (OnTick != null) OnTick(); 37 | tick++; 38 | } 39 | } 40 | 41 | /// 42 | /// Gets the ping value between server and client as a float 43 | /// 44 | /// The ping value as a float 45 | public static float GetPing() { 46 | return NetworkManager.Singleton.LocalTime.TimeAsFloat - NetworkManager.Singleton.ServerTime.TimeAsFloat; 47 | } 48 | 49 | /// 50 | /// Gets the ping value between server and client as a tick 51 | /// 52 | /// The ping value as a int 53 | public static int GetPingTick() { 54 | return (int)(GetPing()/Singleton.minTimeBetweenTicks); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO 2 | This is an implementation of Rigidbody Network Prediction and Reconciliation for Unity Netcode for Gameobjects. Prediction and Reconciliation help hide input latency over the network as they allow the client to apply their inputs instantly and then send them to the server. If the client and server desync, the server's state will override the clients. However, this is a bit difficult to do with Rigidbodies over the network, so I made a few scripts to simplify the process. This is made with Unity's Netcode for Game Objects in mind; however, it can be easily modified to work in any networking solution that is capable of sending RPCs with generic types. 3 | 4 | # Demo: 5 | Note that all the demos are from the client's view. The server does not see any jitter or snapping, as it has the final say in the game state. 6 | At the start of each video, the left side is the player on the client, and the right side is the host machine's player. 7 | All added delays are artificial and are done through NGO's built-in tools. 8 | 9 | ## Over LAN network 10 | 11 | https://github.com/Apollo99-Games/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/f038bd56-6227-4078-ac1e-163c6b721015 12 | 13 | ## Over LAN network + 200 MS RTT delay 14 | 15 | https://github.com/Apollo99-Games/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/1d4feb48-3c41-4b7a-8b7c-8dfb1b1a06b4 16 | 17 | ## Over LAN network + 100 MS RTT delay + 5% Packet loss (both receiving and sending) 18 | Note how the client's cube doesn't look like it's teleporting (unless there is a collision). This is because the program compensates for packet loss by sending redundant inputs. 19 | However, the host (server) does not send redundant information about its player or any other player to the clients. 20 | This is to save on bandwidth, but results in a lot of corrections, especially when there are collisions. 21 | 22 | https://github.com/Apollo99-Games/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/e219ebf5-1fdd-4362-b97f-2acecc84a7f6 23 | 24 | # Programs Needed: 25 | - All the scripts in this GitHub Repository 26 | - Unity Editor 2022.3.17f1 27 | - Netcode for GameObjects 1.7.1 28 | 29 | # Prerequisites For Creating a Demo Scene for Rigidbody Network Prediction and Reconciliation using a basic cube: 30 | - In the Unity editor go to Edit -> Project Settings -> Physics -> Enable Enhanced Determinism 31 | - Have the Network Manager from the NGO setup 32 | - Add a plane with a box collider at coordinates (0, -1, 0) and with a scale of (1000, 1000, 1000) 33 | - Create a box prefab with a box collider, Rigidbody with a mass of 5kg, and scale of (2, 2, 2). The values can be anything you want. It would be helpful if the box is a different colour than the plane. 34 | 35 | https://github.com/Apollo99-Games/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/de89529f-cfa7-4fb2-844e-5814591322af 36 | 37 | 38 | # Setting up the Prediction Manager: 39 | - Create an empty game object and call it "Prediction Manager." 40 | - Add the PredictionTick and PredictionManager Components to the game object. 41 | - We will leave the values as default, but if you would like more info on what they do, hover over them with your mouse in the editor 42 | 43 | https://github.com/Apollo99-Games/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/16c39ffc-f9b7-4d76-b498-dcfafd00d8da 44 | 45 | 46 | 47 | # Setting up the InputPayload for the Cube: 48 | The cube we created before will be our player. First, let's create the payloads that will be sent between the server and the client. 49 | The Client will send inputs to the server. You can have anything here as the input. However, you must use the IPayLoad interface, which forces you to implement tick and ID methods. 50 | Note: The objectID needs to be Serialized but not the tick. 51 | Here is an example: 52 | 53 | ```cs 54 | public struct BoxInputPayload: IPayLoad 55 | { 56 | // This tick is important so the client knows at which time the desync happened in order to rewind and correct it 57 | private int tick; 58 | // This is our input from the arrow keys on the keyboard 59 | public float vertical; 60 | public float horizontal; 61 | // This is the objectID. Ensures the input is applied to the correct object 62 | private byte objectID; 63 | 64 | public int Tick { get => tick; set => this.tick = value; } 65 | 66 | public byte ObjectID { get => objectID; set => this.objectID = value; } 67 | 68 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 69 | serializer.SerializeValue(ref objectID); 70 | serializer.SerializeValue(ref vertical); 71 | serializer.SerializeValue(ref horizontal); 72 | } 73 | } 74 | ``` 75 | 76 | # Setting up the StatePayload for the Cube: 77 | This is the final state the server will send to the client to ensure they are synced. The methods required here are the same as the input method. 78 | Here is an example of a state: as we are syncing a rigid body, we need to send over the velocity and angular velocity to the client: 79 | ```cs 80 | public struct BoxStatePayload: IPayLoad 81 | { 82 | private int tick; 83 | private byte objectID; 84 | public Vector3 position; 85 | public Quaternion rotation; 86 | public Vector3 velocity; 87 | public Vector3 angularVelocity; 88 | 89 | public int Tick { get => tick; set => this.tick = value; } 90 | 91 | public byte ObjectID { get => objectID; set => this.objectID = value; } 92 | 93 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 94 | serializer.SerializeValue(ref objectID); 95 | serializer.SerializeValue(ref position); 96 | serializer.SerializeValue(ref rotation); 97 | serializer.SerializeValue(ref velocity); 98 | serializer.SerializeValue(ref angularVelocity); 99 | } 100 | } 101 | ``` 102 | 103 | # Adding the input and state payloads to the messages: 104 | The PayLoad.cs file contains a Message class. This is used to compile all the inputs and states of all prediction objects into one class. 105 | We must add a list containing our payloads for the program to add our inputs and states. For Example: 106 | 107 | ```cs 108 | 109 | public class Message: EventArgs { 110 | // Our stuff 111 | public List states; 112 | public List inputs; 113 | 114 | // Required stuff 115 | public int tick; 116 | public ushort OwnerID; 117 | public ClientRpcParams sendParams; 118 | 119 | public StateMessage() { 120 | states = new List(); 121 | inputs = new List(); 122 | tick = 0; 123 | OwnerID = 0; 124 | } 125 | 126 | public Message(WorldStatePayload worldPayload) { 127 | this.states = new List(worldPayload.states); 128 | this.inputs = new List(worldPayload.inputs); 129 | this.tick = worldPayload.tick; 130 | } 131 | 132 | public Message(WorldInputPayload worldPayload) { 133 | this.inputs = new List(worldPayload.inputs); 134 | this.tick = worldPayload.tick; 135 | } 136 | } 137 | ``` 138 | # Adding the input and state packets to the world packets: 139 | Once the states and inputs have been compiled in the message, they are converted to structs so they can be sent over to the client/server: 140 | Note that the PayLoads do not need an OwnerID; it is only needed in the message class. 141 | 142 | ```cs 143 | public struct WorldInputPayload: INetworkSerializable 144 | { 145 | public int tick; 146 | //The inputs to be sent over to the server 147 | public BoxInputPayload[] inputs; 148 | 149 | public static WorldInputPayload Create(InputMessage inputMessage) { 150 | WorldInputPayload holder = new WorldInputPayload(); 151 | holder.inputs = inputMessage.inputs.ToArray(); 152 | holder.tick = inputMessage.tick; 153 | 154 | return holder; 155 | } 156 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 157 | serializer.SerializeValue(ref inputs); 158 | serializer.SerializeValue(ref tick); 159 | } 160 | } 161 | 162 | public struct WorldStatePayload: INetworkSerializable 163 | { 164 | public int tick; 165 | //This stores the inputs and states all the objects needed to be sent from the server to all clients 166 | public BoxStatePayload[] states; 167 | public BoxInputPayload[] inputs; 168 | 169 | public static WorldStatePayload Create(StateMessage statemessage) { 170 | WorldStatePayload holder = new WorldStatePayload(); 171 | holder.states = statemessage.states.ToArray(); 172 | holder.inputs = statemessage.inputs.ToArray(); 173 | holder.tick = statemessage.tick; 174 | return holder; 175 | } 176 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 177 | serializer.SerializeValue(ref states); 178 | serializer.SerializeValue(ref inputs); 179 | serializer.SerializeValue(ref tick); 180 | } 181 | } 182 | ``` 183 | 184 | # Creating the movement logic Class: 185 | Now that we have created the data to be sent, we can start writing the movement logic to control our cube. Let's create a new class called "CubeMove." 186 | We will ensure it inherits the PredictionObject. The first generic will be our input payload; the next will be the state. For example: 187 | 188 | ```cs 189 | // Unity Netcode for Gameobjects requires tags when working with generics 190 | [GenerateSerializationForTypeAttribute(typeof(BoxInputPayload))] 191 | [GenerateSerializationForTypeAttribute(typeof(BoxStatePayload))] 192 | public class CubeMove : PredictionObject 193 | { 194 | // As we are working with a Rigid body, we will need a reference for that component 195 | [SerializeField] Rigidbody _rigidbody; 196 | } 197 | ``` 198 | 199 | # Getting and setting the input: 200 | Inside this class, we will override the GetInput() method to write the logic in order to get the user input and return it as an InputPayload 201 | 202 | ```cs 203 | public override BoxInputPayload GetInput() 204 | { 205 | float horizontalInput = Input.GetAxis("Horizontal"); 206 | float verticalInput = Input.GetAxis("Vertical"); 207 | 208 | BoxInputPayload inputPayload = new BoxInputPayload(); 209 | inputPayload.horizontal = horizontalInput; 210 | inputPayload.vertical = verticalInput; 211 | 212 | return inputPayload; 213 | } 214 | ``` 215 | 216 | Then, we will override the SetInput() function to apply our input. Here, we will apply a force on the cube. 217 | ```cs 218 | public override BoxInputPayload SetInput(BoxInputPayload inputPayload) 219 | { 220 | _rigidbody.AddForce(new Vector3(inputPayload.horizontal, 0f, inputPayload.vertical) * 100f); 221 | } 222 | ``` 223 | 224 | # Getting and setting the states: 225 | We will now do the same with the states: 226 | ```cs 227 | public override BoxStatePayload GetState() { 228 | return new BoxStatePayload() 229 | { 230 | position = _rigidbody.position, 231 | rotation = _rigidbody.rotation, 232 | velocity = _rigidbody.velocity, 233 | angularVelocity = _rigidbody.angularVelocity 234 | }; 235 | } 236 | 237 | public override void SetState(BoxStatePayload statePayload) { 238 | _rigidbody.position = statePayload.position; 239 | _rigidbody.rotation = statePayload.rotation; 240 | _rigidbody.velocity = statePayload.velocity; 241 | _rigidbody.angularVelocity = statePayload.angularVelocity; 242 | 243 | // This is not needed in this case. In objects involving more complex physics, such as ray casts, it could help prevent jitter 244 | transform.position = statePayload.position; 245 | transform.rotation = statePayload.rotation; 246 | } 247 | ``` 248 | 249 | # Defining the conditions for a Reconcile 250 | We must define what conditions constitute a desync that requires a correction. We will do this by overriding the ShouldReconciliate() method. 251 | It gives us two parameters: the latest received server State (which is in the past due to latency) and the client state that happened around that time. 252 | ```cs 253 | public override bool ShouldReconcile(BoxStatePayload latestServerState, BoxStatePayload ClientState) { 254 | // We will get the error in rotation and position between the server and client states 255 | float positionError = Vector3.Distance(latestServerState.position, ClientState.position); 256 | float rotDif = 1f - Quaternion.Dot(latestServerState.rotation, ClientState.rotation); 257 | 258 | //If the error is above a certain threshold, we will tell the client to correct itself to sync with the server 259 | if (positionError > 0.01f || rotDif > 0.001f) { 260 | return true; 261 | } 262 | return false; 263 | } 264 | ``` 265 | 266 | # Sending and receiving inputs 267 | First, we will write the logic to send all the inputs from the client to the server 268 | We will override the SendClientInputsToServer() method to add all our input payloads to the input list we created in the input message: 269 | ```cs 270 | public override void SendClientInputsToServer(List inputPayloads, InputMessage sender) 271 | { 272 | sender.inputs.AddRange(inputPayloads); 273 | } 274 | ``` 275 | Now we will apply the inputs we received from the client on the server: 276 | ```cs 277 | public override void ReceiveClientInputs(InputMessage receiver) 278 | { 279 | // We will find all the inputs that have the ID associated with this object 280 | List objectInputs = PayLoadBuffer.FindObjectItems(receiver.inputs, ObjectID); 281 | // We will add it to the client inputs, where it will eventually be applied 282 | AddClientInputs(objectInputs, receiver.tick); 283 | } 284 | ``` 285 | 286 | # Sending and receiving states 287 | It is a similar process for states but requires one extra method called SortStateToSendToClient(). This extra method allows you to compare all the states of all objects at once. 288 | This will give you more control over what you want to send, as the state payload costs a lot of bandwidth. We must only send what we need. 289 | For example, if one client is far away from another, there is no point of sending these two clients the state about each other. 290 | 291 | ```cs 292 | public override void ReceiveServerState(StateMessage receiver) { 293 | ApplyServerInputs(PayLoadBuffer.FindObjectItems(receiver.inputs, ObjectID), receiver.tick); 294 | ApplyServerState(PayLoadBuffer.FindObjectItem(receiver.states, ObjectID), receiver.tick); 295 | } 296 | 297 | public override void CompileServerState(BoxStatePayload statePayload, List inputPayloads, StateMessage Compiler) { 298 | Compiler.states.Add(statePayload); 299 | Compiler.inputs.AddRange(inputPayloads); 300 | } 301 | 302 | public override void SortStateToSendToClient(StateMessage defaultWorldState, StateMessage sender) { 303 | //Here, we just send all the states and inputs. Ideally, you would only want to send what is needed 304 | sender.states.AddRange(defaultWorldState.states); 305 | sender.inputs.AddRange(defaultWorldState.inputs); 306 | } 307 | ``` 308 | Note that we can also send inputs to all the clients here. However, be careful when applying movement from other clients on devices that are not on the server. 309 | As the clients already get the state, they will then apply the input on top of that, essentially doing the same thing twice. This can result in jitter. 310 | One potential fix is to only apply any input related to movement on the object owner or the server. For example: 311 | ```cs 312 | public override BoxInputPayload SetInput(BoxInputPayload inputPayload) 313 | { 314 | if (IsOwner || IsServer) { 315 | // the movement code 316 | _rigidbody.AddForce(new Vector3(inputPayload.horizontal, 0f, inputPayload.vertical) * 100f); 317 | } 318 | } 319 | ``` 320 | 321 | Here is an example of using the SortStateToSendToClient() function to send only information about players that are close to each other: 322 | ```cs 323 | public override void SortStateToSendToClient(Message defaultWorldState, Message sender) { 324 | for (int i = 0; i < defaultWorldState.states.Count; i++) 325 | { 326 | // If the object data is already in the sender, we don't need to do anything, so skip it so we don't have any duplicates. 327 | if (PayLoadBuffer.ContainsID(sender.states, defaultWorldState.states[i].ObjectID)) continue; 328 | 329 | //If we find this object's data, we will add it to the sender 330 | if (defaultWorldState.states[i].ObjectID == ObjectID ) { 331 | sender.states.Add(defaultWorldState.states[i]); 332 | } 333 | 334 | // For all the other objects, we will make sure they are within 100 meters; if not, we won't send their data as they are too far. 335 | else if (Vector3.Distance(defaultWorldState.states[i].position, GetState().position) < 100) { 336 | sender.states.Add(defaultWorldState.states[i]); 337 | sender.inputs.AddRange(PayLoadBuffer.FindObjectItems(defaultWorldState.inputs, defaultWorldState.states[i].ObjectID)); 338 | } 339 | } 340 | } 341 | ``` 342 | 343 | # Interpolation 344 | This demo provides a basic interpolator. You would probably want to write your own, as this one is meant for testing but works. 345 | We will not be interpolating the actual rigid body but simply the visual. To do this: 346 | - create a new cube in your cube prefab made earlier and call it "visual." Make sure both cubes are identical (colour and scale). 347 | - Remove Mesh Renderer and filter scripts from the parent. Add the CubeMove script to the parent 348 | - Add the Interpolator script to the "visual" and pass a reference of it to our CubeMove script 349 | - Finally, we will write this logic that runs right after the physics simulation in the CubeMove script that sends the cube's new state to the interpolator for interpolation 350 | 351 | 352 | https://github.com/Apollo99-Games/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/bd4b4a4d-92b8-412a-a0b5-ef8a7389105f 353 | 354 | 355 | ```cs 356 | public override void OnPostSimulation(bool DidRunPhysics) 357 | { 358 | // No point in sending the position and rotation if the client is reconciliation as we won't see it anyway 359 | if (!PredictionManager.Singleton.IsReconciling && interpolator != null) { 360 | // NeededToReconcile would return a bool if the client did reconciliation sometime during this tick 361 | interpolator.AddPosition(GetState().position, NeededToReconcile); 362 | interpolator.AddRotation(GetState().rotation); 363 | } 364 | } 365 | ``` 366 | 367 | # Compressing Redundant Inputs 368 | Because of packet loss, we have to send redundant inputs to the server to mitigate this. This could increase bandwidth. 369 | The input may often be the same as the last, so we can remove these redundant copies before sending them to the client and duplicate them once the server receives them. 370 | To do this, we will have to modify our input payload and the receiver and sender methods for inputs: 371 | ```cs 372 | public struct BoxInputPayload: ICompressible 373 | { 374 | //The number of duplicates this input had 375 | private byte numberOfCopies; 376 | 377 | //Our previous input code goes here 378 | 379 | public byte NumberOfCopies { get => numberOfCopies; set => this.numberOfCopies = value; } 380 | 381 | public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { 382 | //Our previous code goes here 383 | serializer.SerializeValue(ref numberOfCopies); 384 | } 385 | } 386 | ``` 387 | For our Send and Receive functions, we will merely pass them into the compress function, and that's it: 388 | ```cs 389 | public override void SendClientInputsToServer(List inputPayloads, InputMessage sender) 390 | { 391 | PayLoadBuffer.Compress(inputPayloads); 392 | sender.inputs.AddRange(inputPayloads); 393 | } 394 | 395 | public override void ReceiveClientInputs(InputMessage receiver) 396 | { 397 | List objectInputs = PayLoadBuffer.FindObjectItems(receiver.inputs, ObjectID); 398 | PayLoadBuffer.Decompress(objectInputs); 399 | AddClientInputs(objectInputs, receiver.tick); 400 | } 401 | ``` 402 | Here is an example of a server receiving input from a client with no compression. Note redundant inputs is set to 9 in all the images: 403 | ![no compression](https://github.com/ApolloGames99/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/2065738c-3a08-4815-8b43-ed82d650c9ab) 404 | 405 | Here are the inputs above but with compression ON (the InputPayLoad is the same, but not necessarily the values). 406 | Note the peaks here will be higher as we need an extra byte to store the number of copies: 407 | ![Max bytes compression](https://github.com/ApolloGames99/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/69e4d350-ca6a-4e02-b2ff-ca9d893a33a7) 408 | 409 | However, the troughs will be a lot lower: 410 | ![Min bytes compression](https://github.com/ApolloGames99/Rigidbody-Network-Prediction-and-Reconciliation-for-Unity-NGO/assets/163193765/a9929459-1afd-4eab-a984-8299db6e9acf) 411 | 412 | This is most useful when you have large input packets with lots of data and redundant inputs. 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | --------------------------------------------------------------------------------