How to handle Events in Unity DOTS! (C# Events from ECS)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this video we're going to look at two ways we can handle events in unity dots events are very powerful but as you know that is not about being data oriented so that means we have to be smart in order to implement events which are object oriented let's begin [Music] hello and welcome I'm your code monkey and this channel is all about helping you learn how to make your own games with enough tutorials made by a professional indie game developer so if you find the video helpful consider subscribing okay so here we're going to cover several ways of handling events in unity dots first of all if you're not familiar with dots and check out the playlist linked in the description I've also covered c-sharp events in another video so check that out events are excellent for helping keep your code clean and making sure that your scripts are nicely decoupled from each other however events are normally object based which makes them tricky when applied to dots which is that oriented and very heavy on multi grab now unity dots is still early in development which means the best practices aren't yet known so that's why here I will cover two different possible ways of handling events first I will cover handling events using a native Q&A struct for our event type and then for the second method we're going to do it by creating entities and adding an event component then in the end I will showcase a nice complete class I built in order to make events very easy to use which as always you can download included in the project files ok so here is the demo scene I have my player object over here working as an entity and when I press space I can make it jump and pipe start being spawned this is taken from a project I've been working on trying to make flappy bird completely with dots so if you're watching this in the future then check the link in the description the flappy bird dots project should be out within about a week so now our test here is we want an event to be fired whenever we go through a pipe the way that this is set up is the player never actually moves horizontally the pipes are the only ones that are moving here is the game and the scene view side by side you can see if the player is always right down the middle right on x equals zero and the pipes are the only ones moving this video is made possible thanks to these awesome supporters go to patreon.com/scishow to code monkey to get some perks and help keep the videos free for everyone so over here is the pipe move system it is pretty simple all we're doing is an entities for each cycling through every single pipe and grabbing the translation then we are modifying translation based on you move direction moving it to the left by certain mooo speed and at a time but before we do that we store the exposition before moving and then we store the exposition after moving and our event s is right here first we're testing if the exposition before is above zero and afterwards is below or equal to zero then it's in here that we have passed the player and we want to fire off our event so here it is again I start jumping and pipes are coming and I go through the pipe and it's in there that I want the event alright so let's start off by making our very basic event so here inside of the system let's just define a public event after event handler and let's just call it on pipe task so when the pipe passes the player so here it is a very basic event if you want to learn more about events in c-sharp then check the video linked in the description now let's start with one approach based on using a native cue so during our entities for each we're going to have our native cue and over here we're going to add our event onto the cue and then after the job is processed then we're going to read through the cue and fire off our event so first we need our native cue so we define it as a private native cue and now here we need a type so let's define a basic event struck to be our type so just a public struct let's go to our PI past events and now here we could also add a field if we wanted our event to have extra information but in this case and let's keep it simple and all you want to do is now when this event happens so you create a native cue of this time so we have our event queue and now in order to initialize it let's go into protected and override e on create method and in here we construct our event queue now here we pick our allocator and in this case let's use persistent so we're going to use the exact same cue every time we run the system and since we're using persistent then we also need to make sure to be allocated so it's also override the void on destroy and in here we just do even heal dot dispose okay all right so here we have our very simple native queue that contains elements of Pi past event now down here we need to pass the event queue on to our job and here in order to make sure that our job is actually running correctly in parallel then instead of using the event queue directly we need to use a native q parallel right so here we define a native q of the same type then we're going to use the type parallel writer and we simply get it by going into the event queue and as parallel brighter okay and now we can use this one safely inside of our job so when we have the event let's simply in queue something onto our queue and let's just pass in our PI past event and that's pretty much it very simple we just have our queue and we in queue an event whenever something happens now after we schedule our job here is the similar tricky part see what we're doing all the way up here is we're only scheduling the job so by the time we get to the code down here then this job has not yet actually run it has only been scheduled so if the job has in front and that means in native queue hasn't actually been filmed and the job system is also smart enough to know that this one is being used so we can only read from the queue after this job completes and finishes writing - so just to see the error let's try reading the queue anyways so we - a while go into the event queue and we try to DQ so just like this we have our event and let's test and there it is right away we can see our error essentially saying that we cannot read from this native Cuba for the previous job has completed so the way we make this work is by completing the job so before we try to DQ and let's go into our job handle and call complete so this will make sure that job completes which will free up our event queue so we can then safely read it and now if we run here we are and start jumping and yet nowhere and we go through the pipe any of there we go we have our event now let's test using the actual c-sharp event so here let's create a new script call it just testing darts events let's create a new game object and attach our script onto it okay okay now here in our basic monobehaviour script let's test our event so let's go and make a private void start and on start let's subscribe to our event so here first let's access the entities world so we need using unity empties and then here we can access the world access the default game object injection world to get or create the system of type of our pipe move system so we have reference to our system and then inside we have our very nice event and we simply subscribe like no so here we just do a debug mark on the pipe event now finally we just need to actually trigger our event so back in our move system and here instead of doing the debug log we simply do our event and simply invoke all right so there it is let's test ok here we are let's start jumping and as soon as we go through the pipe if there we go we have our pipe event awesome so just like this we have a job component system working in the entities world and it is firing a regular C sharp event which we can then capture from anywhere so for example we could capture this event on the UI to do something so here we have our first way of handling events and the pattern is pretty simple essentially we just need to create an event queue that lives inside of our system so we make it persistent then inside our on update we construct a parallel writer so we can work multi-threading then we do our normal entities for each whenever we have something that we want to fire an event we use the parallel writer in order to in queue our event then afterwards we force the job to complete and we simply try to DQ every event inside of our event queue and once we do we simply call our regular C sharp event that we can listen from anywhere and again here we are jumping and we go through a pipe and there you go we have our piped event so everything is working perfectly fine great now if you're familiar with how the job system works then you will have noticed how calling complete here will have some consequences essentially we're creating a sync point we're stopping the rest of our game once this job is completing that is obviously not ideal ideally we want to make this job but then let the job system decide when it should be completed so one approach we can take to try to solve that problem is to create another system so here I have another system and now we can make sure that this system runs at the end of the frame by using the attribute update in group then we can pass in the group for the unlaid simulation system group and just like this now this system would run at the end the frame so then in here we could call complete on our job and in doing so we would make sure that we would only actually try to complete right at the end the frame however as you can see that means that we need to store a reference job so we can access it from this system so we need to store this and expose it then we also need to expose the queue and we would need to handle the event in a different way since we can't evoke it directly from another system so this approach of having a separate system would be better for performance however at the cost of increased complexity so here is the first way of handling events we're using a native cue inside of our system now let's check out the second method which won't be based on creating an entity and adding an event component so here we have the system back on its starting satan like we saw before now this approach is going to be based on creating an entity and adding a component onto it so first let's make that event component so in here we can simply make our Publix truck let's call this the event component and we simply implement I component data okay here's our very simple component and now when we have our event we want to create an entity and add our component so the simplest way would be we go into the empty manager and come create entity however when using the entity manager we are forcing a sync point so this would break everything related to our job threads so instead of using the entity manager directly we need to use a entity command buffer in order to queue up our actions so let's create one in order to do that we need to access the command buffer system so let's grab it on our protected override on create we go into the world in order to get or create the system of type and simulation command buffer system this is the command buffer system that runs where at the end of the sim so let's store this okay we have a reference to our command buffer system and now in here we access a system in order to create a command buffer so this returns an entity command buffer and in here this works the same way as the native queue meaning we do not use the empty command buffer but rather we use the entity command buffer dot concurrent so this is the same as a native Q dot parallel writer and we grab it by going into the command buffer and to concur all right so we have a concurrent struct of our entity command buffer and now this is what we can use down here on our job so we simply use the command buffer in order to create an entity here for job index we pass in the entity in query index and just like this we are creating an empty entity however we don't want it to be empty we wanted to have our nice event component so if we inspect this function you can see that there is a version that takes an archetype so we can use that let's go up here in order to construct our entity archetype and in order to construct we simply access the entity manager and call create archetype and we pass in our type of our event component so now here we have an empty archetype when we can use it when creating our entity all right so just like this it should be working whenever the Pinecrest is the player we're going to create an entity and that entity won't be added with the event component now we just need one thing which is to make sure that the empty command buffer actually runs after this job completes so we need to make sure that we add this job as a dependency to our and simulation command buffer system so we just go in here and we add job handle for producer and we pass in this job camel so this ensures that our command buffer will run only after this job panel has been completed ok so let's test ok here we are so let's go and go through the pipe and now pause and now we can look into the entity debugger and here we see all of our entities right down here we see a new entity and if there is our nearly created entity with our event component awesome okay so we have our entity being created with the event component being added when the event is triggered now we need to go through our entities and look for this event component so back in our script after we do everything let's do an entities dot for each and we're going to look for entities with our event component and when you do find them then in here and let's fire off the event so it's the find event like we did previously so event event handler there it is we have the on pipe past event and in here we simply invoke it now in order to access this we need to be running on the main thread so we do an empties for each and we call run now after we run we want to clean up and destroy all of the entities with our event component so to do that we can simply use the entity manager and call destroy and theme and in here there's a version which takes an entity query so we can get our entity query of type of of our event component so this won't destroy all the entities with our event component okay so this seems like it would work however we have several issues now first of all by default the entities for each won't be using burst however we can have access references while I using burst so if we try to run like this if there it is we got in there right away so we need to make sure that we can't run and use without burst so we do entities without first and then the normal for each okay and now we still have the same issue that we had with a native queue method which is that down here all we have is our job has been scheduled it has not actually run so if we run the code just like this there go over here we have an error the error is because we were using the NT manager to modify the own list of entities but we cannot do that once the job still requires a reference to those entities so down here we're trying to access the entity native array well we cannot modify since it's currently been scheduled to run on this job so one way we can solve this is by ensuring that previous job completes very much like we did previously so here we simply call job handle dot complete so here we are and it looks good and as soon as we go through the pipe if there you go we have our pipe event correctly being fired awesome so just like this we have our second method working we create an entity command buffer then when we have our event we write to the command buffer the action to create an entity and add our event component then afterwards we complete our job and we cycle through all of our entities and have the event component and if we find some then we invoke our event and after doing so we destroy all the entities with our event component okay so here you can see our second method working now the one issue here is we are creating a sync point so as soon as you get in here we stop in order to complete the jump so that means the whole game is going to be paused whilst this job is working now the benefit of this approach is that we can listen to events on the exact frame that they happen so as soon as an event happens up here we catch it down here and the downside is that we're forcing the game to sound whilst our job runs however right now we're actually both pausing the entire game and on listening to events on the next frame so the reason is actually on the way up here we're using a command buffer created on the end simulation system so that means that the command buffer will only run at the end of the simulation now in order to actually listen to events on the same frame we need to manually make a new command buffer and handle it ourselves so here instead of getting the command buffer from the system we can do a new entity command buffer let's all keep to the temp job everything else goes through the same okay then here we do not add the job handle for producer but instead we go into the command buffer and we call playback pass in our entity manager and then we make sure to dispose of our command buffer now to test that the events are happening on the same frame let's go up here and add a field onto our event component so let's add a public double call it the elapsed time and in here let's go and pick up the time it's time and this is also how we add more information on to our events so in here we create that then we get our created entity and then we can use the command buffer in order to set the component onto our event entity and we set the new component and we pass in the elapsed time and then down here when we listen to our event let's print both so let's do a debug log on our event component down elapsed time so the elapsed time inside even component and then also the one that we captured up here somehow we can test if we're capturing the event on the exact same frame if both of these are exactly the same so let's test ok here we are and let's go and as soon as we pass the event any of there you go you can see the numbers so you can see seven point five six five five six five yep there you go so here we have the event firing correctly and we can indeed verify that they are happening on the exact same frame awesome now again as I was saying the issue with this approach is we're pausing the game in here was job completes so if we don't absolutely need to have our events be triggered in the exact frame then we can essentially what they could run and only test for the events that happen on the previous frame so here let's not call job handled are complete and let's go back to using the end simulation commands off first so let's get this and remove this okay so everything's the same as previously so doing it like this means that we're not going to have this code actually run before we run this code so essentially when we go through the entities and here we're going to go before the command buffer runs so we won't be running one frame behind and if we run like this we're going to have the same error that we saw before so we cannot use the ante manager in here but what we can do is create another command buffer so we construct a new command buffer and as we go through our entities with events then in here we can queue up the action to destroy the entity and we pass in this entity and yep just like that all right so now this should be working let's test so here we are and jump and as we go through the pike any of there it is we have our event being fired and if we look at the timing you can see that event was fired on 4.80 and yet it was captured on four point eight one so the event was actually triggered in one frame and then we only captured it in the next frame the benefit of this approach is we will let the job system handle whenever it's going to run the pipe move system so if you don't absolutely need the event to be caught in the exact frame that it's fired then this approach would be better since it wouldn't let the game continue uninterrupted so here is the complete second method we define a I component data for our event component and then we use a entity command buffer in order to construct an entity and add our event component then afterwards we read through all the entities with that component we fire off the event and we destroy that entity so this is the second method fully working now over here I have two nice classes that I wrote to handle events based on the entity method they work with generics so they should be usable in just about any scenario so this one uses the approach where it runs on the exact same frame and this one uses approach on the next frame I think the NT method is likely better than the native cube method so that's why I made both classes using this approach and I also made them to be as easy to use as possible so if you want you can inspect it yourself to see how it works it seems like a lot of code but it's really doing the same thing that we try to do in here so let's see how we can use this in the simplest way possible so here is the pipe move system using that very nice class so as you can see you define the normal C sharp event then on create we construct our nice object as you can see it's working with generics so as a generic you will pass in a certain component type for our event so in this case won't be working with this nice price passed and you can define your own component with whatever values you want just one thing the way that I set up it is necessary for the component to have at least one field even if you don't use it the reason is because later we're doing an eye job for each which does not work on empty tack components so we construct our object using our event component then down here we have the normal entities for each so before we do that we go into our dots events object in order to get an event trigger so this is struck that we can use safely inside of our entities for each then in here when we have our event we simply call on our trigger event and then we have two methods we can either pass in the event if we want to set it to a certain value or we can use a simplified version which simply creates the event with the archetype containing our event component in this case we're also passing in a value then afterwards we schedule we get our job handle so the same as previously and then we use our data fence objects you can't capture events we pass in the job handle where the events were scheduled and then we pass in a simple delegate to handle our events so delegate receives our event component and then we do whatever action we want to deal with our events so here we're invoking our normal c-sharp event and we're also printing the values just the test if this is working on the same frame or the next one so in this one I'm using the last version on the next frame so let's test okay so here we are and we start jumping and we go through the pipes and if there you go we have our event and you can verify that this one is using the version where it captures the event on the next frame so it's fired on three four three and it's captured on three four four and now here if I have an instance where I wanted to use the other method that captures on the same frame all I really need to do is use the other version so dots events use the same frame change all these and if there you go everything else is exactly the same and now if we test here we are and we go through and if there you go now you can see that we are now capturing on the exact same frame so here you can see how this class is extremely simple to use we just need to define it define the component for our event then we get the struct that we can use in our job we fire off the event and we capture the events very simple both of these classes are included in front so feel free to use them in your own projects alright so here we saw two approaches for handling events when using a native cue and when using an entity and an event component again that's is still in development so the best practices aren't yet known perhaps one of these has some benefits or some cons over the other that I'm not aware of or perhaps a completely different approach won't win out if you know another way of doing c-sharp events and dots and please post it in the comments either way here you have two possible ways of handling events events are extremely important so with this it should really help out when trying to make game as much three dots as possible if you're watching this in the future then check out the video where I made flappy bird almost entirely in dots for example the connection between the dots worm and the normal UI is handled using these events this video is made possible thanks to these awesome supporters go to patreon.com/scishow new code monkey to get some perks and help keep the videos free for everyone as always you can download the project files in utilities from in Tacoma com subscribe to the channel for more unity tutorials post any question have in the comments and I'll see you next time [Music]
Info
Channel: Code Monkey
Views: 26,344
Rating: undefined out of 5
Keywords: unity dots events, unity ecs events, unity dots ui, unity dots tutorial, unity dots, unity ecs, unity ecs ui, unity event system, unity events, c# events, unity 2019.3, code monkey, brackeys, unity tutorial, unity game tutorial, unity tutorial for beginners, unity 2d tutorial, unity 3d, unity, game design, game development, game dev, game development unity, unity 2d, unity 3d tutorial, programming, coding, c#, code, software development, learn to code, learn programming
Id: fkJ-7pqnRGo
Channel Id: undefined
Length: 25min 43sec (1543 seconds)
Published: Wed Feb 26 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.