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.

I recently ran in to a “similar” scenario previously.

When you go in to the world of Multiplayer, things get complicated real fast. What worked beautifully in single player, can require a complete overhaul for MP. You have to keep in mind what objects exist on the server and EACH client. You have to mindful that the Server knows about all player controllers (including itself), and yet the clients only know about one (themselves).
You have to be aware of components only accessible on the server, and what items should/should not be replicated and that those replications come from the server.

You’ll incorporate “Switch Has Authority” nodes, as well as a healthy use of Server and Client only functions.

On to your point with delegates. When you’re adding the dynamic delegates, you have to be aware of where that Dynamic Delegate exists when it is being fired, called, broadcast… And whether or not that delegate should exist in some form of interface that is known to the server and all clients.

Enough “theory”, let me give you a recent example.

I wanted to make some client objects aware of certain activities from a part of the online sub-system.
I found the perfect delegate already existing, so I didn’t have to create one. I setup my client side code to add the desired class functions in to the delegate signature.
I had everything correct, proper includes, proper syntax; it all compiled perfectly! Yet, my clients were completely unaware when the delegate fired.

I quickly came to realize, that the delegate being fired, existed within an interface of the online subsystem. In order for my client delegates to properly bind (add) themselves to the live “instance” of the delegate, I had to get a SharedPtr reference to the active interface in use.
I don’t have a code snippet to give you off hand, but essentially it was (this isn’t exactly syntactically correct, so don’t try to copy :slight_smile: :

IOnlineSubsystem* Online = IOnlineSubsystem::Get();
IOnlineSessionPtr Session = Online->GetSessionInterface();
if (Session.IsValid())
{
  // Bind my class object & function to my class local delegate variable
  Session->Add //the rest of the name depends on the delegate name you're hooking in to, but you are attaching your locally defined class delegate to the desired multicast (in this example) delegate
}

If you can bear through a bit longer answer, here would be a more complete answer for this example.
Lets assume you’ve found a NetMultiCast delegate in the Session Interface, we’ll call it FEpicDelegate

In your custom class’s .h file; you’ll need to make a local Delegate of the same type
In my case, I also made a BP callable function to execute the binding during construction if some other conditions where met:

// Make a function to explicitly do the binding
UFUNCTION(BluePrintCallable)
  void BindMyDelegate();

// My function that I'm trying to bind
void MyDelegateCallBack(); //<Make sure your parameters match the Delegate Signature's

// Define Delegate Handle
FEpicDelegate MyEpicDelegateHandle;

In your .cpp file for this class; I’d do something to the effect of:

void MyClassName::BindMyDelegate()
{
    IOnlineSubsystem* Online = IOnlineSubsystem::Get();
    IOnlineSessionPtr Session = Online->GetSessionInterface();
    if (Session.IsValid())
    {
      MyEpicDelegateHandle.BindUObject(this, &MyClassName::MyDelegateCallBack);
      Session->AddEpicDelegate_Handle(MyEpicDelegateHandle);
    }
}

void MyClassName::MyDelegateCallBack()
{
  // Do your stuff here
}

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: