Better alternative to list of delegates for one-shot notifications?

Got a question about delegates.

In my game, I’m using a List to keep track of a list of methods to call when an enemy is destroyed. It’s a one time notification that will go to 0-3 other components in either the same GameObject or components in other GameObjects, depending on the type of enemy and the context. I use object pooling for the enemies, so I need to be able to easily clean up the list when an enemy is despawned.

There are various scenarios that have different notification requirements. For instance, an enemy spawned as part of a mission needs to tell the mission class that it’s dead, what wave it’s part of, and whether it’s dead or just disabled. The mission class can decrement a counter for a particular enemy wave to determine completion of mission objectives, etc. There are a few other scenarios that are probably too detailed to get into.

So far it’s working fine. I’m just wondering if there’s a better way to go here.

What I’ve tried and rejected (and why):

UnityEvent - can’t pass args, so that’s out

Traditional C# delegate events - worked well with the traditional technique of subscribing to an event, like:

newFighter.OnShipNeutralized += _mission.OnShipNeutralized;

But my concern is that since the enemy GameObjects are pooled, I’d need to clean up the subscriptions when the enemy is despawned. Yeah, you can do -=, but you have to have references to all that stuff, which I don’t. There are ways of cleaning those up, but they’re ugly. It seems that event subscriptions are better suited for events that are going to be fired more than once and that aren’t tied to pooled objects.

BroadcastMessage - shouldn’t have to say much here. I went to the dark side shortly, since this worked for components within the same GameObject and was easy, but the main downside is that it simply wasn’t flexible enough.

So I ended up putting a public field on my main enemy class that other classes can add their methods to, like:

newFighter.ShipIsNeutralizedDelegateList.Add(_mission.OnShipNeutralized);

Then when the enemy is dead, the enemy class does this:

            for (int i = 0; i < ShipIsNeutralizedDelegateList.Count; i++)
            {
                ShipIsNeutralizedDelegateList[i].Invoke(new ShipEventArgs(GroupName, false));
            }

            // Now clear the list
            ShipIsNeutralizedDelegateList.Clear();

The downside for general use is that you have to wait for those methods to finish, but in my case, I’m not doing much in those, and besides, the enemy is dead at that point anyway, so there’s no particular rush.

Any thoughts on a better approach? What I have is working for what I need, but I’m curious.

You can pass any argument you want to a UnityEvent provided you create a new class that derives from a generic UnityEvent.

Simply do this:

[Serializable]
public class MyEvent : UnityEvent<MyArgs>{}

And you’ll be able to pass your args to it.

Usually in pooling systems GameObjects are disabled when unused. If your pooling system is set-up that way (which it most likely should) then you can create an “Enemy” MonoBehaviour and attach it to your enemies / ships and simply do this:

private void OnEnable(){
    // Subscribe to event...
}

private void OnDisable(){
    // Unsubscribe from event...
}

Ah, I didn’t know that. I’ll definitely have to check that out. It’s appealing, since it looks like UnityEvent has a straightforward RemoveAllListeners method.

Yes, despawned enemies are disabled with the pooling system I use (I typically use its OnSpawned and OnDespawned methods for that sort of initialization and cleanup). I can have between none and three subscriptions, so I’d have to have logic to know which ones to unsubscribe. Also, I don’t have references to the subscribers from the enemy class in all cases.

For instance, one ship type is a shield generator that has my main component (Ship.cs). It needs to tell a separate GameObject (a shield) to play its collapse sequence when the generator is destroyed. But I don’t want to keep a specific reference to the shield in Ship.cs; that doesn’t seem flexible enough.

Anyway, I will definitely play with UnityEvent. That’s starting to feel like the right way to go. I don’t know that I necessarily need it to solve this particular problem, but it could be a much better general solution to get in the habit of using.

Thanks!

You don’t need references to all the subscribers. You just need a reference to the object with the event/delegate.

fighterToDestroy.OnShipNeutralized = null;

If it’s tagged as an event with the ‘event’ keyword, then you’ll have to set it null in the scope of the class that owns the event.

This is because ‘event’ forces it so that only the owner of the event can actually invoke/dispose the event.

    public class Foo
    {
        public event Action Bar;
       
        public void Dispose()
        {
            this.Bar = null;
        }
    }

Really, it’s that easy?

It’s weird though, I did a Google search to see the proper way to clear all subscriptions, and this was the first hit:
http://stackoverflow.com/questions/91778/how-to-remove-all-event-handlers-from-a-control

Lots of crazy ways to unsubscribe everything.

One of the answers even specifically says “you cannot simply set the event to null.”

If that’s all there is to it, I might go with that (my event/delegate code that was working is still commented out).

Yes, I’ve seen such things in the past.

They’re hack ways of dealing with the fact that a event tagged with the ‘event’ keyword can only be set null from within the class that declares it (see my previous post for example).

If in my previous example of the class foo we did this:

var obj = new Foo();
obj.Bar += SomeCallback;
obj.Bar(); //compiler error, only the owner can invoke the event
obj.Bar = null; //compiler error, can only use operators += and -=

You get a compiler error.

This is just the implication of the ‘event’ keyword.

You could EASILY just say:

public class Foo
{
    public Action Bar;
}

And it will act pretty much the same… BUT you can then set it null, or even call the delegate:

var obj = new Foo();
obj.Bar += SomeCallback;
obj.Bar();
obj.Bar = null;

All legal, unlike with the ‘event’ keyword.

The point of the ‘event’ keyword is to encapsulate the invokation of the event. You can subscribe, and unscubscribe… that’s it. Unless the calling code is scoped in the class that declared the event, that’s the only code that can invoke.

SO, when you have a ‘Button’ from System.Windows.Controls, or some other class that is from a library that you don’t have access to the source of… there’s no way to remove events accept with the -=.

BUT, in your own code, you do have access.

1 Like

Ah, that makes so much more sense now.

THANKS!!!