Search Unity

Multiplayer physics game where collisions between players is the core

Discussion in 'Multiplayer' started by lorux, May 28, 2019.

  1. lorux

    lorux

    Joined:
    Feb 9, 2017
    Posts:
    31
    Hello, i'm making a real-time multiplayer game which basically consists in players hitting each others over physics collisions. Character velocity is not as fast a racing game but not as slow as an rpg. Each collision is also boosted to get exaggerated bouncing.
    I'm using Unity 2019.1.
    Builtin physics engine (PhysicsX).
    Unity transport library (https://unity.com/solutions/real-time-multiplayer/network-transport-layer). NOT HLAPI.
    There is an authoritative server.
    Also is good to mention thanks to Jared for making this talk at gdc, is gold:


    Here is my netcode so far:
    • Every character has its own RigidBody.
    • Every tick/simulation, packet sending/receiving is run in a fixed time-step at 60hz (client & server).
    • Local player stands out in "future ticks" from server. So if server is at tick 50 the client tick is 50 + RTT + fixedInputBuffer.
    • Local player & peers interpolate character visual model.
    • Local player captures input. Applies it locally predicting movement and hopes for the best.
    • Local player records the tick as input, position & velocity in a buffer. (1024 sized)
    • The input that has been captured also is sent through the network to an authoritative server including the specific tick where the input has occurred.
    • The server receives the input packet and applies it in its own simulation.
    • The server records the tick in its own buffer.
    • The server sends back the input, position & velocity to all clients.
    • The local player which originally generated the input receives the resultant data and compares the tick results to what has recorded.
    • If local player simulation is wrong (different position), goes back in tick history, reset all players (in local simulation) to that given tick and re-simulates everything to the running tick.
    • Peers receives the input and applies it in its own simulation similar to local player.
    • Peers also record its tick history, so when a packet from server is received, it compares the tick results and if the prediction is wrong, makes a reconciliation like the local player.
    • Character visual model is rendered on a tick between last received tick from server and local prediction tick: lastReceivedTickFromServer + (currentLocalPredictionTick - lastReceivedTickFromServer) * ratio. This generates the input to feel more laggish, but hides some of the de-syncs.
    So basically this does not work as well as i would expect. Thats because peer's prediction is never correct, because its data that is unknown in future. Collisions occurs in places that didnt happen in server, so there is so much reconciliation to do, that clients looks jittery. It gets worse on higher latency, making it unplayable.

    I think this type of issue can be found also on racing games.
    Any workaround ideas? Has this issue been solved in any known game?
     
    Last edited: Jun 26, 2019
    reinfeldx and MrHaribo like this.
  2. newlife

    newlife

    Joined:
    Jan 20, 2010
    Posts:
    1,081
    Hello, did you manage to find a fix for this? Im also searching for a good solution for a multiplayer racing game, but we dont need collisions between cars.
    Someone suggested me smartfox server, what do you think?
     
  3. lorux

    lorux

    Joined:
    Feb 9, 2017
    Posts:
    31
    Still working on it and didnt reach an acceptable solution, but i've implemented something that helped to hide some of the de-syncs. Instead of rendering the current frame of the local simulation, i render a frame between the last received from server and the current. So if the current tick (or predicted tick) is 50 and the last received tick (or server tick) is 40 the render tick is: 40 + (50 - 40) * ratio. So that way i hide some of the predicted ticks that may fail. The difference between predicted tick and server tick will be higher depending on a smoothed RTT. The down side of this is that the input feels more laggish.
     
    Last edited: Jun 17, 2019
    RogDolos likes this.
  4. Jos-Yule

    Jos-Yule

    Joined:
    Sep 17, 2012
    Posts:
    292
    We made an online car-combat game (AutoAge: Standoff) and had to deal with the issues of car's colliding with each other. As we are a small studio and needed to "make it good enough", we made some compromises. Each player had local authority over their own vehicle's position. When you _locally_ impacted another player, we would "detach" that other player's network-update loop and allow the local physics engine to update that vehicles Ridgidbody. After a brief (< 0.5s) delay we start to LERP between the "local" physics position and the incoming network updates over another brief span (<0.5s). So you locally see yourself hitting the other car and get good physics sim. However, it does mean that there is a discontinuity between your view of the universe and the other players, equal to the lag between your updates and theirs -- so in your universe you might hit player B, and see that reaction locally, but in player B's universe, you missed the hit. Again, this was a compromise we made to gameplay to keep each individual's experience rich, without worrying about 100% consistency between player worlds. YMMV, my 2c, etc etc, and this worked for the requirements and realities of *our* game, for yours it might not be an option. Good-Luck!
     
  5. lorux

    lorux

    Joined:
    Feb 9, 2017
    Posts:
    31
    I tried that a while ago by using a weight curve for lerping after the collision. The collision looked more natural but leaded into more desyncs. Now that im rendering frames back in time, i could re-implement it. I may look better.
     
  6. MrHaribo

    MrHaribo

    Joined:
    Aug 9, 2012
    Posts:
    16
    Hello. I have been working on a similar problem for a Space Asteroids Game. I have based my implementation also on the talk of Rocket League Networking and also on the Overwatch Networking talk. I will post the core source code of the networked physics simulation below.

    The idea in my implementation is, to rewind & replay all players together using
    Physics2D.Simulate
    from the oldest received authoritive server state. Up to a latency of 100ms the results look promising.

    But i have some problems in my implementation:
    • I cant get smoothing righ in a way that looks good.
    • Also my approach to tell the client to tick faster or slower is not very robust. I have another approach commented out that tries to optimize the server input buffer size, but that one works worse.

    What do you think of my approach? Can this work in a real-world scenario? Any advice for improvement? Or any ideas to solve my remaining problems?

    Simulation main code

    Code (CSharp):
    1.  
    2. using Jnet;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using UnityEngine;
    6. using UnityEngine.SceneManagement;
    7.  
    8. namespace Jsim
    9. {
    10.     public class JsimSimulation : MonoBehaviour
    11.     {
    12.         // Common stuff
    13.         private JnetServer _server;
    14.         private JnetClient _client;
    15.         private bool _isServer => _server != null;
    16.         private bool _isClient => _client != null;
    17.  
    18.         private Dictionary<int, JsimPlayer> _players = new Dictionary<int, JsimPlayer>();
    19.         private Dictionary<int, JsimDynamic> _dynamics = new Dictionary<int, JsimDynamic>();
    20.  
    21.         // Scene Management
    22.         public Scene Scene
    23.         {
    24.             set
    25.             {
    26.                 _simulatedScene = value;
    27.                 _physicsScene = value.GetPhysicsScene2D();
    28.             }
    29.         }
    30.  
    31.         private Scene _simulatedScene;
    32.         private Scene _unsimulatedScene;
    33.  
    34.         private PhysicsScene2D _physicsScene;
    35.  
    36.         // Unsimulated scene thresholds
    37.  
    38.         private float _simulationSceneInnerRange = 400.0f;
    39.         private float _simulationSceneOuterRange = 500.0f;
    40.         private float _simulationSceneTimer;
    41.         private float _simulationSceneInterval = 2.0f;
    42.  
    43.  
    44.         // Timing stuff
    45.         private const float _tickRateDefault = 1f / 60f;
    46.         private float _tickRateAdjustment = 1.0f;
    47.         private float _tickRate => _tickRateDefault * _tickRateAdjustment;
    48.         private float _frameTimer = 0.0f;
    49.  
    50.         private uint _localTickNumber = 0;
    51.  
    52.         private void Awake()
    53.         {
    54.             _server = GetComponent<JnetServer>();
    55.             _client = GetComponent<JnetClient>();
    56.  
    57.             if (_isServer)
    58.             {
    59.                 _server.OnSpawn.AddListener(OnEntitySpawn);
    60.                 _server.OnDespawn.AddListener(OnEntityDespawn);
    61.             }
    62.  
    63.             if (_isClient)
    64.             {
    65.                 _client.OnSpawn.AddListener(OnEntitySpawn);
    66.                 _client.OnDespawn.AddListener(OnEntityDespawn);
    67.  
    68.                 // Create a scene that is not simulated for objects out of view range
    69.  
    70.                 var sceneCreationParameter = new CreateSceneParameters(LocalPhysicsMode.Physics2D);
    71.                 _unsimulatedScene = SceneManager.CreateScene("UnsimulatedScene", sceneCreationParameter);
    72.             }
    73.         }
    74.  
    75.         private void OnDestroy()
    76.         {
    77.             if (_isClient)
    78.                 SceneManager.UnloadSceneAsync(_unsimulatedScene);
    79.         }
    80.  
    81.         private void Update()
    82.         {
    83.             // Fixed update loop
    84.  
    85.             _frameTimer += Time.deltaTime;
    86.             while (_frameTimer >= _tickRate)
    87.             {
    88.                 _frameTimer -= _tickRate;
    89.  
    90.                 if (_isServer)
    91.                     ServerTick();
    92.  
    93.                 if (_isClient)
    94.                     ClientTick();
    95.             }
    96.         }
    97.      
    98.         private void ServerTick()
    99.         {
    100.             if (_physicsScene == null || _physicsScene.IsEmpty() || !_physicsScene.IsValid())
    101.                 return;
    102.  
    103.             foreach (var player in _players.Values)
    104.                 player.ServerTick(_localTickNumber);
    105.  
    106.             foreach (var dynamic in _dynamics.Values)
    107.                 dynamic.ServerTick(_localTickNumber);
    108.  
    109.             _physicsScene.Simulate(_tickRateDefault);
    110.  
    111.             _localTickNumber++;
    112.         }
    113.  
    114.         private void ClientTick()
    115.         {
    116.             if (_physicsScene == null || _physicsScene.IsEmpty() || !_physicsScene.IsValid())
    117.                 return;
    118.  
    119.             // Scene Management Processing
    120.  
    121.             ProcessSceneManagement();
    122.  
    123.             // Local Player Processing
    124.  
    125.             var localPlayer = _players.Values.SingleOrDefault(p => p.Entity.IsLocalPlayer);
    126.  
    127.             TickRateAdjustment(localPlayer);
    128.  
    129.             localPlayer?.ProcessInput(_localTickNumber);
    130.  
    131.             // Entity Simulation
    132.  
    133.             // Resimulate all ticks from the oldest received state
    134.             var minLastRecievedPlayerTick = _players.Count > 0 ? _players.Values.Min(p => p.ClientLastRecievedTick) : uint.MaxValue;
    135.             var minLastReceivedDynamicTick = _dynamics.Count > 0 ? _dynamics.Values.Min(d => d.ClientLastRecievedTick) : uint.MaxValue;
    136.  
    137.             // Cap last received Tick
    138.             var tickStart = (uint)Mathf.Min(minLastRecievedPlayerTick, minLastReceivedDynamicTick);
    139.             tickStart = (uint)Mathf.Max(_localTickNumber - 24, tickStart);
    140.             var tickEnd = _localTickNumber;
    141.  
    142.  
    143.             //Debug.Log($"----------------------TICK {tickStart} - {tickEnd}------------------------------------");
    144.  
    145.             for (uint i = tickStart; i <= tickEnd; i++)
    146.             {
    147.                 foreach (var player in _players.Values)
    148.                     player.ClientTick(i);
    149.  
    150.                 foreach (var dynamic in _dynamics.Values)
    151.                     dynamic.ClientTick(i);
    152.  
    153.                 _physicsScene.Simulate(_tickRateDefault);
    154.  
    155.                 foreach (var player in _players.Values)
    156.                     player.CaptureState(i + 1);
    157.  
    158.                 foreach (var dynamic in _dynamics.Values)
    159.                     dynamic.CaptureState(i + 1);
    160.             }
    161.  
    162.             _localTickNumber++;
    163.         }
    164.  
    165.         private void ProcessSceneManagement()
    166.         {
    167.             if (Time.time < _simulationSceneTimer + _simulationSceneInterval)
    168.                 return;
    169.  
    170.             var anchorPoint = Camera.main.transform.position;
    171.             anchorPoint.z = 0;
    172.  
    173.             foreach (var dynamic in _dynamics.Values)
    174.             {
    175.                 // TODO: Use Squared Magnitude
    176.                 var distance = dynamic.transform.position - anchorPoint;
    177.  
    178.                 if (distance.magnitude < _simulationSceneInnerRange && dynamic.gameObject.scene != _simulatedScene)
    179.                     SceneManager.MoveGameObjectToScene(dynamic.gameObject, _simulatedScene);
    180.  
    181.                 else if (distance.magnitude > _simulationSceneOuterRange && dynamic.gameObject.scene != _unsimulatedScene)
    182.                     SceneManager.MoveGameObjectToScene(dynamic.gameObject, _unsimulatedScene);
    183.             }
    184.  
    185.             _simulationSceneTimer = Time.time;
    186.         }
    187.  
    188.         private void TickRateAdjustment(JsimPlayer localPlayer)
    189.         {
    190.             if (_players.Count == 0)
    191.                 return;
    192.  
    193.             // This is a workaround for when the player is a spectator
    194.             var averageLastRecievedTick = localPlayer != null ? localPlayer.ClientLastRecievedTick : (uint)_players.Values.Average(p => p.ClientLastRecievedTick);
    195.             var averageLastRecievedTickOffset = localPlayer != null ? localPlayer.ClientLastRecievedTickOffset : (int)_players.Values.Average(p => p.ClientLastRecievedTickOffset);
    196.  
    197.             //Debug.Log($"Client Update: localTick:{_localTickNumber} lastReceivedTick:{localPlayer.ClientLastRecievedTick} offset:{localPlayer.ClientLastRecievedTickOffset} frameDuration:{_tickRate}");
    198.  
    199.             var calculatedOffset = (int)averageLastRecievedTick - (int)_localTickNumber;
    200.             var receivedOffset = averageLastRecievedTickOffset;
    201.             if (Mathf.Abs(calculatedOffset) > 60 || Mathf.Abs(receivedOffset) > 60)
    202.             {
    203.                 // Client is to far behind -> Send him into the future
    204.                 //Debug.Log($"Snapping Tick calc:{calculatedOffset} rec:{receivedOffset}");
    205.                 _localTickNumber = averageLastRecievedTick + 10;
    206.             }
    207.  
    208.             if (averageLastRecievedTickOffset < 0)
    209.                 _tickRateAdjustment = 1.1f;
    210.             else if (averageLastRecievedTickOffset > 0)
    211.                 _tickRateAdjustment = 0.9f;
    212.             else
    213.                 _tickRateAdjustment = 1.0f;
    214.         }
    215.  
    216.         #region Spawn Management
    217.  
    218.         private void OnEntitySpawn(JnetEntity entity)
    219.         {
    220.             var player = entity.GetComponent<JsimPlayer>();
    221.             if (player != null)
    222.             {
    223.                 _players.Add(entity.NetId, player);
    224.                 return;
    225.             }
    226.  
    227.             var dynamic = entity.GetComponent<JsimDynamic>();
    228.             if (dynamic != null)
    229.             {
    230.                 _dynamics.Add(entity.NetId, dynamic);
    231.                 return;
    232.             }
    233.         }
    234.  
    235.         private void OnEntityDespawn(JnetEntity entity)
    236.         {
    237.             var removed = _players.Remove(entity.NetId);
    238.             if (removed)
    239.                 return;
    240.             _dynamics.Remove(entity.NetId);
    241.         }
    242.  
    243.         #endregion
    244.     }
    245. }
    246.  

    Player simulation code

    Code (CSharp):
    1.  
    2. using BeyondGalaxiesShared.ViewModels;
    3. using Jnet;
    4. using System.Collections.Generic;
    5. using UnityEngine;
    6. using UnityEngine.Events;
    7.  
    8. namespace Jsim
    9. {
    10.     public class JsimPlayer : MonoBehaviour, INetHook
    11.     {
    12.         public JnetEntity Entity { get; set; }
    13.  
    14.         #region Nerworking Variables
    15.  
    16.         // Common Variables
    17.  
    18.         private const uint _bufferSize = 1024;
    19.  
    20.         // Client Variables
    21.  
    22.         public uint ClientLastRecievedTick { get; private set; }
    23.         public int ClientLastRecievedTickOffset { get; private set; }
    24.  
    25.         private InputViewModel[] _clientInputBuffer = new InputViewModel[_bufferSize];
    26.         private StateViewModel[] _clientStateBuffer = new StateViewModel[_bufferSize];
    27.  
    28.         // Client Smoothing
    29.         private Vector2 _visiblePosition;
    30.         private Vector2 _realPosition;
    31.         private float _visibleRotation;
    32.         private float _realRotation;
    33.  
    34.         // Server Variables
    35.  
    36.         private uint _serverLatestInputTick;
    37.         private int _serverInputTickOffset;
    38.         private const int _serverInputTickOffsetIncrement = 2;
    39.         //private ExponentialMovingAverage _bufferSizeOptimizer = new ExponentialMovingAverage(180);
    40.  
    41.         private Queue<InputViewModel> _serverInputQueue = new Queue<InputViewModel>();
    42.         private InputViewModel _serverLastInput;
    43.  
    44.         // RPCs
    45.  
    46.         private CommandInput _commandInput = new CommandInput();
    47.         private EventState _eventState = new EventState();
    48.  
    49.         #endregion
    50.  
    51.         private Rigidbody2D _rigid;
    52.         private JsimMove _move;
    53.         private JsimCombat _combat;
    54.         private Stats _stats;
    55.  
    56.  
    57.         public void Init(UnityAction<INetVar> addVar, UnityAction<JnetRpc> addRpc)
    58.         {
    59.             addRpc(_commandInput.SetCallback(OnInput));
    60.             addRpc(_eventState.SetCallback(OnState));
    61.         }
    62.  
    63.         private void Awake()
    64.         {
    65.             _rigid = GetComponent<Rigidbody2D>();
    66.             _move = GetComponent<JsimMove>();
    67.             _combat = GetComponent<JsimCombat>();
    68.             _stats = GetComponent<Stats>();
    69.  
    70.             // Initialize Buffers
    71.  
    72.             var initialInput = new InputViewModel();
    73.             var initialState = new StateViewModel();
    74.             initialState.Position = transform.position;
    75.  
    76.             for (int i = 0; i < _clientStateBuffer.Length; i++)
    77.                 _clientStateBuffer[i] = initialState;
    78.  
    79.             for (int i = 0; i < _clientInputBuffer.Length; i++)
    80.                 _clientInputBuffer[i] = initialInput;
    81.         }
    82.  
    83.         private void Update()
    84.         {
    85.             // Smoothing
    86.             if (Entity.IsClient)
    87.             {
    88.                 var positionOffset = _realPosition - _visiblePosition;
    89.                 var snapPosition = positionOffset.sqrMagnitude >= 10.0f;
    90.  
    91.                 var rotationOffset = (_realRotation - _visibleRotation) % 360;
    92.                 var snapRotation = rotationOffset > 5;
    93.  
    94.                 _visiblePosition = snapPosition ? _realPosition : _visiblePosition + positionOffset * 0.9f;
    95.                 _visibleRotation = snapRotation ? _realRotation : (_visibleRotation + rotationOffset * 0.9f) % 360;
    96.  
    97.                 //_move.Correct(_visiblePosition, _visibleRotation);
    98.             }
    99.         }
    100.  
    101.         #region Networking
    102.  
    103.         private void OnState(StateViewModel state)
    104.         {
    105.             // Client receives states independent of simulation tick
    106.  
    107.             ClientLastRecievedTick = state.TickNumber;
    108.             ClientLastRecievedTickOffset = state.TickOffset;
    109.  
    110.             var bufferSlot = state.TickNumber % _bufferSize;
    111.             _clientStateBuffer[bufferSlot] = state;
    112.  
    113.             // For ghost entities, use received inputs corresponding to states
    114.             if (!Entity.IsLocalPlayer)
    115.                 _clientInputBuffer[bufferSlot] = state.Input;
    116.         }
    117.  
    118.  
    119.  
    120.         private void OnInput(InputsViewModel inputs)
    121.         {
    122.             //Debug.Log("---------------------------------SERVER INPUT START----------------------------------------------");
    123.  
    124.             // Server receives a window of states, starting from last acked tick
    125.  
    126.             var firstClientInputTick = inputs.StartTickNumber;
    127.             var lastClientInputTick = inputs.StartTickNumber + inputs.Inputs.Count - 1;
    128.  
    129.             for (int i = 0; i < inputs.Inputs.Count; i++)
    130.             {
    131.                 uint inputTick = inputs.StartTickNumber + (uint)i;
    132.  
    133.                 // Discard inputs already received
    134.                 if (inputTick <= _serverLatestInputTick)
    135.                     continue;
    136.  
    137.                 var input = inputs.Inputs[i];
    138.                 input.TickNumber = inputTick;
    139.  
    140.                 // store new inputs
    141.                 _serverInputQueue.Enqueue(input);
    142.             }
    143.  
    144.             //Debug.Log($"Enquing: count:{inputs.Inputs.Count} lastestCT:{_serverLatestInputTick} firstCT:{firstClientInputTick} lastCT:{lastClientInputTick}");
    145.  
    146.             _serverLatestInputTick = (uint)lastClientInputTick;
    147.         }
    148.  
    149.         #endregion
    150.  
    151.         #region Ticks
    152.  
    153.         public void ClientTick(uint tick)
    154.         {
    155.             var bufferSlot = tick % _bufferSize;
    156.             var state = _clientStateBuffer[bufferSlot];
    157.             var input = Entity.IsLocalPlayer ? _clientInputBuffer[bufferSlot] : PredictInput(tick);
    158.  
    159.             // rewind & replay
    160.  
    161.             SetState(state);
    162.  
    163.             // Apply inputs to physics (e.g. AddForce)
    164.  
    165.             _move.PrePhysicsStep(input);
    166.  
    167.             _combat.Tick(tick, input, state);
    168.         }
    169.  
    170.         public void ServerTick(uint tick)
    171.         {
    172.             //Debug.Log("---------------------------------------SERVER TICK START----------------------------------------");
    173.  
    174.             InputViewModel input = null;
    175.  
    176.             if (_serverInputQueue.Count > 0)
    177.             {
    178.                 var nextInput = _serverInputQueue.Peek();
    179.                 if (nextInput.TickNumber <= tick)
    180.                 {
    181.                     input = _serverInputQueue.Dequeue();
    182.  
    183.                     var tickOffset = (int)input.TickNumber - (int)tick;
    184.  
    185.                     while (tickOffset < -5 && _serverInputQueue.Count > 0)
    186.                     {
    187.                         //Debug.Log("Server Skipped Input: " + input.TickNumber);
    188.  
    189.                         input = _serverInputQueue.Dequeue();
    190.  
    191.                         _combat.Tick(input.TickNumber, input, GetState());
    192.  
    193.                         _serverInputTickOffset -= _serverInputTickOffsetIncrement;
    194.                     }
    195.  
    196.                     //Debug.Log("Server Processed Input: " + input.TickNumber);
    197.  
    198.                     _serverLastInput = input;
    199.                 }
    200.             }
    201.             else
    202.             {
    203.                 input = _serverLastInput;
    204.  
    205.                 //Debug.Log("Server No Input Available");
    206.                 _serverInputTickOffset += _serverInputTickOffsetIncrement;
    207.             }
    208.  
    209.             _move.PrePhysicsStep(input);
    210.  
    211.             // Buffer Size Optimization Approach for Tick Adjustment (no good results)
    212.  
    213.             //_bufferSizeOptimizer.Add(_serverInputQueue.Count);
    214.             //short bufferTendency = 0;
    215.  
    216.             //if (_bufferSizeOptimizer.Value > 3)
    217.             //    bufferTendency = -1;
    218.             //else if (_bufferSizeOptimizer.Value < 2)
    219.             //    bufferTendency = 1;
    220.  
    221.             // Send State to Client
    222.             var state = GetState();
    223.             state.TickNumber = tick;
    224.             //state.TickOffset = bufferTendency;
    225.             state.TickOffset = (short)_serverInputTickOffset;
    226.             state.Input = input;
    227.  
    228.             //Debug.Log($"Server Tick: {tick} tickOffset: {state.TickOffset} bufferSize: {_serverInputQueue.Count}");
    229.  
    230.             _eventState.Send(state);
    231.  
    232.             // Direct Tick Rate Adjustment
    233.             if (_serverInputTickOffset > 0)
    234.                 _serverInputTickOffset--;
    235.             if (_serverInputTickOffset < 0)
    236.                 _serverInputTickOffset++;
    237.  
    238.             _combat.Tick(tick, input, state);
    239.         }
    240.  
    241.         #endregion
    242.  
    243.         #region Input Helpers
    244.  
    245.         private InputViewModel PredictInput(uint tick)
    246.         {
    247.             // For ghost entities, predict input - use last known input
    248.  
    249.             tick = tick > ClientLastRecievedTick ? ClientLastRecievedTick : tick;
    250.             var bufferSlot = tick % _bufferSize;
    251.             return _clientInputBuffer[bufferSlot];
    252.         }
    253.  
    254.         public void ProcessInput(uint tick)
    255.         {
    256.             // For the local player capture input and send it to server
    257.  
    258.             if (!Entity.IsLocalPlayer)
    259.                 return;
    260.  
    261.             var input = SampleInputs();
    262.             var bufferSlot = tick % _bufferSize;
    263.  
    264.             input.TickNumber = tick;
    265.  
    266.             _clientInputBuffer[bufferSlot] = input;
    267.  
    268.             SendInput(tick);
    269.         }
    270.  
    271.         private void SendInput(uint clientTick)
    272.         {
    273.             InputsViewModel inputMsg = new InputsViewModel();
    274.             inputMsg.StartTickNumber = ClientLastRecievedTick;
    275.             inputMsg.Inputs = new List<InputViewModel>();
    276.  
    277.             // build window of inputs
    278.             for (uint tick = inputMsg.StartTickNumber; tick <= clientTick; ++tick)
    279.                 inputMsg.Inputs.Add(_clientInputBuffer[tick % _bufferSize]);
    280.  
    281.             // send input packet to server
    282.             _commandInput.Send(inputMsg);
    283.         }
    284.  
    285.         public InputViewModel SampleInputs()
    286.         {
    287.             InputViewModel inputs = new InputViewModel();
    288.             inputs.Up = Input.GetKey(KeyCode.W);
    289.             inputs.Down = Input.GetKey(KeyCode.S);
    290.             inputs.Left = Input.GetKey(KeyCode.A);
    291.             inputs.Right = Input.GetKey(KeyCode.D);
    292.             inputs.Boost = Input.GetKey(KeyCode.LeftShift);
    293.             inputs.MousePos = Extensions.GetMousePosition();
    294.             inputs.Attack = Input.GetMouseButton(1);
    295.             return inputs;
    296.         }
    297.  
    298.         #endregion
    299.  
    300.         #region State Helpers
    301.  
    302.         public void CaptureState(uint tick)
    303.         {
    304.             var bufferSlot = tick % _bufferSize;
    305.             var state = GetState();
    306.  
    307.             _clientStateBuffer[bufferSlot] = state;
    308.  
    309.             // Capture Last State for Smoothing
    310.             _realPosition = state.Position;
    311.             _realRotation = state.Rotation;
    312.         }
    313.  
    314.         public StateViewModel GetState()
    315.         {
    316.             StateViewModel stateMsg = new StateViewModel();
    317.             stateMsg.Position = _rigid.position;
    318.             stateMsg.Rotation = _rigid.rotation;
    319.             stateMsg.Velocity = _rigid.velocity;
    320.             stateMsg.AngularVelocity = _rigid.angularVelocity;
    321.             return stateMsg;
    322.         }
    323.  
    324.         public void SetState(StateViewModel state)
    325.         {
    326.             _rigid.position = state.Position;
    327.             _rigid.rotation = state.Rotation;
    328.             _rigid.velocity = state.Velocity;
    329.             _rigid.angularVelocity = state.AngularVelocity;
    330.         }
    331.  
    332.         #endregion
    333.     }
    334. }
     
  7. lorux

    lorux

    Joined:
    Feb 9, 2017
    Posts:
    31
    Did you try lower lerping values here:
    Code (CSharp):
    1.  _visiblePosition = snapPosition ? _realPosition : _visiblePosition + positionOffset * 0.9f;
    Another thing i do is rendering not the last tick state but a state back in time depending on player latency.
    Also why do you always rewind? It would be preferable to rewind only if there was a prediction error.

    About tickrate adjustements, i do it only on client side. For example: if the last received tick from server is 50, the client current simulation should be at tick 50 + RTT + fixedInputBuffer. If current tick its outside some threshold starting from that tick i make adjustements by going faster or slower.
     
    Last edited: Jun 26, 2019
  8. MrHaribo

    MrHaribo

    Joined:
    Aug 9, 2012
    Posts:
    16
    Hi lorux, thanx for your reply.

    Good advice on the smoothing problem. The smoothing was way to abrupt especially on high FPS like in a Release Build. I will try to make the smoothing frame rate dependent and smooth it over 0.5s or so. I hope the results will be better then. One thing im afraid of are high velocities where objects will be behind farther. I will update my post when i have results.

    I rewind the whole simulation because Unity Physics allows only to step the whole simulation at once. I did not find a way to step entities individually. It would be necessary to check the server state of one object for errors and then only step this object, but i havent found a way to implement this. I used this blogpost: http://www.codersblock.org/blog/client-side-prediction-in-unity-2018 as a reference. He disables all objects except the simulated one, but for many objects i think this is a bad approach.

    And Finally, your tick rate adjustment approach seems quite promising. I Will check this out as soon as i get the chance.

    Cheers
     
  9. lorux

    lorux

    Joined:
    Feb 9, 2017
    Posts:
    31
    I finally solved the issue and looks decent enough. The problem was that in my first approach every character was running its own tick (synchronized through network). So every time two characters collided, the collision tick was different on every client.
    What i did is let the server to handle a shared tick and send the snapshot of the game state of every tick to the clients. On the other hand, the clients runs the same simulation in a shared tick but in future time: serverTick + RTT + fixedInputBuffer. The input buffer is an offset to ensure that the server will always have an input to execute when the tick comes.
    It is really important to have a synchronized clock working as accurately as possible, so the predicted client tick is correct.
     
  10. vagho

    vagho

    Joined:
    Mar 25, 2013
    Posts:
    85
  11. hyouuu

    hyouuu

    Joined:
    Nov 26, 2020
    Posts:
    42
    Hi @lorux and @MrHaribo I'm pretty new to Unity and am trying to build a physics based realtime multiplayer game, where each player can bump into each other (or non-player objects), and shoot bullets that impact others or objects. Wondering do you have any recommended examples or readings to make the tick & input buffer systems? Did you use Mirror or any other network libraries in your games?

    Thanks a lot!