Replication race condition between Replicated GameState and RPC call on client.

Hello,

I’m working on a multiplayer card game. Think of my game flow as a state machine. And in each state, there will be data going from the server to the client, and then back again.

State machine:

  1. Player1 does something
  2. Player2 does something
  3. Both players do something simultaneously.
  4. Done. Reveal outcome.
  5. Loop back to the beginning, and do it again.

I have replication and even RPCs working great. So far so good. But I ran into a race condition which is apparently normal, and I have some workaround ideas on how to do this, but I really want to know the proper way to do this so I’m not hacking nonsense together.

The race condition:

  1. My server sets one or more variables in GameState
  2. Server makes an RPC call to client to do some stuff.
  3. Client receives the RPC call, but the client’s GameState has not yet updated with that change made in the previous step… So it’s out of sync.

I had the idea to use RPC calls to drive my state machine (the number steps shown above). But my client relys on the GmaeState being perfectly up to date from the server, which I apparently cannot rely on if I’m using RPCs. Because it seems GameState changes are batched and can be a little slow.

My ideas for a proper solution here. Let me know what you think or if you have a better more proper solution

Solution idea A.) Just drive my state machine from the server solely off RepNotfiy, instead of RPCs. But my key question & concern here is, will I still have out of sync issues? Let’s say my server needs to set 5 replicated variables in GameState before it can finally send the signal to the client via RepNotify. Will I still get out of sync issues of the LAST variable I set in my GameState is that signal used to tell the client to begin? How do I know that all 5 are updated on the client’s GameState? What if only 3 or 4 are updated and the last one didn’t make it into the batch update? This solution doesn’t seem to guarantee that it’s in sync.

Set Variable1 in GameState
Set Variable2 in GameState
Set Variable3 in GameState
Set Variable4 in GameState
Set Variable5 in GameState
Set StateReady Variable in GameState that tells the client to GO and process the state.

Solution idea B.) Keep driving my state machine via RPCs. Somehow do some sort of checksum on all the data saved in GameState. When the server sends a message to the client telling it to do stuff, send a parameter that represents the checksum, and the client will just sleep/delay until it’s local client GameState checksum matches the checksum in the param.

Edit: I just made another observation. I don’t want to put game logic functions in my GameState blueprint. So that means if I fully switch over to using RepNotify I’ll likely need to use a bunch of Event Dispatchers to send signals out of the GmaeState and into other classes (like my GameMode for example). This is a lot of over engineering feel :-\ where as the RPC approach felt very simple and clean but came with the out of sync issues.

i’d recommend RepNotify, RPCs can have issues (drops, desync, unreliable)

the logic can still be in the GM, GM->SetValue on GS and GS replicates.

regarding getting all the data together you can use structs to package the data together or add the data to the RPC if you stick with that model.

you can use structs to package the data together

Right, in my particular case I don’t want to package it but I’ll definitely keep that in mind to minimize how many RepNotifies I need to use.

i’d recommend RepNotify, RPCs can have issues (drops, desync, unreliable)… the logic can still be in the GM, GM->SetValue on GS and GS replicates.

So is it “acceptable” to have some game logic functions in the GameState class though? Because if I move everything to GameState and lean on RepNotify, it feels like a huge oerengineering effort to leave these function calls out of GameState because it would require me to make a ton of bindings and have my GameMode & UI class bind to them, then call. it’s just so much stuff to maintain and add and it makes the code huge for something I feel should be simple.

Here is what I did, I took a shortcut and left the following code in my BP_GameState class. I just don’t know if it’s “acceptable” to put any code/function calls in GameState. I’m a noobie here but when I think of GameState of think of just purely variables that are set by the server and replicated to the client. That feels pure and clean. Where as adding state machine logic functions in the GameState feels unpure and incorrect.


I just did this refactor to switch over to RepNotify and completely avoid RPS for this. It works of course, but it just feels wrong to have this logic in my BP_GameState. But maybe that’s my noobness showing.

Option 1 : replicate variables via RPC (parameters). Update variables on client when receiving the RPC, then do the processing. Make sure the RPC is reliable to avoid drops. With this, you don’t really need to replicate variables anymore, unless you need to support late-join replication, in which case you should enable rep condition InitialOnly.

Option 2 : put all variables that must “go together” into a struct, and replicate the struct, and use RepNotify instead of RPC.

Option 3 : if you have way too many variables for either option to be a solution, checksum sounds like a good idea.


Clients do not have a GameMode object, so for them the GameState would be an equivalent of it.
Having client-side game logic run off GameState’s RepNotify or Multicasts seems perfectly fine to me.

1 Like

Could there not be an Option 4 which is the same as my Solution A in the OP? Solution A above only works assuming that saved variables in GameState are replicated in order. So in my example, if I set my “StateReady” variable in GameState I know 100% that Variabe1 through Variable5 have been set and I don’t need to worry about checksum. I just don’t know if I can make this assumption or if I need to use a checksum…

Clients do not have a GameMode object, so for them the GameState would be an equivalent of it. Having client-side game logic run on GameState’s RepNotify or Multicasts seems perfectly fine to me.

Yes thank you for that, I am aware of that. In my screenshot above, the GmaeMode only exists on the server, but on the client I have the BP_UIManager (only exists on Client, spawned by playerController) which essentially manages UI logic that is high level for the client.

No order guarantees, see

Dude. That breaks my noob-heart

For changes made to multiple properties across multiple frames, generally these changes should be received in the order that they were made. However, unideal network conditions can result in these changes being dropped and resent, so they may be received later. For example:

What I could do to alleviate my noob-worry, is this when the client receives GO, I’ll delay/sleep for like 50ms. It’s a turn based card game, so no one will care or notice a 50ms delay. I don’t think anyone would notice a 200ms delay in my game. There will be animations and stuff going on anyways.

So 99.99% of the time, it will be
Variable1
Variable2
Sleep for 50ms then GO

Then 0.001% of the time, there’s an out of order issue, it will be
Variable1
Sleep for 50ms
Variable2
GO

And of course the alternative is doing a checksum. Which I suppose I could implement but I’m not in the mood right now lol. Maybe I’ll wait and see how often this becomes a problem.

Reliable RPC from Server Pawn or Controller.

GM sets and manages the states (Rules etc). When ready it calls an event on the pawn or controller server-side copy (authority). The Auth Proxy would then relay the parameters via reliable RPC (owning client).

GM → Server Pawn/Controller (Authority) [parameters]
Pawn/Controller → Reliable RPC to Owning Client [parameters]

Everything the client needs for the move/state is received and processed in order. You can of course archive the new states on the GS, but there’s no real-time ordered reliability here.

If other clients need the same data, then instead you could Multicast from the Pawn (Authority), then use role/auth conditions to limit who does what with the passed data.

Autonomous (client), Simulated (copy of client on all other simulations), Authority (Server)