I'm Chris.
I'm making games and I'm writing tools for different things.
Such as games ;)

  • View media
  • View media
  • View media
  • View media
  • View media
  • View media
RSS My Blogs

Event and Unity

damagefilter Blog

Events!

Events, events. Unity and events. It's a gravely undervalued tool in your toolbox. There are many ways to go about events. Today I've read an article about it in the context of notGDC and I felt inspired to expand on the topic. Lets look at a very powerful and flexible approach to events. To understand this, you need to have a good understanding of generic types. That is not essential to using it though.

Lets see how one might approach events, given that we're looking for some sort of central event pumping facility because spreading this all over the project would be a maintenance nightmare. You'd have a place that can raise events. A dispatcher of sorts. You'd have another place that uses the dispatcher to raise events and yet another that will listen to events. So how about something like this:

// The thing which knows how to raise events.
class EventDispatcher {
	// we need the delegate definition and ...
	public delegate void OnEnemyDeath(Enemy killedEnemy);
	// ... we need to use it to subscribe listeners to it
	public static OnEnemyDeath EnemyDeathCallback;
	
	// and another pair
	public delegate void OnEnemySpawn(Enemy spawningEnemy);
	public static OnEnemySpawn EnemySpawnCallback;
	// and so forth
}

// The spawner will raise the respawn event
class EnemySpawner : MonoBehaviour {
	public int maxEnemies;
	public int currentEnemies;
	
	private void Update() {
		if (currentEnemies < maxEnemies) {
			// Do the spawning logic and then:
			EventDispatcher.EnemySpawnCallback(theFreshspawnedEnemy);
		}
	}
}

// This will listen to that event
class RandomStatisticsCollector : MonoBehaviour {
	private int monstersSpawnedTotal;
	private int witchesSpawnedTotal;
	
	private void Awake() {
		// register for the spawn event. we wanna count some stuff!
		EventDispatcher.EnemySpawnCallback += EnemySpawned;
	}
	
	private void OnDestroy() {
		// don't forget to remove listener when we're gone or else!
		EventDispatcher.EnemySpawnCallback -= EnemySpawned;
	}
	
	// here's the listener that acts on the event that was raised by the EnemySpawner
	private void EnemySpawned(Enemy e) {
		// I'm making things up here, but something like that maybe :)
		if (e.Type == EnemyType.Monster) {
			monstersSpawnedTotal++;
		}
		else {
			witchesSpawnedTotal++;
		}
	}
}

Okay, something like that. Now, there's nothing wrong with that at first glance but from the maintenance and flexibility standpoint, this is very rigid. That EventDispatcher could grow really quickly and you'll be having a whole lot of delegates which you will need to keep track of. What if the requirements change and a signature for one or more of them must be changed? Welp. That can possibly amount to a whole lot of work. The compiler will tell you everything you need to know of course but it would be nice if such a breakage could be avoided alltogether. You also need to know about the EventDispatcher instance in the places you raise events. Imagine that could be avoided. If you could minimise the spots where the EventDispatcher is actually used. Worth it, I would say.

Well then. Here comes my approach. If you don't feel like reading, here's the source with examples.

At first we need to tuck away round about 119 lines of boilerplate code that's gonna be the EventDispatcher. I put a version without comments here, it's shorter. For the actual code, refer to the link to the source, above :)

// Defines the callback for events
public delegate void Callback<in T>(T hook);

internal interface IEventContainer {
	void Call(IEvent e);
	void Add(Delegate d);
	void Remove(Delegate d);
}

internal class EventContainer<T> : IEventContainer where T : IEvent {
	private Callback<T> events;
	public void Call(IEvent e) {
		if (events != null) {
			events((T)e);
		}
	}

	public void Add(Delegate d) {
		events += d as Callback<T>;
	}

	public void Remove(Delegate d) {
		events -= d as Callback<T>;
	}
}

public class EventDispatcher {
	private static EventDispatcher instance;

	public static EventDispatcher Instance {
		get {
			if (instance == null) {
				instance = new EventDispatcher();
			}
			return instance;
		}
	}

	private Dictionary<Type, IEventContainer> registrants;

	private EventDispatcher() {
		registrants = new Dictionary<Type, IEventContainer>();
	}


	public void Register<T>(Callback<T> handler) where T : IEvent {
		var paramType = typeof(T);

		if (!registrants.ContainsKey(paramType)) {
			registrants.Add(paramType, new EventContainer<T>());
		}
		var handles = registrants[paramType];
		handles.Add(handler);
	}

	public void Unregister<T>(Callback<T> handler) where T : IEvent {
		var paramType = typeof(T);

		if (!registrants.ContainsKey(paramType)) {
			return;
		}
		var handlers = registrants[paramType];
		handlers.Remove(handler);
	}

	public void Call<T>(IEvent e) where T : IEvent {
		if (registrants.TryGetValue(typeof(T), out var d)) {
			d.Call(e);
		}
	}
}

Holy skateboarding hamster! There's some generic types going on in there. And a lot of that has a so called type constraint on it (where T : IEvent). We limit the type T so that it must be drived from an interface IEvent. IEvent is but an empty interface to get C# to comply with my weird mind. One could leave it out and use plain old object type instead. But I like the readability and that wee bit extra type safety so I cannot just throw random stuff into the EventDispatcher.

Lets continue with the Callback delegate. That is the one and only delegate we will ever need. The generic parameter will determine which kind of argument the concrete callback receives. This is then tucked away in the EventContainer - what I'm doing here is I'm basically passing on the generic type. Packing it up like that will save us some typecasting later on. The EventContainer uses the generic type to cast the event to the correct type and pass it into the delegate. If that's all too much weirdness for you, do not worry. You can understand this class as a big blackbox - it's not gonna break. You don't need to look at it all that often, if at all. But I shall explain further.

Going forward, lets see how events are registered with that one and only single event delegate type.

public void Register<T>(Callback<T> handler) where T : IEvent {
	var paramType = typeof(T);

	if (!registrants.ContainsKey(paramType)) {
		registrants.Add(paramType, new EventContainer<T>());
	}
	var handles = registrants[paramType];
	handles.Add(handler);
}

You notice there's a generic type again. We use it to identify the event type, key it into a dictionary and assign the handler to the corresponding EventContainer. That's basically it. Not a whole lot of magic going on here. Unregistering works about the same way. If you do not see the sense in all of this yet, do not worry. We'll get to it. But basically, what we have at this stage is a dictionary which we can query. We can say: I want the delegate that knows how to handle my "EnemySpawnEvent". And we'll get it.

Lets look at the Call method. That is the integral part. It is where the events are raised, where delegates are invoked.

public void Call<T>(IEvent e) where T : IEvent {
	if (registrants.TryGetValue(typeof(T), out var d)) {
		d.Call(e);
	}
}

It doesn't get any shorter than that. It's querying the dictionary for the type T and gets the EventContainer for it. Then passes the IEvent into the EventContainer. That, on the other hand, will do the calling. Here:

public void Call(IEvent e) {
	if (events != null) {
		events((T)e);
	}
}

The EventContainer, as you know, has a generic parameter, it fits with the definition of the IEvent that was passed in. We made sure of that when we called Register(). As such we can savely cast e to the specified target type T (feeling like explaining a mathematical formula right now).

You'll notive that we use the IEvent object for two things. For one, we use its type to identify which delegate, which EventContainer, we want to use. And for another, we pass it to the listeners as argument. That way, should we ever need to change what kind of data is passed on to listeners, we change the IEvent implementation but we do not have to touch the listeners. At all. Additionally, it can act as state. As it's a reference type, changes made to it by one listener will be seen by the next. Due to the nature of delegates however, the order of the invocation list is not guaranteed.

So that's the EventDispatcher done. Lots of boilerplate but from here on out, we can forget about it all. Lets do some events!

// That's actually all IEvent is. But hang on!
public interface IEvent {}

// Whot
public abstract class Event<T> : IEvent where T : IEvent {
	
	public void Call() {
		EventDispatcher.Instance.Call<T>(this);
	}

	public static void Register(Callback<T> handler) {
		EventDispatcher.Instance.Register(handler);
	}

	public static void Unregister(Callback<T> handler) {
		EventDispatcher.Instance.Unregister(handler);
	}
}

Okay here it gets juicy. We now, finally, have defined the IEvent interface. As promised. It's just an empty interface for some extra typesafety measures. So that you can't just throw anything in as event type. You probably could if you wanted to though. Interesting point is the abstract Event. I've added a type constraint to T here that states, you can pass in any T that derives from IEvent. The same rule applies everywhere in the EventDispatcher.

With the generic parameters again. Friggin hell! But bear with me, we tuck them away soon. Basically, this abstract Event is a comfort layer between you and the EventDispatcher. You see it has 2 static methods that can handle registering and unregistering at the dispatcher. Because of the generics, we can have this in here and it will work on all derived types automatically. Late static binding ftw!

I said we want to do some events. Lets do them! I'll pick the EnemySpawn example from earlier!

// Here, finally, the generic type T becomes concrete!
public class EnemySpawnEvent : Event<EnemySpawnEvent> {
	public Enemy SpawnedEnemy {
		get;
		set;
	}
}

There's the event. For our purposes that is all we need for it. Yours can have anything in them you wish to transport along. You see, the T parameter became concrete now. Looking back, this information is passed down right to the event registry to identify callbacks that use it. You can define this event anywhere you like. Lets wrap it up and put this to use by the first example code, way above.

// The spawner will rise the respawn event
class EnemySpawner : MonoBehaviour {
	public int maxEnemies;
	public int currentEnemies;
	private readonly EnemySpawnEvent spawnEvent = new EnemySpawnEvent();
	
	private void Update() {
		if (currentEnemies < maxEnemies) {
			// Do the spawning logic and then:
			spawnEvent.SpawnedEnemy = freshSpawnedEnemy;
			spawnEvent.Call();
		}
	}
}

// This will listen to that event
class RandomStatisticsCollector : MonoBehaviour {
	private int monstersSpawnedTotal;
	private int witchesSpawnedTotal;
	
	private void Awake() {
		// register for the spawn event. we wanna count some stuff!
		EnemySpawnEvent.Register(EnemySpawned);
	}
	
	private void OnDestroy() {
		// don't forget to remove listener when we're gone or else!
		EnemySpawnEvent.Unregister(EnemySpawned);
	}
	
	// here's the listener that acts on the event that was raised by the EnemySpawner
	private void EnemySpawned(EnemySpawnEvent e) {
		// I'm making things up here, but something like that maybe :)
		if (e.SpawnedEnemy.Type == EnemyType.Monster) {
			monstersSpawnedTotal++;
		}
		else {
			witchesSpawnedTotal++;
		}
	}
}

There we go. The major differences being that, for registering and unregistering you virtually only work on your Event type. You do not need to throw in the EventDispatcher all over your project. It's used only in one spot, the base event. Events are both, data transport and a way to identify corresponding listeners. If you want new events, just make them and use them, no further maintenance steps required.

Just remember. For every Register() you need to have an Unregister(). Keep your event listeners clean at all times! In Unity, a good way to go about this is to register in Awake and Unregister OnDestroy, as rule of thumb.

And that concludes my approach to doing events. In this case, specifically for Unity but you'll have noticed, you can use this kind of code about anywhere you want. In any project that would lend itself to the usage of events.

Leave me a comment if you like. I'm also keen on knowing, if this, maybe, is something incredibly terrible and that's why nobody does it like that :D

Ah yes, again. Here's the source with examples :).

The Vrennman Case

The Vrennman Case

damagefilter Blog
Start a group Groups
Unity Games

Unity Games

1,788 members Hobbies & Interests

For all Unity developers and developers-to-be, both beginners and professionals!

5TH Generation Gamers

5TH Generation Gamers

571 members Hardware & Tech

This is a group for all people who love or grew up with these 5th gen games. the 5th gen is also known as the 32-64 bit era and is the time of the atari...

Game Kombinat

Game Kombinat

1 member Developer

We're a loose group of people from europe, casually making casual games on the side for fun. That means we're working kinda slow but we'll get it done.

Germany

Germany

538 members Geographic

Germany, a country in Central Europe. Bordering Denmark, France, Austria, Czech Republic, Poland and many other countries.

PlayBlack

PlayBlack

3 members Developer

We're people who share our gaming stuff, including games, code and assets, with others and write about it.

GameArt Studio GmbH

GameArt Studio GmbH

3 members Developer & Publisher

Gameart Studio – Web Games of the Highest Quality | Here at GameArt Studio we've been dedicated to the creation and development of online games since...

Automotive Enthusiasts

Automotive Enthusiasts

180 members Hobbies & Interests

This group is for automotive enthusiasts of all kinds.

Comments  (0 - 10 of 28)
Lоner
Lоner

Your profile picture... Zee Captain...i love it

Reply Good karma Bad karma+2 votes
isz2
isz2

Moooooooinseh Alles klar :D

Reply Good karma Bad karma+2 votes
cW#Ravenblood

Merry X-Mas!

"Christmas is a time of joy, A time for love and cheer, a time for making memories, to last throughout the year"

Reply Good karma Bad karma+2 votes
damagefilter Creator
damagefilter

Weihnachten ist, wenn man nicht modden kann xD

Reply Good karma+2 votes
isz2
isz2

ok :D<3

Reply Good karma Bad karma+2 votes
isz2
isz2

was :'D

Reply Good karma Bad karma+2 votes
damagefilter Creator
damagefilter

Tell me on Steam :)

Reply Good karma+2 votes
Blindxside
Blindxside

Don't have a computer :l
But in Hammer, you can export as a .dxf file which I can import into MAX. Then I animate :D

Reply Good karma Bad karma+2 votes
damagefilter Creator
damagefilter

Yeah I was planning to practise that method since if you don't have surroundings you probbly wouldn't know ow to animate certain tigns right? ;)

Reply Good karma+2 votes
cW#Ravenblood

When you dont have a pc, how you can use max? O.O

Reply Good karma Bad karma+2 votes
Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.

Level
Avatar
Avatar
Last Online
Country
Germany 🇩🇪
Gender
Male
Friends
Become friends
Member watch
Follow
Statistics
Rank
1,772 of 675,443
Visitors
22,500 (2 today)
Time Online
1 week
Activity Points
1,425
Watchers
13 members
Comments
422
Site visits
5,197
Contact
Contact
Send Message
Homepage
Gamekombinat.com
Twitter

Latest tweets from @damagefilter

John sent a vacation photo from his trip to the peaceful town of Glandstery, where people hold barghests as yard do… T.co

Oct 26 2018

I made a wood. In it are evil spirits. You might not want to enter. After all. #gamedev #vrennmancase T.co

Oct 22 2018

If you planned on working today, forget it. Instead, you'll be staring at this all day long. I know I will. T.co

Oct 17 2018

RT @ds_detective: Keeping our arms up like this for 30 days has been exhausting. 4 hours left in the money summoning ritual. Back Se… T.co

Oct 12 2018

RT @ds_detective: We won two awards in the IMIRT 2017 Game Awards, Best Narrative and GAME OF THE YEAR! We were also runner up in two… T.co

Oct 9 2018

Friends
Eyaura
Eyaura Online
Kark-Jocke
Kark-Jocke Online
Blindxside
Blindxside Online
Serenity6Two2
Serenity6Two2 Online
nosfer4tu
nosfer4tu Online
ricekeks
ricekeks Online
Lоner
Lоner Online
shaulhadar
shaulhadar Online
Sklarlight
Sklarlight Online
neahc
neahc Online