Search Unity

2D Top Down Shooter UNET Lag Compensation

Discussion in 'UNet' started by lazylukey, May 23, 2018.

  1. lazylukey

    lazylukey

    Joined:
    May 26, 2017
    Posts:
    36
    Hi, hope someone is able to offer some guidance. I have a game in the app stores (links at the bottom) called Battle Royale. It's a 2D top down multiplayer shooter using UNET with an authoritative server approach when it comes to firing bullets. Unfortunately as the bullets are GameObjects that move relatively slowly (as dodging is very much a part of the gameplay) this leads to bullets not spawning from the gun tip if the player is moving but rather where the player last was by the time the bullet is spawned on the server.

    What would be the best approach to vastly improving the feel of shooting while not opening up the game to cheating (client side authority) or making the game very unfair for people with worse latency than others?

    Android - https://play.google.com/store/apps/details?id=com.solidstategroup.battleroyale
    iOS - https://itunes.apple.com/us/app/battle-royale/id1251457729?ls=1&mt=8
     
  2. newjerseyrunner

    newjerseyrunner

    Joined:
    Jul 20, 2017
    Posts:
    966
    I do both server-side and client-side prediction. I've found that 99% of the time the player's motion has not changed from the previous tick so you can assume that the player is at the last known location plus their last known velocity times delta time. It won't be perfect (nothing ever will) but it will make a considerable difference.

    Feel free to use my implementation, let me know if you find any bugs in it. Add both components to your networked object and link your Interpolator to the NetworkSyncTransform and everything else should just work.

    Code (csharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using System.Runtime.CompilerServices;
    6.  
    7. public class Interpolator : MonoBehaviour {
    8.     public bool isSource;
    9.     public bool dirtyRot;
    10.     public bool dirtyPos;
    11.  
    12.     private const float _posLerpRate = 20;
    13.     private const float _rotLerpRate = 15;
    14.     private const float _posThreshold = 0.1f;
    15.     private const float _rotThreshold = 1f;
    16.  
    17.     public Vector3 _serverPosition;
    18.     public Vector3 _serverSpeed;
    19.     public Vector3 _serverRotation;
    20.  
    21.     private Vector3 previousPosition;
    22.     private Vector3 previousRotation;
    23.  
    24.  
    25.     private Vector3 previousLastPosition;
    26.     private float updateTimer = 999.9f;  //Will do the update on the very first frame
    27.     private Vector3 nextPosition;
    28.  
    29.     private bool sendPositionAnywayFlag;
    30.  
    31.     void Start(){
    32.         previousPosition = nextPosition = _serverPosition = transform.position;
    33.         previousRotation = _serverRotation = transform.rotation.eulerAngles;
    34.     }
    35.  
    36.     void Update(){
    37.         if (isSource){
    38.             updateTimer += Time.deltaTime;
    39.             if (updateTimer > 0.0333333f){  //30ish times per second
    40.                 Vector3 spd = transform.position - previousPosition;
    41.                 if (IsPositionChanged()){
    42.                     if (spd.magnitude > 3.0f) spd = Vector3.zero;  //Likely a teleport
    43.                     SendPosition(transform.position, spd / updateTimer);
    44.                     previousPosition = transform.position;
    45.                 } else if (spd != Vector3.zero){
    46.                     SendPosition(transform.position, Vector3.zero);
    47.                 }
    48.                 if (IsRotationChanged()){
    49.                     SendRotation(transform.localEulerAngles);
    50.                     previousRotation = transform.localEulerAngles;
    51.                 }
    52.                 updateTimer = 0;
    53.             }
    54.         } else {
    55.             InterpolatePosition();
    56.             InterpolateRotation();
    57.         }
    58.     }
    59.  
    60.     //This is called when whatever is using it has retrieved the data
    61.     public void clean(){
    62.         dirtyPos = dirtyRot = false;
    63.     }
    64.  
    65.     private void InterpolatePosition(){
    66.         Debug.Assert(!isSource);  //Only interpolate if it's a remote object
    67.         if (previousPosition != _serverPosition){
    68.             previousPosition = nextPosition = _serverPosition;
    69.         } else {
    70.             nextPosition += _serverSpeed * Time.deltaTime / 1.25f;  //Prediction isn't perfect and only doing a little interpolation makes it smoother
    71.         }
    72.  
    73.         if (Vector3.Distance(transform.position, nextPosition) > 3.0f){  //If it's really laggy or if we teleport the character
    74.             transform.position = nextPosition;
    75.         } else {
    76.             transform.position = Vector3.Lerp(transform.position, nextPosition, Time.deltaTime * _posLerpRate);
    77.         }
    78.     }
    79.  
    80.     private void InterpolateRotation(){
    81.         Debug.Assert(!isSource);  //Only interpolate if it's a remote object
    82.         transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.Euler(_serverRotation), Time.deltaTime * _rotLerpRate);
    83.     }
    84.  
    85.     private void SendPosition(Vector3 pos, Vector3 spd){
    86.         _serverSpeed = spd;
    87.         _serverPosition = pos;
    88.         dirtyPos = true;
    89.     }
    90.  
    91.     private void SendRotation(Vector3 rot){
    92.         _serverRotation = rot;
    93.         dirtyRot = true;
    94.     }
    95.  
    96.     public void debugOverride(Vector3 spd, Vector3 pos, Vector3 rot){
    97.         _serverSpeed = spd;
    98.         _serverPosition = pos;
    99.         _serverRotation = rot;
    100.     }
    101.  
    102.     private bool IsPositionChanged(){
    103.         Debug.Assert(isSource);  //We only care if position has changed if local
    104.         if (Vector3.Distance(transform.position, previousPosition) > _posThreshold){
    105.             sendPositionAnywayFlag = false;
    106.             return true;
    107.         }
    108.         //Even if we're not moving much, sync it, just slower
    109.         if (sendPositionAnywayFlag) return true;
    110.         sendPositionAnywayFlag = !sendPositionAnywayFlag;
    111.         return false;
    112.     }
    113.  
    114.     private bool IsRotationChanged(){
    115.         Debug.Assert(isSource);  //We only care if rotation has changed if local
    116.         return Vector3.Distance(transform.localEulerAngles, previousRotation) > _rotThreshold;
    117.     }
    118. }
    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using UnityEngine.Networking;
    4.  
    5. public class NetworkSyncTransform : NetworkBehaviour {
    6.     public Interpolator interpolator;
    7.  
    8.     [SyncVar]
    9.     public Vector3 _serverPosition;
    10.  
    11.     [SyncVar]
    12.     public Vector3 _serverSpeed;
    13.  
    14.     [SyncVar]
    15.     public Vector3 _serverRotation;
    16.  
    17.     void Update(){
    18.         if (isLocalPlayer){
    19.             interpolator.isSource = true;
    20.             if (interpolator.dirtyPos){
    21.                 if (interpolator.dirtyRot){
    22.                     CmdSendRotation(interpolator._serverRotation);
    23.                 } else {
    24.                     CmdSendBoth(interpolator._serverPosition, interpolator._serverSpeed, interpolator._serverRotation);
    25.                 }
    26.             } else if (interpolator.dirtyRot){
    27.                 CmdSendRotation(interpolator._serverRotation);
    28.             }
    29.             interpolator.clean();
    30.         } else {
    31.             interpolator._serverPosition = _serverPosition;
    32.             interpolator._serverSpeed = _serverSpeed;
    33.             interpolator._serverRotation = _serverRotation;
    34.         }
    35.     }
    36.  
    37.  
    38.     [Command(channel = Channels.DefaultUnreliable)]
    39.     private void CmdSendPosition(Vector3 pos, Vector3 spd){
    40.         _serverSpeed = spd;
    41.         _serverPosition = pos;
    42.     }
    43.  
    44.     [Command(channel = Channels.DefaultUnreliable)]
    45.     private void CmdSendRotation(Vector3 rot){
    46.         _serverRotation = rot;
    47.     }
    48.  
    49.     [Command(channel = Channels.DefaultUnreliable)]
    50.     private void CmdSendBoth(Vector3 pos, Vector3 spd, Vector3 rot){
    51.         _serverSpeed = spd;
    52.         _serverPosition = pos;
    53.         _serverRotation = rot;
    54.     }
    55.  
    56.     public override int GetNetworkChannel(){
    57.         return Channels.DefaultUnreliable;
    58.     }
    59.  
    60.     public override float GetNetworkSendInterval(){
    61.         return 0.01f;
    62.     }
    63. }
    64.  
    65.  
     
    Musketeer likes this.
  3. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    Two other approaches off the top of my head.

    The easiest would be to require players to be stationary or move slowly while shooting, which would make it far more likely that when the bullet appears on the client that fired it that it will be at or near the end of the gun.

    Second would be on the player firing the bullet to instantiate it immediately locally and then sync it up with the bullet that the server instantiates when it eventually is spawned on the client that fired it. Might be tricky depending on how quickly bullets are moving.
     
  4. lazylukey

    lazylukey

    Joined:
    May 26, 2017
    Posts:
    36
    @newjerseyrunner Thanks for the code! I should have mentioned that player movement is being sync'd using the 3rd party asset Smooth Sync (prior to that it was just the standard Network Transform component). I don't think I can use your components on a bullet because clients cannot spawn GameObjects. I assume those components are really for the player GameObject.

    @Joe-Censored I really like your 2nd idea. I am wary tho that it might look a bit strange. It may well be a price I have to pay. I would imagine that if you're strafing left shooting forward, the bullet would appear to curve off from the gun tip to the right as it interpolates over to the server instantiated bullets position.
     
  5. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    I have an article on Lag Compensation where I sync positions at different "ticks". When I actually want to do compensation I also submit the interpolation state between the previous and current tick. Might help you https://twotenpvp.github.io/lag-compensation-in-unity.html

    Edit:
    The advantage of sending the interpolation state aswell as the tick is that we get a exact position. In the MLAPI we have a similar tehcnique but it's time based. And in the MLAPI it's also garbage free
     
    Last edited: May 25, 2018
  6. lazylukey

    lazylukey

    Joined:
    May 26, 2017
    Posts:
    36
    Thanks for the link @TwoTen. That was an interesting read. I ended up going with @Joe-Censored's idea. The curve effect isn't that bad unless latency is high and it was not too difficult to implement. Blood is only spawned by the server in my game so players do have an indication of whether they actually did damage or not.