Friends, food, and flourishing

Delegates, actions, events,.. Oh my!

Another in a series of posts to help me think. This post is about how one object lets another know something has happened. C# and Unity.

Context

Here are three versions of Kyle, a free Unity character.

Three robots

The only thing they can do (for now) is jump. There’s an animation controller with two animations, idle and jump:

Animator controller

The transition from idle to jump happens when the Jump trigger is set.

Each robot has a simple controller:

				
					    public class RobotController : MonoBehaviour
    {
        private Animator _animator;
        private static readonly int Jump = Animator.StringToHash("Jump");

        private void Awake()
        {
            _animator = gameObject.GetComponent<Animator>();
        }
        
        public void JumpForJoy()
        {
            _animator.SetTrigger(Jump);
        }

    }
				
			

(Rider suggested the StringToHash thing. I don’t know whether it makes a difference.)

When JumpForJoy() is called, the trigger Jump is set, the animator goes to the Jump state, plays an animation, and goes back to Idle.

Jumping

Other assets:

  • The animations are from the Basic Motions asset. It’s a collection of a bazillion animations you can use with humanoid animation models. There’s a free version, too. Good stuff.
  • The landscape was made using MicroVerse, a world-building system I’m really digging. (Pun intended. You gotta own your puns.) It’s my world-building tool of choice.
  • The plants are from Dreamscape Meadows. You can make beautiful places with it.
  • The hats are from Low Poly Hats #1. Robots like hats.

Let’s go over some different ways to call JumpForJoy() and other methods we’ll add later.

Direct method call to each robot

The simplest way is to have a separate call to JumpForJoy() for each robot. Let’s do that first.

The DirectSinglePlayer component is part of a GameObject that handles input:

GameObject handling input

GameInputActions has one binding in the Robot map: the space key is bound to Jump.

Here’s the component’s code:

				
					    public class DirectSinglePlayer : MonoBehaviour
    {
        [SerializeField] private RobotController kyle;
        [SerializeField] private RobotController kieth;
        [SerializeField] private RobotController kelly;

        public void OnJump()
        {
            kyle.JumpForJoy();
            kieth.JumpForJoy();
            kelly.JumpForJoy();
        }
    }
				
			

It makes a direct call to every single robot’s jump method.

It works. However, if you add a robot to the scene and want it to jump, you have to change the code. A game designer 🐱‍👤 could not make the change by themselves. They’d need a programmer 😜, too. Slows things down.

Let’s improve the fit, as Alvor of Riverwood might say.

Direct calls to a list of robots

I’ve often done things this way in the past. Here’s the code:

				
					    public class DirectListPlayer : MonoBehaviour
    {
        [SerializeField] private List<RobotController> robots;

        public void OnJump()
        {
            foreach (RobotController robot in robots)
            {
                robot.JumpForJoy();
            }
        }

    }
				
			

Line 3 makes an array of robots. OnJump() calls JumpForJoy() for each element in the array, however many that is. One, three, fifty… whatevs. You add to the robots in the array in the editor, dragging and dropping away:

Direct calls to a list of robots

The game designer 🐱‍👤 can add as many robots as they want, without needing a programmer 😜 to change a thing. That’s a win!

It’s good even if you work alone. You can add robots in the Unity editor, without having to open the code, remember how it works, and change it.

We could just leave it there, and call it good. Big productivity win! Buuuuuut… let’s geek into it.

Delegates

Time passes. Kyle goes to a standup open mic. Banana Man 🍌 joins in on the fun. Beary 🐻 does, too.

The team changes

Why is Beary 🐻 wearing a fire hat? Don’t ask.

Recall that Kieth and Kelly, Kyle’s pals, have controllers with a JumpForJoy() method. Banana Man 🍌 and Beary 🐻 have different methods, reflecting their different natures. Banana Man 🍌 has HopForHappiness(). Beary 🐻 has LeapForLove().

The previous player controller had code like this:

				
					        [SerializeField] private List<RobotController> robots;

        public void OnJump()
        {
            foreach (RobotController robot in robots)
            {
                robot.JumpForJoy();
            }
        }
				
			

We could rewrite it, adding lists for Banana Men 🍌 and Bears 🐻. Then add two new loops, calling HopForHappiness() and LeapForLove().

That would work, but we’d have to rewrite the player controller every time we have a new type of jumpist.

We can use delegates to improve things. A delegate is a reference to a method. When you tell a delegate…

“Hey, delegate! Run you, d00d!”

… it will run whatever it points to. By changing what it points to, we change what happens when the delegate runs.

Here’s the new player controller’s GameObject, now called DelegatePlayer:

As before, it has a PlayerInput to grab keystrokes, and send them to the script DelegatePlayer. When the player hits the jump key, DelegatePlayer tells everyone, “Hey! The player jumped! Do whatever you need to do!”

For this to work, everything wanting to know about jumping will register itself with DelegatePlayer when the game starts.

To summarize:

  • When the game starts, every jumpist, no matter what type of thing it is, registers itself with the player controller (the code handling player input).
  • When the player jumps, the player controller tells everything that registered the player has jumped.
  • Each jumpist does whatever it needs to do to.

OK, let’s apply it all to this example. Here’s one of the jumpist’s controllers, Beary 🐻 in this case:

Bear GameObject

The controller has a reference to the player controller, the thing receiving player input. The robot and Banana Man 🍌 controllers look the same.

When it starts, each jumpist (robot, bear, spaceship, whatevs) tells PlayerDelegate (monitoring user input):

“Hey, PlayerDelegate! Tell me when the player wants to jump!”

When the player jumps, PlayerDelegate says,

“You blokes who wanted to know when the player jumped? They just did.”

PlayerDelegate doesn’t need to know how the jumpists get into the air. They could jump, hop, leap, or something else. It doesn’t care.

Now we can add new jumpist types, as many as we want, without changing the player controller’s code. Kangaroos, cats, koalas, whatevs. Cool! Neato keen! Groovy, baby! (Yeah, I’m a hep cat.)

Now for the code. Let’s start with the jumpist’s controllers. Here’s the robot controller.

				
					public class RobotControllerDelegate : MonoBehaviour
    {
        private Animator _animator;
        private static readonly int Jump = Animator.StringToHash("Jump");

        // Reference to the player controller.
        [SerializeField] private PlayerDelegate player;
        private void Awake()
        {
            _animator = gameObject.GetComponent<Animator>();
            // Register with the player controller to receive jumps.
            player.onJump += JumpForJoy;
        }

        private void JumpForJoy()
        {
            _animator.SetTrigger(Jump);
        }

    }
				
			
It has a reference to PlayerDelegate, in line 7, the thing that knows when the player jumps.We’ll see the code for that in a minute. Line 12, in Awake(), tells the PlayerDelegate to call JumpForJoy() when the player jumps. This is how RobotControllerDelegate registers itself with PlayerDelegate. JumpForJoy() is the method that does the jumping. Here’s Beary’s 🐻 and Banana Man’s 🍌 controllers:
				
					    public class BearControllerDelegate : MonoBehaviour
    {
        private Animator _animator;
        private static readonly int Leap = Animator.StringToHash("Leap");

        // Reference to the player controller.
        [SerializeField] private PlayerDelegate player;
        private void Awake()
        {
            _animator = gameObject.GetComponent<Animator>();
            // Register with the player controller to receive jumps.
            player.onJump += LeapForLove;
        }

        /// <summary>
        /// What to do when This gets a jump.
        /// </summary>
        private void LeapForLove()
        {
            _animator.SetTrigger(Leap);
        }
    }
    
    
    
    public class BananaManControllerDelegate : MonoBehaviour
    {
        private Animator _animator;
        private static readonly int Hop = Animator.StringToHash("Hop");

        // Reference to the player controller.
        [SerializeField] private PlayerDelegate player;
        private void Awake()
        {
            _animator = gameObject.GetComponent<Animator>();
            // Register with the player controller to receive jumps.
            player.onJump += HopForHappiness;
        }

        private void HopForHappiness()
        {
            _animator.SetTrigger(Hop);
        }
    }
				
			

The same thing, except they leap and hop rather than jumping. Lines 12 and 37 show the methods they call when PlayerDelegate says the player jumped.

Here’s PlayerDelegate, the thing monitoring user input. When the spacebar is pressed, OnJump is called.

				
					    public class PlayerDelegate : MonoBehaviour
    {
        
        public delegate void Jumpers();
        public Jumpers onJump;
        
        public void OnJump()
        {
            onJump?.Invoke();
        }

    }
				
			

Lines 4 and 5 make onJump, the thing the jumpists register themselves with, when they do player.onJump += JumpForJoy, or player.onJump += LeapForLove, or player.onJump += HopForHappiness, in their Awake()s.

Line 9 calls all the methods added to onJump:

onJump?.Invoke();

PlayerDelegate doesn’t know what sort of things they are. BTW, the ? means Invoke() isn’t run when onJump is null, that is, nothing has been added to it.

Let’s go back to the declarations. Line 5…

public Jumpers onJump;

… looks like it creates a field of type Jumpers. Huh. Where is that type? The type is created by the previous line:

public delegate void Jumpers();

The delegate keyword creates a new type called Jumpers. As you know, types hold data, like the int type holds whole numbers, and the GameObject type holds references to game objects.

Types created with delegate hold references to methods, with the signature given in the declaration. So Jumpers can hold references to methods with no arguments, that return void.

Here are delegates with different signatures. This…

public delegate bool Talkers(Animal animal);

… says Talkers is a type containing references to methods taking one Animal argument, and returning a bool.

This…

public delegate float SpitAt(Animal animal, Substance substance, float quantity);

… creates the type SpitAt. It can reference methods with three arguments of the given types, returning a float.

Let’s put it all together. In PlayerDelegate, this…

public delegate void Jumpers();

… creates a type, Jumpers, that can contain references to methods with no arguments that return void. This…

public Jumpers onJump;

… creates a field of type Jumpers.

In the jumpist controllers, you have…

// Reference to the player controller.
[SerializeField] private PlayerDelegate player;
...
private void Awake()
{
...
// Register with the player controller to receive jumps.
player.onJump += HopForHappiness;
}
...
private void HopForHappiness()
{
...

Notice that HopForHappiness() has the signature Jumpers requires: no arguments, returns void.

With delegates, it’s the method signatures that matter, not their names. So…

private void JumpForJoy()
private void HopForHappiness()
private void LeapForLove()

… all have the same signature, and all are compatible with the Jumpers type in PlayerDelegate

public delegate void Jumpers();

So we can do this in the jumpists’ controllers:

player.onJump += JumpForJoy;
player.onJump += HopForHappiness;
player.onJump += LeapForLove;

To add a new jumpist type, like AirplaneController, we make a method with the right signature…

private void TakeOff()

… and in AirplaneController‘s Awake(), add TakeOff() to the set of methods PlayerDelegate calls when the player jumps…

player.onJump += TakeOff;

Delegates can be tricky to use the first few times, but you soon get used to it. Delegates reduce your code’s coupling, that is, what classes know about each other.

Unsubscribing

The code we’ve seen makes sense, but one thing is missing. Inside Awake(), we have code like this: player.onJump += JumpForJoy; Often, games create and destroy GameObjects with gay abandon. Here’s an example where an Evildoer is spawned, and later destroyed.
				
					private void SpawnEvilDoer() {
    EvilDoer evilDoer = Instantiate(prefab, transform.position, Quaternion.identity);
    player.onDetectEvilDoers += evilDoer.Detect();
    ...
}

private void EvilDoerSmooshed(EvilDoer evilDoer) {
    Destroy(evilDoer);
    ...
}
				
			

SpawnEvilDoer() makes evilDoer, and adds something to player. EvilDoerSmooshed() destroys evilDoer, reclaiming its memory. What about the data SpawnEvilDoer() added to player? Hopefully, it’ll be garbage collected. That would happen, right? Right?

Hmm.

What if you use object pooling? You instantiate fifty EvilDoers when the program starts, each with SetActive(false). To make a “new” EvilDoer, you don’t instantiate a new object. You take an existing one that isn’t being used from the pool, and SetActive(true).

You want this…

player.onDetectEvilDoers += evilDoer.Detect();

… to work when evilDoer is active, but not when it isn’t. In Detect(), you could add a check that evilDoer is active, but that might be, say, forty-eight more calls than you need to do.

The solution? Just as you can do this…

player.onDetectEvilDoers += evilDoer.Detect();

… you can…

player.onDetectEvilDoers -= evilDoer.Detect();

The difference is the -=. You could do the first one in, say, OnEnable(), and the second in OnDisable().

Actions and Funcs

Say you have this code:

				
					public delegate void Jumpers();
public Jumpers onJump;

public delegate void Crouchers();
public Crouchers onCrouch;

public delegate void Spinners();
public Spinners onSpin;

public delegate void Runners();
public Runners onRun;

...

player.onJump += JumpMethod;
player.onCrouch += CrouchMethod;
player.onSpin += SpinMethod;
player.onRun += RunMethod;
				
			

Jumpers, Crouchers, Spinners, and Runners all reference methods with the same signature: no arguments, returning void. Could we do this?

				
					public delegate void DoerOfThings();
public DoerOfThings onJump;
public DoerOfThings onCrouch;
public DoerOfThings onSpin;
public DoerOfThings onRun;

...

player.onJump += JumpMethod;
player.onCrouch += CrouchMethod;
player.onSpin += SpinMethod;
player.onRun += RunMethod;

				
			

Why, yes, we could. (Why do people say why? Maybe they don’t. It’s like “What’s the meaning of this?” Do people actually say that, or only in movies?)

C# has two DoerOfThings-like things bulit-in: Actions, and Funcs. Action references methods that can have arguments, but don’t return anything. There are different versions of Action for zero to 16 (!) arguments. Funcs are similar, but they return stuff.

The no-arg version is as if you had typed:

public delegate void Action();

But you don’t have to. So you could have in the player manager:

public Action onJump;

In, say, Beary’s 🐻 controller:

player.onJump += LeapForLove;

You can take it further, using events.

C# events

There are:

  • C# events, built into the language, and
  • Unity events, added to the Unity framework by the Unity team

We’re talking about the first one here.

Earlier, we had delegates. Then we had Actions and Funcs, that were shortcut ways of making delegates.

Now we have events. They

  • Standardize how event emitters send arguments to event receivers
  • Restrict what handlers do with delegate objects, avoiding unwanted side effects.

A warning: the names given to things are Strange. The symbol EventHandler doesn’t mean code that handles events. 😲 It’s a delegate, a reference to code handling events.

Here’s some code. I wasn’t at the PC with the jumping robots project when I wrote it. It’s a console app, with penguins throwing fish to seals. You know, as they do.

				
					class Program
{
    static void Main(string[] args)
    {
        // Make a penguin.
        Penguin p = new Penguin();
        // Make a seal. Tell it about the penguin.
        Seal s = new Seal(p);
        // Hurl a fish!
        p.ThrowFish();
    }
}

public class Penguin
{
    // Make an event other ojects can register with.
    // When the Penguin throws a fish, tell the 
    // registered objects about the incoming fish.
    public delegate void FishEventHandler(object source, EventArgs args); 
    public event FishEventHandler FishThrown;

    // When a fish is thrown, tell registered Seals about it.
    public void ThrowFish()
    {
        // C# events have two arguments, whether needed or not.
        FishThrown?.Invoke(this, EventArgs.Empty);
    }
}

public class Seal
{
    // Make a new seal. When the given Penguin tosses a fish,
    // call CatchFish().
    public Seal(Penguin p)
    {
        p.FishThrown += CatchFish;
    }

    // With events you get two arguments, whether you want them
    // or not.
    public void CatchFish(object source, EventArgs args)
    {
        Console.WriteLine("Seal: caught a fish");
    }
}
				
			

A penguin throws a fish to a seal. Lines 19 and 20 make a delegate:

public delegate void FishEventHandler(object source, EventArgs args);
public event FishEventHandler FishThrown;

Note the arguments:

  • An object, the thing throwing the event
  • An EventArgs

Events have both.

Other classes subscribe to the event as usual…

p.FishThrown += CatchFish;

… where p is a Penguin.

Here’s the penguin code invoking the method:

FishThrown?.Invoke(this, EventArgs.Empty);

The first argument is who threw the event. The second is MT in this case.

Now, let’s use EventArgs to send fish deets.

				
					    class Program
    {
        static void Main(string[] args)
        {
            // Make a fish.
            Fish f = new Fish("Trout", 2.1f);
            // Make a Penguin throwing the Fish.
            Penguin p = new Penguin(f);
            // Make a Seal catching what the Penguin throws.
            Seal s = new Seal(p);
            // Fish ho!
            p.ThrowFish();
        }
    }

    // Fish is-a EventArgs. You could have more
    // EventArgs subclasses for whales, horses, cakes,
    // or other things you might want to throw.
    public class Fish : EventArgs
    {
        // Normal classy stuff. Properties and a constructor.
        public string SpeciesName
        {
            get; private set;
        }

        public float Weight
        {
            get; private set;
        }

        public Fish(string speciesName, float weight)
        {
            SpeciesName = speciesName;
            Weight = weight;
        }
    }

    // Mostly the same.
    public class Penguin
    {
        public delegate void FishEventHandler(object source, Fish args);
        public event FishEventHandler FishThrown;

        // What the Penguin will throw.
        private Fish _fish;

        // A coupla constructors.
        public Penguin(string faveFishSpeciesName, float weight)
        {
            _fish = new Fish(faveFishSpeciesName, weight);
        }

        public Penguin(Fish fish)
        {
            _fish = fish;
        }

        // Launch a fish!
        // Tell whoever wants to know.
        public void ThrowFish()
        {
            FishThrown?.Invoke(this, _fish);
        }

    }

    // Mostly the same.
    public class Seal
    {

        public Seal(Penguin p)
        {
            p.FishThrown += CatchFish;
        }

        // We gots a Fish now.
        public void CatchFish(object source, Fish fishCaught)
        {
            Console.WriteLine($"Seal: caught a fish! Species: {fishCaught.SpeciesName} weighing {fishCaught.Weight} kilos");
        }
    }
				
			
The Fish class extends EventArgs. It holds data passed to methods handling events. Penguin passes fish deets along with the event, like this: private Fish _fish; ... FishThrown?.Invoke(this, _fish); Event delegates don’t return anything directly: public delegate void FishEventHandler... You can return data via the EventArgs argument, though. Add a read/write property, and the called method can change it. Maybe like this: class Fish : EventArgs {     ...     public bool IsCooked     {         get; set;     }     ...     } ... FishThrown?.Invoke(this, _fish); if (_fish.IsCooked) {     // An event receiver cooked the fish.     ... Or something.

UnityEvents

Unity adds a feature called a UnityEvent. Summary: they’re cool.

Above, we’ve used code to link event emitters and receivers, with code like p.FishThrown += CatchFish;. But wait, you can also link event emitters and receivers in the inspector! You’ve done this many times, like with this UI button:

Event in inspector

When ButtonOfDoom is clicked, call the Cheer method of Kyle’s RobotController. So, the link between the emitter and receiver is set in the inspector, not in code.

Why do this, you ask? A coupla reasons. First, civilians (nonprogrammers) can set this up. Second, the calls are more explicit. You can see them in the inspector.

The ButtonOfDoom call has no arguments. Just a call to the Cheer() method, with no data on what to cheer about.

We can also add an argument to the call:

A static event argument

Here, SaySomething gets called on three robots, with the arguments given.

There are three UnityEvent variants. The first is…

No arguments

No data is passed to the receiver, apart from the event call itself. Like the ButtonOfDoom we just saw. Here are the robots camping:

Eventing campsite

Let’s add a controller to the tent, for this case.

				
					    /// <summary>
    /// UnityEvent with no parameters.
    /// </summary>
    public class TentController : MonoBehaviour
    {

        public UnityEvent tented;
        
        public void TentMeBaby()
        {
            tented?.Invoke();
        }
        
    }
				
			
Not much code! We’ll arrange for the T key to call TentMeBaby(). It invokes whatever is bound to tented, a UnityEvent. No arguments This is like the setup for ButtonOfDoom. Drag and drop to link event emitters and receivers. Couldn’t get much easier! Here’s part of RobotController:
				
					        public void Grumble()
        {
            Debug.Log($"{gameObject.name} grumbles");
        }
        
        public void Cheer()
        {
            Debug.Log($"{gameObject.name} cheers");
        }
        
        public void Moan()
        {
            Debug.Log($"{gameObject.name} moans");
        }
				
			

One static argument

That works, but there’s a problem. What the robots say is locked in code. To change it, you’d have to get those pesky programmers involved. This would be more convenient for civilians: A static event argument There’s just one method, instead of three. Designers can make the robots say whatever they want. Let’s have the campfire controller handle this. CampFireMeBaby() runs when someone presses F:
				
					    public class CampFireController : MonoBehaviour
    {
        
        public UnityEvent campFired;
        
        public void CampFireMeBaby()
        {
            campFired?.Invoke();
        }
        
    }
				
			

Hang on, that’s the same as the tent’s code! That’s right, it’s the same code that handles the zero argument case. The differences are in:

  • The method that is called, SaySomething() in this case, has an argument.
  • How the links between emitter and receiver are set up in the inspector.

Here’s the method for the RobotController setup in the inspector, along with one of the earlier methods for comparison:

				
					public void SaySomething(string whatToSay)
{
    Debug.Log($"{gameObject.name} says: {whatToSay}");
}

public void Cheer()
{
    Debug.Log($"{gameObject.name} cheers");
}
				
			
SaySomething() has an argument, but it isn’t sent by the event emitter code: campFired?.Invoke();. Instead, the value for whatToSay comes from the inspector. Neato cool beans! You have to be careful when you set this up in the inspector. Setting up a call with a static argument in the inspector SaySomething() is in the list twice, once in the dynamic section, and once in the static section. To get data through the inspector, choose the second one. It’s called “static” because the argument’s value is from each entry in the list (unless you do tricky codefu we won’t talk about). This approach is limited. You can only send one argument this way, and it can only be a simple value type, like int or string. I tried sending a struct (and a class), but it didn’t work. 🙁 You can send multiple parameters, but not using the inspector.

Dynamic arguments

If you want to send more than one argument, or send a reference, you need to use dynamic arguments. The log in the campsite does this. Here’s its code:
				
					    public class LogController : MonoBehaviour
    {

        [System.Serializable]
        public class LogEvent : UnityEvent<string, float>{}

        public LogEvent logFired;

        public void LogMeBaby()
        {
            logFired?.Invoke("I am log!", Random.Range(0f, 1f));
        }
        
    }
				
			
Lines 4 and 5 create a subclass that’s essentially a container for sending arguments around. logFired in line 7 is an instance, and it appears in the inspector: Three weighty calls No arguments are given in the inspector. They’re supplied in code, in LogMeBaby(), triggered when the player presses L. LogMeBaby() invokes whatever is bound to logFired, set up in the inspector. LogMeBaby() sends two arguments to its subscribers:
  • A string, “I am log!”
  • A random float
As you can see in the screenshot, Kyle, Kieth, and Kelly have all subscribed with their WeightMessage method:
				
					        public void WeightyMessage(string message, float weight)
        {
            Debug.Log($"{gameObject.name} says {message} with a weight of {weight}");
        } 

				
			
What’s logged? The GameObject’s name (Kyle, Kieth, or Kelly), and the random value, both sent from the emitter (the log). Here’s what you see: Same number You might expect to get different random numbers (I did), but that’s not what happens. The code logFired?.Invoke("I am log!", Random.Range(0f, 1f)); sends the same data to every subscriber to logFired.

Conclusion

There are lotsa ways for scripts to call GameObjects (GOs). Directly one at a time, all the GOs in a list, or various forms of callbacks, including delegates, Actions, Funcs, and UnityEvents.

Direct calls are tightly coupled, but I’m not sure that’s a Bad Thing all the time. Some objects are tightly coupled in the real world. A horse and its rider, for example. It makes sense for their GOs to know about each other, if we’re trying to simulate real-world interactions between the pair.

For other cases, loose coupling is better. Say you have a bunch of animals in an area, and a squirrel runs by. It might be better for the animals not to have a direct reference to the squirrel, if squirreling is just one thing of many that can happen. Let doggos subscribe to squirrelly things; they’ll want to chase it. Cattos might or might not care. Horssos likely won’t care.

UnityEvents are pretty cool, since you can set them up in the inspector. Decouple game designers and coders.

Leave a Reply

Your email address will not be published. Required fields are marked *

css.php