I'm Chris.
I would like to make games but all that comes out are prototypes and mountains of assets. Am I doing this right?

  • 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,859 members Fans & Clans

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

Germany

Germany

530 members Geographic

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

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.

PlayBlack

PlayBlack

3 members Developer

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

5TH Generation Gamers

5TH Generation Gamers

563 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...

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

177 members Hobbies & Interests

This group is for automotive enthusiasts of all kinds.

Post comment Comments  (0 - 10 of 28)
Lоnerboner
Lоnerboner

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

Your comment will be anonymous unless you join the community. Or sign in with your social account:

X

Latest posts from @damagefilter

RT @VladCircusGame: Dear Dr. Jasper, I am happy. I have received one letter from Vlad Petrescu. We will all be together again. Next sh… T.co

Jul 2 2022

Unity be like ... xD T.co

May 24 2022

I have achieved full toon shadery, screen space cavity and temporal anti aliasing in Unitys URP. I'll call that a s… T.co

May 22 2022

Because this whole screen space cavity journey started in Unity, I felt compelled to implement it in #unity3d as we… T.co

May 21 2022

Uh. Successfully managed to re-create blenders screen space cavity overlay in #UnrealEngine - I'm happy with that r… T.co

May 18 2022

Material based toon shading in the #UnrealEngine without posterization (seriously ... wtf). Sadly, shadows and spe… T.co

May 17 2022