How to Implement Blackboard Architecture in Unity C#

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
today we're going to build a shared data store known as a Blackboard we're going to divide and conquer today so the first part will be about building the Blackboard that can handle many types efficiently and with as little boxing as possible then we'll build an editor tool so that we can easily configure starting values for any Blackboard from the inspector and finally we're going to discuss and Implement Blackboard architecture which is a way of coordinating multiple AI systems together now this is a pattern where you have a combination of a Blackboard an Arbiter and a group group of experts lots to do today so let's get started so let's get started with the Blackboard class the first thing we're going to need is a dictionary so we can keep everything organized by keys and values in a naive implementation of a Blackboard we would store all the keys as strings but we could actually store them as their own type that calculates its own hash code this would be type safe and slightly more performant than just using strings to get fast lookups in a dictionary you should implement the IE equatable interface this has one method which is called Equals we'll fill this in in one second I still want to store a string here and that's going to be the name of this particular property that the user chooses to make the lookups fast in the dictionary we'll use integers instead and I'll call that hashed key now let's accept the name through the Constructor and set it here our integer representing the hash key will actually be this string passed through a hashing algorithm now I've set it up as an extension method let's take a look at that really quick this algorithm iterates over every character in the string X ores the current hash with the character's asky value and then multiplies the result by a prime number this results in a new hash value for each character now this algorithm is known for having very low Collision rates which makes it great for producing unique keys in things like a dictionary we can now fill out our equals method and we can say if the hash key of this one is equal to the hash key of another one then they are equal a few more things we can Implement in this class as well is that we could override the normal equals object method so that we can compare a Blackboard key against any object and we can override the get hash code this is essential for the dictionary operation we just want to return the hashed key and let's override two string return the name and for convenience let's also implement the equals equals operator and let's also implement the not equals operator which actually is is just the inverse of the equals equals operator okay that's enough with the key if we come down to the Blackboard class where I was using string in the dictionary let's just replace that with the Blackboard key type that's all we're going to do with the Blackboard key class for right now so I'm just going to collapse it up so we can keep it in this file and just keep working here so users are going to be creating all kinds of strings to represent values in their Blackboard what would be very convenient for us is to have another dictionary where we can quickly do a look up by string to see if a key exists for that string or not so to facilitate that let's make a method just for getting or registering a key if it doesn't exist yet I'm going to do a simple null check here but we could expand this to make sure there's no blank or empty Keys going in as well the preconditions class is a class that we made several videos ago and I'll have a link to the code for that in this repository and also in the description so let's check and see if this string is actually in our key registry if it's not in the registry we need to make a new one so we can say new Blackboard key and we'll pass pass in the string that is the key name and then we can add this created key into the key registry either way we've got a key that we can return to the caller of this method now keep in mind that if you were to make a new key this way it's still not associated with any value yet so we can add another method here called contains key and we can check to see if our entries actually has a value associated with that key likewise if we want to remove an entry completely we can just pass in the key to this remove method I'm going to make a wrapper class for all the values that are going to go into the blackboard's dictionary we'll make this a serializable class and we'll call it Blackboard entry of type T the Blackboard entry can know about the key that it's related to and that'll help us with equality and comparisons but it also needs to know its value T and we should be able to know its type too we'll call that property value type let's make sure that we accept the key and the value through the Constructor here and we can set those and we can also set the value type at the same time now now since we have a reference to the key that belongs to this entry we can actually just use that to override this objects equals method and it's get hash code method too okay let's see how we can use this Blackboard entry generic class in our Blackboard I'll just collapse it up because we're all done with that class let's get back to the Blackboard the most frequently called method in this class is going to be to try to get a value out of the dictionary by passing in a key in this first line we're going to attempt to retrieve the value associated with the key from the entries dictionary and if successful we'll check whether the retrieved value is of the type Blackboard entry T and we'll cast it to this type if it is if we're successful we'll assign that value into our out parameter and we'll return true otherwise our out parameter becomes default and we return false we're also going to make a type safe version of a Setter here so let's have a set value of type T and for that we can accept any value T along with its key of course and we'll create a new Blackboard entry for that key value combination put it into the dictionary there so everything coming in and out of our dictionary should be a Blackboard entry of type T now we're going to go over the advantages of using these generic Blackboard entries in a moment but first let's write a debug method so that we can easily see what's going on in the blackboard at any time we can use some reflection to do this so don't be calling it every update but what we can do is just iterate over the entries and for each entry we can get its type now as long as that type matches the generic type Blackboard entry we know what properties are on there already right and one we're looking for is value so we can use the get property method to actually get access to that if it's null for some reason just continue otherwise let's get the value of that property and then let's just log out the key and the value to the console okay we've got a pretty robust little Blackboard system going on now so why don't we jump over to the hero class we were using in the last video and let's see how we can actually use it so I'm going to add a using statement to the top of the hero class and then I'm going to come down here and add some fields of course the main thing we're interested in is a reference to the actual Blackboard so let's create a new Blackboard for the hero and we're also going to have a reference to the is safe key now if you recall in the last video the top priority of our agent was to move to the safe spot Whenever there was danger so instead of maintaining that state in the hero class we're now going to have it as a value on the Blackboard that means any component any node in the behavior tree any other game object that has access to this Blackboard will be able to read and potentially write to that value as well so right now we're doing this purely in code so if I come down to the awake method here what I can do is actually register a new key we'll make this a little more user friendly in a bit but for now let's also set the value for this key to be false now a little ways down we have a local method here is safe now is safe was checking a member on the class called in danger we don't want to use that anymore we're going to use the Blackboard so let's comment that out now our new code isn't very different just instead of checking this in danger field we're instead going to query the Blackboard for the value because I called this one is safe instead of in danger I'm going to swap the true and false values here just so it's very clear to us what's going on with the value when we're safe that should mean we're not in danger to test this out let's come down to the update method I'm going to pick in a little helper get key down here so when I press the space bar I expect to get a value out from the Blackboard and I'm going to set it to its inverse so we're going to look for that is safe key and change it and then we'll debug that out to the console so we can see what's going on and that's enough to have a very basic Blackboard data store going on I don't need that in danger field anymore so I'm going to scroll up to the top and I'm just going to cut it out of here and that's really it we can go into unity recompile and test this thing out just by way of reminder we've got a little safe spot indicated by a Transformer right here at the head of this carpet so whenever the wizard is in danger he'll abort his other activities from the behavior tree and come back to the safe Zone let's hit play here and just watch him behave he's going to go for Treasure first because he's not in danger now if I press space he goes right to the safe spot if I press it again it toggles the value on the Blackboard it goes back so I'm just going to flip it a couple times here if I toggle the flag again he'll go grab that other treasure and then he'll just go on to patrol because that's his lowest priority thing and there is no more treasure so that's all he has to do right so that in a nutshell is how a Blackboard works in the absolute simplest way I mean we've got one Boolean value on the Blackboard but the Blackboard is now a data store that you can share not just with the wizard but with any node in the behavior tree any of the strategies being used by the behavior tree and potentially other things in the game now let's make this a lot more user friendly let's jump back into code and add even more functionality to make this better what would be really nice is if we could set some starter values for the Blackboard right inside of unity with a scriptable object so let's work on that next I'm going to call this Blackboard data the challenge here is going to be handling all the different types of data there might be now we're going to have some Primitives and some things are going to be passed by reference let's wrap them all up in a new type and I'm going to call that Blackboard entry data this is going to be similar to our Blackboard entry that we're using in the actual Blackboard but it needs to be a little bit more robust because it's handling input from the user this will make it convenient just to have a list of all these entries in our scriptable object and the users can add or remove them as they see fit so let's define that quickly and then I'm also going to make a struct here that will wrap up the actual value of each entry and this is going to help us return things without boxing values and also make sure that we're getting the right type so let's make sure that we have an enum here so that the users are going to be able to choose the type that they want and then each of these will be able to store its own type on top of that we are going to actually have to have storage for the different types of values we want to store so right now I'm just going to do one type and that's going to be Boolean after we've got this type working it'll be easy to implement integer float and so on the first thing I'm going to do is add just a small helper that's going to be called ASB so if the type parameter T matches the expected type and the value can be safe cast a t then it's going to return the value as type T otherwise it Returns the default value for type T to avoid any runtime errors and we're going to use this in another method called convert value this is going to use a switch on the type to determine which stored value to return and it will cast it to type of T if possible and here it's going to do that using the ASB Helper and we could inline that but just for readability I'm going to keep them separate finally to put all this together I'm going to add add an implicit operator bu so if we're looking for a boou it's going to call the convert Value method here for type bu not just some generic T okay now we've got a little system where we avoid any unnecessary boxing and we preserve type Integrity how can we use this let's come back up to the Blackboard entry data class this is what the user is going to actually be able to specify in the inspector for every single entry they want to start on the Blackboard so we need a string key we need them to be able to choose the type and we're also going to let them set the initial value before we leave this I'm going to fill out the two methods we need for I serialization callback receiver now the only thing I'm going to put here is when we deserialize this object let's make sure that the values type is actually the one that the user chose now let's suppose that you have gone and filled out all of your entries into the scriptable object let's get these starter values set into the actual Blackboard we can pass the Blackboard that we're going to use into this scriptable object and have it do all the heavy lifting of assigning keys and values we'll just iterate over all of the entries and to make this super convenient I'm going to come down into the Blackboard entry data and create a dispatch table so A dispatch table is a basically a dictionary that's going to look up between types and an action delegate that will do the setting the action delegate will accept a Blackboard the Blackboard key and the any value that means if we're setting a boo in we know that our action delegate should actually set as type bu and not just some type t or object we already set up the bu operator down below in the any value struct so this is going to make sure that when we're setting a value of Bo On The Blackboard it is a Boolean and not some object and something that has to be boxed or anything like that now to use the dispatch table we're going to make a public method and we can call that set value on blackboard and what that'll do is actually accept the black Blackboard but this entry knows what its key is so we can get or register a key onto the Blackboard and then we'll use the dispatch table based on the type to actually make the setting onto the Blackboard so in this case Boolean We'll add the other types A little bit later to finish this off we can just come back up to the Blackboard data scriptable object and where we were looping over every entry all we have to do is call that entry's set value on blackboard method okay that was probably the most complicated part of this entire video so I hope that all made sense what we want to have here is a system where the user can just pick any type they want in the inspector and then we're able to handle their Selections in a typesafe manner with minimal boxing so we need to jump back over to the hero class now because we're actually going to need a reference to these starter values as a scriptable object so if I come up here near the top I can actually just add another serialized field here and we'll just call it Blackboard data and that's it we'll be able to drw a reference into here but how do we actually use this thing here in this class well down here where we were setting things in code before now instead we can use the Blackboard data's public method just passing the Blackboard it will set all the values that also means I don't need this line here anymore I can just comment that out now let's build a custom inspector so that we can use this in a really nice way in the inspector we're going to do that by presenting them in a reorderable list we're going to set that up in the on enable method so first of all we're going to create a new reorderable list there's a lot of prams going on there why don't we hover over this and have a look at what they all mean so the four TRS there are draggable display header display add button and display remove button an entry list can also have several callbacks so let's zoom out here and we'll add them the first one is the draw header callback so what do we want to do when we're actually drawing the header well what I'd like to do is show labels for each of the key the type and the value now the other call back is when you actually want to draw the entries that the user is going to be filling out so that is called the draw element call back so you can see the call back accepts an index we're going to use that here to grab whichever element we're working on so we can grab that out of the serialized property get array element at index and then I'm just going to bump the Y by a little bit and then let's get each of the properties the key name value type and the actual value then we need a little wct for each one of these so we'll create a rect for the first one then for the type and then we'll make one for the value as well and then we'll put all that together we can just say editor guy property field so we'll make a property field for each of them the only difference here is that we're going to handle values a little bit differently now most of the values we're using are pretty straightforward so we can just go through a switch and that's because we're storing them all as individual types so if the value type that was chosen by the user is BU well we know that's going to be in the property named bull value so we can present it that way and the property field method will be smart enough to show the correct type in the inspector and we'll see that in just a minute now I'm not going to do any other types just yet so let's have a default argument out of range we just need to scroll down a little bit here and add on inspector guey first let's update the serialized object then make sure to call the do layout list method on our entry list and afterwards let's call the apply model ified properties method on the serialized object that'll make sure that all the changes the user made are preserved feels like we were out of unity for quite a while there well let's get to work so now we've got a field on the wizard that can accept the Blackboard data so let's make a new instance of Blackboard data scriptable object so that'll give us a new one here I'm just going to leave the default name for now that's fine but if we come up into the inspector you'll see if I've clicked plus I can now make some selections here so it's going to show me all the types but we've only got support for bu right now so we can leave it as that now if I was to select int you'll see I do get that argument out of range exception but let's leave it as buou and I'm actually going to put the key in here that matches what we're expecting which was is safe and why don't we select it as true to start with now I need to remember to come over to my Wizard and lock the inspector and I'm going to drag this over into there so we've got a reference in the hero to our starting values and now when I press play it should just behave exactly how it did before so let's check it out when I hit space yeah he goes to the safe spot yep exactly what I expect let's let him get a treasure and I'll hit it just before you got the other one perfect okay that's great let's go back to code for just one second I've just filled out all these other types so that you'll have an idea of how you can add more and how I've worked up the dispatch table here it's exactly what you'd expect you know when we're looking for a float we're going to set the value of type float for example let's come down here a little bit more and see I've added more entries here so we've got data types for INS floats bus Vector 3 you can easily add game objects transforms anything else that you want and we have implicit operators for each of them and also we have convert value in the switch is going to do the correct thing and so I've added a few more helpers below for as int and as float uh now you can inline all these things if you want but they just did it this way for readability and if you take a look at these reference types you see I'm passing them back as objects they don't need any special handling because they're not Primitives you know they're just going to be passed by reference anyway of course like always I'm going to check all this into the repository and the link will be in the description so if you need to take a closer look at it after you're done watching the video it'll be there so over in the editor class again I've just filled out the switch with the different types to make sure that they're calling the correct field on the any value object okay now we're going to get into the the topic of actual Blackboard architecture so the Blackboard architecture focuses on an actual Blackboard is kind of the centerpiece of the whole thing but it involves a few other components and the most important one is called an Arbiter the Arbiter is the decision maker of the entire thing now Beyond The Arbiter we are going to have also a panel of experts the experts are usually different AI systems that all would like access to the Blackboard to read and sometimes to write and act on values that are present there so they all want to take turns it's the Arbiter that decides who's most important and whose turn it actually is the experts all get a chance to say hey I think my request is this important and they can express that in a number value and then the Arbiter will take the one that is the most important and actually let them run some operations so you can think about this as a panel of experts could be anything from like a SWAT team to maybe a complex firing system on a tank that requires aiming firing and actually maneuvering around could have three different systems controlling that with the Arbiter deciding who's most important and executing things in the proper sequence so let's start here with an interface for each of the experts experts only really need to take care of two things the first one is going to be like putting your hand up hey I want to turn to use the Blackboard pick me and you're going to return a value of how important it is for you to use the Blackboard or get the arbiter's attention the other one is for the expert to be able to actually perform some kind of action so the Arbiter will choose which of the experts should be doing something and then we'll call its execute method we'll do a concrete expert in a little bit here let's move on to the Arbiter so the Arbiter is the decision maker and it needs to know a list of all of the experts experts should be able to register themselves with the Arbiter and and we can just make a simple method for that um we'll just make sure it's not null and then we can add it into the list now we also need to understand the concept of past actions so you could use the command pattern here but for Simplicity let's just keep a list of actions we want to perform on the Blackboard as experts decide hey I would really like to do this thing they can add an action to the Blackboard the Arbiter is going to decide which expert is the best one at any given time and then give them permission to add it'll pass the Blackboard to them and that expert can add actions as they see fit and then the Arbiter can execute them so we're going to have another method here called Blackboard iteration and the Blackboard iteration is going to figure out exactly what those actions are by selecting which expert is the best at this moment in time so we'll keep an INT that is the highest insistence then we'll iterate over all of the experts the experts can tell us us how important they think their request is and we'll record that whichever one is the highest we'll consider that to be the best expert right now once we've decided on the best expert well then we can actually get the best expert to execute and we can pass them the Blackboard that's like saying hey it's your turn to do something here's the Blackboard do whatever you need to do and that expert will start adding actions to the Blackboard and after they're done we'll grab all those actions they wrote there then we'll clear out the blackboard's actions and we'll return them to a controller object or whatever it might be so because I actually want the next example to work with multiple different systems we are going to make a controller so here like I had on The Wizard or the hero class I'm going to have a reference for the Blackboard data a Blackboard but now we're also going to have an Arbiter now you could Supply this controller in different ways even serialized Fields but I'm going to use the service locator so I'm going to register it and then I'm going to set the new values on our Blackboard and I'm also going to just debug it because I don't think we've actually use that yet in this video so I'll just make sure we print it out as soon as we've set the starter values now we need some public methods we need to be able to get the Blackboard from the controller we want to be able to register experts through the controller which will just go through to the Arbiter and I'll leave a note to myself to add a d register expert method later on in update is where we're going to start handling all the magic here now because the controller knows about the Blackboard and it knows about the Arbiter all we have to do really is just get that Arbiter to run its Blackboard iteration by passing in the Blackboard it's going to return us all the actions we can just execute them here now we could have executed them all from the Arbiter itself but this might give us the convenience of actually executing them on a mono Behavior besides which the controller is actually going to act like a factory for the Blackboard that means classes that are going to use this black board don't get a reference to it directly they're going to ask the controller to supply them with the correct Blackboard okay let's actually create an expert I'm going to make a new character called a scout so it's going to implement the I expert interface it's going to need to retain a reference to the Blackboard once it gets that reference from the controller and it's going to be the one that's going to decide if the wizard is safe or not so the Scout will look for danger and if it finds any danger it's going to post that now instead of building a full AI for this I'm going to bring over the get key down method from The Wizard so the Scout will be the one that decides not the wizard the Wizard's just going to do his other stuff in the start method let's get a reference to the Blackboard using the service locator then we can also use the service locator to register this expert with the controller now every expert has two methods to implement the first one was to get the insistence which returns an integer value when the Scout decides that needs to say something it has something to do it needs to let the Arbiter know and so we can say if that Boolean flag that I'm about to toggle is on we're going to return a value of 100 but if it's off we'll just return zero but what happens if the Arbiter gives us permission to execute something on the Blackboard well if it's our turn then we can add an action and we can just pass that in as a Lambda so that will be to actually set the value to toggle is safe this is exactly what we were doing before except it's not the wizard doing it this time it's the Scout doing it when the Arbiter decides it's time for the Scout to do something now if there was another another expert that sent back a higher priority than 100 the Scout's not going to get to do anything in that particular round maybe there's something more important than getting to safety I don't know now obviously these insistence values are just made up for this example in a real scenario you would be calculating insistence based on things on the Blackboard and in the world around you based on other sensors as well as whatever AI you've implemented for the Scout now here there is no AI I'm the AI pressing the space button right so it would be much more complex than this and can handle more complex scenarios now to save some time I've got everything set up here but let's just review here we've got the controller uh dragged a link already to the starting data which still only has one value in it now I've also created a global Service locator here and of course we've got our new Scout character now you can see him standing up on the the stairs there keeping a lookout so he's the one who decides if there's danger coming and if he updates the Blackboard to say so the wizard should react now I've made some changes to the wizard class as well he's getting the Blackboard from the controller I mean it's exactly the same as the Scout class so I didn't bother showing it in the video here but let's press play and just see what happens so the wizard is going to start going for Treasure right away because he's safe right so let him get one and then I press space and the Scout updates the Blackboard and The Wizard goes back to the safe spot if I press it again he'll grab his treasure and then he'll just start doing his usual Patrol but click it again and he goes back to the safe spot so that's looking pretty good obviously this is an extremely simple example and once you start getting you know four or five different enemies or systems that need to work together to outsmart the player or just do other activities together you know this sort of system becomes extremely powerful now just by way of example let's look at some other types here just to see how that inspector shows them up here I'm just going to put some values in here for you know the basic types so Vector 3 string and I got a float and so now if I press play we should see all of that debugged out so yeah it all looks correct this is great all right maybe half an hour of Blackboard architecture is enough even though that example is simple I hope it conveys the power of what you can get done with that actual Blackboard architecture pack pattern and maybe more importantly than that I hope the Blackboard itself is going to be useful for you not just in like type conversion and all that stuff but you know just having a general data store that you can use to carry data between nodes of your behavior trees or your state machine or whatever it might be so with that I'll just say if you made it to the end of the video I salute you if you have any questions please leave a comment below and I'll try to answer it otherwise I'll see you next week or maybe I'll catch you on Discord first
Info
Channel: git-amend
Views: 8,978
Rating: undefined out of 5
Keywords: unity tutorial, unity arcade game, unity game tutorial, unity, game development, game dev, game development unity, programming, c#, software development, learn to code, learn programming, unity tutorials, unity devlog, indiedev, gamedev, unity3d, unity blackboard, unity blackboard architecture, unity arbiter
Id: HNGJ8KOqdYQ
Channel Id: undefined
Length: 28min 57sec (1737 seconds)
Published: Sun May 12 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.