Ensure System runs at least once every tick/frame?

I’m trying to wrap my head around the ECS lifecycle. I was under the assumption that (at least some) systems are guaranteed to execute at least once every tick/frame? I thought this was how everything could be deterministic and repeatable. Netcode for entities can use prediction ticks to determine what needs to be resimulated. Player input could be captured per frame for precision on physics simulations.

I was very surprised to see ticks being skipped when adding a Debug.Log to a basic ISystem that logs the current Server Tick (netcode for entities). I realize server tick is different than frames, but I thought a server tick was in lock-step with frames for deterministic and repeatable results.

I’ve read so much documentation and (potentially) outdated code that it’s all starting to blend together. I’d really appreciate some answer to a couple lifecycle questions to help anchor my learning:
Basic:

  • Do some systems run at least once per frame?
  • Can I get the current frame somehow? (Edit: Found UnityEngine.Time.frameCount)
  • If systems don’t run at least once every frame, how can you ensure asynchronous one-time events are only executed once, such as a mouse click for player input? Would you have to maintain state somewhere that you’ve already handled that version of input since you can’t use the execution frame as an indicator of if it has been handled or not?

Appreciate any comments or insights

Edit:
I found that UnityEngine.Time.frameCount seems to consistently increase by 1 each system loop, which is what I was expecting. However, ServerTick on Netcode for Entities increments faster than frame count, therefore some ServerTicks are skipped on system update.

I assume it’s not safe to use frameCount to “timestamp” player input, but I’d like to understand why. Likely due to resimulation/prediction purposes? Such a cool concept but hard to understand :frowning:

I found this thread: Mega-thread to help me learn to reason about NetCode

I highly recommend reading this thread for anyone finding this post in the future. I’m going to bookmark it as a source of documentation since the ECS documentation lacks this top-down overview that was beautifully spelled out. I ended up reading every paragraph multiple times because its so rich with information.

For posterity, I will answer my questions here and hopefully someone will correct me if I’m providing mis-information:
Q: Do some systems run at least once per frame?
A: Initialization system group, Presentation system group and (Client-Only) Simulation system group will run once per frame.

Q: Can I get the current frame somehow?
A: UnityEngine.Time.frameCount

Q: If systems don’t run at least once every frame, how can you ensure asynchronous one-time events are only executed once, such as a mouse click for player input?
A: Since the Simulation system group (and Prediction System groups) will run once per Simulation Tick on the server, they follow Simulation Tick behavior. Simulation Ticks can run 0, 1, or more than 1 times per frame. Since most user code belongs in PredictedSimulationSystemGroup, user code should never expect to be handled once per frame.

To my knowledge, there is no (native) way, even with InputEvents to guarantee your input is handled only once. Since the the server runs (and creates) simulation ticks but the client runs once per frame, the only time the client would be guaranteed to have access to the very next Simulation tick would be in the rollback prediction loop (PredictedSimulationSystemGroup). You cannot change input in this system because it’s conceptually already happened on the server in the past.

Also, to my knowledge, there is no way to guarantee the server will actually handle every server tick. This is the most confusing part to me, so please correct me if I’m wrong. The server will actually batch multiple simulation ticks together, represented by the SimulationStepBatchSize field on NetworkTime. This effectively is “skipping” ticks for the sake of CPU and results the principle that you can never depend on a specific simulation tick to run.

Therefore, to guarantee your input is only handled once, you must GetDataAtTick(ServerTick) - which will retrieve the last known input - and check if the tick of that input is within the range of: (ServerTick - SimulationStepBatchSize) to ServerTick.

The only remaining question I have is:

  • What determines when a Simulation tick is completed and when to start a new Simulation tick?
1 Like