Unity Dialogue System - Preparing the Save Data

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Before we start saving our data we should first understand how are we going to structure our folders and files, as well as create the necessary classes. So let's start by understanding the structure. Here is an example of what a simple graph looks like. It has a grouped node, an ungrouped node and a group. We will be separating the structure into two different parts: The Editor Part, and the Runtime Part. The Editor Part will simply hold the graph scriptable object, which contains all the data in the graph and  will be used to load the Graph anytime we want. The Runtime Part will contain all the scriptable objects that will be necessary for the custom  inspector and for the game dialogues. This of course means that updating the runtime  scriptable objects won't affect the Graph, as we'll be loading the data from the editor scriptable object. Therefore, I recommend updating everything through the Graph. So, let's see what each part of the Graph will lead to. The first thing we have here is the toolbar file name. This will go towards the Editor Part to the graph scriptable object that we've talked about early on. This scriptable object will hold the file name, the list of groups and the list of the nodes in the graph. This name will also serve as the  parent folder of our Runtime Part. Every group and every node scriptable  object will be inside of this parent folder. This is basically for organization and  to know where can we find the assets, useful for the custom inspector. To help us out for our custom inspector,  we'll also have an asset with this name inside of the parent folder, which will hold all the groups, grouped nodes and ungrouped nodes, as well as some useful methods later on. For our groups and node scriptable  objects inside of this folder, we'll be separating them between different folders: The "Global" folder and the "Groups" folder. The "Global" folder will hold every  ungrouped node scriptable object. Although there will only be ungrouped nodes there, we will still be adding them inside  of a folder named "Dialogues", which is what we'll be calling  our nodes in the runtime part. The "Groups" folder will hold every  grouped node scriptable object, as well as what group they belong to. Each group will be divided into a folder  with its name, and inside of that folder there will one scriptable object that will hold  the group data, which will just be the group name, as well as a folder named "Dialogues" that will  hold every node scriptable object of that group. Here is an example of the assets already created in a project. We'll go into more detail on what each scriptable  object or class needs when we are creating them. We will start by creating our Editor Part classes and scriptable object. It will be divided into 4 classes, 1 of which is a scriptable object and the  other ones are normal classes: The Node Data, which will be a normal class, the Choice Data, which will also be a normal class, and the Group Data, which is the other normal class. For our scriptable object, we will have our Graph Scriptable Object that uses all of the other classes. So let's start by creating our node data. To do that, first create a new folder in our  Editor "Data" folder, to which I'll name "Save". We will be adding every Editor Save Data class here. Then, inside of this new folder, create a new  C# Script to which I'll name "DSNodeSaveData". Inside, feel free to remove the default methods  as well as the MonoBehaviour inheritance. I'll make it part of the "Data.Save" subnamespace. In our node we have a few things we want to save: The name of the node, the Dialogue Text, the connections of the node, or, choices, the Group it belongs to, the Node, or Dialogue Type, which is  either "SingleChoice" or "MultipleChoice", and where it is positioned in the Graph, to make sure we position it back at the right place. So let's start by creating a public property for all of these. If you are not using Unity 2020 or above, you might just want to make them a normal public variable instead, because we're going to be using the "[field: SerializeField]" attribute here, which works for properties in Unity 2020 and above without needing to create the private property variable ourselves. Every property we will be creating will be an automatic property. So type in "public string Name" and  set it as an automatic property. I'm fine with naming it "Name" here because there  is no built-in "name" variable in this class as we aren't inheriting from anything. Then, type in "public string Text". When that's done, create a new "public List<string>" to which I'll name "Choices". We'll not initialize this list because when we start saving we will simply assign an initialized list into it. For our Group, we'll actually be using an ID. This will make it so we don't really need to reference the group itself and can just get it through its ID, so type in "public string GroupID". This ID will be useful for the way we'll be saving as well. Next, we'll need our Dialogue Type, so make sure  you import the "Enumerations" namespace above and then type in "public DSDialogueType"  and I'll name it "DialogueType". And last but not least, we will have our node  position so type in "public Vector2 Position". As our class stands, it won't really show  in the inspector in our scriptable object, so make sure you make this class serializable  by typing in the "[Serializable]" attribute at the top, which needs the "System" namespace. Then, in Unity 2020 or above, if you want to make a property show up in the inspector, you will need to type in "[field: SerializeField]" just before our property declaration. If you are under Unity 2020, then you could only make it show up by creating the private field  yourself and assigning the SerializeField to it. You can just use public variables if you prefer. Do this for every single property. Our node is almost done, but our  choices won't really be strings. This is because besides the text the choice has,  we also want to know what node it connects to. This will be useful when saving to know  what dialogue does a choice connects to. So we'll be creating a new class that will hold the choice data. So back to our Save Data Editor folder, create a new C# script to which I'll name "DSChoiceSaveData". In here, remove the default methods  and the inheritance as well. I'll make it part of the "Data.Save" subnamespace. We'll be creating two variables: The Text of the Choice, as well as the node the choice is connected to. So type in "public string Text"  and make it an automatic property. Then, we'll want our node reference. However, we'll do it like we did for our Group: We will reference it through the node ID. So type in "public string NodeID" and  make it an automatic property as well. To make this class show up in the inspector, we'll need to do the same as in our node save data script, so make it [Serializable], and don't  forget to import the System namespace, and then type in "[field: SerializeField]" in each of the properties. With the ID's being used now, we need  to make sure our data has that ID in, so go back to our "DSNodeSaveData" script and make sure you create a new property of type "string" to which I'll name "ID". Don't forget to swap our "string" in the Choices  list type to be "DSChoiceSaveData" instead. With this, we will of course need to make sure our "DSGroup" and our "DSNode" have an ID variable as well, so go to our "DSNode" script and create a new  property of type "string" to which I'll name "ID". We will then initialize it in our constructor  by using the System "Guid.NewGuid()" method. Don't forget to call the ".ToString()" method as well. This method simply automatically creates an ID for us. Of course, we need to do the same for our Group so let's head to our "DSGroup" and create a new ID property. Then, initialize it in the group constructor. Don't forget to import the "System" namespace. That's done so let's keep on creating our save data. The one we'll be creating next is the data for our Groups. So in our "Save Data" Editor folder create a new C# script to which I'll name "DSGroupSaveData". Inside, remove the default methods  and the MonoBehaviour inheritance. I'll make it part of the "Data.Save" subnamespace. We'll be having three variables for our group: The ID, the Name of the group, and its position in the Graph. The ID will be useful for us when loading, as we will be able to set it to be the ID the Group had when we first created it. This is because when Initializing, it initializes as a random ID, which we don't want to when we start loading our Graph, as otherwise ID references would be wrong at every load. So create a new automatic property of  type "string" to which I'll name "ID". Then, create a new one for the name of the group. When that's done, create one of type  "Vector2" to which I'll name "Position". Of course, we need to make this serializable so  type in the [Serializable] attribute at the top, importing the necessary namespace, and then make every property have the "[field: SerializeField]" attribute before their declaration. We now have our nodes, groups and choices data done. All that's left for our Editor Part is  the class that holds all of that data. So let's create a new C# script to  which I'll name "DSGraphSaveDataSO", as it will be a scriptable object. Feel free to remove the default methods. We won't be inheriting from MonoBehaviour  but from "ScriptableObject" instead, as this will be our Editor asset. I'll make it part of the "Data.Save" subnamespace. For our Graph, what we'll need to save is quite simple: The file name, which will be the name of the graph file, the list of groups that are present in the graph, and the nodes that are present in the graph. We don't really need to care if they are grouped or ungrouped nodes here because we'll know that through their data, as nodes have a "GroupID" as a variable. However, we'll also have a system where  we'll remove files that have a name of a node or a group that isn't used anymore. For example, if we add "DialogueNode1" and "DialogueNode2" as assets, but then updated the "DialogueNode1" to be "DialogueNode" and save it, we will need to delete the old "DialogueNode1" asset file and create a new one with the name of "DialogueNode". We could simply update the name of  the file to have the new node name, but I find it simpler to just remove it and add a new one. If you decide to update it instead, remember that you can create other nodes  with the name of the old node name, so make sure you update them first  before creating the node asset files. For this system, we'll simply create the  current group, grouped nodes and ungrouped node names at every save. If they already exist, we will simply return the loaded file instead of creating a new one and update its contents. So, we'll need to have lists for the names of  the groups, grouped nodes and ungrouped nodes. Then, with those names, we will  iterate through them and see which ones are different than the  current names we have in the graph. Those that are different can simply be deleted. So, we'll be having: A list of old group names, a list of old ungrouped node names, and a dictionary of old grouped node names, in which the key will be the group name. So create a new automatic property of type  "string" to which I'll name "FileName". You could go with "Name" here if you wanted  to, but because ScriptableObjects have a "name" variable with lower case, I'll just type in a different name. Then, create a new "List<DSGroupSaveData>" and I'll name it "Groups". For our "Nodes", create a new one  of type "List<DSNodeSaveData>". Next are our old names, so create a new "List< string>" and I'll name it "OldGroupNames". Do the same as above but name it  "OldUngroupedNodeNames" instead. For our "OldGroupedNodeNames", create  a new "SerializableDictionary", and make sure that this one is a "SerializableDictionary" and not a normal "Dictionary", as "Dictionary"  is not serializable so our data would disappear, and I'll pass in "string" as a  key, which will be the group name, and a "List<string>" as the value, which will be the nodes that belong to this group. I'll name it "OldGroupedNodeNames". Because this is a ScriptableObject, we  don't need our "Serializable" attribute. We do however need our "[field: SerializeField]"  attribute so type that in before each property. In here, we'll actually need to initialize the  groups and nodes list as well as the file name, so create a new public method to which I'll  name "Initialize" and pass in as a parameter a string named "fileName". Then, assign this "fileName"  parameter to our "FileName" property. Next, initialize both our  "Groups" and "Nodes" list. We won't be initializing the old names  lists here because we will need to know the names every time we save the graph, but because we will always call the "Initialize" method before we start saving, having a list  initialization here would make the list empty, meaning we won't have a way to compare  the old names with the new names anymore. Our other two lists are fine because we'll assign new values to them without caring about the old ones. We now have all the classes our Editor Part needs. However, we still need to make the Runtime Part scripts. We will need: One for the Dialogues, which are our nodes, one for the Groups, a data class for the Choices, which will hold references to the next dialogue scriptable object, and one for our container, which will hold the references for our groups, grouped  and ungrouped nodes scriptable objects. This one will be useful when creating our custom inspector to select the starting dialogues and filter them out by  grouped and ungrouped dialogues. Let's start by creating our dialogue scriptable object and to do that I'll create a  new folder in our non-Editor "DialogueSystem" folder to which I'll name "Scripts". Inside, I'll add yet another folder to  which I'll name "ScriptableObjects". In that folder, we'll create our C#  Script to which I'll name "DSDialogueSO". I'll type in "SO" at the end of our 3 scriptable object scripts, much like we did for the graph one. Inside, feel free to remove the default methods  and swap the inheritance with "ScriptableObject". I'll make its subnamespace "ScriptableObjects". In here, we'll be having a few of the  variables we already have in our Node Save Data but not every single one. We'll be having the "DialogueName", the "Text", the "Choices" and the "DialogueType". We don't really need the "GroupID" here because we will know which group they belong to from another scriptable object. And we don't really need the position either. We will however have a new boolean variable which will basically tell us if the dialogue is a "starting dialogue". A starting dialogue is simply the  dialogue that starts the dialogue chain, so the very first dialogue. We could've added an UI Element to the node to decide it, but what we'll do is in my opinion simpler, or better: If the node does not have an input port  connection, then it is a starting dialogue. So type in "public string DialogueName"  and make it an automatic property, and then "public string Text", for our choices we'll be giving it a "List<string>", in which we'll update it in a bit when we create our choice scriptable object script. Then, import the "Enumerations" namespace above and create a new automatic property of type "DSDialogueType" to which I'll name "DialogueType". For our last property type in "public bool"  and I'll name it "IsStartingDialogue". We don't need our Serializable  attribute here but we do need our "[field: SerializeField]",  so type it in for every property. I'll also add the "[field: TextArea()]"  attribute to the "Text" property, for us to be able to see the text in multiple lines. In scriptable objects people often just use public variables, but because I'm using Unity 2020, I've used properties, as they can be shown in the inspector with  the "[field: SerializeField]" attribute. To set the values for the dialogue scriptable object, we could update each variable one by one, but it's easier to just set them all through one call. Unfortunately, we will be initializing the assets using the "CreateAsset" method that Unity provides, so we can't really use the Object Initializer there. Therefore, we'll create our own "Initialize"  method that accepts all variables and sets them. So type in "public void Initialize" and  I'll pass in the following parameters: "string dialogueName", "string text", "List<string>" to which I'll name "choices" "DSDialogueType dialogueType", and "bool isStartingDialogue". Then, simply assign the values to the corresponding properties. We now have our dialogue scriptable object  done so we will create the choice data next, which won't be a scriptable object but simply a normal class that will be used in  our dialogue scriptable object. So in Unity, go back one folder and create a new one and I'll name it "Data". In here, create a new C# script to  which I'll name "DSDialogueChoiceData". Inside, remove the default methods  and the MonoBehaviour inheritance. I'll make it part of the "Data" subnamespace. Then, we'll be having the same variables as in our other choice save data script but  the dialogue won't be an ID here but a reference to an actual  dialogue scriptable object. So type in "public string Text" for our choice text. Then, import the "ScriptableObjects" namespace,  as we'll be needing it for our dialogue reference. Next, create a new property of type  "DSDialogueSO" and I'll name it "NextDialogue". This is how you'll know what's the next dialogue  you want to show in your game dialogue system. Of course, don't forget to make this class "[Serializable]", in which you'll need to import the "System" namespace. And as always, type in the "[field: SerializeField]" attribute before each property. Our choice data is done so let's swap it  out at our dialogue scriptable object, so go back to our "DSDialogueSO" script and swap the "List<string>" to be "List<DSDialogueChoiceData>". Make sure you import the "Data" namespace above. We have our dialogue and choice done, so let's do our Group. Go back to the "ScriptableObjects" folder and create a new C# script to which I'll name "DSDialogueGroupSO". Inside the script, remove the default methods and  swap the inheritance to be "ScriptableObject". I'll make it part of the "ScriptableObjects" subnamespace. We'll only have one variable here and that's the "GroupName", so type in "public string GroupName"  and set it as an automatic property. Add the "[field: SerializeField]" attribute to this property. We'll have to allow this variable to be initialized, so create a new public method to which I'll name "Initialize" and pass in "string groupName" as a parameter. Then, set the "GroupName" property  to have the value of this parameter. We have our dialogues, dialogue choices and dialogue groups. All that's left now is to create the  scriptable object that will hold all of our groups and dialogues in a way that will allow us to know which dialogue is a grouped dialogue and  which one is an ungrouped dialogue. This scriptable object will be used for our  custom inspector to select the starting dialogue. So create a new C# script to which I'll name "DSDialogueContainerSO". Inside, remove the default methods and swap the  inheritance to be "ScriptableObject" instead. I'll make it part of the "ScriptableObjects" subnamespace. As variables, we'll be having the file name, the groups, the grouped dialogues and the ungrouped dialogues. So create a new "public string" to which I'll name "FileName". This will be needed to load the assets in the correct path, as the parent folder has the same name as this file name. Then, we'll be having the groups and the  grouped dialogues in the same property. We can do that by making it a dictionary  with the key of "DSDialogueGroupSO" and the value a "List<DSDialogueSO>". This makes it so that if we need the list of the groups, we simply iterate through the dictionary keys and get them. If we wanted the dialogues of a certain group,  we simply get the value of a certain key. So type in "public SerializableDictionary<DSDialogueGroupSO, List<DSDialogueSO>>" and I'll name it "DialogueGroups". Do make sure you are using the "SerializableDictionary" here, as otherwise your data won't be saved, as  Unity doesn't serialize normal Dictionaries. Then, create a new "public List<DSDialogueSO>"  and I'll name it "UngroupedDialogues". Don't forget to add the "[field: SerializeField]" to each one of the properties. As a last thing, we need to initialize them, so create a new public method to which I'll name "Initialize" and pass in "string fileName" as a parameter. Inside, set the "FileName" property to be "fileName" and initialize both the dictionary and the list. With that done, we now have both our Editor  Part classes and our Runtime Part classes ready.
Info
Channel: Indie Wafflus
Views: 388
Rating: undefined out of 5
Keywords: Unity Dialogue System, Unity Dialog System, Unity Graph View, Unity Node, Unity UI Toolkit, 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
Id: iFOXSxeCXwM
Channel Id: undefined
Length: 25min 2sec (1502 seconds)
Published: Mon Sep 13 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.