Search Unity

Client Side Prediction *Code Snippets*

Discussion in 'Multiplayer' started by Mohican, Oct 6, 2012.

  1. Mohican

    Mohican

    Joined:
    May 17, 2012
    Posts:
    49
    I am working on an online FPS/MELEE action game, and was dismayed to find that Unity does not offer native support for Authoritative Server + Client Side Prediction.

    While getting an Authoritative Server working is not a big deal (plenty of free examples around), there is virtually no info on Client Side Prediction (even inside ridiculously priced asset store "network tutorials").

    So I went through the pain of writing some Client Side Prediction code, and while this is very much *WIP*, the results so far are quite satisfactory.

    I have decided to post my code here for free, because frankly I would not want others to go through this painful trial and error process over and over again. I am only posting code snippets, which means it is up to you to setup the Authoritative Server project. It time allows, I will in the future try to produce a simple "example" project.

    You project should basically consists of:
    (1) RPC loading the scene from Server to Clients.
    (2) Network.Instantiate() Character Object on server (if should contain the NetworkInterp component snippet, and your own PlayerController component).
    (3) Network.Instantiate() Input Object on client (it should contain the NetworkInput component snippet).
    (4) if (Network.isServer || clientPrediction), run physics Update() on playerController using the Data Fetchers of the Input Object

    **Remember to attach Network Views to the Network components**

    Acknowledgment: The code inside NetworkInterp() started out from some non-authoritative code someone else wrote (who? I don't know...).


    Code (csharp):
    1.  
    2. /*--------------------------------------------------
    3.     Copyright (c) 2012 - SavageLive Development Team
    4.         Author: Anthony Beaucamp (aka Mohican)
    5.         Email: ant_the_mohican@hotmail.com
    6.  
    7.     This Class forwards Player Inputs to Server.
    8.     Input Objects are NetworkInstantiated by Remote Clients.
    9. --------------------------------------------------*/
    10.  
    11. #pragma strict
    12.  
    13. enum inputStates { fwd, bwd, left, right, button0, button1, sprint, walk, jump, enter, order,
    14.                    inv0, inv1, inv2, inv3, inv4, inv5, inv6, inv7, inv8, inv9 };
    15.  
    16. class NetworkInput extends MonoBehaviour
    17. {
    18.     // Public Variables
    19.     public var localInput = false;
    20.  
    21.     // Input States
    22.     private var progAxes = Vector3.zero;   
    23.     private var currAxes = Vector3.zero;
    24.     private var currStates = new boolean[21];
    25.     private var prevStates = new boolean[21];
    26.     private var frameID = 0;
    27.    
    28.        
    29.     ////////////////
    30.     // Unity Events
    31.     function Awake()
    32.     {
    33.         // Re-initialize Variables
    34.         frameID = 0;
    35.     }
    36.  
    37.     function Update()  
    38.     {
    39.         if (!localInput)
    40.             return;
    41.            
    42.         // Progressively update Mouse (for smooth camera)
    43.         var yDir:int = PlayerPrefs.GetInt("mouseInverted", 1) > 0 ? 1.0 : -1.0;
    44.         var mouseSensitivity:float = PlayerPrefs.GetFloat("mouseSensitivity", 15.0);
    45.         progAxes = Vector3(Mathf.Repeat(progAxes[0]+Input.GetAxis("Mouse X")*mouseSensitivity, 360.0),
    46.                            Mathf.Clamp(progAxes[1]+Input.GetAxis("Mouse Y")*mouseSensitivity*yDir, -30.0, 60.0),
    47.                            Input.GetAxis("Mouse Z"));      
    48.        
    49.         // Input running from local server don't serialize data...
    50.         if (Network.isServer)
    51.             ProcessLocalInput();
    52.     }
    53.    
    54.     ////////////////////////////////
    55.     // Network Data Synchronization
    56.     function OnSerializeNetworkView(stream:BitStream, info:NetworkMessageInfo)
    57.     {
    58.         var i:int;
    59.                
    60.         // Serialize Input Data
    61.         if (stream.isWriting) {
    62.             // Process Input State
    63.             ProcessLocalInput();   
    64.                
    65.             // Outgoing data
    66.             stream.Serialize(frameID);
    67.             stream.Serialize(currAxes);
    68.             for (i=0; i<currStates.length; ++i)
    69.                 stream.Serialize(currStates[i]);
    70.         } else {
    71.             // Check frame order
    72.             var sentID:int;
    73.             stream.Serialize(sentID);
    74.             if (sentID < frameID) {
    75.                 Debug.Log("NetworkInput:OnSerializeNetworkView - Received out-of-order frame.");
    76.                 return;
    77.             } else {
    78.                 frameID = sentID;
    79.             }
    80.            
    81.             // Save previous Input States
    82.             for (i=0; i<currStates.length; ++i)
    83.                 prevStates[i] = currStates[i];
    84.                
    85.             // Incoming data
    86.             stream.Serialize(currAxes); progAxes = currAxes;
    87.             for (i=0; i<currStates.length; ++i)        
    88.                 stream.Serialize(currStates[i]);           
    89.         }      
    90.     }  
    91.  
    92.     ///////////////////////
    93.     // Process Local Input
    94.     function ProcessLocalInput()
    95.     {                  
    96.         // Save Input States
    97.         for (var i=0; i<currStates.length; ++i)
    98.             prevStates[i] = currStates[i];
    99.         frameID++;             
    100.        
    101.         // Update changed Input States
    102.         UpdateState(inputStates.fwd, Input.GetAxis("Vertical")>0);
    103.         UpdateState(inputStates.bwd, Input.GetAxis("Vertical")<0);
    104.         UpdateState(inputStates.left, Input.GetAxis("Horizontal")<0);
    105.         UpdateState(inputStates.right, Input.GetAxis("Horizontal")>0);
    106.        
    107.         UpdateState(inputStates.button0, Input.GetKey(KeyCode.Mouse0));
    108.         UpdateState(inputStates.button1, Input.GetKey(KeyCode.Mouse1));
    109.         UpdateState(inputStates.sprint, Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift));
    110.         UpdateState(inputStates.walk, Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl));
    111.         UpdateState(inputStates.jump, Input.GetButton("Jump"));
    112.         UpdateState(inputStates.enter, Input.GetKey(KeyCode.E));
    113.         UpdateState(inputStates.order, Input.GetKey(KeyCode.F));
    114.        
    115.         UpdateState(inputStates.inv0, Input.GetKey(KeyCode.Alpha1));
    116.         UpdateState(inputStates.inv1, Input.GetKey(KeyCode.Alpha2));
    117.         UpdateState(inputStates.inv2, Input.GetKey(KeyCode.Alpha3));
    118.         UpdateState(inputStates.inv3, Input.GetKey(KeyCode.Alpha4));
    119.         UpdateState(inputStates.inv4, Input.GetKey(KeyCode.Alpha5));
    120.         UpdateState(inputStates.inv5, Input.GetKey(KeyCode.Alpha6));
    121.         UpdateState(inputStates.inv6, Input.GetKey(KeyCode.Alpha7));
    122.         UpdateState(inputStates.inv7, Input.GetKey(KeyCode.Alpha8));
    123.         UpdateState(inputStates.inv8, Input.GetKey(KeyCode.Alpha9));
    124.         UpdateState(inputStates.inv9, Input.GetKey(KeyCode.Alpha0));
    125.     }
    126.  
    127.     function UpdateState(ID, val)
    128.     {
    129.         // Only register state changes
    130.         if (prevStates[ID] != val)
    131.             currStates[ID] = val;
    132.     }
    133.    
    134.     /////////////////  
    135.     // Data Fetchers
    136.     function GetCurrAxes() : Vector3
    137.     {
    138.         return currAxes;
    139.     }          
    140.     function GetProgAxes() : Vector3
    141.     {
    142.         return progAxes;
    143.     }
    144.     function GetState(state:int) : boolean
    145.     {
    146.         return currStates[state];
    147.     }          
    148.     function SetState(state:int, val:boolean)
    149.     {
    150.         currStates[state] = val;
    151.     }  
    152.     function SwitchOn(state:int) : boolean
    153.     {
    154.         return (currStates[state]  !prevStates[state]);
    155.     }
    156.     function SwitchOff(state:int) : boolean
    157.     {
    158.         return (!currStates[state]  prevStates[state]);
    159.     }
    160. }
    161.  
    Code (csharp):
    1.  
    2. /*--------------------------------------------------
    3.     Copyright (c) 2012 - SavageLive Development Team
    4.         Author: Anthony Beaucamp
    5.         Email: [email]ant_the_mohican@hotmail.com[/email]
    6.  
    7.     This Class interpolates the Network position of Remote Clients.
    8.     It also adjusts local Client position by comparing previous Server
    9.     and Client position snapshots (see clientPrediction flag).
    10. --------------------------------------------------*/
    11.  
    12. #pragma strict
    13.  
    14. class NetworkInterp extends MonoBehaviour
    15. {  
    16.     private class Snapshot
    17.     {
    18.         var timestamp : double;
    19.         var position : Vector3;
    20.         var rotation : Quaternion;
    21.     }
    22.  
    23.     // Client-side prediction
    24.     public var clientPrediction = false;    
    25.                    
    26.     // Store 99 client/server snapshots
    27.     private var svSnapshotBuffer = new Snapshot[99];
    28.     private var clSnapshotBuffer = new Snapshot[99];
    29.    
    30.     // Delta position/correction logics
    31.     private var deltaVect = Vector3.zero;
    32.     private var deltaThreshMin = 0.1;       // Min threshold value (when not moving)
    33.     private var deltaThreshMax = 0.5;       // Max threshold value (when moving)
    34.     private var deltaThreshTime = 0.0;      // Last time of threshold violation (when moving)
    35.     private var deltaThreshPeriod = 0.33;   // Max period of threshold violation (when moving)
    36.     private var deltaLerpState = 0;         // Lerp Correction state (0: disabled, 1: enabled)
    37.    
    38.     // Debug Window
    39.     private var infoRect = Rect(10,10,150,50);
    40.    
    41.     // Linked components
    42.     private var characterController:CharacterController;
    43.     private var playerController:PlayerController;
    44.    
    45.    
    46.     ////////////////
    47.     // Unity Events
    48.     function Awake()
    49.     {
    50.         // Get linked Components
    51.         characterController = GetComponent(CharacterController);
    52.         playerController = GetComponent(PlayerController);     
    53.     }  
    54.  
    55.     // Only run on remote Clients
    56.     function Update()
    57.     {
    58.         if (Network.isServer)
    59.             return;
    60.            
    61.         if (!svSnapshotBuffer[0])
    62.             return;
    63.            
    64.         // Adjust interp delay
    65.         var interpDelay = Mathf.Max(0.1, 0.001*Network.GetAveragePing(Network.connections[0]));
    66.            
    67.         // Use client-side prediction algorithm?
    68.         if (clientPrediction) {
    69.             // Save current Frame in Client Snapshot Buffer
    70.             InsertSnapshot(clSnapshotBuffer, Network.time, transform.localPosition, transform.localRotation);
    71.             //Debug.Log("Client," + Network.time.ToString() + "," + transform.localPosition[0].ToString() + "," + transform.localPosition[2].ToString());
    72.            
    73.             // Compute discrepancy between Client/Server 100ms ago.
    74.             var clDelayed = new Snapshot();
    75.             var svDelayed = new Snapshot();
    76.             var startPos = Vector3.zero;
    77.             var endPos = Vector3.zero;         
    78.             if ( InterpSnapshot(clSnapshotBuffer, Network.time-interpDelay, clDelayed)
    79.                  InterpSnapshot(svSnapshotBuffer, Network.time-interpDelay, svDelayed) ) {
    80.                 // Compute position delta              
    81.                 startPos.Set(transform.localPosition.x, 0.0, transform.localPosition.z);
    82.                 deltaVect.Set(svDelayed.position.x-clDelayed.position.x, 0.0, svDelayed.position.z-clDelayed.position.z);
    83.                
    84.                 // Check if delta above threshold
    85.                 if (deltaVect.magnitude > deltaThreshMax) {
    86.                     if (!deltaLerpState) {
    87.                         deltaLerpState = 1;
    88.                         deltaThreshTime = Network.time;
    89.                     }
    90.                 } else {
    91.                     deltaLerpState = 0;
    92.                 }
    93.                
    94.                 // Check if above max threshold violation period
    95.                 if (deltaLerpState  Network.time > deltaThreshTime+deltaThreshPeriod) {
    96.                     // Slowly LERP Character Controller
    97.                     characterController.Move(Mathf.Min(1.0,Time.deltaTime)*deltaVect.magnitude*deltaVect.normalized);
    98.                 }
    99.  
    100.                 // Check if not moving and above min threshold violation perios
    101.                 var currVelocity = playerController.GetVelocity(); currVelocity.y = 0;
    102.                 if (currVelocity.magnitude < 0.1  deltaVect.magnitude > deltaThreshMin) {              
    103.                     // Quickly Snap Character Controller
    104.                     transform.localPosition.x += Mathf.Min(1.0,4*Time.deltaTime)*deltaVect.magnitude*deltaVect.normalized.x;
    105.                     transform.localPosition.z += Mathf.Min(1.0,4*Time.deltaTime)*deltaVect.magnitude*deltaVect.normalized.z;
    106.                 }
    107.                
    108.                 // Update snapshots with actual adjustment
    109.                 endPos.Set(transform.localPosition.x, 0.0, transform.localPosition.z);
    110.                 AdjustSnapshots(clSnapshotBuffer, endPos-startPos);                
    111.             }
    112.         } else {
    113.             // Interpolate Server Snapshots
    114.             var interpSnapshot = new Snapshot();
    115.             InterpSnapshot(svSnapshotBuffer, Network.time-interpDelay, interpSnapshot);
    116.             transform.localPosition = interpSnapshot.position;
    117.             transform.localRotation = interpSnapshot.rotation;
    118.         }
    119.     }
    120.    
    121.     function OnGUI()
    122.     {
    123.         if (deltaVect.magnitude > 0)
    124.             infoRect = GUILayout.Window(345, infoRect, InfoWindow, "");
    125.     }
    126.    
    127.     function InfoWindow(id:int)
    128.     {
    129.         GUILayout.Label(String.Format("Position Delta : {0,3:f3}", deltaVect.magnitude));
    130.         if (deltaLerpState  Network.time > deltaThreshTime+deltaThreshPeriod)
    131.             GUILayout.Label("LERPING");
    132.         var currVelocity = playerController.GetVelocity(); currVelocity.y = 0;
    133.         if (currVelocity.magnitude < 0.1  deltaVect.magnitude > deltaThreshMin)
    134.             GUILayout.Label("SNAPPING");   
    135.     }  
    136.    
    137.    
    138.     ////////////////////////////////
    139.     // Network Data Synchronization
    140.     function OnSerializeNetworkView(stream:BitStream, info:NetworkMessageInfo)
    141.     {
    142.         var position : Vector3;
    143.         var rotation : Quaternion;
    144.  
    145.         if (stream.isWriting) {
    146.             // Outgoing data
    147.             position = transform.localPosition;
    148.             rotation = transform.localRotation;
    149.             stream.Serialize(position);
    150.             stream.Serialize(rotation);
    151.         } else {
    152.             // Incoming data
    153.             stream.Serialize(position);
    154.             stream.Serialize(rotation);
    155.            
    156.             // Save Frame in Server Snapshot Buffer (with delay of 50% ping)
    157.             if (!InsertSnapshot(svSnapshotBuffer, info.timestamp-Network.GetAveragePing(Network.connections[0])*(0.001/2), position, rotation))
    158.                 Debug.Log("NetworkInterp:OnSerializeNetworkView - Received out-of-order frame.");
    159.             //Debug.Log("Server," + info.timestamp + "," + position[0].ToString() + "," + position[2].ToString());
    160.             //Debug.Log("Ping," + info.timestamp + "," + Network.GetAveragePing(Network.connections[0]));  
    161.         }
    162.     }
    163.    
    164.    
    165.     /////////////////////////////
    166.     // Snapshot Buffer Functions
    167.     function InsertSnapshot(buffer:Snapshot[], timestamp:double, position:Vector3, rotation:Quaternion) : boolean
    168.     {
    169.         // Find timestamp insertion point
    170.         var index = 0;
    171.         while (index < buffer.Length-1  buffer[index]  timestamp < buffer[index].timestamp)
    172.             index++;
    173.  
    174.         // Shift buffer content 1-step right.
    175.         for (var i=buffer.Length-1;i>index;i--)
    176.             buffer[i] = buffer[i-1];
    177.        
    178.         // Save received snapshot in buffer.
    179.         var snapshot = new Snapshot();
    180.         snapshot.timestamp = timestamp;
    181.         snapshot.position = position;
    182.         snapshot.rotation = rotation;
    183.         buffer[index] = snapshot;
    184.        
    185.         return true;
    186.     }
    187.    
    188.     function InterpSnapshot(buffer:Snapshot[], interpTime:double, interpSnapshot:Snapshot)
    189.     {  
    190.         if (!buffer[0])
    191.             return false;
    192.            
    193.         if (buffer[0].timestamp > interpTime) {
    194.             // Find snapshot which matches the interpolation time
    195.             for (var i=1; i<=buffer.Length; i++)
    196.             {
    197.                 // Was this the last snapshot?
    198.                 if (i == buffer.Length || !buffer[i]) {
    199.                     // Important: Use "Set()" to keep the referenced pos/rot instances!!
    200.                     interpSnapshot.timestamp = interpTime;
    201.                     interpSnapshot.position = buffer[i-1].position;
    202.                     interpSnapshot.rotation = buffer[i-1].rotation;
    203.                     return true;
    204.                 }
    205.                            
    206.                 // Check this snaphot's timestamp
    207.                 if (buffer[i].timestamp <= interpTime) {
    208.                     // Get left and right snapshots
    209.                     var lhs = buffer[i];
    210.                     var rhs = buffer[i-1];
    211.                    
    212.                     // Use the time between the two slots to determine if interpolation is necessary
    213.                     // As the time difference gets closer to 100 ms t gets closer to 1 in which case rhs is only used
    214.                     var t = 0.0;
    215.                     var length = rhs.timestamp - lhs.timestamp;
    216.                     if (length > 0.001)
    217.                         t = parseFloat(interpTime - lhs.timestamp) / parseFloat(length);
    218.                     interpSnapshot.timestamp = interpTime;
    219.                     interpSnapshot.position = Vector3.Lerp(lhs.position, rhs.position, t);
    220.                     interpSnapshot.rotation = Quaternion.Slerp(lhs.rotation, rhs.rotation, t);
    221.                     return true;
    222.                 }
    223.             }
    224.         } else {
    225.             // Use extrapolation (For now, we just repeat the last received snapshot).
    226.             interpSnapshot.timestamp = interpTime;
    227.             interpSnapshot.position = buffer[0].position;
    228.             interpSnapshot.rotation = buffer[0].rotation;
    229.             return true;
    230.         }  
    231.     }  
    232.    
    233.     function AdjustSnapshots(buffer:Snapshot[], adjustment:Vector3)
    234.     {  
    235.         for (var i=0; i<buffer.Length; i++)
    236.         {          
    237.             // Was this the last snapshot?
    238.             if (!buffer[i])
    239.                 return;
    240.                
    241.             // Adjust position
    242.             buffer[i].position += adjustment;
    243.         }
    244.     }      
    245. }
    246.  
     
    Last edited: Oct 6, 2012
  2. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    This code is extremely inefficient (using a lot of bandwidth) and possible client side exploits, so it's not very authoritative at all. And I'm not sure where you got your info, but this is a super weird implementation.

    And this is not just taking into account rendering artifacts you will get such as temporal aliasing, etc.
     
    Deleted User likes this.
  3. Mohican

    Mohican

    Joined:
    May 17, 2012
    Posts:
    49
    I am open to constructive criticism, as I said this is WIP.
    But the client-side exploit comment is a bit far fetched (client sends input, server sends back position... have I missed something obvious?)

    Please do feel free to correct/improve my code based on your experience, and share back with others.
    I hope that a few people can come together and make a robust "generic" implementation.

    (For the record: I did not refer to your Lidgren setup when I talked about ridiculously priced Tutorials, $20 is fair imo. But from the description, I understand that it does not contain Client-Side prediction code right?).
     
    Last edited: Oct 7, 2012
  4. gsus725

    gsus725

    Joined:
    Aug 23, 2010
    Posts:
    250
    Here is my code for movement prediction

    Most people will probably say its a hunk of turd but it works pretty good in-game and is simple. The predictions are very accurate but of course if your going really fast and stop suddenly it does overshoot some. But every network prediction code I've seen does that even in AAA games like Halo
     
    Last edited: Oct 7, 2012
  5. Mohican

    Mohican

    Joined:
    May 17, 2012
    Posts:
    49
    Where is it? Seems you forgot to attach it.
     
  6. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    Also, I read the code again today after the PM you sent. You have coupled the rendering with the simulation in your code (it all runs at variable time inside Update) which is a huge issue when doing networked games, it might work for a small example like this, but it causes big troubles when you actually need to build a game and not just move the position and rotation of an object.

    Secondly, yes - the code contains a client side exploit, the most obvious thing I could think to use it for is moving a lot faster then other players.
     
  7. Mohican

    Mohican

    Joined:
    May 17, 2012
    Posts:
    49
    If I don't update the local player's position when rendering, you get noticeable "jerking" of the character (the camera lerps independently, which makes the effect more noticeable).

    I do plan however to introduce a "tick rate" system, compounded with extrapolation (based on position+velocity returned from the physics simulation). That I believe, is how valve does it in Source (https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking).

    Would you tell me which line of code contains the exploit?
     
    Last edited: Oct 8, 2012
  8. Zaddo67

    Zaddo67

    Joined:
    Aug 14, 2012
    Posts:
    489
    Mohican, good on you for sharing your code and getting the discussion going.

    I am struggling with the same problem. I was excited when I saw the heading on this thread because I thought it might provide the code I need.

    I have written a FPS (not published) with simple extrapolation and this works well. See the smartfox FPS sample project for a good example of simple extrapolation. http://docs2x.smartfoxserver.com/ExamplesUnity/fps

    My new game is a car racing game. Simple straightline extrapolation doesn't work as well at high speed. The corrections are too big when cornering. I am currently lerping my way back onto the correct path.

    The math for doing some sort of spline prediction, I unfortunately have to admit, is too difficult for me to understand and code..

    My best hope at the moment is this c++ example. I just found it a few minutes ago. It looks promising.
    http://www.mindcontrol.org/~hplus/epic/
     
    Last edited: Oct 18, 2012
  9. Mohican

    Mohican

    Joined:
    May 17, 2012
    Posts:
    49
    I have been working more on the code since I posted the snippets, and implemented the tick rate.
    I have now mostly jerk free movements, but my FPS involves leaping characters. These leaps are a nightmare for extrapolations (they are short-burst accelerations).

    Thanks for sharing this. The concept idea looks interesting (looks like some form of splining).
    Although if the blue line is just over the edge of a cliff.... it may look weird to see floating cars/players!

    I hope we will eventually manage to come up with a "standard" unity class for client-side prediction.
    Something anyone writing a FPS can use as if it where part of the Unity API...
     
  10. quincunx

    quincunx

    Joined:
    Apr 22, 2012
    Posts:
    15
    Hello everyone!

    Hey Zaddo. I'm also interested in cubic splines for interpolation and after some researching I found a few useful answers and code snippets.

    Here are some functions for creating splines mathematically;
    Note - Bezier2 is a quadratic spline and Bezier3 is a cubic spline, if you didn't realize that immediately.
    Code (csharp):
    1.  
    2. function Bezier2(Start : Vector2, Control : Vector2, End : Vector2 , t :float) : Vector2
    3. {
    4.     return (((1-t)*(1-t)) * Start) + (2 * t * (1 - t) * Control) + ((t * t) * End);
    5. }
    6.  
    7. function Bezier2(Start : Vector3, Control : Vector3, End : Vector3 , t :float) : Vector3
    8. {
    9.     return (((1-t)*(1-t)) * Start) + (2 * t * (1 - t) * Control) + ((t * t) * End);
    10. }
    11.  
    12. function Bezier3(s : Vector2, st : Vector2, et : Vector2, e : Vector2, t : float) : Vector2
    13. {
    14.     return (((-s + 3*(st-et) + e)* t + (3*(s+et) - 6*st))* t + 3*(st-s))* t + s;
    15. }
    16.  
    17. function Bezier3(s : Vector3, st : Vector3, et : Vector3, e : Vector3, t : float) : Vector3
    18. {
    19.     return (((-s + 3*(st-et) + e)* t + (3*(s+et) - 6*st))* t + 3*(st-s))* t + s;
    20. }
    21.  
    ref - http://answers.unity3d.com/questions/12689/moving-an-object-along-a-bezier-curve.html

    Also, this is a really great read if you would like to put your new code into perspective.

    In my case, I think I will be implementing cubic splines for predicting where a player will land when he jumps, and calculating the exact path the player will follow.

    Thank-you for your contributions, Mohican! Your class looks great. Mind I asking if you are going to publicly release the new revised version? (or if I can get my hands on it via pm ;))

    Furthermore, I notice that you never use the y-axis for calculating anything. Are you letting the client simulate gravity and acceleration of the y-axis or is there no jumping and/or falling in your game currently? I assume the latter.
     
  11. Mohican

    Mohican

    Joined:
    May 17, 2012
    Posts:
    49
    I am currently trying to get the code working well in all scenarios (smooth mvt, sudden mvt).
    When I have an acceptable solution, I'd like to consolidate everything into just 1 or 2 classes that Abstract the networking code for Auth Server + Client Prediction.
    Next step will be to make a very simplistic project (no animations, just a block character moving around) demonstrating the Abstraction.

    ATM, I let the client simulate gravity, but it's only a temporary measure.
    My final code needs to sync the y-axis position also.
     
  12. PrimeDerektive

    PrimeDerektive

    Joined:
    Dec 13, 2009
    Posts:
    3,090
    There are no networking packages (except for fholms unfinished FPS networking project, using lidgren) that handle prediction and lag compensation as elegantly as Valve with Unity's built-in networking because it simply isn't possible, unless you roll your own physics solution. Unity does not provide granular enough control over PhysX to roll back the physics simulation for rewind-replay corrections like source does.

    Also I'm not sure what you're on about with the tickrate thing; being that your code is currently dependent on network serialization, your tickrate is already implemented, its the update rate you set in the editor.

    If I was resigned to having to use the built-in networking, I probably wouldn't use 100% input forwarded authoritative control with client-side prediction, because corrections are always going to look funny, and you're going to be constantly correcting when network-aware physics enabled objects are near each other and trying to push each other around... In fact, they'll probably be correcting a lot anyway.

    I'd probably forward predicted positions, and do a bunch of sanity checks on the authoritative server and send back corrections. Like "is this dude moving too fast", "does it make sense that he's flying upward", "why is he underneath the ground".
     
    Last edited: Oct 18, 2012
  13. Zaddo67

    Zaddo67

    Joined:
    Aug 14, 2012
    Posts:
    489
    Here is my current code for moving a network players vehicle/character. It is working quite well. It uses straight line extrapolation, it looks good, but cubic splineing would probably smooth it out a bit more. (Thanks quincunx for formula's above, I will try these when I get a chance).

    Before I settled on this solution, I tried a different approach of sending client inputs and use these in my local vehicle model combined with extrapolated positions. It kind of worked, but, the corrections were too large. As PrimeDirektive points out above, unless you have a physics solution, there is no way to rewind and replay corrections. So this approach was doomed to failure.

    I couldn't work out how to extrapolate rotations using Quaternion's, so I created a messy solution using euler's. If you have a better way to do this, I would really appreciate a code example.

    To use this code. Attach to your transform, call StartReceiving method to begin. Each time you receive a transformation from the other networked player, call ReceivedTransform passing the transform class, I haven't included this, but it is quite simple:
    Vector3 Position
    Quaternion Rotation
    double TimeStamp (synchronised time stamp)
    double ReceivedTimeStamp (Synchronised time stamp of when the transform was received)

    EDIT: Updated code to fix a couple bugs. Also, note original code was taken from smartfox FPS sample and modified extensively.

    Code (csharp):
    1.  
    2. using System;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using UnityEngine;
    6.  
    7.  
    8.  
    9. // This script is used to interpolate and predict a path to simulate a networked players position.
    10. // It takes the last received transform And extrapolates the path and rotation of the vehicle.
    11. // Note, the networked vehicle we see is lagging the actual position.  
    12. // This lag is the time it takes for the transform to be sent over the network.
    13. // We still need to extrapolate a path, so that we can see the expected path the vehicle will take
    14. // until we receive the next transform.
    15. // In the FixedUpdate the script will move the vehicle along the extrapolated path
    16. // Future enhancement suggestions:
    17. // - Use cubic splines to smooth the path.
    18.  
    19. public class NetworkTransformInterpolation : MonoBehaviour
    20. {
    21.  
    22.  
    23.     public static readonly float sendingPeriod = 100f; // save calculating must be same as NetworkTransformSender
    24.  
    25.     private bool running = false;
    26.  
    27.     // We store 6 states with history information and proved additional buffer space for our extrapolated positions
    28.     NetworkTransform[] buffer = new NetworkTransform[13]; // 0-5 are for received transforms, 6-11 are for extrapolating, 12 is for current position
    29.     int bufferLength = 6;
    30.     int bufferCurrent = 12; // Used for current transform position.
    31.  
    32.     Vector3 lastPos = new Vector3();
    33.     double lastTime = 0;
    34.     Quaternion lastRot = new Quaternion();
    35.  
    36.  
    37.     PathPoint[] path = new PathPoint[5];
    38.     int pathLength = 5;
    39.     int pathCurr = -1;
    40.  
    41.  
    42.     // Keep track of what slots are used
    43.     int statesCount = 0;
    44.  
    45.     private double averagePing = 0;
    46.     private int pingCount = 0;
    47.     private double interpolationBackTime = 100;
    48.     private double extrapolatePeriod = 100;
    49.  
    50.     private readonly int averagePingCount = 10;
    51.     private double[] pingValues;
    52.     double pingSum = 0;
    53.     private int pingValueIndex;
    54.     private int pingLastIndex = 0;
    55.  
    56.     private double averageJitter = 0;
    57.     private double[] jitterValues;
    58.     double jitterSum = 0;
    59.  
    60.     // for testing
    61.     public Boolean showExtrapolation = false;
    62.     private Color[] trajColor = new Color[10];
    63.     private int currTraj = -1;
    64.     private int trajCount = 5;
    65.     private int logID = 0;
    66.  
    67.     Quaternion turnNone;
    68.  
    69.     // We call it on remote player to start receiving his transform
    70.     public void StartReceiving()
    71.     {
    72.         pingValues = new double[averagePingCount];
    73.         jitterValues = new double[averagePingCount];
    74.         pingCount = 0;
    75.         pingValueIndex = 0;
    76.         pingSum = 0;
    77.         for (int i = 0; i < averagePingCount; i++)
    78.         {
    79.             pingValues[i] = 0;
    80.             jitterValues[i] = 0;
    81.         }
    82.  
    83.         for (int i = 0; i < pathLength; i++)
    84.         {
    85.             path[i] = new PathPoint();
    86.             buffer[bufferLength + i] = new NetworkTransform();
    87.         }
    88.  
    89.         turnNone = Quaternion.FromToRotation(Vector3.up, Vector3.up);
    90.  
    91.         // Initialise Last Position/Rotation/Time
    92.         lastPos = transform.position;
    93.         lastRot = transform.rotation;
    94.         lastTime = TimeManager.Instance.NetworkTime;
    95.  
    96.         // For testing (when we drop cubes onto road so we can see what has happened)
    97.         trajColor[0] = Color.blue;
    98.         trajColor[1] = Color.cyan;
    99.         trajColor[2] = Color.green;
    100.         trajColor[3] = Color.magenta;
    101.         trajColor[4] = Color.red;
    102.         trajColor[5] = Color.yellow;
    103.         trajColor[6] = Color.grey;
    104.         trajColor[7] = Color.grey;
    105.         trajColor[8] = Color.grey;
    106.         trajColor[9] = Color.grey;
    107.  
    108.         running = true;
    109.  
    110.     }
    111.  
    112.  
    113.     void Update()
    114.     {
    115.         if (Input.GetKeyUp(KeyCode.D))
    116.         {
    117.             showExtrapolation = !showExtrapolation;
    118.         }
    119.  
    120.         // Move vehicle if we have a path to follow
    121.         if (pathCurr > -1)
    122.         {
    123.             // If we are at the end of this path, then extrapolate next leg of the path (should only happen occassionly eg: Jitter jumps)
    124.             if (TimeManager.Instance.NetworkTime > path[pathCurr].end)
    125.             {
    126.                 pathCurr++;
    127.                 if (pathCurr < pathLength)
    128.                     ExtrapolatePosition(pathCurr, extrapolatePeriod); // Just in time extrapolation
    129.                 else pathCurr = -1;
    130.                 print("2nd extrap called");
    131.             }
    132.  
    133.             if (pathCurr > -1)
    134.             {
    135.                 // Move car along path
    136.                 transform.position = path[pathCurr].CurrentPos(TimeManager.Instance.NetworkTime);
    137.                 transform.rotation = path[pathCurr].CurrentRot(TimeManager.Instance.NetworkTime);
    138.  
    139.             }
    140.         }
    141.         else print("Exceed extrapolation: " + TimeManager.Instance.NetworkTime);
    142.  
    143.         // Remember last position, so we can extrapolate from here
    144.         lastPos = transform.position;
    145.         lastRot = transform.rotation;
    146.         lastTime = TimeManager.Instance.NetworkTime;
    147.  
    148.  
    149.         // For Testing. Drop a cube at our current location
    150.         if (showExtrapolation  currTraj > -1) CreateCube(transform.position, transform.rotation, 0.04f, trajColor[currTraj]);
    151.     }
    152.  
    153.     public void ReceivedTransform(NetworkTransform ntransform)
    154.     {
    155.  
    156.         if (!running) return;
    157.  
    158.         // Discard if tranform is out of sequence (Possible with UDP)
    159.         if (statesCount > 1  ntransform.TimeStamp < buffer[0].TimeStamp) return;
    160.  
    161.         // For the first 4 transform received. Move vehicle to current location
    162.         // ** Note - should trigger this if vehicle is snapped to a position
    163.         // eg: resets after going off road
    164.         // Might be able to do this if change of position, is above a given threshhold
    165.         if (statesCount < 5)
    166.         {
    167.             transform.position = ntransform.Position;
    168.             transform.rotation = ntransform.Rotation;
    169.         }
    170.  
    171.         // Shift buffer contents, oldest data erased, 4 becomes 5, ... , 0 becomes 1
    172.         for (int i = bufferLength - 1; i >= 1; i--)
    173.         {
    174.             buffer[i] = buffer[i - 1];
    175.         }
    176.  
    177.         // Save currect received state as 0 in the buffer, safe to overwrite after shifting
    178.         ntransform.ReceivedTimeStamp = TimeManager.Instance.NetworkTime;
    179.         buffer[0] = ntransform;
    180.  
    181.         // Move to next trajectory bucket
    182.         int prevTraj = currTraj;
    183.         currTraj++; if (currTraj == trajCount) currTraj = 0;
    184.  
    185.         // for testing
    186.         if (showExtrapolation  prevTraj > -1)
    187.         {
    188.                 // Show actual trajectory of vehicle as received
    189.             if (ntransform.IsDifferent(buffer[1]))  CreateVector(buffer[0].Position, buffer[1].Position, 0.01f, 0f, trajColor[prevTraj]);
    190.         }
    191.  
    192.         // Calculate ping for this player
    193.         double ping = (TimeManager.Instance.NetworkTime - ntransform.TimeStamp);
    194.         CalculateAveragePing(ping);
    195.  
    196.         // Increment state count but never exceed buffer size
    197.         statesCount = Mathf.Min(statesCount + 1, bufferLength);
    198.  
    199.         logID++;
    200.  
    201.  
    202.         // If we have at least 4 states, then calculate predictive values
    203.         // We will attempt to extrapolate position from last received position, until we expect to get a new position
    204.         // Note, this places the vehicle at a position where it was player lag time ago.  (i.e. We are seeing an approximation of where the vehicle was)
    205.         if (statesCount > 4)
    206.         {
    207.  
    208.             pathCurr = 0;
    209.  
    210.             extrapolatePeriod = sendingPeriod * 1.7; // +(((averageJitter * 2) % Time.fixedDeltaTime) + 1) * Time.fixedDeltaTime;
    211.             ExtrapolatePosition(0, extrapolatePeriod);
    212.            
    213.  
    214.         }
    215.  
    216.     }
    217.  
    218.     public void ExtrapolatePosition(int pathId, double duration)
    219.     {
    220.         Vector3 deltaRot;
    221.         float velocity;
    222.         float acceleration;
    223.         Vector3 rotEnd = new Vector3();
    224.         Quaternion turning;
    225.         double turningDelta;
    226.         Vector3 forward;
    227.         int p0=0, p1=0, p2=0, p3=0;
    228.  
    229.         // p0 is the most recent position, p1 next most recent, etc
    230.         switch (pathId)
    231.         {
    232.             case 0:
    233.                 p0=0; p1=1; p2=2; p3=3;
    234.                 break;
    235.             case 1:
    236.                 p0 = bufferLength; p1 = 0; p2 = 1; p3 = 2;
    237.                 break;
    238.             case 2:
    239.                 p0 = bufferLength+1; p1 = bufferLength; p2 = 0; p3 = 1;
    240.                 break;
    241.             case 3:
    242.                 p0 = bufferLength + 2; p1 = bufferLength+1; p2 = bufferLength; p3 = 0;
    243.                 break;
    244.             case 4:
    245.                 p0 = bufferLength + 3; p1 = bufferLength + 2; p2 = bufferLength + 1; p3 = bufferLength;
    246.                 break;
    247.         }
    248.  
    249.         path[pathId].SetTime(lastTime - interpolationBackTime, buffer[p0].TimeStamp + duration, interpolationBackTime);
    250.  
    251.         float f = (float)(duration / (buffer[p0].TimeStamp - buffer[p1].TimeStamp));
    252.  
    253.         // *** POSITION ***//
    254.  
    255.         path[pathId].pos1 = lastPos;
    256.  
    257.         // Last segment movement
    258.         Vector3 segment1 = buffer[p0].Position - buffer[p1].Position;
    259.         Vector3 segment2 = buffer[p1].Position - buffer[p2].Position;
    260.         Vector3 segment3 = buffer[p2].Position - buffer[p3].Position;
    261.  
    262.         // Current direction vehicle moving (not the front of the vehicle, could be sideways if sliding)
    263.         forward = segment1.normalized;
    264.  
    265.         // Calculate velocity
    266.         velocity = segment1.magnitude / (float)(buffer[p0].TimeStamp - buffer[p1].TimeStamp);
    267.  
    268.         // Calculate acceleration
    269.         float velocity2 = segment2.magnitude / (float)(buffer[p1].TimeStamp - buffer[p2].TimeStamp);
    270.         acceleration = (velocity - velocity2) / (float)(buffer[p0].TimeStamp - buffer[p1].TimeStamp);
    271.  
    272.         // Calculate turning
    273.         turningDelta = (buffer[p0].TimeStamp - buffer[p1].TimeStamp);
    274.         turning = Quaternion.FromToRotation(segment2.normalized, segment1.normalized);
    275.  
    276.         // Turn forward vector
    277.         deltaRot = (segment1.normalized - segment2.normalized);
    278.         deltaRot *= f;
    279.         forward = (segment1 + deltaRot).normalized;
    280.  
    281.         // Distance
    282.         float magnitude = (velocity * (float)duration);
    283.  
    284.         // New extrapolated position
    285.         path[pathId].pos2 = buffer[p0].Position + forward * magnitude;
    286.  
    287.  
    288.         if (showExtrapolation) CreateVector(path[pathId].pos1, path[pathId].pos2, 0.01f, 0.0f, Color.grey);
    289.            
    290.         // *** ROTATION *** //
    291.  
    292.         path[pathId].rot1 = lastRot;
    293.  
    294.  
    295.         // How far did the vehicle rotate between the last 2 states
    296.         deltaRot = (buffer[p0].AngleRotation - buffer[p1].AngleRotation);
    297.         rotEnd.x = ShortestRot(deltaRot.x, buffer[p0].AngleRotation.x, f);
    298.         rotEnd.y = ShortestRot(deltaRot.y, buffer[p0].AngleRotation.y, f);
    299.         rotEnd.z = ShortestRot(deltaRot.z, buffer[p0].AngleRotation.z, f);
    300.         path[pathId].rot2 = Quaternion.Euler(rotEnd);
    301.  
    302.         // Save info into dummy buffer position so we can extrapolate another path leg
    303.         buffer[bufferLength + pathId].Position = path[pathId].pos2;
    304.         buffer[bufferLength + pathId].AngleRotation = rotEnd;
    305.         buffer[bufferLength + pathId].TimeStamp = path[pathId].rt2;
    306.  
    307.  
    308.     }
    309.  
    310.     /// <summary>
    311.     ///
    312.     /// </summary>
    313.     /// <param name="a">How far to turn in degrees</param>
    314.     /// <param name="c">Starting angle in degrees</param>
    315.     /// <param name="f">What fraction or multiple of "a" to turn</param>
    316.     /// <returns></returns>
    317.     public float ShortestRot(float a, float c, float f)
    318.     {
    319.         float b;
    320.  
    321.         // Adjust turning to be represented as angle closest to zero
    322.         // eg: if 350 degrees, change to -10 degrees.
    323.         // i.e 350 degrees = -10 degrees and we want to rotate around the shortest path
    324.         // otherwise we have issues when applying "f".
    325.         // eg: f=0.7, a=350, if we don't change 0.7*350=270, if we 0.7*-10=-9.3
    326.         if (a > 180) b = a - 360f;
    327.         else if (a < -180) b= a + 360f;
    328.         else  b= a;
    329.  
    330.         // Apply fraction or multiple to our turning angle
    331.         b = b * f;
    332.  
    333.         // Turn "c" by our calculated turning angle,
    334.         b = (b + c) % 360;
    335.  
    336.         // always return angle represented as a positive number
    337.         if (b < 0) b += 360;
    338.         return b;
    339.     }
    340.  
    341.     public double AveragePing
    342.     {
    343.         get
    344.         {
    345.             return averagePing;
    346.         }
    347.     }
    348.  
    349.     public double AverageJitter
    350.     {
    351.         get
    352.         {
    353.             return averageJitter;
    354.         }
    355.     }
    356.  
    357.     private void CalculateAveragePing(double ping)
    358.     {
    359.         // Update ping total, by taking off oldest value, and adding new value;
    360.         pingSum -= pingValues[pingValueIndex];
    361.         pingSum += ping;
    362.         pingValues[pingValueIndex] = ping;
    363.         averagePing = pingSum / averagePingCount;
    364.  
    365.         jitterSum -= jitterValues[pingValueIndex];
    366.         double jitter = ping - pingValues[pingLastIndex];
    367.         if (jitter < 0) jitter *= -1;
    368.         jitterValues[pingValueIndex] = jitter;
    369.         jitterSum += jitter;
    370.         averageJitter = jitterSum / averagePingCount;
    371.  
    372.         pingLastIndex = pingValueIndex;
    373.  
    374.         pingValueIndex++;
    375.         if (pingValueIndex >= averagePingCount) { pingValueIndex = 0; UpdateValues(); }
    376.  
    377.     }
    378.  
    379.     private void UpdateValues()
    380.     {
    381.  
    382.         if (averagePing < 40)
    383.         {
    384.             interpolationBackTime = 40;
    385.         }
    386.         else if (averagePing < 80)
    387.         {
    388.             interpolationBackTime = 80;
    389.         }
    390.         else if (averagePing < 120)
    391.         {
    392.             interpolationBackTime = 120;
    393.         }
    394.         else if (averagePing < 170)
    395.         {
    396.             interpolationBackTime = 170;
    397.         }
    398.         else if (averagePing < 250)
    399.         {
    400.             interpolationBackTime = 250;
    401.         }
    402.         else if (averagePing < 400)
    403.         {
    404.             interpolationBackTime = 400;
    405.         }
    406.         else if (averagePing < 600)
    407.         {
    408.             interpolationBackTime = 600;
    409.         }
    410.         else
    411.         {
    412.             interpolationBackTime = 1100;
    413.         }
    414.     }
    415.  
    416.     public class PathPoint
    417.     {
    418.         public Vector3 pos1;  // Most recent received transform
    419.         public Vector3 pos2;  // Extrapolated transform
    420.  
    421.         public Quaternion rot1; // Most recent received transform
    422.         public Quaternion rot2; // extrapolated transfrom
    423.  
    424.         // Our system time for begin/end of segments
    425.         public double t1;
    426.         public double t2;
    427.  
    428.         // Remote players time for begin/end of segments
    429.         public double rt1;
    430.         public double rt2;
    431.  
    432.         // Length of segments
    433.         public double l1;
    434.  
    435.         public double end
    436.         {
    437.             get { return t2; }
    438.         }
    439.  
    440.         public double length;
    441.  
    442.         public void SetTime(double pt1, double pt2, double interpolateBackTime)
    443.         {
    444.  
    445.             if (pt1 > pt2) print("**** Extrapolation Start time after end time ****");
    446.             rt1 = pt1; rt2 = pt2;
    447.  
    448.             t1 = rt1 + interpolateBackTime;
    449.             t2 = rt2 + interpolateBackTime;
    450.  
    451.             l1 = rt2 - rt1;
    452.  
    453.         }
    454.  
    455.         public Vector3 CurrentPos(Double currTime)
    456.         {
    457.             if (currTime < t1)
    458.                 return pos1;
    459.             else if (currTime < t2)
    460.                 return Vector3.Lerp(pos1, pos2, (float)((currTime - t1) / l1));
    461.             else
    462.                 return pos2;
    463.         }
    464.  
    465.         public Quaternion CurrentRot(Double currTime)
    466.         {
    467.  
    468.             if (currTime < t1)
    469.                 return rot1;
    470.             else if (currTime < t2)
    471.                 return Quaternion.Slerp(rot1, rot2, (float)((currTime - t1) / l1));
    472.             else
    473.                 return rot2;
    474.         }
    475.  
    476.     }
    477.  
    478.     public GameObject CreateCube(Vector3 pos, Quaternion rot, float size, Color colour)
    479.     {
    480.         GameObject block = GameObject.CreatePrimitive(PrimitiveType.Cube);
    481.         block.collider.isTrigger = true;
    482.         block.transform.position = pos;
    483.         block.transform.rotation = rot;
    484.         block.transform.localScale = new Vector3(size, size, size);
    485.         block.renderer.material.color = colour;
    486.  
    487.         return block;
    488.     }
    489.  
    490.     public void CreateVector(Vector3 pos1, Vector3 pos2, float size, float height, Color colour)
    491.     {
    492.         Vector3 v1 = pos2 - pos1;
    493.         GameObject block = GameObject.CreatePrimitive(PrimitiveType.Cube);
    494.         block.collider.isTrigger = true;
    495.         block.transform.position = pos1;
    496.         float l1 = Mathf.Sqrt(v1.x * v1.x + v1.z * v1.z);
    497.         block.transform.localScale = new Vector3(size, size, l1);
    498.         block.transform.LookAt(pos2);
    499.         block.transform.position += block.transform.forward * (l1 / 2);
    500.         block.transform.position += block.transform.up * (height / 2f);
    501.         block.renderer.material.color = colour;
    502.  
    503.         GameObject block2 = CreateCube(pos1, block.transform.rotation, size, colour);
    504.         block2.transform.localScale = new Vector3(size, height, size);
    505.         GameObject block3 = CreateCube(pos2, block.transform.rotation, size, colour);
    506.         block3.transform.localScale = new Vector3(size, height, size);
    507.  
    508.  
    509.     }
    510.  
    511.     public double stf(double ltf)
    512.     {
    513.         Int64 zz = (Int64)(ltf / 10000);
    514.         return (double)(ltf - (zz * 10000));
    515.     }
    516.  
    517.  
    518.  
    519. }
    520.  
     
    Last edited: Nov 1, 2012
  14. quincunx

    quincunx

    Joined:
    Apr 22, 2012
    Posts:
    15
    Any updates on your work, Mohican?

    Also, thank you for sharing your code, Zaddo!
    I hope the formula's will help you in the long run :)