Previous Post:
I know it’s been a long time since an update, and I apologize for that. I was working at an ID Tech camp for 6 weeks and didn’t have a whole lot of time to work on this game. However, I (nearly) finished something that I’m really happy about! Introducing my custom made branching dialogue system! I almost completely reworked my code that I showed off in the last post, so it’s all going to be new stuff here!
Here’s a video of it in action!
A quick note, the [x, y] you see before each line is a marker for myself, something I’ll explain later in this post.
I didn’t just want to make a dialogue system for Laika, I also wanted to make something modular so I could take it with me to other projects easily. In order to do this, I had to make sure not to hard code anything except for the required classes and objects. Things like the dialogue canvas, GameMaster, and Buttons were all givens in this project, so I felt comfortable making them an essential part of my code.
When I first started, I wanted to create a system similar to the Animation window, where there was a visual node based editor for me to use. I quickly realized that was way too out of scope for my skills, and I tried thinking of a different way of making a dialogue tree. After a day or two of brainstorming, I realized I was looking at the problem all wrong. Instead of thinking of a conversation as a tree, I needed to think of it as a 2D array. I drew out my thoughts on a piece of paper, pictured below, and started working.
I’ll quickly go over the sheet’s contents.
- Top Left: A quick sketch of what I want the dialogue screen to look like. The black square represents the speaking character’s portrait.
- “Display”: This is a recreation of my dialogue tree for the first level, seen below. There is a region in it labeled “Test Region” that is referenced below.
- Middle Right: These are proposed inputs for the dialogue, allowing me to edit the game as the conversation is happening.
- “Programming”: This is a quick sketch of what I wanted the inspector to look like.
- “Test Region”: This is a blow up of the highlighted region from the “Display”. I edited it to show off what I thought it would look like as a 2D array.
- Bottom: These are basic tests for me to help understand how a conversation would flow in a 2D array.
I want to split the next section into two parts, how a conversation works and under the hood.
How a Conversation Works:
Forgoing the usual trunk and branch metaphors, I decided to use the words Index and Frame to represent my dialogue. An Index is a general topic while a Frame is any flavor text that happens to describe the topic. For example, if I was having a conversation about the weather, I would call the weather an Index, while stuff like “It sure is hot” and “Wow it’s nice out today” would be Frames. Forgive me if this is a little confusing, I didn’t realize how weird my naming was until I started typing it out here. It’ll make more sense in the next section.
Regardless, because I now have a 1D array for our conversation (“Hi there! This weather is something!” -> “It is crazy hot out here!”), I can add more of those Index‘s to create a 2D array, simulating an entire conversation! Also, since I store it as a literal 2D array, it makes it very easy to skip around due to game situations. The best way to show this off is through the first time interaction.
In the video, that first time you talk to Scoria she asks you if you’re ok, and then says “Second Frame”. Second frame is just a placeholder (all of the dialogue is a placeholder right now) but you get the point. As you watch keep the [x,y] in mind. It first is [0, 0], showing that it is the very first frame of the conversation, then moves on to [0, 1], the second frame. Afterwards, it jumps to [2, 0]. Why is that? Shouldn’t it go to [1, 0] or even [0, 2]? Well valiant reader, this is because I need my conversation to start from a certain point after the first interaction! You’ll see this once I talk to Scoria for the second time.
But once it skips to [2, 0], something else happens. A question is raised! This pops up the dialogue answers, which are displayed in the buttons. Unlike the E button in the bottom right (which, as I forgot to mention, progresses the conversation), the answer buttons are marked with A and D, the keys the player must press to answer. After answering, the conversation then jumps to the correct response in the scenario. In this case, I answered with “Whimper” and I was thrown to [4, 0] in the conversation. After progressing once to [4, 1] to insure that my code was still working, the conversation ends and I regain control of my character.
When the conversation is restarted, I start at [1, 0] instead of [0, 0]. This shows a different response after the first interaction, and it insures that the NPC won’t be reintroducing themselves every single time. All of the skips are customizable, as you will see in the next section.
Under the Hood:
I have a GIF of what my inspector looks like, shown below.
(Click on it to expand. It doesn’t work well on mobile, sorry about that.)
Under my DialogueMaster, I have my NPC_ID which holds the NPC game objects and their Dialogue Tree. Inside the Dialogue Tree, there are the Elements, which are the Indexes that I referred to earlier, and inside those are the Dialogue Frames. There are Enums titled First and Second Response, and these control if the Index is a question or not. Most of the time that are set to NONE, which is key for continuing to the next Frame (or next Index), but sometimes they are NONQUESTION_SKIP. These are used for when the conversation needs to skip around but it isn’t due to a player’s response to a question. An example of this is the first time interaction I’ve been talking about. The player doesn’t have direct interaction, so the NONQUESTION_SKIP is needed.
There is also a field called Interactable, but this is unused for now. It is going to be used for checking other game situations (for example, does the player have all 3 parts to something). Below that is the IndexSkip. This allows the code to skip around in the event of an answered question or a NONQUESTION_SKIP. I need two sets of the Index and Frame so that both answers to a question can lead to different scenarios. Below that is a simple bool called IsLastFrame. If checked, this tells the code that after the Frame is typed out, the next step will be to close the conversation. Without this, the conversation would loop forever.
Lastly, I showed off code under the NPC characters. This code is the NPCDialogueOptions, and it just stores some public data specific to each character. It contains data such as the first interaction skip and where to go once it is done, as well as other scenario specific data (such as things to say once all collectibles have been obtained). I considered using ScriptableObjects for this portion, but I don’t think it would save me time in the long run, so I didn’t bother with it and just used what worked.
Other Changes:
Some of the other features I added/modified:
- Added a drag to the character on movement key up. This allowed the player to get up to speed gradually but add a quicker stop, making it more suited for the sudden stops when a player enters conversation with an NPC.
- Redid the dialogue screen with new colors.
- Completely messed up my layer changing mechanic for the planet. Like, it’s completely broken now. Oops. Thankfully, I know how to fix it, I just want to get my dialogue system working first. That should be done in the next two-ish blog posts!
- I followed a tutorial from here showing me how to make hidden inspector values. However, it doesn’t want to work with my custom classes and structs, so for now it has been removed. I still have the code and I want to incorporate it into my modular dialogue system if I have the time later on.
If you’re still reading, I appreciate the support. If you want to get my code for the dialogue system, just reach out! I’m always happy to help!
Next Post: