Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411500 Posts in 69373 Topics- by 58428 Members - Latest Member: shelton786

April 25, 2024, 11:07:29 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)C# Event System from Game Coding Complete
Pages: [1]
Print
Author Topic: C# Event System from Game Coding Complete  (Read 5568 times)
Juskelis
Level 1
*



View Profile
« on: June 22, 2015, 12:42:42 PM »

I'm a bit new here still so bear with me, but I wanted to talk out this event system I made a while back.

And by made I mean translated from Game Coding Complete 4th Ed's C++ version to C#. Also, there are a few bits that I know for a fact I did weird and I'm not sure if that's because of Unity/C# or my idiocy. Here we go.

The whole thing goes through three overarching steps:
  • Spawn event
  • Process event
  • Alert involved parties

GCC advises going for a class based approach because, as you will quickly learn if you ever do this, the number of events you have gets ridiculous and can become highly specialized to the specific event. It's a lot easier to set it up as classes. So, we start out with having a class of BaseEventData. BED is never called directly by anything, it's just a parent class that we're using to generalize our system. It looks something like this.

Code:
public class BaseEventData
{
private readonly static System.Guid evType = System.Guid.NewGuid();
public static System.Guid EvType
{
get
{
return evType;
}
}
public virtual System.Guid GetEvType() { return evType; }


private readonly float timeStamp;

public BaseEventData(float time = 0.0f) {timeStamp = time;}

public float GetTimeStamp() { return timeStamp; }
}
Note: one of the things I think is weird/poorly done is how the GUID needs so many different ways to access it.

BED, as stated, holds only what every single event has, which, in this case, is just a GUID and timestamp. The GUID is used to distinguish events quickly, a kind of who am I? typechecking that comes in handy. The timestamp may seem like a strange thing to add, and in a lot of cases it isn't going to be used, but it can be used if you have a ton of these events firing all the time. We'll get back to this in a bit, but suffice to say that the timestamp can help you out if things get slow.

With the BED set up, we can start working on the second and third step: processing events and sending them out. We won't do the first step until the end because we need to know what to call first.

The processing is handled in an EventManager, which for the sake of code simplicity/stupidity prevention I will make a singleton. Some people don't like singletons while others think they are super powerful. It really doesn't matter how you do it so long as you are sure you have everything talking to just one EventManager, be it a singleton or whatever.

The EM does a lot of stuff: it keeps track of which classes listen to which events, takes in events, and calls the specific classes for each event. The first step is to set up that relationship between classes and events, which since we're in C#, also helps with calling those classes when that event fires. How does it do this? The fancy pants things called delegates!

Delegates are sweet, in case you don't know. They are, in a really simplified statement, function pointers. However, delegates also have simple arithmetic applied to them. That is, you can add and subtract functions from it. So, a user can have one delegate call several functions, which can be built up one at a time and altered at runtime. Pretty freakin sweet!

We're going to use delegates to define which classes are listening to which events. This is nicer than having the EM carry a reference to the whole class for each event the class is listening to, because we could have a class listen to several events, and having the EM carry all of that extra data is just a mess. Not to mention that in order to use that option we would have to define an overarching EventListener class that all classes inherit from, and that's just disgusting. So! Let's use a dictionary, because those allow easy lookup on a value, in this case we're going to use those GUIDs from the BED as the keys and a special delegate EventHandler as the value. EventHandler is a void function which takes in a BED, just to keep things simple.

This guy is going to look something like this:
Code:
public delegate void EventHandler(BaseEventData data);

private Dictionary<System.Guid, EventHandler> EventListenerDict;

Adding and removing listeners from this is pretty easy.
Code:
public bool AddListener (System.Guid type, EventHandler helperDelegate)
{
if(!EventListenerDict.ContainsKey(type))
{
EventHandler temp = null;
EventListenerDict.Add(type, temp);
}
EventListenerDict [type] += helperDelegate;
return true;
}

public bool RemoveListener(System.Guid type, EventHandler helperDelegate)
{
if(EventListenerDict.ContainsKey(type))
{
EventListenerDict[type] -= helperDelegate;
return true;
}
return false;
}

The boolean return isn't really necessary, but can be helpful if your class has ambiguity in keeping track of whether or not it is in the system or not. These two functions are taking advantage of what I was talking about earlier, where we can use arithmetic operations or delegates to compile a list of all the functions we want to call.

Now we can move on to the part we've all been waiting for... adding events! First, we need to set up how exactly we're storing these events. Naturally, you'd want to use a Queue for these guys. First event fired should be the first event to be heard, right? It's a good idea to do this not just from a fairness point, but sometimes you have events that fire in a sequence, so the queue can make these operate correctly and as expected. The simple Queue however, does not have all we need. Imagine that we have a system where there are literally thousands of events fired each second. Okay maybe that's a little unrealistic, but imagine we have a system where a thousand events can fire occasionally all at once, while usually the events never go above 50 per frame. Normally, the game runs fine, but when those thousand events hit, the game stumbles, because our EM has to handle all of these events all at the same time, in one frame. That's no good!

So, we need to somehow limit how many events can be processed each frame. The way our EM is going to do it is through a timer: it can only run for x milliseconds before it just stops and waits for the next opportunity. Okay, so what does this have to do with the Queue itself? We just keep going afterwards, right? Events can very frequently cause new events to happen. If you have a simple queue like we do, those events will just go to the back of the queue, and you may never actually finish the queue up. That's a big problem! So, we're going to do a "double buffering" for events, have an active queue (events being currently handled) and an upcoming queue. In our version of the EM, we're going to abstract it to however many upcomings you want, but really two should be fine. If any of you have a good reason for more, tell me! I'm curious.

Code:
private const int NUM_QUEUES = 2;

private Queue<BaseEventData>[] eventQueues = new Queue<BaseEventData>[NUM_QUEUES];
private int activeQueue = 0;

Our adding events is pretty easy using this
Code:
public void EnqueueEvent(BaseEventData evt)
{
eventQueues[(activeQueue+1)%NUM_QUEUES].Enqueue (evt);
}
Basically this code is putting the event passed in into the next queue in the list. If we're at the final spot in the queue list, we wrap around to the first element.

Alright, so that's cool. Now all we have to do is handle the processing of the event.
Code:
public void ProcessEvents(float milliseconds)
{
float endTime = Time.time + milliseconds / 1000;
while(eventQueues[activeQueue].Count > 0 && Time.time <= endTime)
{
BaseEventData process = eventQueues[activeQueue].Dequeue();

if(EventListenerDict.ContainsKey(process.GetEvType()))
{
EventListenerDict[process.GetEvType()](process);
}
}
if(eventQueues[activeQueue].Count <= 0)
{
activeQueue = (activeQueue+1)%NUM_QUEUES;
}
}
So the first thing we do is figure out when we need to end. This is all done in milliseconds, so we need to convert those milliseconds to seconds. The while loop keeps going until either we're out of events or we've run out of time, whichever comes first. Then we check to see if we're done with the current events, and, if we are, move on to the next queue of events, again wrapping if we reach the end.

When I'm doing this in Unity, this function is called in FixedFrame. The float is really a number you have to toy with to get right. I went with 10, because I don't fire many events.



Alright! We have steps 2 and 3 down. Now onto step 1, spawning events.

The first thing we have to do is define events that make sense. For this example I'm going to use an example from my own space fighter game's EventData_SpaceshipDeath, which is fired whenever any spaceship blows up.

In GCC they recommend using a bitstream to store extra data, but for the most part this is overkill on a singleplayer, low impact game. They mostly say to use bitstreams for when you are on a network, which makes a lot of sense. However, since this isn't a network, we're just going to have our classes be defined with extra data members. Our EventData_SpaceshipDeath has the ID of the ship, the tag that is used for that ship, and the name of the pilot.
Code:
public class EventData_SpaceshipDeath : BaseEventData {
private readonly static System.Guid evType = System.Guid.NewGuid();
new public static System.Guid EvType
{
get
{
return evType;
}
}
public override System.Guid GetEvType() { return evType; }


private int shipID;
public int ID
{
get
{
return shipID;
}
}

private string tag;
public string Tag
{
get
{
return tag;
}
}

private string name;
public string Name
{
get
{
return name;
}
}

public EventData_SpaceshipDeath(Spaceship s) : base()
{
shipID = s.gameObject.GetInstanceID ();
tag = s.tag;

name = s.name;
}
}
Note: for some reason the GUID stuff is needed. I don't know why, and I feel like it is a problem with the BED's GUID setup. If someone knows how to fix it, that'd be awesome!

After that we have to define in our classes a way to spawn events, as well as listen for events. The system for listening goes something like this:
Code:
if(EventManager.Instance != null)
{
EventManager.EventHandler hlp = OnShipDeath;
EventManager.Instance.AddListener (EventData_SpaceshipDeath.EvType, hlp);
}
First we check to see if an EventManager exists (for the singleton implementation) and then we define an EventHandler for the function we want called when a spaceship dies. Then we pass the corresponding GUID and EventHandler to the AddListener function. Easy peasy!

I won't include the code, but the remove process is the exact same, just replace AddListener with RemoveListener. I usually call this in the OnDisable function in Unity, but really it depends on the system. You need to make sure you call RemoveListener though, but you'll usually get some errors in your EventManager class if you forget to RemoveListener.

Also note that you have to do the above for each event you want to listen to. One possible addition to this system could be allowing one call to add multiple.

To create an event is even simpler:
Code:
EventData_SpaceshipDeath evt = new EventData_SpaceshipDeath (this);
EventManager.Instance.EnqueueEvent (evt);
We just create an EventData_SpaceshipDeath object with our ship as the parameter (or whatever ship you want), and then call EnqueueEvent. It should be noted that you do still need to check that EventManager.Instance is not null.


That's it! The biggest downside of this system is that these last few steps tend to get littered in weird places in your program. For me most of my coding errors come from forgetting to check if EventManager.Instance exists before doing things. If you have any questions, feel free to ask! Also, get Game Coding Complete 4th Ed! It's pretty sweet.
Logged

Hydorah
Level 0
**


Improved Push Movement (Final?)


View Profile
« Reply #1 on: June 24, 2015, 02:52:05 AM »

Syntax weak, lines are heavy
Error while compiling already, Code's spaghetti
It's broken, but on the surface it looks done and ready
to drop bombs but It keeps on resetting
dropping all the pointers down, the whole goto goes so loud.
It's crashing, how? Everything's segfaulting now!
The memory runs out, freeze up, over. Blow!
Logged
Juskelis
Level 1
*



View Profile
« Reply #2 on: June 24, 2015, 12:48:34 PM »

Syntax weak, lines are heavy
Error while compiling already, Code's spaghetti
It's broken, but on the surface it looks done and ready
to drop bombs but It keeps on resetting
dropping all the pointers down, the whole goto goes so loud.
It's crashing, how? Everything's segfaulting now!
The memory runs out, freeze up, over. Blow!

so is this an actual complaint or are you code rapping because either way I'm a little confused but thank you
Logged

InfiniteStateMachine
Level 10
*****



View Profile
« Reply #3 on: June 24, 2015, 03:21:59 PM »

Decent implementation. Maybe a little heavy in that there's a lot of ceremony in setting up the event. Part of that issue is the language itself only allows objects though.

I never thought I'd say this but the win32 event system is actually quite well done and terse, even more impressive considering it was authored in the early 80's. Just make a switch statement that handles the messages you are interested in and then pass it on to the next piece of code that cares about events.
Logged

Juskelis
Level 1
*



View Profile
« Reply #4 on: June 25, 2015, 08:24:53 AM »

Decent implementation. Maybe a little heavy in that there's a lot of ceremony in setting up the event. Part of that issue is the language itself only allows objects though.

I never thought I'd say this but the win32 event system is actually quite well done and terse, even more impressive considering it was authored in the early 80's. Just make a switch statement that handles the messages you are interested in and then pass it on to the next piece of code that cares about events.

Yeah, there's definitely a lot of silly stuff you have to do on the client's end to get a class hooked in properly. Do you know if you can hook in your own custom events to the win32 event system?
Logged

InfiniteStateMachine
Level 10
*****



View Profile
« Reply #5 on: June 25, 2015, 08:27:38 AM »

Decent implementation. Maybe a little heavy in that there's a lot of ceremony in setting up the event. Part of that issue is the language itself only allows objects though.

I never thought I'd say this but the win32 event system is actually quite well done and terse, even more impressive considering it was authored in the early 80's. Just make a switch statement that handles the messages you are interested in and then pass it on to the next piece of code that cares about events.

Yeah, there's definitely a lot of silly stuff you have to do on the client's end to get a class hooked in properly. Do you know if you can hook in your own custom events to the win32 event system?

Yup. Send WM_COMMAND with a sub command that you define. I do it frequently when I need to communicate with photoshop on its main thread.
Logged

drjeats
Level 0
*



View Profile
« Reply #6 on: June 25, 2015, 11:26:11 AM »

Note: for some reason the GUID stuff is needed. I don't know why, and I feel like it is a problem with the BED's GUID setup. If someone knows how to fix it, that'd be awesome!

I might be interpreting this wrong, but it sounds like you're expecting the static BaseEventData.EvType property to be polymorphic? If so, no dice! That's why you need the virtual method there, the runtime needs an instance target (the this parameter) to select a method to call, and since static methods and properties get no instance, BaseEventData.EvType just goes directly to BaseEventData. I apologize if that was already obvious to you.

The GUID's also not required if you want to associate each unique event type with an event data subclass. You can can store the handlers in a Dictionary<System.Type, EventHandler>, then use eventObject.GetType() or typeof(EventData_OnShipDeath) to get a reference to the relevant System.Type.

Anyway, nice job! A certain major Unity GUI extension didn't buffer event disptaches like you do here, so everyone had to handle it themselves (after first getting a hang and having to force kill and restart Unity). Smiley
Logged
Juskelis
Level 1
*



View Profile
« Reply #7 on: June 28, 2015, 06:22:34 PM »

I might be interpreting this wrong, but it sounds like you're expecting the static BaseEventData.EvType property to be polymorphic? If so, no dice! That's why you need the virtual method there, the runtime needs an instance target (the this parameter) to select a method to call, and since static methods and properties get no instance, BaseEventData.EvType just goes directly to BaseEventData. I apologize if that was already obvious to you.
I had figured that was what was happening, but good to know that I wasn't wrong!

I might try out the typeof implementation you're talking about to see if I can clean up some of the implementation code with it.

Thanks for the feedback! Good to know I'm on the right track with this thing. Smiley

Logged

lithander
Level 3
***


View Profile WWW
« Reply #8 on: June 28, 2015, 11:50:02 PM »

There's a lot of stuff you need to code yourself in C++ that C# handles out of the box. Isn't that the point of using high level languages in the first place?

And one of the things that C# offers is language support for events. Why do you feel that the built-in solution was inadequate to your needs? Just curious...

(I've got similar concerns about using a GUID as event type.)
Logged

Juskelis
Level 1
*



View Profile
« Reply #9 on: June 29, 2015, 12:59:26 PM »

I actually did not know that existed.. ..hm. Looking through the C# documentation for it though it looks like you have to pass the whole object as well as the EventArgs whenever you're using the event delegate or the EventHandler delegate, which I really don't want to do because some objects that fire events could be stupid large and that would just be a waste of everyone's time. If I'm wrong, I'm gonna make the excuse that I just wanted to code it myself  Wink

I definitely have concerns about the GUID as event type, but more for the reason that I don't want to have a WhoAmI existing at all for this thing, which is more just me being fussy than anything.
Logged

Layl
Level 3
***

professional jerkface


View Profile WWW
« Reply #10 on: June 29, 2015, 01:15:59 PM »

Looking through the C# documentation for it though it looks like you have to pass the whole object as well as the EventArgs whenever you're using the event delegate or the EventHandler delegate, which I really don't want to do because some objects that fire events could be stupid large and that would just be a waste of everyone's time.

C# has value types but generally your classes are going to be reference type. Passing them is done by reference, not by value.
Logged
InfiniteStateMachine
Level 10
*****



View Profile
« Reply #11 on: June 29, 2015, 02:04:04 PM »

There's a fair amount of discourse regarding the event keyword. Enough that it's caused a lot of groups to make their own solutions. There's nothing wrong with making your own event system in c#.

Logged

lithander
Level 3
***


View Profile WWW
« Reply #12 on: June 29, 2015, 02:30:41 PM »

There's a fair amount of discourse regarding the event keyword. Enough that it's caused a lot of groups to make their own solutions. There's nothing wrong with making your own event system in c#.

It's not wrong if you have a good reason! Otherwise it's... well... a good exercise, maybe. Smiley
Logged

drjeats
Level 0
*



View Profile
« Reply #13 on: June 29, 2015, 08:03:40 PM »

E.g. having a BoxedAction<T> class so you can use reference semantics for delegates now and then.  Toast Right
Logged
Juskelis
Level 1
*



View Profile
« Reply #14 on: June 30, 2015, 02:28:02 PM »

So I've learned a lot! Namely that I don't know very much about C#. I totally forgot about the whole pass by reference thing for C# with my last reply. I need to do more reading!  Toast Right
Logged

Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic