InputEvent does not fire exactly once

Documentation states: By using the InputEvent type within IInputComponentData inputs you can guarantee one off events (for example gathered by UnityEngine.Input.GetKeyDown) will be synchronized properly with the server and registered exactly once.

This ignores the fact that the client must set the InputEvent to default on the following frame for this to be accurate.

Depending on how you interpret the documentation, this is a feature request, but squabbling aside, I would expect the following behavior out of InputData:

Frame 0: Client → Set (Count 1)
Frame 0: Server → Count 0
End of Frame 0 → Client automatically sends the input event data and sets InputEvent count back to 0
… Network Delay…
Frame 15: Server Receives the input data (Count 1)
Frame 15: Count stays 1 the entire Frame
End of frame 15: Server sets the count back to 0

Here is a MWE or the issue and the resolution:
Branch with issue: DebugECS/Assets/Scripts/DebugSystem.cs at iinputcomponentdata-debug ¡ Misterturtle/DebugECS ¡ GitHub
Branch with resolution: https://github.com/Misterturtle/Deb...nentdata-solved/Assets/Scripts/DebugSystem.cs

1 Like

Also, the documentation on this page under the Example Code section is outdated and still uses
[GenerateAuthoringComponent]

1 Like

The correct approach seems to be this:
[GhostInputSystemGroup]
playerInputComp.jump = default;
if(jumpKeyDown)
playerInputComp.jump.Set();

[PredictedSimulationSystemGroup]
var networkTime = SystemAPI.GetSingleton();
if (networkTime.IsFirstTimeFullyPredictingTick)
if(playerInputComp.jump.IsSet)
//jump

The documentation is unfortunately incorrect. The current requirement is that the events must be reset every tick.

Is IsSet supposed to become false at the end of the server tick? Because I’m having currently a situation where I’m calling Set() on an InputEvent on the client and IsSet turns itself false before the next frame and turns true again in the subsequent frame without me doing anything even before the server runs its tick.

It is quite annoying because I have the same button for crouching and then lying down and one press makes the character controller lay down because the system on the client runs several times before the server tick.

Like we mentioned above, the pattern is to reset your InputEvent each time before setting it conditionally. Are you saying you’re trying this and it doesn’t work?

MyInput = default;
if (Input.KeyDown ...)
    MyInput.MyEvent.Set();

Edited 25/11/24 for clarity.
Note that ‘Rollback & Resimulatable’ character controller actions (like moving, jumping, sliding, turning etc) should not be guarded by IsFirstTimeFullyPredictingTick. IsFirstTimeFullyPredictingTick should be used exclusively to guard gameplay logic that cannot be rewound (events like entity creation, V&SFX creation etc).

I’ll use a (simplified as ignoring partial ticks) example to illustrate why:

Scenario: A human player presses spacebar (jump) to launch their character controller over a wide hole/ledge. In this example, they are in-flight for about 500ms (30 ticks @ 60Hz). A jump particle and sound effect (V&SFX respectively) is triggered on the first tick that the player predicts leaving the ground.

[1] At some point the client will collect this Spacebar jump input, and store it into their IInputComponentData. The client will then ‘client predict’ this jump for the first time (on the tick the client raised the Spacebar jump input for) by running this tick in the PredictedSimulationSystemGroup. Therefore, they will calculate a jump velocity, update their character controller, and lets say this logic also triggers the jumping V&SFX (a sound and a particle system at their feet).

[2] However, while mid-air, we can expect this players ClientWorld to receive many snapshots containing their own character controller ghost. Using default settings, that is ~30 opportunities for this client to receive an updated, server authoritative collection of updated [GhostField] values for this ghost.

Every time one such snapshot arrives, Netcode for Entities will (simplified) discard the latest client prediction, rollback to the newest (server authoritative) snapshot state received from the server, and then re-predict (i.e. re-simulate) from that authoritative state.

Important note: IsFirstTimeFullyPredictingTick will be true the first predicted tick they react to the jump input for (i.e. scenario [1] in this example), but IsFirstTimeFullyPredictingTick will be false for [2] i.e. for every re-prediction of these older ticks.

Thus, we use IsFirstTimeFullyPredictingTick to trigger jump V&SFX once. I.e. We use this API to prevent the sound and particle system from being triggered/re-started every single time we rollback & re-simulate. If we did not do this, we’d hear horrible distorted audio, and see like 10-30 overlapping particle systems, each triggered 1-2 ticks after the first.

However, importantly (and hopefully intuitively), we do not use IsFirstTimeFullyPredictingTick to guard the jump itself, as that should be re-predicted every time we re-predict this ghost, exactly as the input denotes.

IsFirstTimeFullyPredictingTick TL:DR:

  • Do not use IsFirstTimeFullyPredictingTick for client predicted gameplay logic which needs to be re-simulated - like character controller movements (running, turning, jumping) and gameplay logic like weapon ROF cooldowns, ability timers, physics collision results, buffs and debuffs etc.
  • Do use IsFirstTimeFullyPredictingTick for ‘one-shot’ client predictions like V&SFX, client predicted spawns (like bullets & projectiles) etc.

Further example: Imagine the player is hit by an enemy stun just before they jump, causing them to slide into the hole in the server authoritative simulation.

This cannot be predicted by the victim jumping player (as they don’t have this stun information yet), so the victim human player will visually see themselves beginning to jump across the ledge. They’ll hear the audio queue and see the particle system spawn.

They will be corrected by the server (most likely while they’re still mid-air), and find themselves falling into the hole, with a ‘you’ve been hit by a stun’ sound FX triggering, and likely a HUD particle effect triggering on them.

I do exactly that. I reset my InputEvent Dodge each frame and set it exactly once on button pressed.
But
playerInput.ValueRO.Dodge.IsSet is true in multiple subsequent frames in a system than processes inputs (in PredictedSimulationSystemGroup).
Did you resolve your issue?

Frames or Ticks?
I’m asking it because it is an important distinction.

Frames: one update of the editor/application loop
Tick: one full update of the prediction loop.

You can have multiple “partial-ticks” per frame on the client. Not on the server.

Therefore on the client it is correct and plenty possible that the IsSet is true multiple times per frame in the prediction loop if you press a button in case of partial ticks.
Each partial tick is re-simulated from the beginning of that Tick for a variable amount of delta time ( < simulation tick interval).

The inputEvent uses a counter internally to determine how many times a button even has been raised since the last tick (leaving aside for a sec the details how it is calculated).
The IsSet just check if the counter is greater than 0.

From the tick simulation perspective this is correct: you pressed the button at least once, therefore at time T (now) even though the real input device must be released, the actual “accumulated” actions are X, Y, Z. Where X,Y,Z can be either button pressed or axis changes (i.e mouse)

Hi, thanks for responding!
Frames. I get the distinction and I debug.log frame numbers to see what’s happening. I set my Dodge just once, hence the first log, then I check if the dodge event is set in my process input system. Here’s how logs look:

Dodge is set
ClientWorld Dodge:False Frame:1343 
ClientWorld Dodge:False Frame:1343 
ClientWorld Dodge:False Frame:1343 
ClientWorld Dodge:True Frame:1343 
ServerWorld Dodge:False Frame:1344 
ClientWorld Dodge:False Frame:1344 
ClientWorld Dodge:False Frame:1344 
ClientWorld Dodge:True Frame:1344 
ClientWorld Dodge:False Frame:1344 
ServerWorld Dodge:False Frame:1345 
ClientWorld Dodge:True Frame:1345 
ClientWorld Dodge:False Frame:1345  
ClientWorld Dodge:False Frame:1345 
ClientWorld Dodge:False Frame:1345  
ServerWorld Dodge:True Frame:1346

Dodge IsSet in 3 subsequent frames, but not in all ticks.

I think you got it almost right. But you are forgetting one aspect of it. Before I gave you my answer, and also as a way to confirm the behaviour, could you please add to the log not only the Frame, but also the current Tick ?
It will become apparent immediately what it is going on (tips: it has something to do with rollbacks and replay).

Ok, not sure if I am getting it. So client processes the Dodge input at 1397 and then after getting info from server, rolls itself back to some tick (in this case before the Dodge input)? And then replays itself to its own tick before rollback. And when replaying, the client processes input again at tick 1397 so that’s why on that tick Dodge is always true regardless of a frame in which the replaying happens?

ClientWorld Dodge:False Frame:1210 ServerTick:1394 InterpolationTick:1390 PredictedTick:1
ClientWorld Dodge:False Frame:1210 ServerTick:1395 InterpolationTick:1390 PredictedTick:2
ClientWorld Dodge:False Frame:1210 ServerTick:1396 InterpolationTick:1390 PredictedTick:3
ClientWorld Dodge:True Frame:1210 ServerTick:1397 InterpolationTick:1390 PredictedTick:4
ServerWorld Dodge:False Frame:1211 ServerTick:1394 InterpolationTick:1394 PredictedTick:0
ClientWorld Dodge:False Frame:1211 ServerTick:1395 InterpolationTick:1391 PredictedTick:1
ClientWorld Dodge:False Frame:1211 ServerTick:1396 InterpolationTick:1391 PredictedTick:2
ClientWorld Dodge:True Frame:1211 ServerTick:1397 InterpolationTick:1391 PredictedTick:3
ClientWorld Dodge:False Frame:1211 ServerTick:1398 InterpolationTick:1391 PredictedTick:4
ServerWorld Dodge:False Frame:1212 ServerTick:1395 InterpolationTick:1395 PredictedTick:0
ClientWorld Dodge:False Frame:1212 ServerTick:1396 InterpolationTick:1392 PredictedTick:1
ClientWorld Dodge:True Frame:1212 ServerTick:1397 InterpolationTick:1392 PredictedTick:2
ClientWorld Dodge:False Frame:1212 ServerTick:1398 InterpolationTick:1392 PredictedTick:3
ClientWorld Dodge:False Frame:1212 ServerTick:1399 InterpolationTick:1392 PredictedTick:4
ServerWorld Dodge:False Frame:1213 ServerTick:1396 InterpolationTick:1396 PredictedTick:0
ClientWorld Dodge:True Frame:1213 ServerTick:1397 InterpolationTick:1393 PredictedTick:1
ClientWorld Dodge:False Frame:1213 ServerTick:1398 InterpolationTick:1393 PredictedTick:2
ClientWorld Dodge:False Frame:1213 ServerTick:1399 InterpolationTick:1393 PredictedTick:3
ClientWorld Dodge:False Frame:1213 ServerTick:1400 InterpolationTick:1393 PredictedTick:4
ServerWorld Dodge:True Frame:1214 ServerTick:1397 InterpolationTick:1397 PredictedTick:0

Yes

The client is ahed of the server. And apply the dodge at tick 1397.
The client does does prediction multiple time per frame for two reasons:

  • by rolling back to to the latest tick received by the server (depending how frequently he receive the updates)
  • when continue prediction, by rolling back to last full-tick backup and simulate a partial tick

In Client/Server setup, with network simulator ON (but 0 latency) sockets are used (not IPC) and the client is usually (as you can see from the number) 4 ticks ahead.
The client when rollback and replay, re-apply all the input from tick X to tick Y (they are in the input buffer).

When the client received the message from server for tick 1394, he rollback the state and resimulate tick 1395,1396,1397,, 1398.
The client when rollback and replay, re-apply all the input from tick X to tick Y (they are in the input buffer).
And only in tick 1397 (and only in that one) dodge is set.
In all other ticks it is false (as it should be).

You will start seeing again Dodge always false when the client receive the tick 1397 from the server. At that point, he will reply 1398,1399,1400,1401 and dodge (unless you pressed again) will be false.

1 Like