Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question How to deal with trigger input and lagging clients?

Discussion in 'NetCode for ECS' started by Jawsarn, Jun 12, 2022.

  1. Jawsarn

    Jawsarn

    Joined:
    Jan 12, 2017
    Posts:
    215
    I'm back at looking at my side project agian. And I delved into trying to solve my issue with clicking a button to enter a "controlling" state, where the player would enter and leave with one press of button.

    With some logging done, it seems that when the player laggs my code will not create commands for every server tick, which leads to the server to copy the command for the missing ticks - introducing more trigger input. (Is this a correct observation?)

    My InputSystem is running in the GhostInputSystemGroup, and only adding a command for the
    ClientSimulationSystemGroup.ServerTick.

    My question then is, what would be the proper way to solve this?
    My current thought is to store an extra field of the generated tick & require a match for the executing tick to deal with trigger logic.
     
  2. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    774
    The safest way to deal with with button presses is to use a counter.
    You should keep an event counter in your system (or in another entity, up to you) and increment that counter every time the action/event is triggered and put that in your input, instead of the bool.

    Code (csharp):
    1.  
    2. struct MyInput {
    3.  //is using uint so that we can deal with wraparound correctly.
    4.  //can a byte or short, does not matter. byte is probably more than enough.
    5.  public uint ButtonPressed;  
    6. }
    7.  
    The when executing the code in you input handling system, you should just check if the counter is incremented in respect the last input received from that player for the previous tick. This way you never miss a button press event, event though if the client is lagging behind a little bit, the input may be processed a little later by the server (so at a different tick).

    Code (csharp):
    1.  
    2. [UpdateInGroup(typeof(GhostPredictionSystemGroup))]
    3. class ProcessInputSystem : SystemBase
    4. {
    5.  public void OnUpdate()
    6.  {
    7.     ...
    8.     inputBuffer.GetDataAtTick(currentTick, out var currentInput);
    9.     var prevTick = currentTick - 1;
    10.     if(prevTick == 0)  --prevTick;
    11.     inputBuffer.GetDataAtTick(prevSampledTick, out var prevInput);
    12.     if((currentInput.ButtonPressed - prevInput.ButtonPressed) > 0)
    13.     {
    14.             //Process the button pressed
    15.     }
    16.  }
    17. }
    18.  
    or something similar.
     
    Kmsxkuse, philsa-unity and WAYNGames like this.
  3. Jawsarn

    Jawsarn

    Joined:
    Jan 12, 2017
    Posts:
    215
    Thank you! That sounds like a good direction. This would solve losing a packet even outside of the bufferd commands to the server compared to what I was suggesting. I was also thinking along the lines of sending the tick of the trigger, which could work similarly and you could use the knowledge of what tick it should have been triggered at, but with a counter you could also do logic around how many times it was pressed to either consume or modulus the value ^^. Not sure if either of these additional features will be useful to me though. Thank you!
     
  4. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,029
    Is possible to just use RPC? This solution seems complicated.
     
  5. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    774
    RPC does not make your code easier. You don't have any guarantee they arrive in time to be processed for the tick you want and they are completely separated from the input stream.
    You will need to write specific systems to handle them and checks for the edge case scenario as well.

    We are actively working on way to handle all that for you (reliable events) using a new way to handle inputs and that further simply the code you are wring,
    It will come as part of 1.0 (so not very soon)
     
  6. Jawsarn

    Jawsarn

    Joined:
    Jan 12, 2017
    Posts:
    215
    I noticed when I rewrote my FPS camera rotation from sending the full state in the command to using deltas that a tangential issue arises when lagging. Since it seems to duplicating commands on server and use last on client, it will use deltas multiple times boosting the rotation when lagging both for predicting and server which feels weird. Right now the only solution I have for this is to add a "Generated tick" field to check vs the tick fetched and clear any deltas if they don't match. Would there be a better approach here?
     
  7. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,029
    I would like to confirm this with u. At 1.0 release does it means it will have reliable events feature and then RPC feature will be completely removed or it will stay there?
     
    Last edited: Sep 13, 2022
  8. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    We are planning to keep RPCs. RPCs and reliable input events solve different problems and neither is a replacement for the other.
     
  9. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,029
    Actually wat's the use case for RPC? Currently I just use it to like send select hero RPC from client to tell server which hero to choose and also upgrade hero level and upgrade hero skill level RPCs from client to server
     
  10. tylo

    tylo

    Joined:
    Dec 7, 2009
    Posts:
    154
    Code (csharp):
    1.  
    2. [UpdateInGroup(typeof(GhostPredictionSystemGroup))]
    3. class ProcessInputSystem : SystemBase
    4. {
    5.  public void OnUpdate()
    6.  {
    7.     ...
    8.     inputBuffer.GetDataAtTick(currentTick, out var currentInput);
    9.     var prevTick = currentTick - 1;
    10.     if(prevTick == 0)  --prevTick;
    11.     inputBuffer.GetDataAtTick(prevSampledTick, out var prevInput);
    12.     if((currentInput.ButtonPressed - prevInput.ButtonPressed) > 0)
    13.     {
    14.             //Process the button pressed
    15.     }
    16.  }
    17. }
    18.  
    I do not fully understand line 10 of this code. Is this a typo? If the prevTick happens to equal 0, then make it equal -1?
    Were you meaning to make sure prevTick would never end up negative?

    Also, I implemented this today (except for line 10) and it works great in the editor. But for some reason in my builds, some of my inputs are being "eaten" and never firing. It's very inconsistent. I am not sure why, will require more investigation on my part.
     
  11. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    Tick is a uint, it cannot be negative and will wrap around so 0 - 1 is 0xffffffff. The line is there because tick 0 signals an invalid /unintialized tick which is never simulated and cannot be used for arithmetic operations, so the tick simulation tick order when updating is 0xffffffff, 1, 2. We are skipping tick 0 while simulating and also need to do that when calculating the previous tick.
    In 1.0 this is changed so the tick is wrapped in a NetworkTick struct that handles more of this automatically.
     
    tylo likes this.
  12. tylo

    tylo

    Joined:
    Dec 7, 2009
    Posts:
    154
    I have a project here where I implemented the above suggestion of
    However, it is producing some odd results for me.

    I made this project modeled after how Rival has their online FPS example set up. How they handle inputs is like this:

    1.
    OnlineCharacterCommandsSystem.cs
    (only runs on Client)
    Takes inputs on Client and puts them into a
    ICommand 
    struct and uses
    AddCommand 
    to put it into a buffer. This is where I do the incrementing of "you pressed jump" into a variable called
    byte JumpRequested
    .

    2. Ghost Authoring Component has "Support Auto Command Target" checked in Inspector, so Commands are sent to Server.

    3.
    OnlinePlayerControlSystem.cs
    (runs on both Client and Server)
    Reads the
    ICommandBuffer
    , gets the
    ICommand 
    buffer at a tick and a previous tick, and performs the subtraction (as suggested). If the
    tick - prevTick > 0
    , then sets the
    bool 
    value of Jump to true.

    4.
    OnlineCharacterSystem.cs
    (runs on both Client and Server)
    Loops through all Entities that have the
    OnlineCharacterInputs 
    struct. If any of those have a Jump value set to true, then the character (a cube) moves up 1 meter on the y-axis.

    However, when I make a build of the project on my machine, I get many inconsistent instances where the Server never seems to receive the command. Here is a set of simple Debug.Log statements I have.

    The first block here, is what the Debug.Logs look like on a failure.
    Code (csharp):
    1. Client: Jump Pressed 1 times at frame: 398. Added to buffer at tick 152
    2. Client Jumped. Tick: 152 Frame:398
    3. Client: Character Jumped. Frame:398
    What ends up happening is that the Client makes a very short attempt to jump, but then the Server corrects it and a jump never occurs.

    Essentially what it looks like to me, is that the Server never receives an
    ICommand 
    at tick 152 that has
    JumpRequested 
    with a value of 1. Instead it receives one that has
    JumpRequested 
    with a value of 0.

    And here is what the Debug.Logs look like on a success.
    Code (csharp):
    1. Client: Jump Pressed 1 times at frame: 993. Added to buffer at tick 476
    2. Client Jumped. Tick: 476 Frame:993
    3. Client: Character Jumped. Frame:993
    4. Client Jumped. Tick: 476 Frame:994
    5. Client Jumped. Tick: 476 Frame:995
    6. Client Jumped. Tick: 476 Frame:997
    7. Server Jumped. Tick: 476 Frame:998
    8. Server: Character Jumped. Frame:998
    This causes a successful jump, however I'm not a big fan of how Jump ends up being true for 5 frames. But this is not yet a problem for me that I want to address.

    I would really, really appreciate it if someone could take a look at this project and see if this is a problem with my implementation, is a Unity bug, or if it even happens on your machine. I am attaching a zip of the project files.

    This uses Unity 2020.3.34f1.

    To build it, go to the
    BuildConfig 
    folder, highlight
    OnlineClientServerBuildConfig
    , press
    Build 
    in the top-right of the Inspector.

    All you need to do to run the build is press the Host button on the main menu screen. This will create a server and connect to it as a client.

    In order to see the Debug.Logs, I find the best way is to go into Powershell and paste this in there after running the build.

    Code (csharp):
    1. Get-Content "$($env:LOCALAPPDATA)\..\LocalLow\tylo\NetCodeProblem_ClientServer\Player.log" -Wait
     

    Attached Files:

  13. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    113
    Something I'm noticing for now is that in
    OnlinePlayerCommandsSystem
    , a new
    OnlinePlayerCommands
    is created every frame, and the Jump counter is incremented from the default value of 0 in that new commands struct. What should be done here instead is that the Jump counter value should be stored somewhere locally, incremented when jump is pressed, and then have that incremented value stored in the commands component. This way, the jump counter value never gets reset to 0; it constantly gets incremented from one frame to another.

    From what I can tell right now, the subsequent jump handling in
    OnlinePlayerControlSystem
    and
    OnlineCharacterSystem
    appear to be fine

    Note: the next release of the Rival samples will address this issue & handle input in a more correct way, using the IInputComponentData that comes with netcode 1.0
     
    Last edited: Nov 14, 2022
  14. tylo

    tylo

    Joined:
    Dec 7, 2009
    Posts:
    154
    Hello Phil, thanks for the answer. Your suggestion did work! No more mysterious Ticks that contain a value of 0.

    However it does leave me with one lingering question. If I never reset to 0, then how can I get away with using a
    byte
    as my value type for
    JumpRequest
    ?

    My first thought was to write some kind of "wrap around" logic to reset it to 0 once I get near 255. But if you can think of an easier way, I am all ears.
     
    Last edited: Nov 14, 2022
  15. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    113
    I haven't tested in practice, but here's what I'm thinking:

    Because we only ever increment the counter, we can assume that the counter value at tick X can only ever be greater or equal than the counter value at tick X-1. It should never be lower, because we never decrement or reset it.

    With that in mind, we can deduce that if the counter value at tick X is lower than at tick X-1, it's necessarily because a value wrap-around has happened. So when this happens (when counter value is lower than on previous tick), some special handling can be used to determine that 0 is greater than 255 for example.

    In theory, I think the only case where this will fail to detect that the input was pressed is if someone manages to press the jump button exactly 255 times during one single tick (1/60th of a second); which is extremely unlikely to happen
     
    Last edited: Nov 15, 2022
    tylo likes this.