#How to handle server side events with CSP

44 messages · Page 1 of 1 (latest)

gilded lintel
#

Hello, i've got question about handling server side events with client side prediction. I've got something like server side hit where server adds "delta force" to the CSP code. Problem is jittery result when player receive hit from the server. I'll post code below.

Hitbox code

    private void LaunchCharacter(Vector3 hitNormal)
    {
        Vector3 hitDirection = -new Vector3(hitNormal.x, 0f, hitNormal.z);
        Vector3 upForce = Vector3.up * _upPushScalar;
        Vector3 forwardForce = hitDirection * _forwardPushScalar;
        Vector3 hitForce = forwardForce + upForce;
         
        _playerStateMachine.CacheDeltaForce(hitForce);
    }
    
    public void RequestHit()
    {
        PreciseTick preciseTick = TimeManager.GetPreciseTick(TickType.LastPacketTick);
        ServerHit(preciseTick);
    }
    
    [ServerRpc]
    private void ServerHit(PreciseTick preciseTick)
    {
        RollbackManager.Rollback(preciseTick, RollbackManager.PhysicsType.ThreeDimensional, IsOwner);
        int mask = 1 << LayerMask.NameToLayer("HitBox");

        Transform t = transform;
        
        if(Physics.Raycast(t.position, t.forward, out RaycastHit hit, 1f, mask))
        {
            if (hit.collider.TryGetComponent(out CombatModule combatModule))
            {
                combatModule.LaunchCharacter(hit.normal);
            }
        }
        
        RollbackManager.Return();
    }
#

Replicated method in PlayerStateMachine

        [Replicate]
        private void Simulation(NetworkInput input, bool asServer, Channel channel = Channel.Unreliable, bool isReplaying = false)
        {
            float deltaTime = (float)TimeManager.TickDelta;
            _playerAnimations.CacheInput(input, _movementStatsModule.Acceleration, _movementStatsModule.Deceleration);
            
            if(!isReplaying)
                CheckDeltaForce();
            
            _currentBaseState.Tick(input, asServer, isReplaying, deltaTime);
            _currentCombatState.Tick(input, asServer, isReplaying, deltaTime);
            _currentBaseState.CheckStateChange(input, false, isReplaying);
            _currentCombatState.CheckStateChange(input, false, isReplaying);
        }

Reconcile method

        [Reconcile]
        private void Reconcile(PlayerStateReconcileData data, bool asServer, Channel channel = Channel.Unreliable)
        {
            CharacterMovement.State state = new CharacterMovement.State(data.Position, data.Rotation, data.Velocity,
                data.IsConstrainedToGround, data.UnconstrainedTimer, data.HitGround, data.IsWalkable,
                data.GroundNormal);
            
            _characterMovement.SetState(state);
            _deltaForce = data.DeltaForce;
            
            _context.CurrentPrepareSwingTime = data.CurrentPrepareSwingTime;
            _context.CurrentSwingTime = data.CurrentSwingTime;
            
            ChangeBaseState(data.BaseStateID, true);
            ChangeSecondaryState(data.CombatStateID, true);
        }
#

Launching character into air, also that method is used in replicate method

#
        private void CheckDeltaForce()
        {
            if(_deltaForce == default)
                return;
            
            if(_characterMovement.isConstrainedToGround)
                _characterMovement.PauseGroundConstraint();
            
            _characterMovement.LaunchCharacter(_deltaForce);
            _deltaForce = default;
        }
#

Result video:
Left is client, right is host

#

I'm using NetworkTransform and PredictedObject

#

Server and client are running with 90 tickrate (Just using that high number for tests)

#

And that code runs in CSP on the client side in replicate method

// Swing state tick method
        public void Tick(NetworkInput input, bool asServer, bool isReplaying, float deltaTime)
        {
            PlayerStateMachineContext context = _stateMachine.Context;
            CombatModule combatModule = context.CombatModule;

            context.CurrentSwingTime = Mathf.MoveTowards(context.CurrentSwingTime,
                combatModule.SwingTimeInSeconds, deltaTime);

            if (context.CurrentSwingTime >= combatModule.SwingTimeInSeconds && !isReplaying && !asServer)
            {
                combatModule.RequestHit(); // requesting hit from hit box - just for testing 
            }
        }
rugged grail
#

You want to probably add a delay to the effect so the client can get it, and simulate it locally.

#

I was working on a guide for this but its not yet complete. There's some talk in #chatting too; let me see if I can find it.

gilded lintel
#

@rugged grail thanks

vapid narwhalBOT
#

FirstGearGames received thanks.

gilded lintel
#

Crap, knockback still looks jittery

#

Probably im doing something wrong

#

this is server side request for the knockback

#
        public void CacheDeltaForce(Vector3 deltaForce, uint tickDelta) // tickDelta is constant delay - 3 ticks
        {
            double tickDeltaMS = TimeManager.TickDelta * 1000d // tick delta as ms;
            double ping = Owner.Ping / 2f; // Ping to the client

            uint pingAsTicks = (uint)Math.Round(ping / tickDeltaMS) // convert ping into ticks;
            
            _serverTick = TimeManager.Tick + tickDelta // Knockback apply tick on server; 
            uint clientTickWithOffset = Owner.LocalTick + pingAsTicks + tickDelta // Knockback apply tick on client + ping offset;
            
            _deltaForce = deltaForce; // knockback force
            
            Debug.Log($"Serv tick {_serverTick}, Cl local tick {Owner.LocalTick}, Cl offset tick {clientTickWithOffset}");
            TargetCacheDeltaForce(Owner, clientTickWithOffset, deltaForce);
        }
#

Target rpc

#
        [TargetRpc]
        private void TargetCacheDeltaForce(NetworkConnection connection, uint tick, Vector3 deltaForce)
        {
            _deltaForce = deltaForce;
            _clientTick = tick;
            
            Debug.Log($"Local tick {TimeManager.LocalTick}, Received tick {tick}");
        }
#

This method is inside Replicate method

#
private void CheckDeltaForce(NetworkInput input, bool asServer, bool isReplaying)
        {
            if(_deltaForce == default || isReplaying)
                return;

            if (asServer && TimeManager.Tick == _serverTick) // Server side launch
            {
                LaunchCharacter(_deltaForce);
                _deltaForce = default;
            }

            if (!asServer && TimeManager.LocalTick >= _clientTick) // Client side launch
            {
                _knockbackCache.Add(new System.Tuple<uint, Vector3>(TimeManager.LocalTick, _deltaForce)); // Knockback actions cache for testing, TODO refactor later
                LaunchCharacter(_deltaForce);
                _deltaForce = default;
            }
        }
#
private void LaunchCharacter(Vector3 force) // Knockback method
        {
            if(force == default)
                return;

            if(_characterMovement.isGrounded)
                _characterMovement.PauseGroundConstraint();
            
            _characterMovement.LaunchCharacter(force);
        }
#

Full reconcile method

#
        [Reconcile]
        private void Reconcile(PlayerStateReconcileData data, bool asServer, Channel channel = Channel.Unreliable)
        {
            CharacterMovement.State state = new CharacterMovement.State(data.Position, data.Rotation, data.Velocity,
                data.IsConstrainedToGround, data.UnconstrainedTimer, data.HitGround, data.IsWalkable,
                data.GroundNormal);

            // if (!asServer)
            // {
            //     Vector3 clientPosition = _characterMovement.GetPosition();
            //     Vector3 velocity = _characterMovement.velocity;
            //     bool isGrounded = _characterMovement.isConstrainedToGround;
            //     
            //     Debug.Log($"Before recon: {clientPosition}, {velocity}, {isGrounded}");
            //     Debug.Log($"After recon: {data.Position}, {data.Velocity}, {data.IsConstrainedToGround}");
            // }
            
            _characterMovement.SetState(state);

            _context.CurrentPrepareSwingTime = data.CurrentPrepareSwingTime;
            _context.CurrentSwingTime = data.CurrentSwingTime;
            
            ChangeBaseState(data.BaseStateID, true);
            ChangeSecondaryState(data.CombatStateID, true);
        }
#

Full replicate method

        [Replicate]
        private void Simulation(NetworkInput input, bool asServer, Channel channel = Channel.Unreliable, bool isReplaying = false)
        {
            float deltaTime = (float)TimeManager.TickDelta;
            _playerAnimations.CacheInput(input, _movementStatsModule.Acceleration, _movementStatsModule.Deceleration);

            if (isReplaying && !asServer && _knockbackCache.Count > 0)
            {
                System.Tuple<uint, Vector3> knockbackToRemove = null;
                
                foreach (System.Tuple<uint, Vector3> knockback in _knockbackCache)
                {
                    if (knockback.Item1 == input.SimulationTick)
                    {
                        knockbackToRemove = knockback;
                        break;
                    }
                }

                if (knockbackToRemove != null)
                {
                    //LaunchCharacter(knockbackToRemove.Item2);
                    _knockbackCache.Remove(knockbackToRemove);
                }
            }
            
            CheckDeltaForce(input, asServer, isReplaying);
            
            _currentBaseState.Tick(input, asServer, isReplaying, deltaTime);
            _currentCombatState.Tick(input, asServer, isReplaying, deltaTime);
            _currentBaseState.CheckStateChange(input, false, isReplaying);
            _currentCombatState.CheckStateChange(input, false, isReplaying);
        }
#

Video results:
Left is client, right is host

#

Small Latency, ~30ms

#

Big Latency, ~ 230ms

gilded lintel
#

Well i've fixed that

#

But dunno if that is fine approach to CSP

#

Instead of caching incoming server events localy into variable

#

I've included _deltaForce, _clientEventTick into input of replicate method

#

Downside of that approach is high amount of bandwidth usage

#

Anyway events are played correctly without any jitters and rubber banding

#

Even if client cheat _deltaForce or knockback tick, client will be corrected by the server

#

so my new networked input is:

struct NetworkInput
{
  public Vector2 MovementInput;
  public float RotationInput;
  public KnockbackEvent[] IncomingKnockbackEvents //very high bandwidth usage ;/
}

struct KnockbackEvent
{
  public Vector3 KnockbackForce;
  public uint InvokeTick;
}
#

and in replicate method i'm running

private void Replicate(NetworkInput input, bool asServer, Channel channel = Channel.Unreliable, bool isReplaying = false)
{
  if(asServer)
  {
    foreach(var knockbackEvent in _serverKnockbackEvents)
    {
      if(TimeManager.Tick != knockbackEvent.InvokeTick)
        continue;
      
      ApplyKnockback(knockbackEvent.KnockbackForce);
      // remove used knockback event
    }
  }
  
  if(!asServer)
  {
    foreach(var knockbackEvent in input.IncomingKnockbackEvents)
    {
      if(TimeManager.LocalTick < knockbackEvent.InvokeTick)
        continue;
      
      ApplyKnockback(knockbackEvent.KnockbackForce);
      // remove used knockback event
    }
  }
}
#

so is it right to run that logic like that? @rugged grail, I'm sorry for the ping.

gilded lintel
#

Nvm

#

Baerikus solution works fine

rugged grail
#

@gilded lintel I actually wrote a guide for this in a cs file on my last live stream. It will be added to the docs within a week