How to deal with trigger input and lagging clients?

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.

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.

struct MyInput {
 //is using uint so that we can deal with wraparound correctly.
 //can a byte or short, does not matter. byte is probably more than enough.
 public uint ButtonPressed;  
}

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).

[UpdateInGroup(typeof(GhostPredictionSystemGroup))]
class ProcessInputSystem : SystemBase 
{
 public void OnUpdate()
 {
    ...
    inputBuffer.GetDataAtTick(currentTick, out var currentInput);
    var prevTick = currentTick - 1;
    if(prevTick == 0)  --prevTick;
    inputBuffer.GetDataAtTick(prevSampledTick, out var prevInput);
    if((currentInput.ButtonPressed - prevInput.ButtonPressed) > 0)
    {
            //Process the button pressed
    }
 }
}

or something similar.

3 Likes

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!

Is possible to just use RPC? This solution seems complicated.

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)

4 Likes

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?

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?

We are planning to keep RPCs. RPCs and reliable input events solve different problems and neither is a replacement for the other.

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

[UpdateInGroup(typeof(GhostPredictionSystemGroup))]
class ProcessInputSystem : SystemBase
{
 public void OnUpdate()
 {
    ...
    inputBuffer.GetDataAtTick(currentTick, out var currentInput);
    var prevTick = currentTick - 1;
    if(prevTick == 0)  --prevTick;
    inputBuffer.GetDataAtTick(prevSampledTick, out var prevInput);
    if((currentInput.ButtonPressed - prevInput.ButtonPressed) > 0)
    {
            //Process the button pressed
    }
 }
}

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.

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.

1 Like

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.

Client: Jump Pressed 1 times at frame: 398. Added to buffer at tick 152
Client Jumped. Tick: 152 Frame:398
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.

Client: Jump Pressed 1 times at frame: 993. Added to buffer at tick 476
Client Jumped. Tick: 476 Frame:993
Client: Character Jumped. Frame:993
Client Jumped. Tick: 476 Frame:994
Client Jumped. Tick: 476 Frame:995
Client Jumped. Tick: 476 Frame:997
Server Jumped. Tick: 476 Frame:998
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.

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

8580337–1149499–NetCodeProblem.zip (78.7 KB)

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

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.

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

1 Like