When is it safe to call AddDynamic on a delegate?

Our project has an array of structs used to track information about sections of the level, one struct per section. The array itself and the structs are not replicated. Each client sets this up when they load the level based on static level data. The server just uses RPC’s to tell each client when and how to modify a section’s struct. We then have a bunch of systems that can react to changes in these structs. I’ve used delegates to achieve this. Basically, each system calls AddDynamic on the delegate for each section. Then if that section changes at all, it calls the delegate, which notifies each system, who in turn can decide how (or if) they want to react to that change. Works amazingly well, until we try it in multiplayer!

For some reason, in a multiplayer game the various systems on the first connected client are not responding to changes in the section structs. Any additional connected clients after the first work fine. For example, in a listen server scenario with two players, then the player on the server doesn’t work, while the player on the client works fine. In a dedicated server scenario, the first client to connect doesn’t work, while the second client works fine.

After a lot of debugging, I eventually tracked the problem down to the systems not receiving the call to the delegate. AddDynamic works correctly and if I inspect the delegates registered array by stepping through the code it shows all the systems in there, so it makes no sense to me. When a section changes and it calls the delegate, the systems are never notified even though they’re registered with the delegate!?!?

The sections struct array is set up on the client side inside the game state (non-replicated array). Then AddDynamic is called by each system once they have been created by the player controller, again client side. I thought maybe it might be some sort of network latency issue since the game state itself is replicated. So I tried calling AddDynamic in the Tick function (after waiting 1 second) of each system instead of in BeginPlay… and it worked! So this is telling me that for some reason delegates are not being registered correctly due to some sort of pending network transfer. I tried waiting 0.5 seconds, but that failed. It only works if I wait 1 or more seconds before calling AddDynamic. I find this to be a very dodgy hack as obviously different network conditions would affect the required delay time, which is too unreliable.

Is there anyone that might have a better understanding of how and why this is happening? Ideally, I would like to work out precisely when it would be safe to call AddDynamic and know that it will work.

Thanks for your reply, but I don’t think your answer really applies in this circumstance unfortunately. You appear to be referring to binding to an existing delegate. I’m trying to bind to a delegate that I’ve created on an object that the client owns. The system does in fact work for everyone except for the first player, who needs to wait at least 1 second before trying to bind for it to work. This is what I’m trying to figure out.

I’m half tempted to do away entirely with the delegate system and just pass the change through via direct references instead of delegates since I can’t figure out this problem and don’t want to “hack” it with a 1 second delay.

Found the problem. Turns out I was actually referencing the incorrect GameState instance when calling a multicast function on it, so the multicast wasn’t being broadcast correctly. Always use GetWorld()->GetGameState(), not a previously saved variable, which may now be wrong. As soon as I changed that, everything worked correctly. Thanks anyway TX_AlphaMale. Your comments actually got me thinking which helped me track down the issue.

I found this thread while searching around for why calling AddUniqueDynamic() on a multicast delegate was breaking in TArray with “Array has changed during ranged-for iteration!”. Despite my doing everything in what seemed like the same way as some very similar code in our code base.

For anyone else arriving here in that situation, the problem could also be that you haven’t marked the receiving method you’re registering as a UFUNCTION(). :expressionless: