Unity Dialogue System - Creating the Custom Inspector (IMGUI)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Our node based dialogue system is finally done. However, there still is an extra feature we'll be adding and that's our own Custom Inspector. Its purpose is quite simple: to make it easier for us to select the in-game starting dialogue for that particular game object. We'll be able to filter the dialogues by grouped and ungrouped dialogues, as well as only show starting dialogues or simply show all dialogues. Before we start doing that though, I do have a few Graphs prepared to make it easier for us to see if the custom inspector is working as it should. I'll be opening up the Graphs now so feel free to copy them and save them, to be able to see it yourself while you're following the tutorial. I'll also be leaving a link to download all the Graph Scriptable Objects as well as the Container Scriptable Objects, if you prefer that. Do note that it might only work if your dialogue system has the same code as mine, or most importantly, classes, variable names and folder paths. To make this Custom Inspector, we'll be using Unity's "Immediate Mode GUI" system. As for a Custom Inspector using UI Elements, I talk about the reason on why I won't be doing it at the "What about UI Elements?" chapter. Unity's "Immediate Mode GUI" is a code-based UI System. This is a separate system from their GameObject oriented UI system, also known as "Unity UI", or the Canvas System. This code-based UI System is mostly seen being used in Custom Inspectors or Editor Windows, so even if we're not going to be using UI Elements, this will likely still be useful for you to know. Unity provides us with a few classes that allows us to use this UI System, but we'll talk more about them in a bit. Before we do that, we should first create our Custom Inspector script. To do that, let's go to our Editor "DialogueSystem" folder and start by creating a new folder to which I'll name "Inspectors". Inside of this folder, create a new C# Script to which I'll name "DSInspector". Opening up our script, we'll do the usual removal of the default methods. For our inheritance we'll be swapping the "MonoBehaviour" inheritance with "Editor" instead which we'll need to import its namespace. I'll make this script be part of the "Inspectors" subnamespace. We now have a script inheriting from the "Editor" class, but we are not yet telling Unity that this script should be the custom inspector of a specific MonoBehaviour class. For Unity to know that, we need to give this class an attribute named "CustomEditor". When Unity sees that a class has this attribute, it will draw whatever UI we've specified there for every component that the custom inspector is for. Of course, because of that, this attribute requires the type of the class we want to make a custom inspector for. However, we haven't yet created that class. The class we want to make a custom inspector for will be the "DSDialogue" class, which will be the class that the runtime dialogue system you'll develop will be at. That simply means methods that show the current dialogue, methods that move on to the next dialogue, etc. We will not be doing that part in this tutorial series as that's really up to how you want your game dialogue system to work, as well as whether you want to do it using the old or the new input system, and because there are plenty of tutorials on doing that part out there, I'll leave that for you to explore. Regardless, we still need that class, so back in Unity, go to our non-Editor "DialogueSystem" folder and in the "Scripts" folder create a new C# Script to which I'll name "DSDialogue". Inside, feel free to remove the default methods. We'll leave the inheritance intact as we want to be able to add this class as a game object component. I'll also be leaving it in the "DS" namespace. Our class is created so we can now pass its type to the Custom Editor attribute. So back to our "DSInspector" script, above our class, type in the attribute of "[CustomEditor()]" and pass in "typeof(DSDialogue)". With that done, we need to start creating our dialogue variables, as we need to show some UI in the Inspector and save its values somewhere. These variables will be separated into two different variable types: The Dialogue Variables, which the "DSDialogue" script needs, and the Inspector Variables, which will be used for our Inspector only, to make it easier for us to choose the starting dialogue. Seeing the final result, we see that our Inspector is divided into 4 Areas: The Dialogue Container Area, the Filters Area, the Dialogue Group Area and the Dialogue Area. If there was no Custom Inspector to ease the selection of the starting dialogue, our "DSDialogue" script would simply hold the "dialogue" variable. The reason why is simply because that's the only variable we need to start a dialogue chain. That means that every other variable we see in our Inspector Areas are Inspector Variables. We now have our variables divided into "Dialogue" and "Inspector" variables. However, we'll need every single one of those variables, Dialogue and Inspector, to be created in our "DSDialogue" script. The reason why we need that is due to how the Custom Inspector script works. Let's say we were to create a variable of any type in our "DSInspector" script. In that same script, we would also update it whenever we clicked on a button. In Unity, whenever we click in a game object and we see the component custom inspector, the variable would be created. Then, if we were to click on the button, that variable value would be updated. The problem lies in our variable being created every time we see the custom inspector. This simply means that if we were to deselect the game object and select it again, the variable would get created again and its value would be reset. Because of that, we don't really have a way to store our data in the inspector script and instead need to save it in the "DSDialogue" script, where we know the data persists. I assume that the reason for this behaviour is so that Unity can simply draw the inspector according to the component data without needing to store a custom inspector for each of the existing components of that type. Of course, Unity does provide us with a way to access that data even though it's in another script. Knowing that, we can start creating our variables. So back in our "DSDialogue" script, we'll be separating the variables in 2 different areas for now: The Dialogue Scriptable Objects and the Filters. So, create our first "private" variable of type "DSDialogueContainerSO", to which I'll name "dialogueContainer". Make sure you import the "ScriptableObjects" namespace. Then, create another "private" variable of type "DSDialogueGroupSO", to which I'll name "dialogueGroup". As the last scriptable object variable, create a new "private DSDialogueSO", to which I'll name "dialogue". Make sure this last variable is of type "DSDialogueSO" and not "DSDialogue". For our "Filters", we'll be having the "Grouped Dialogues" filter and the "Starting Dialogues Only" filter. The "Grouped Dialogues" filter will allow us to filter through "Grouped Dialogues" when it's set to "true", or "Ungrouped Dialogues" when it's set to "false". The "Starting Dialogues Only" filter will only show Starting Dialogues if it's set to "true", which simply means that the dialogue "IsStartingDialogue" property is also set to "true". or all Dialogues if it's set to "false". So type in "private bool groupedDialogues" and then "private bool startingDialoguesOnly". To make sure these variables show in the Inspector and to make sure we can get them in our Inspector script, add the [SerializeField] attribute to each of the variables. Our variables are done so we can go back to our "DSInspector" script. In here, we need a place to be able to draw our UI. We can do it in a method named "OnInspectorGUI", which we need to override. To do that, simply press "Alt + Enter" in Visual Studio and select "Generate overrides". In there, press "Deselect All" and then tick the "OnInspectorGUI" option. When that's done, simply press "OK" to override the method. Feel free to remove the base method call. Anything we add in here will be shown in the component custom inspector. One thing to note about this method is that it gets called multiple times while the Inspector is active, which will turn out to be useful for us as it redraws the UI, which we'll need it to later on. We can now start drawing our UI here, but we are currently facing a problem: If our variables are in our "DSDialogue" script, how do we get to access them? We can easily do that by using the provided "serializedObject" Editor variable, together with its "FindProperty" method. Simply put, the "serializedObject" variable holds the object that's currently being inspected, which is our Dialogue object. Its "FindProperty" method allows us to get the data of a variable in that dialogue object, which we can then store in a variable of type "SerializedProperty". The difference here is that we can have this variable be in our Inspector script, as every time we enable the inspector, we'll be assigning a value to it using the "FindProperty" method and because of that, we don't really need to worry about it resetting, as it's always getting the dialogue object variable data. We'll go into a bit more detail on the SerializedObject and SerializedProperty later on. With that in mind, we can start creating our serialized property variables. Of course, we need one of these properties per variable, so we'll also divide them into the same areas: The Dialogue Scriptable Objects and the Filters. For our scriptable objects, type in: "private SerializedProperty" and I'll name it "dialogueContainerProperty". I'll simply add "Property" at the end of the "DSDialogue" variable names. Next, type in "private SerializedProperty dialogueGroupProperty", and for our last scriptable object, type in "private SerializedProperty dialogueProperty". For our filters, type in: "private SerializedProperty groupedDialoguesProperty" and "private SerializedProperty startingDialoguesOnlyProperty". We have all of our serialized properties created so we should now initialize them to hold the "DSDialogue" variables data which we'll do using the "FindProperty" method. To do that, we'll do it in the "OnEnable" method, so that we call it once every time we open the custom inspector. So type in "OnEnable" and accept the autocomplete. Initialize the first property by typing in: "dialogueContainerProperty = serializedObject.FindProperty()". This method requires an argument, which is simply the name of the variable in our "DSDialogue" script so that it can bind this property to the correct data. So simply pass in "dialogueContainer". Do the same for the rest of our properties. With our properties initialized, we can finally start drawing our UI. Before we do that though, we'll go through what the final result will look like as well as what are we going to use to draw it. Although we already have looked at what the final result with a dialogue selected should look like, we haven't really seen how it looks until we get there. Let's start by taking a look at how our Inspector looks like when we haven't yet selected a dialogue container. When that's happening, a message should be showing saying that we should select a dialogue container if we want the rest of our custom inspector to show up. There should also be a warning message saying that we should select a dialogue for this component to work properly, which will always show until we select a dialogue. Both of these will end up kind of "fixing" some small bugs that would happen if we didn't have them. Those bugs are fixable, but would require more code in our part or just a small update. I will of course be explaining later on what those little bugs are whenever we get to them, in case you prefer not to have the Custom Inspector be this way. Then, whenever we select a dialogue container, our Filters will show up. What shows next to the filters depends on what filters are selected and the contents of the container. If we tick the "Grouped Dialogues" filter, there are two possible outcomes. If the container has groups, then the Groups Area will show up with the corresponding property. If the container does not have groups, then a message will show up saying that this container holds no Groups. Before we head to the next filter, let's first talk about the Dialogues Area. If we are filtering by Grouped Dialogues and the currently selected Group does have Dialogues, we'll show the Dialogues Area and will be able to select a Dialogue. If the Group does not have Dialogues, then a message will show up saying there are no Dialogues in the current Group. If we were seeing Ungrouped Dialogues, then if the container has Ungrouped Dialogues, the Dialogue Area would show up and we would be able to select a Dialogue. If the container does not have Ungrouped Dialogues, then a message would show up saying there are no Ungrouped Dialogues in this Container. If we were to select the second filter, regardless of what the first filter was, if there are starting dialogues in the current scope, then the Dialogues Area would look the same, but with possible less Dialogues to choose from. If there are no starting dialogues in the current scope, Grouped or Ungrouped Dialogues, then the same message as before will show but with an added "Starting" word to remind the user that there are no "Starting" Dialogues, but there can possibly be non-Starting Dialogues. Of course, when we do have a Dialogue selected, no messages should be showing anymore. If you are wondering what's the reason we have the Dialogue Group and Dialogue dropdowns, that's simply that I couldn't find a way to only show the current container groups or dialogues in the normal Asset Picker, as it can only filter by their type and not by where they are located, so we'll instead select our scriptable objects through a dropdown menu. And that's basically what our Custom Inspector will look like. With our Custom Inspector structure explained, we can finally start drawing it. Of course, we should first understand what are we going to use to draw our UI. Unity provides us with a few classes that use the "Immediate Mode GUI" System to draw the UI for us. Those classes are the "GUI" class and the "EditorGUI" class. The GUI class can be used both for Runtime UI and Editor UI, but ended up, I believe, becoming deprecated for Runtime UI in favour of the current Unity UI System. The EditorGUI class can be used for the Editor only and might have some UI differences compared to the GUI class. It is also possible that one class has different UI options. These classes however offer fixed layouts. This means that you would need to set the positions and so on of your elements yourself. This is good if you want to have more control on how your Inspector looks like. However, we don't really want to do that ourselves for the Inspector and want it to just automatically look like a normal Inspector. Thankfully, both of these classes have another class that basically wraps them but make it so that their layouts are automatic. Those classes are the "GUILayout" class and the "EditorGUILayout" class. For our Custom Inspector, we'll only be using the "EditorGUILayout" class. Of course, you are free to use both of them at the same time if you want to. We now know what we need to do, so go back to our "DSInspector" script. We'll actually start by drawing the base of our UI without the message boxes that we've seen and then start drawing those step by step as we go through the Custom Inspector features. So, in our "OnInspectorGUI" method, start by calling in a new method we'll be creating named "DrawDialogueContainerArea()". I'll place this method inside of a new region named "Draw Methods". We'll be drawing our Dialogue Container Area here. There are 2 elements we need to draw: The Header Label and the Dialogue Container Property. Let's start with our Header. To draw it, we'll be using the "LabelField" method. So type in "EditorGUILayout.LabelField()" and for the text of this label we'll pass in "Dialogue Container". However, as it stands, this Label won't really look like an Header, as its text isn't bold. To make this label text bold, we can pass in a second argument of type "EditorStyles", which has several pre-made editor styles. If we type in a ".", we can see the many styles it has. If we search for "label", we can see that there are a few pre-made label styles as well. We will select the "boldLabel" style. Our Header is now looking as it should, so we should draw our Property next. To do that, type in "EditorGUILayout.PropertyField()". There's also the "ObjectField" method for the asset picker UI in which you can select what type of asset you want to pick, but the PropertyField already does that automatically for us and even checks what type of UI should be drawn depending on the variable type. The PropertyField automatically adds the field label for us as well. For the "PropertyField" argument, it requires the "SerializedProperty" that we want to draw, so simply pass in "dialogueContainerProperty". We do not need to add anything anywhere for our UI to be drawn, as Unity automatically does that for us. So with that done, our Dialogue Container Area is now being drawn nicely. The next area we'll be drawing will be our Filters Area. So, in our "OnInspectorGUI" method, call in a new method we'll create to which I'll name "DrawFiltersArea". I'll place it in the "Draw Methods" region. In here, let's start by drawing our header by typing in "EditorGUILayout.LabelField()" and pass in "Filters" for the text, and "EditorStyles.boldLabel" for the label style. Then, draw the properties by typing in: "EditorGUILayout.PropertyField(groupedDialoguesProperty)" and "EditorGUILayout.PropertyField(startingDialoguesOnlyProperty)". That's all we need for our Filters Area. Next, let's draw our Dialogue Group Area, so in our "OnInspectorGUI" method, call in a new method we'll create named "DrawDialogueGroupArea". I'll place it in the "Draw Methods" region. For this one, feel free to copy the code we have in our Dialogue Container Area method and paste it here. Then, simply swap the Header text to be "Dialogue Group" and the PropertyField to be "dialogueGroupProperty" instead. There is another element we will be adding though and that's the dropdown element you see in the Inspector. Unity actually calls this dropdown a "Popup", so just above our PropertyField, type in, "EditorGUILayout.Popup()". In our case, we'll be needing to pass in 3 arguments: The field label, the list of the Popup options and the currently selected option index. Start by passing in our label by typing in "Dialogue Group". Our second argument is the selected option index, but we currently have no way of knowing it. If we were to pass in "0" right now, when selecting a Popup option, it would always stay with the first element selected, because the "OnInspectorGUI" is being called multiple times and because it's redrawing the UI, it will see that the selected index is "0", therefore not changing to the one we chose. Thankfully, the method does return the current selected index, which updates when we select a new option, so we can simply save it somewhere and then pass it in as the second argument, so that the next time it is redrawn, it will be the same index as the last selected option. Of course, we know that a variable in our Inspector would reset every time we open it, so saving the currently selected index in here wouldn't work, as it would be simply reset to "0", or any other default value we could give it, whenever we opened the Custom Inspector. For that reason, we'll create a variable in our dialogue script that holds the currently selected index. So, in our "DSDialogue" script, add a new area to the variables that I'll name "/* Indexes */". For the variables, we'll need one index variable for each Popup, so one for our Dialogue Group and another one for our Dialogue. So, type in "private int selectedDialogueGroupIndex" and "private int selectedDialogueIndex". Make sure both of these variables have the [SerializeField] attribute. With that done, go back to the "DSInspector" script and we'll be creating our properties, so add in a new area for our Indexes and then create the properties by typing in "private SerializedProperty selectedDialogueGroupIndexProperty" and "private SerializedProperty selectedDialogueIndexProperty". Then, initialize them in our "OnEnable" method. When that's done, go back to our "DrawDialogueGroupArea" method, and we can now pass in the "selectedDialogueGroupIndexProperty" as the second argument of the Popup. However, we need an integer value for this parameter. To get the actual value of our property, we can simply type in "." and we'll see multiple value variables for each type. We want to choose the "intValue" variable. We are now passing in the actual integer value of that property, so we can now pass in our third argument: a string array of options. In here, we'll be having the names of the existing Groups. However, we'll actually do that later, so for now we can simply pass in a "new string[] { }". All of our arguments are in, but remember that if we leave things as they are, our selected index value will never change and we'll have the same problem. This is because we need to set its value to the returned Popup value, so before our Popup, in the same line, type in "selectedDialogueGroupIndexProperty.intValue" equals to the Popup returned value. We're now passing in the old selected index value and then assigning the new selected index value to the property, so that it is updated in the next Redraw. With our Dialogue Group Area done, it's time for us to draw our last area: The Dialogue Area. So in our "OnInspectorGUI" method, call in a new method we'll create named "DrawDialogueArea". I'll place it in the "Draw Methods" region. This area will be pretty much the same as our Dialogue Group Area, so feel free to copy its code and paste it here. Then, simply remove the "Group" part of all the variables and texts. And that's our Dialogue Area being drawn as well. With that done, all of our Areas are being drawn. However, they are currently standing too close to each other, so I would like to give it some spacing between them so that the Inspector looks a bit better. Thankfully, that's pretty easy to do using the EditorGUILayout "Space" method. We'll be adding it at the end of the first 3 areas, as the last one has nothing under it, so it needs no spacing there. So, in our "DrawDialogueContainerArea" method, at the end, simply type in "EditorGUILayout.Space()" and for the amount of space we want to give, I'll pass in "4". Feel free to try it out and pass in whatever amount you like. Then, we'll copy this line and simply paste it both in our "DrawFiltersArea" method and in our "DrawDialogueGroupArea" method as well. If we save and go to Unity, we can't yet see our UI being drawn, because we don't really have an Object with it. So create a new empty object in the Scene and I'll name it "Dialogue" and then simply add the "DSDialogue" component to it. We should now be able to see our UI. If you are having a weird bug here where it says the reference of the property is null, I've copied the "DSDialogue" script code, deleted the file and recreated it in the same place, pasting in the copied and it no longer gave the error, so if you currently find yourself with that bug, that might fix it. Another possible fix is to make sure you have the [SerializeField] attribute in the variables and their names are typed in correctly in the "FindProperty" method. However, we have a problem. If we try to update a field value, let's for example click in one of our Filter toggles, their values don't really get updated. To better understand the reason why this is happening, we'll talk about the Serialized Object and the Serialized Properties in a little bit more detail. The Serialized Object is simply a representation of the data of our class, which in this case, is the "DSDialogue" class, as that's the class this custom inspector is for. Whenever we select a GameObject with this component, the serializedObject variable of that component custom inspector becomes a representation of that specific component data. Because that object is representing that component class data, it means it also has a way to understand what variables that class has and a way to get their values. That's what the "FindProperty" method is for: to find a serialized property. Property is simply the word Unity gives to a "field", or, a "variable". Because this method tries to find a "serialized" property, it means that for it to be able to find it, our property, or variable, needs to be serialized. That's the second reason why we have added the "SerializeField" attribute to our "DSDialogue" class variables. The "SerializedProperty" is simply a class that is able to hold data related to a property, like its type, so that it can then set the correct value in the correct variable and return it when we need it. That's how it knows the value of the field whenever we type in something like "intValue", like we did in our indexes. Now, for the reason why our values aren't being updated: These variables hold the representation of our data. Because it is a representation, when we update the properties, their values aren't really being synchronized with the actual "DSDialogue" variables. So what we need to do once we update the property values, which represent our data, is to simply make sure we apply those changes, so that Unity synchronizes the "DSDialogue" variables values to hold these new values, meaning that the next time the UI is drawn, they will now have their UI updated to represent those new values. We can do that by using the serialized object "ApplyModifiedProperties" method. To make sure we get an Updated data representation, just in case some property was modified elsewhere, we need to call in the serialized object "Update" method. Therefore, the "Update" method should be called at the beginning of the "OnInspectorGUI" method, to make sure we are going to be drawing our UI with the correct data, and our "ApplyModifiedProperties" method should be called at the end, after we draw all of our UI and possibly change the data of one of the elements. Now that we know what's happening, we can easily fix it by going back to our "DSInspector" script and in our "OnInspectorGUI" method, type in at the start of the method "serializedObject.Update()" and then at the end of the method, type in "serializedObject.ApplyModifiedProperties()". If we save and go back to Unity, our property values should now be updating. We can now draw the rest of our Custom Inspector. However, we'll do one last thing before we do that. When we finish up our Custom Inspector, it is likely that we'll want to add new properties to our dialogue script. That simply means that we'll need to add their UI here as well. We can do that either using the "DrawDefaultInspector" or "DrawPropertiesExcluding" methods, but in case we want to do something like our current UI, we'll need to repeat the already existing code. What we'll be doing is to simply allow us to call something like "DrawHeader" without needing to specify ourselves the label styles or simply call in "serializedPropertyVariable.DrawPropertyField()" to draw the Property in the Inspector. To be able to do that, we'll be creating our own Inspector Utility Script. So, in our Editor "DialogueSystem" "Utilities" folder, create a new C# Script to which I'll name "DSInspectorUtility". Inside, feel free to remove the default methods together with the "MonoBehaviour" inheritance. I'll make it part of the "Utilities" subnamespace. I'll also make this class be "static". We'll be creating a method for our Headers, our Properties, our Popups and our Spaces. The last one isn't really necessary but just makes it so we call the Inspector Utility like the rest of our code. Let's start with the Header method by typing in "public static void DrawHeader()" and as the first argument we'll need to pass in a "string" to which I'll name "label". Inside, call in "EditorGUILayout.LabelField()". Make sure you import the necessary namespace. Then, pass in "label" as the first argument, and "EditorStyles.boldLabel" as the second one. Our "DrawHeader" method is now done. So, next, let's do one for our Properties. To do that, type in "public static void" and I'll name it "DrawPropertyField". We'll make it so we can call this method through the variable itself, so pass in a new parameter saying "this SerializedProperty serializedProperty". We can now simply do "ourSerializedProperty.DrawPropertyField()". Inside, call in "EditorGUILayout.PropertyField()" and pass in "serializedProperty" as the argument. When that's done, create the Popup method by typing in "public static void DrawPopup()". As parameters, we'll have a "string label", a "SerializedProperty selectedIndexProperty" and a "string[] of options". The second parameter is just so we can send in the property without needing to type in the "intValue" part every time we call this method. Inside, call in "EditorGUILayout.Popup()" and pass in "label", "selectedIndexProperty.intValue" and "options". Of course, make sure to return this line and update the Draw method return type to be "int" instead, as the Popup needs to return the new selected index. If you do prefer to pass in the int value instead as the selected index, that's fine as well. Simply copy the DrawPopup method and paste it in again, and then swap "SerializedProperty" with "int" and name it "selectedIndex" instead. Then, remove the "Property.intValue" from the second "Popup" method argument. We can now call this method the way we prefer. All that's left is our Space method so type in "public static void DrawSpace()" and pass in "int amount" as the parameter. I'll default this value to "4". Inside, simply call in "EditorGUILayout.Space()" and pass in "amount" as the argument. That's all of our methods done, so we now simply need to update our Inspector script to use them. So, back to our "DSInspector" script, start by importing the "Utilities" namespace. Then, we'll start updating our method calls. Start by updating every "LabelField" to call in the "DSInspectorUtility.DrawHeader" method instead. When that's done, lets swap our properties to use the new "DrawPropertyField" method. Remember that we can just type in "dialogueContainerProperty.DrawPropertyField()" because of the "this" keyword. Do the same for the rest of the properties. Next, swap our "Space" methods with our "DrawSpace" method. We don't need to send in "4" because it's already defaulted to that value. When that's done, swap our "Popup" methods with the "DrawPopup" method instead. I'll be using the method overload that sends in the serialized property. Our Custom Inspector is now using all of our Utility Methods. We won't be doing one to Initialize our Properties because they don't seem to be of reference type, and passing in "ref", "out" and "in" did not seem to work either, so we'll leave them as is. With that out of the way, we'll start drawing the remaining elements of our Inspector. This time though, we'll draw the UI in the order of our properties selection and finish the rest of the features as we do that. The first thing we'll be drawing is the message that shows up when no dialogue containers are selected. To do that, go to our "OnInspectorGUI" method, and in here, we'll simply need to check if the dialogue container property value is null, and if it is, draw the message and return from the method, as we don't want to keep on drawing. So, just under our "DrawDialogueContainerArea" method call, type in "if (dialogueContainerProperty.objectReferenceValue == null)". We get the "objectReferenceValue" variable here because our property allows us to select objects using the Asset Picker. Inside of the if statement, we can draw the box using the "HelpBox" method, so type in "EditorGUILayout.HelpBox()" and as the text, we'll pass in "Select a Dialogue Container to see the rest of the Inspector.". Then, as the second argument, we can pass in what type of box it will be, which really only updates the icon that appears before the text. I'll pass in "MessageType.Info". As the third argument, its simply whether we want the box to be the width of the Inspector or simply the width of the property field, label width excluded. Because I want it to be wide, I'll actually not pass in anything, as it defaults to true, which is exactly what we want. Of course, let's also add this method as an Utility Method. So, in our "DSInspectorUtility" script, create a new "public static void" method to which I'll name "DrawHelpBox". I'll pass in "string message" as the first parameter, "MessageType messageType" as the second, to which I'll default to "MessageType.Info", and then "bool wide" as the third one, to which I'll default to "true". Inside, simply call in "EditorGUILayout.HelpBox()" and pass in the 3 parameters as arguments. Back to our "DSInspector" script, swap the current "HelpBox" method to use the "DrawHelpBox" method instead. With our Help Box now being shown, we can stop drawing the rest of our UI. To do that, we can simply type in "return;" at the end to return from the "OnInspectorGUI" method. Of course, because we are returning here, our "ApplyModifiedProperties" method call at the bottom of the method will not be called, so we need to make sure we call it before returning as well, so type in "serializedObject.ApplyModifiedProperties()". Because we'll be doing this several times, one for each Help Box, we'll just select this code without the "return" part and extract it to a method. I'll name this method "StopDrawing". I'll place it in the "Draw Methods" region. In here, I'll add in a parameter of type "string" to which I'll name "reason". This is simply the reason why we want to stop drawing, which will be the Help Box message. Then, copy or cut the text in the "DrawHelpBox" method and swap it with "reason" instead. Once that's done, make sure that in our method call we pass in the text we've just copied. If we save and go back to Unity, if we do not select a dialogue container, we should see there's an Help Box showing with no other UI under it. If we do select a container, the rest of our UI should show up. With that done, we'll now make it so our Dialogue Groups only show up if the "Grouped Dialogues" filter is on and also show an Help Box if there are no Groups in the current Container. To do that, back in our "DSInspector" script, in the "OnInspectorGUI" method, type in just above our "DrawDialogueGroupArea" method call, "if (groupedDialoguesProperty.boolValue)". If this is true, then we can draw our dialogue group area, so simply move the "DrawDialogueGroupArea" method call inside. In here, we'll be getting the Dialogue Group Names in this container so that we can pass them in as the Popup options. However, if this list of group names is empty, we can simply throw in the info message and stop drawing, as that means there are no Dialogue Groups in the current Container. Of course, we don't yet have a way to get the names of the groups, so we'll have to create that first. To do that, let's go to our "DSDialogueContainerSO" script. In here, we'll create a new method that gets us the names of the groups, so type in "public List<string>" and I'll name it "GetDialogueGroupNames". Inside, we'll simply iterate through the dialogue groups and add their names to a list that we'll need to return, as we'll need it in our inspector script. So, create that List by typing in "List<string>" and I'll name it "dialogueGroupNames". Don't forget to initialize it. Then, iterate through the DialogueGroups dictionary keys, which are all of the container Groups, by typing in "foreach (DSDialogueGroupSO dialogueGroup in DialogueGroups.Keys)" Inside, simply add the dialogue group name to the list by typing in "dialogueGroupNames.Add(dialogueGroup.GroupName)". Once that's done, simply "return" the list. We are now able to get our group names through our dialogue container scriptable object, so go back to our "DSInspector" script and we'll be needing to call the method we've just created. However, we don't really have a reference to our dialogue container scriptable object, so we have no way of calling in the method we've just created. Thankfully, that's quite easy to get as we just need to cast the dialogue container property value to the corresponding type. So above, just after we draw our Dialogue Container, create a new variable of type "DSDialogueContainerSO" and import its namespace. I'll name the variable "dialogueContainer". Then, assign to it "dialogueContainerProperty.objectReferenceValue" and cast the result. Note that's it's important for you to do this after drawing the container property. I'll be explaining why later. When that's done, back in our group filter if statement, create a new "List<string>" to which I'll name "dialogueGroupNames". Then, assign to it "dialogueContainer.GetDialogueGroupNames()". Now that we have the reference to our container, we can also swap it in our if statement to check if this variable is null instead. With our list here, we can stop drawing our Inspector and show the info message if the list is empty. so type in "if (dialogueGroupNames.Count == 0)" and inside, call in "StopDrawing()" and pass in "There are no Dialogue Groups in this Dialogue Container." as the reason why we've stopped drawing. Of course, don't forget to "return;" at the end. If our list is not empty though, we'll show our Dialogue Groups. In there, we need to show every group name as a Popup option, so that we can select the Group we desire. To do that, pass in our "dialogueGroupNames" list to the "DrawDialogueGroupArea" method. If you're in Visual Studio, press "Alt + Enter" and select "Add parameter to method". This simply automatically adds the parameter to the method for us. Then, in our "DrawDialogueGroupArea" method, we'll swap the empty string array in the "DrawPopup" method to be "dialogueGroupNames" instead. Of course, don't forget that the method requires an array and we've passed in a list, so we need to convert it to an array by typing in ".ToArray()". If we save and go back to Unity, a dialogue container with no Groups should show the Help Box. If we however select a dialogue container with Groups, the Dialogue Group Area should show instead, now with the list of the group names in the Popup. We are also able to select the option we desire in the Popup, however, even though we're selecting the group we want, we aren't really setting the property to be this group scriptable object. To do that, we'll need to get the reference to the selected group by loading its asset. Thankfully, we already have the name of the group we want to load in the Popup option, and we also have the dialogue container name to know in what folder the group is on, so we can use both of those to load our Group Asset. The only thing we need now is to have a way to load that asset. If you've done the Save and Load Systems, we have a "DSIOUtility" that holds a method for that purpose. Of course, back then, we haven't made them public, but we'll do that now, as they're methods that can be called in other places. So, go to our "DSIOUtility" script and in our "Utility Methods" region, set every method except the Clone method to be "public". Then, back to our "DSInspector" script, in our "OnInspectorGUI" method, pass in the "dialogueContainer" variable to the "DrawDialogueGroupArea" method to add as a new parameter. This is simply to be able to load the group asset at the correct path. In our "DrawDialogueGroupArea" method, just after we draw the Popup, create a new "string" variable to which I'll name "selectedDialogueGroupName". Then, assign to it "dialogueGroupNames[ selectedDialogueGroupIndexProperty.intValue]". Then, with this name, we can load the asset we want, so create a new variable of type "DSDialogueGroupSO", to which I'll name "selectedDialogueGroup" and load the asset by typing in "DSIOUtility.LoadAsset<DSDialogueGroupSO>()" and for the path the asset is in, we'll pass in an interpolated string ($"") saying $"Assets/DialogueSystem/Dialogues /{dialogueContainer.FileName}/Groups /{selectedDialogueGroupName}". This path leads us to the Group folder, so now we simply need to load its asset by passing in "selectedDialogueGroupName" as the second argument. With this, we're now loading in our asset. Then, set the group property to be this value by typing in "dialogueGroupProperty.objectReferenceValue = selectedDialogueGroup". If you're wondering why we need this asset, we'll use it to get our Grouped Dialogues. If we now save and go back to Unity, selecting a Group in the Popup should load the respective asset. However, we have a few problems. They are simply regarding the selected index whenever a list of options is updated. This can happen when we select a new dialogue container, tick and untick filters or update the Graph. Let's say we chose the last Group in the Popup options in the "Full" container. This Group has the index of "2". If we were to select a new dialogue container, and that dialogue container didn't have enough groups for one of them to be in the index of "2", then Unity would throw an error as it cannot find a member in its names array at the index of "2". The same happens when we select certain filters or update the Graph, as we might have saved the Graph with less elements than before. Another problem with updating the Graph is that we might remove an element whose index was lesser than the currently selected index, which would mean the names list would go down one element, so the currently selected index would be a name that was 1 index up before. Because of that, we need to make sure we reset our indexes to 0 again, or, if the old name is now different than the name at the old index, search for the correct old name index in the names list. Of course, we'll also need to reset our dialogue index for the same reason. Now, I've said before that the help boxes helped us out fixing two small bugs. One of them is right here. Whenever we set the index to be "0", if our array turns out to be empty, then the index of "0" is also invalid, as that simply means "the first element". Thankfully, due to our Help Box if statement, this will never throw an error. The reason why is simply because if the list is empty, we'll stop drawing and return from the "OnInspectorGUI" method, meaning, we'll never get to our "Popup" line. So, let's fix this problem by going back to our "DSInspector" script, and in our "DrawDialogueGroupArea" method we'll need to get the old selected index to be able to know if the old name is different than the current names list element at the old index. We can do that pretty easily by creating a new variable just before we draw our Popup. So, before drawing our Popup, type in "int oldSelectedDialogueGroupIndex = selectedDialogueGroupIndexProperty.intValue". We now have a reference to the old selected index. Now, this might be a bit confusing. If we're applying the changes at the end of the method, how is it possible to get the old index when the UI is being redrawn? Shouldn't the index value be updated by now? Well, I've tested it out myself, and apparently those changes that we applied are only applied when we draw the element again. Until then, the values of the element will remain their old values. This was easily tested in the final result of the Custom Inspector. I have a dialogue selected here. And in the "OnEnable" method, I have a Debug Log printing out what's the currently selected dialogue. If we now were to update the dialogue container to "None", we should see the info and warning messages and no dialogue should be selected. However, if we do get to print our "OnEnable" debug log again, we can see that the old dialogue is still selected. This is because we haven't yet drawn the PropertyField again, so the values remain unchanged, even though we've applied the changes. This is the second bug that I've talked about, that the Warning message helps out mitigating. If the user does not select a starting dialogue, it is possible that the dialogue variable would have a value in it even though it shouldn't. Of course, this would be easily fixable by simply setting the property value to null whenever we stop drawing. However, this would also make it required for us to get the old dialogue group and dialogue through a different way, which would probably be an extra variable in our "DSDialogue" script. Because I personally know I should always choose a Dialogue, I'll leave it as is. Feel free to try to do it the other way if you prefer. With that explained, let's now reset our group index. To do that, we need our old dialogue group as well so before our Popup simply type in "DSDialogueGroupSO oldDialogueGroup = dialogueGroupProperty.objectReferenceValue" and cast the result. Then, if the old dialogue group was null, we can simply reset the index as that means no group was selected so we don't really care about searching for the name, so type in "if (oldDialogueGroup == null)" and inside type in "selectedDialogueGroupIndexProperty.intValue = 0". "Else", if the group was not null, we want to check if the old name is different than the currently selected name, so type in "if (oldDialogueGroup.GroupName != dialogueGroupNames[oldSelectedDialogueGroupIndex])". However, we need to remind ourselves that the oldSelectedDialogueGroupIndex can be an index higher than the current maximum index on the names list, and if it is, then it's the same as saying that the names are different. So, type in, just before the current condition "oldSelectedDialogueGroupIndex > dialogueGroupNames.Count - 1" because indexes start at "0", "||" the other condition. Make sure to place this condition before the other so that the second one doesn't throw an index out of bounds error. If one of these is true, we can update the currently selected index. Then, inside, we'll need to check if the old name exists in the list, and if it does, we can set the current index to the index of that element. If it can't find it, we can just set the index to be "0" instead. So type in "if (dialogueGroupNames.Contains(oldDialogueGroup.GroupName))" and if it does, we type in "selectedDialogueGroupIndexProperty.intValue = dialogueGroupNames.IndexOf(oldDialogueGroup.GroupName)". Otherwise, or, "else", we can simply set the "selectedDialogueGroupIndexProperty.intValue" to be 0. I'll now select these if statements and extract them to a new method to which I'll name "UpdateIndexOnDialogueGroupUpdate". I'll place this method in a new region named "Index Methods". However, we want to be able to reuse this method in our dialogue area as well, as it faces the same problem, so, we'll be updating it to be reusable. To do that, first swap the "DSDialogueGroupSO" parameter to be "bool isOldPropertyNull" instead. When that's done, swap the first if statement to use this variable and if that's true, simply "return" from the method, which makes it so we can remove our "else" statement. Of course, this made it so we don't have the group and can't get its name because of it, so create another parameter before our "bool" parameter of type "string" to which I'll name "oldPropertyName". Then, swap it with our "oldDialogueGroup.GroupName" usage. When that's done, we'll rename our "dialogueGroupNames" parameter to be "optionNames" instead and the "oldSelectedDialogueGroupIndex" to "oldSelectedPropertyIndex" as well. The only thing left to do is to update the correct index property, so add a new parameter after our string list of type "SerializedProperty" to which I'll name "indexProperty". Then, swap the "selectedDialogueGroupIndexProperty" with "indexProperty" instead. This works even though "SerializedProperties" aren't by reference because the property we pass in is a global variable in this script. One last thing we'll do is to add the conditions as variables, so above our if statement type in "bool oldIndexIsOutOfBoundsOfNamesListCount = oldSelectedPropertyIndex > optionNames.Count - 1" and then "bool oldNameIsDifferentThanSelectedName = oldIndexIsOutOfBoundsOfNamesListCount || oldPropertyName != optionNames[oldSelectedPropertyIndex]". When that's done, swap the if statement condition with "oldNameIsDifferentThanSelectedName" instead. I'll also update the method name to be "UpdateIndexOnNamesListUpdate". Back when we call the method, start by calling it with the correct name. Then, pass in "selectedDialogueGroupIndexProperty" as the second argument and for the last 2 we'll be creating two variables so above type in "bool isOldDialogueGroupNull = oldDialogueGroup == null", and then "string oldDialogueGroupName = isOldDialogueGroupNull ? "" : oldDialogueGroup.GroupName". We aren't using the null conditional operator here because apparently you shouldn't be using those in Unity Objects, as what's "null" here is decided by Unity and it might be something we didn't expect. Then, pass these two variables as the last 2 arguments, removing the "oldDialogueGroup" argument. If we save and go back to Unity, our Popups should now work correctly no matter what we update. All that's left now is our Dialogue Area. We'll show it in two different situations: When the "Grouped Dialogues" filter is set to "true", which will show the Dialogues of the selected Group, or when the "Grouped Dialogues" filter is set to "false", which will show the Container Ungrouped Dialogues. The differences between these situations are: The method we call in to get the dialogue names, as one gets the grouped dialogue names and the other one gets the ungrouped dialogue names, The path of where we need to load the asset from, as one is inside of the "Groups" folder and the other one is inside of the "Global" folder, and the message we want to show if there are no dialogues, as in one we need to say there are no dialogues in the group while in the other one we need to say there are no ungrouped dialogues in the container. This means that we'll need to create one variable for each of these and set them accordingly depending on the filter value, which we can do in its "if" statement for the grouped dialogues situation, and in an "else" statement for the ungrouped dialogues situation. So, in our "DSInspector" script, in the "OnInspectorGUI" method, above our "Grouped Dialogues" filter if statement create a new "List<string>" to which I'll name "dialogueNames". We don't really need to initialize it here because we'll be setting the variable in both of our "if" and "else" statements. Then, create a new "string" to which I'll name "dialogueFolderPath" and we'll be initializing this variable with the path that's equal to both situations, which is until the container parent folder, so initialize it with an interpolated string ($"") and type in $"Assets/DialogueSystem/Dialogues/{dialogueContainer.FileName}" Next, we want a "string" variable for our "dialogueInfoMessage", which will be the message that will be shown if no dialogues can be found for that specific situation. When that's done, we'll set the values for when our "Grouped Dialogues" filter is on. So, we'll be setting the variables inside of the if statement. The first one we want to get is the list of grouped dialogue names, but we don't really have a way to get them. Much like we did for our groups, we'll create a method that returns them in our dialogue container. So go to our "DSDialogueContainerSO", and create a new "public" method that returns a "List<string>" to which I'll name "GetGroupedDialogueNames". We'll be getting the dialogues of a specific group, which we can do through the DialogueGroups dictionary, so we'll pass in the group as a parameter, so type in "DSDialogueGroupSO dialogueGroup". Inside, start by getting the list of the grouped dialogues by creating a variable of type "List<DSDialogueSO>", to which I'll name "groupedDialogues". Then, assign to it "DialogueGroups[dialogueGroup]", which simply returns the list of dialogues present in this group. Then, create a new "List<string>" to which I'll name "groupedDialogueNames" and initialize it. Next, we'll iterate through the grouped dialogues list by typing in "foreach (DSDialogueSO groupedDialogue in groupedDialogues)" and inside, we'll add this dialogue name to the list by typing in "groupedDialogueNames.Add()" and pass in "groupedDialogue.DialogueName". When that's done, simply return the list at the end. We can now set our dialogue names list, so back to our "DSInspector" script, just after we draw our Dialogue Group Area, type in "dialogueNames = dialogueContainer.GetGroupedDialogueNames()". We do need to pass in the group the dialogues belong to here, so above type in "DSDialogueGroupSO" and I'll name it "dialogueGroup" and assign to it "dialogueGroupProperty.objectReferenceValue" and cast the result. Then, pass this variable into the "GetGroupedDialogueNames" method. For our dialogue folder path, we'll be using the current string and concatenate the rest of the path, so type in "dialogueFolderPath += $"/Groups/{dialogueGroup.GroupName}/Dialogues"". For our "dialogueInfoMessage" we'll pass in the string of "There are no Dialogues in this Dialogue Group". With that done, all of our variables are set up for when we have our "Grouped Dialogues" filter set to true. Of course, we should now do the situation where the filter is off, so start by typing in "else". In here, we'll start by setting our dialogue names list, which we of course need to create the method that gets us the names, so go back to our "DSDialogueContainerSO" script, and in here create a new "public" method that returns a "List<string>" and I'll name it "GetUngroupedDialogueNames". Inside, we'll do what we did for the other methods, so type in "List<string> ungroupedDialogueNames" and initialize it. Then, iterate through the "UngroupedDialogues" list by typing in "foreach (DSDialogueSO ungroupedDialogue in UngroupedDialogues)". Inside, add the name to the list by typing in "ungroupedDialogueNames.Add()" and pass in "ungroupedDialogue.DialogueName". At the end, simply return the list. When that's done, go back to the "DSInspector" script, and inside of the "else" statement type in "dialogueNames = dialogueContainer.GetUngroupedDialogueNames()". For the folder path, type in "dialogueFolderPath += "/Global/Dialogues". For the info message, type in "dialogueInfoMessage" equals to "There are no Ungrouped Dialogues in this Dialogue Container.". All of our variables are now set for both of the situations. Before we pass the necessary info to our "DrawDialogueArea" method though, we'll stop drawing if the dialogue names list is empty, so after our if/else statement type in "if (dialogueNames.Count == 0)", we can then call in "StopDrawing()" with the reason of "dialogueInfoMessage". Of course, don't forget to "return;" at the end. That's it for the situation where there are no dialogue names, so we'll now do the situation where there are dialogue names in the list. So, start by passing in 2 arguments for our "DrawDialogueArea" method, "dialogueNames" and then "dialogueFolderPath". Then, press "Alt + Enter" and choose to add them as parameters to the method. When that's done, inside of our "DrawDialogueArea" method, update the empty string array to be "dialogueNames" instead. Don't forget to add ".ToArray()" as well. Once we have our Popup with its options, we can load the selected asset and set the property to use it. So first create a new "string" to which I'll name "selectedDialogueName" and assign to it "dialogueNames[selectedDialogueIndexProperty.intValue]". Then, create a new variable of type "DSDialogueSO", to which I'll name "selectedDialogue" and load the asset by typing in "DSIOUtility.LoadAsset<DSDialogueSO>()" and pass in "dialogueFolderPath" for the path, and "selectedDialogueName" as the asset name. Once we have our loaded dialogue, we can set the property value to hold this value by typing in "dialogueProperty.objectReferenceValue = selectedDialogue". Our Dialogue property is now being set correctly. We also have no fields that we can select depending on the chosen dialogue, so we don't really need to reset any index. However, we do also need to update our index whenever an update to the Graph, container or filters happens. To do that, just before we draw our Popup, type in "int oldSelectedDialogueIndex = selectedDialogueIndexProperty.intValue" and also create the old dialogue by typing in "DSDialogueSO oldDialogue = dialogueProperty.objectReferenceValue", casting the result. Then, copy both the "bool" and the "string" variables from the dialogue group area and paste them here. When that's done, simply remove the "Group" part of the names and swap the "GroupName" with "DialogueName" instead. When that's done, call in "UpdateIndexOnNamesListUpdate" and pass in "dialogueNames", "selectedDialogueIndexProperty", "oldSelectedDialogueIndex", "oldDialogueName" and "isOldDialogueNull". That's all we need for our index update. Do note that updating the element names in the Graph will make it so the index is reset or swapped to the element whose name is equal to the old selected name, even if it's not the same element. But I'm fine with that. We are almost done with our Custom Inspector, but we still need to do 3 small features: Filtering the dialogues by starting dialogues only, Disabling our PropertyFields and Adding the warning Help Box. Let's start by filtering our dialogues. Thankfully, that's quite a simple thing to do. To do it, let's head to our "DSDialogueContainerSO" script again, and we'll need to update both of our dialogue names methods. I'll start with the "GetGroupedDialogueNames" method and then just copy everything to the "GetUngroupedDialogueNames" method. So, pass in a parameter of type "bool" to which I'll name "startingDialoguesOnly". In here, we'll filter the dialogues out by adding the following condition: If the parameter we pass in is true, then we'll only want to add the dialogue name to the list if the current dialogue "IsStartingDialogue" property is set to true. That means that if the parameter is true and this variable is not, we want to continue to the next element. If the passed in parameter is false, this condition code will not run and the loop will run as it did before, adding every dialogue name to the list. So, in the foreach loop, type in "if (startingDialoguesOnly && !groupedDialogue.IsStartingDialogue)", and inside simply "continue;" to the next element. That's all we need to do, so when you're done doing that, copy this whole if statement and pass it in to our "GetUngroupedDialogueNames" method foreach loop as well. Of course, don't forget to copy the bool parameter as well. Then, swap the "groupedDialogue" variable with "ungroupedDialogue" instead. Both of our methods are now able to filter by starting dialogues only, so, back to our "DSInspector" script, after we draw our filters, create a new variable of type "bool" to which I'll name "currentStartingDialoguesOnlyFilter" and set it to be "startingDialoguesOnlyProperty.boolValue". Then, we simply pass this variable to both methods. That's done, but I'll also make it so our info message gets the "Starting" word added to it before the "Dialogues" word in case the filter is set to true, just so the user knows it could not find "Starting Dialogues", but can still possibly find non-Starting dialogues. To do that, concatenate the string after the "no" word and type in between parenthesis () "(currentStartingDialoguesOnlyFilter ? "Starting" : "")". This is called a ternary condition. If the condition is true, then the first statement will run and it will concatenate " Starting" to the string. If the condition is false, then the second statement will run and it will concatenate an empty string to the string. Copy this concatenation and paste it in in the other dialogueInfoMessage text assignment as well. Saving and going back to Unity, we should now be able to filter the dialogues by starting dialogues only or all dialogues. If there are no starting dialogues, the info message will also be updated to reflect that. The next thing we need to do is to disable our dialogue group and dialogue property fields, just to make sure we do not accidently select a random asset from it and break the inspector. To do that, we'll be using two EditorGUI methods named "BeginDisabledGroup" and "EndDisabledGroup". Let's create this method in our Inspector Utility right away, so go to our "DSInspectorUtility" script and in here, create a new "public" method above all the others to which I'll name "DrawDisabledFields". Inside, type in "EditorGUI.BeginDisabledGroup()" and pass in "true" as the argument, simply to say we want to disable what's going to be in between the two methods. Then, at the end, type in "EditorGUI.EndDisabledGroup()". Any element between these two lines will be disabled. Because we can possibly want other things to be disabled, we'll just make it possible for us to send in any element we want. To do that, we'll pass in a new parameter of type "Action" to which I'll name "action". Be sure to import its namespace. Then, between the two lines, call in "action.Invoke()". This allows us to pass in any method call we want and it will be called in between those two method lines. With that done, back to our "DSInspector" script, in our "DrawDialogueGroupArea" method, copy the "DrawPropertyField" line and swap it with "DSInspectorUtility.DrawDisabledFields()" and pass in an empty action, "() =>" and paste the copied "DrawPropertyField" line. This field will now be drawn inside of the disabled group. When that's done, copy this line and do the same in our "DrawDialogueArea" method. Saving and going back to Unity, our property fields should now be disabled. We don't really need to disable our dialogue container property because we don't really use popups to select it but use the actual property field instead. There's only one thing left for us to do and that's our Warning Box. So back to our "DSInspector" script, go to our "StopDrawing" method, and in here, we'll simply add the warning box at the end. We can do this because we always show this warning message until we actually have a selected dialogue. So, before applying the modified properties, simply type in, "DSInspectorUtility.DrawHelpBox("You need to select a Dialogue for this component to work properly at Runtime!")", and then pass in "MessageType.Warning" as the second argument. Between the two help boxes we'll be adding a space to separate them a bit, so type in "DSInspectorUtility.DrawSpace()". I'll also make it so we can pass in the message type for the first help box, in case we ever wanted to show a different icon, so add a new parameter of type "MessageType", to which I'll name "messageType" and default it to be "MessageType.Info". When that's done, simply pass it in to the first "DrawHelpBox" method. If we now save and go to Unity, our warning Help Box should be showing until we select a dialogue. Our Custom Inspector is done! We can now select a starting dialogue. There are a few optimizations that could be done if you want to dive deeper into the code. I'll be leaving two possible optimizations you could do. The reason for those Optimizations is the "OnInspectorGUI" method, because it runs multiple times whenever we have our inspector opened. The first one would be to cache the names of the groups and the dialogues whenever we save the Container Scriptable Object in our "DSIOUtility" script "Save" method. This would be as simple as creating 3 lists of strings and add the names there just before we save the asset. This would be better because we wouldn't call the "GetNames" methods at every redraw, which can become expensive if there are a lot of groups or dialogues. The second one would be our "LoadAsset" method. I'm not entirely sure if Unity already caches something internally, but if it doesn't, we're then loading the asset at every redraw, which can be expensive as well. One way to fix this would be to cache in the loaded asset into a static dictionary or HashSet in our "DSIOUtility" script, which we would need to clear once in a while to make sure it doesn't waste unnecessary memory. But, I'll leave that up to you because as far as my usage of this dialogue system went, I didn't feel it being laggish or slow, so for me, it's fine as is. One small warning though, this inspector was not done with multiple object editing in mind so do not add the [CanEditMultipleObjects] attribute as otherwise selecting multiple objects will update the values of a selected object to the values of another selected object. With that, our Custom Inspector is finished. I've said it before, but we're not going to be making a Custom Inspector using UI Elements. The reason why is because when we use UI Elements, we need to use the "CreateInspectorGUI" method. The good thing about this method is that it's only called once when we open up the Inspector. We can then use the "RegisterValueChangeCallback" method to set a callback and do something when a certain value is changed. However, the problem lies in our Help Boxes and in updating our existing elements. Because our method doesn't get called again, the UI won't be redrawn. Because of that, our Help Boxes won't be drawn if we update a value that would make them show up, nor would our existing popups and property fields update when we select a new container, for example. This can be easily fixed by separating the areas into different visual elements that we would save a reference to, and then remove the elements inside and redraw them again by calling our Draw methods. However, for some reason, properties don't seem to redraw when we do this. This means that our only option would be to instead of removing the UI, to just hide it, by updating the Visual Element Visibility. The problem with this approach is that we would need to hold a reference to all of the Popups and Property fields so that we could update them whenever we change something like the dialogue container. This is because we wouldn't be redrawing them, but updating them, which would be done in the "RegisterValueChangeCallback" method. This means that any time we updated the container we would have to set their options and values again. This would also make it so we have the same logic in multiple places when we could instead just redraw them whenever we wanted, which would have our logic stay in one place only. Of course, we could have these references in our Inspector script, but I still felt like what I was doing wasn't right and I believe I just personally couldn't find how to get to redraw the Property Fields. So while I wasn't able to find a proper way to do it, or at least, one that I think it's proper, I do leave you with a possible way of doing it, which is referencing the elements and having the logic be in two places of the code. The only thing you need to keep in mind, is I believe, the way Popup works here compared to the "Immediate Mode GUI" Popup. In the "Immediate Mode GUI" Popup, it accepted "strings" as the options, hence why we had to get a list of the names of the dialogues. In UI Elements Popups though, we can just pass in the list of the actual class type. This means that we are able to get the selected element casted to the class right away. It does end up showing a bit of a weird option text instead but it also allows you to customize that text by setting its "formatSelectedValueCallback", which updates what the selected element text looks like, and its "formatListItemCallback" as well, which updates what the options list text looks like. You can also get the currently selected element of the Popup using its "value" variable. Another thing you need to take note is that you do need to return a visual element in the "CreateInspectorGUI" method, which basically is the main visual element that will hold all of the other UI Elements. You can think of it as the Editor Window rootVisualElement. It also seems to not need the serializedObject "Update" and "ApplyModifiedProperties" methods. And that's likely what you need to know if you want to try diving a bit deeper into using UI Elements in the Inspector. I do apologize that I wasn't able to find a way that I thought was at least appropriate using UI Elements, and I'll leave you with the Custom Inspector using the "Immediate Mode GUI" System instead. Of course, this is likely the most used UI System for Custom Inspectors, so it's likely to still be useful for you. So, with that done, that finishes up our Node Based Dialogue System Tutorial Series. I hope I was able to teach you at least the very basics of using the GraphView API with this series and hopefully your basic node based dialogue system is also working. Just as a final tip, there's one element that might be useful for you if you want to add more options to the node, such as an audio clip to play when the dialogue plays, and that's the "ObjectField" element. This simply allows you to choose an asset of a certain type, which can be an AudioClip or anything else. Of course, you would need to update your code to also be able to save this new element. You are free to update the system as you see fit or as you need for your game. A link to this project GitHub page should now be available in the description of all videos as well, so if you ever need to see it again without watching the videos, feel free to do so there. If you did enjoy the tutorial series, please feel free to give your feedback on it in the "Overview" video, so that people know if it's worth it or not to watch it, or just give any feedback in any specific video if the feedback is regarding that video theme. This also allows me to know what to improve in future tutorials so don't feel shy to specify what you did not enjoy. If you'd like, subscribing would also help me out a lot. So, with our tutorial series done, I hope you've enjoyed it and see you in the next one.
Info
Channel: Indie Wafflus
Views: 532
Rating: undefined out of 5
Keywords: Unity Dialogue System, Unity Dialog System, Unity Graph View, Unity Custom Inspector, Unity IMGUI, Unity Dialogues, Unity Node Based Dialogue System, Dialogue System, Node Based Dialogue System, Unity Dialogs, Unity Visual Elements, Node System, Dialogue System Tutorial, Unity 2D, Unity, Unity Tutorial, Unity Dialogue System Tutorial, Unity 3D, Unity3D, Unity2D, Unity Node, Unity UI Toolkit
Id: cefLGWA5OM4
Channel Id: undefined
Length: 89min 3sec (5343 seconds)
Published: Fri Oct 22 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.