Posted February 25, 2021 by World Eater Games
#Godot #dialogues
Sup y'all, long time no see! Our last devlog seems to have sparked quite a lot of interest in how we've built our game, so we though why not give it another go?
Let's face it, outside of combat and gameplay elements, most of the heart and soul that we deliver on our games is delivered to the players via unique and interesting conversations with the inhabitants of our games.
Lets just say, special ones...
They all add a bit of magic to what we're doing.
There are two important aspects that we must take into account when building dialogues for our game:
Honestly, the "what" part of this equation is VEEEEERY subjective, and it's something that will be completely different from game to game, so we're going to focus on the "how" aspect for the most part. What I want to say is, we're gonna build the ✨s y s t e m✨ in this post, what you will make your game say with it is up to you 😎.
With that out of the way, let's begin shall we?
I'm gonna be honest here, there's no way that we can cover every single functionality of our dialogue system in just one post, and most of it is custom-tailored to our game, so it probably won't apply to what you are doing. Instead of explaining everything, I'm gonna reduce the scope and just tackle the following functionalities:
The end result of what we'll build will be a screen with 3 buttons, each one will trigger one conversation like this:
A brief disclaimer before we get started:
({p=\\d([.]\\d+)?[}])
YOU HAVE BEEN WARNED Regular expressions are your friends, there's nothing to be afraid of.
By the way, while this tutorial is specifically made for Godot, it's based on several high level development concepts that should be applicable to any platform!
Since source code is a bit difficult to read in itch, I've decided to use images instead of actual code for my snippets. But don't worry, if you want to see the actual source code for the example (for example, to copy some or all of the snippet), you can still click on the image, and it should take you directly to a gist with that snippet's content, for example:
Clicking on that image should take you here. Keep in mind that these are incomplete code snippets, and will mostly be used to illustrate a specific section of this tutorial.
You can also check out the Github Repo for this project, which contains all the resources used here as well as the complete source code for the project.
Last (but not least), I want to do a small and shameless plug: we're launching our INDIEGOGO PRE-LAUNCH PAGE!!!! Please be sure to check it out here if you find the game or this article interesting.
Let's start by creating a control node called "Dialogue.gd" with the following structure:
Note: while we could use a plain old regular Label for our content, we would be missing out on quite a lot of styling capabilities that Godot offers via bbcode, which basically allows us to spice our text like this:
You can find more information on how to use this in the BBCode documentation, and the used code was the following:
[color=#ff0000]Hi[/color] [wave]I was generated[/wave] for the [rainbow]dialogue[/rainbow] system test for the godot game [shake rate=20 level=10]engine[/shake]
If you want to use the same style that I'm using for the example, you can find the texture I'm using here, the font is Roboto size 32, and the NinePatchRect is configured with that background image like this:
The Node itself will look like this in the preview, considering that it has a size of 798x149, and the rich text label will have a size of 773x128 and be slightly offsetted to simulate a padding, this gives us 3 lines to type our text per "conversation step":
The "update_message()" method will be used to update what we're gonna type (we'll fill in this function later), and what we have defined in the "_ready()" method a simple message that will fire the typing logic after a small delay, just so that we can run the standalone scene while we're building the node (we'll delete it later).
Note: don't worry too much about that yield call, the gist of it is that the "update_message()" method will be called after 1 second. If you want to expand more on what it does, you could read more about the yield declaration, and about the coroutine concept itself (It helps me a lot to see the yield keyword as it meaning await, like "wait for this to finish before continuing").
Anyways, running this as a standalone scene won't do anything yet, so let's populate that "update_message()" method with the following:
Note that we set the message to the "bbcode_text" property, not to the "text" one. Next, we're going to connect the timer's "timeout" signal to the following method:
Quick refresher on labels/rich text labels: the "visible_character" property will hide any character whose index is greater than it, greatly simplifying the "typing" behavior:
This callback is basically displaying the "next" character every time the timer finishes, until the message is fully visible, and to achieve this we'll be setting the following values for the timer's exported properties:
Wait Time | 0.04 |
One Shot | false |
Autostart | false |
I know, that 0.04 seems rather...extreme, but I actually think that the result looks quite nice! Running it as a standalone scene should look like this:
The "Wait Time" will determine the typing speed, increase it to make the text appear slower.
We are going to be using the "blob" sound effect found here, just with a small modification where we remove the initial silence:
And for our first approach, let's play the sound for every letter typed.
Start by adding an AudioStreamPlayer to the scene, assigning it to a "voice_player" variable and calling "play" every time a new character is revealed:
Play the scene aaand...
Perfect!...for a machine gun 😅.
OK that doesn't sound quite right for what we want so instead, we are going to follow this logic:
First, add a "_playing_voice" boolean variable to the script, which we will use to re-fire the audio clip (Note: this can also be done by altering the resource's import flags and setting the "loop" property to "true", but I personally prefer to have this logic managed by the code as it's a bit clearer on WHY it is looping).
After this, we're gonna connect the player's "finished" signal to the following method:
And finally: we're gonna add these lines of code to the bottom of our "update_message" method (remember to also remove the voice_player.play(0) call from the timeout method, otherwise the machine gun will return):
Play it and...
Much better!
Finally, we want to feel like we are not talking to a robot (I mean we kind of are but, that's not the point!!!), so we're gonna apply a little variation on each "blob", just to mimic different word sounds. To do this, we're simply going to add a new script to our AudioStreamPlayer with the following code:
What we are doing here is that for every "play" we call, the voice's "pitch" will vary between 95% and 108% of the original value (and about that RandomNumberGenerator, you can find more info here, but for now just keep in mind that we want to generate a random number between 0.95 and 1.08).
With all these changes: this is the result we are left with:
Nice
Now, let's implement something that allows us to modify the pause behavior like this:
Here I want to introduce a concept that we use extensively on our dialogues: custom tags. These are NOT custom bbcode tags, which are mainly used to modify how godot renders the text inside the rich text label, rather, they are a custom tag format that we define that allows us to modify how the node types the content.
I'm gonna demonstrate how we use them, but keep in mind that you can define any pattern you want, as long as it doesn't mess with the text you are typing or that they don't overwrite with the bbcode standard.
We currently use this format: "{tag=param}" to specify custom tags, and specifically for pauses we're gonna use {p=number}, where the "number" is the amount of time that the typing logic will be paused for.
With that said, the text shown in the gif renders with this string:
Howdy!{p=0.5} I was generated for the dialogue system test...{p=0.5} for the godot game engine!
Notice the two {p=0.5} definitions in that string, is what tells the typing service to stop for a set time at that position. (Wait 0.5 seconds after typing "Howdy!", and another 0.5 seconds after "test...").
But I mean, if you just plug that into the "update_message" method, the {p=%f} tags will just be printed without any special treatment, so we need to add some additional logic to actually parse them. To achieve this, I'll divide the explanation in two parts:
Heads up, from here on I'll refer to these pause tags by writing {p=%f} to identify the general pattern
Ok so our main objectives here are cleaning any custom tags we find on our string, and keeping a reference to the pause's positions and durations found inside the string. All of this will be encapsulated inside a node that I'm gonna name "PauseCalculator". A quick graphical overview of the process could be shown as this:
Create a new Node inside a scene and add this code inside:
Inside this class we can see that we have some props and methods, which are empty for now but they will have these responsibilities:
And we're gonna hook it up inside our Dialogue scene. Instance it, add a reference to it and perform the following changes to "update_message":
Now, instead of just assigning the bbcode directly, we first process it through the "extract_pauses_from_string()" method, and assign the result to the property. Note that running it right now will not do anything differently, as that extraction method is just returning its input for now (don't worry, we'll change that soon).
The best way to search for patters inside a string is using regular expressions, which godot provides a powerful API via the RegEx class.
Now, explaining the topic of regular expressions is a WHOLE can of worms, so I'm gonna cut to the chase and straight up tell you what patterns we are using with what we've defined so far so, if you want to expand a bit more on that topic, you can get started here
To detect pauses, we use the following regular expression definition inside our PauseCalculator script:
Don't worry too much about that pattern, just know that it will detect {p=%f} patterns inside a string. Also take note of the double backslashes, these are not 100% standard in regexes patterns, as the regex pattern would actually be something like this in other programming languages:
({p=\d([.]\d+)?[}])
However, those languages normally treat regex patterns as a primitive type, and since godot initializes regular expressions with patterns defined inside a string, we must actually escape the backslash character, otherwise the framework will just detect it as a line break.
Ok, let's modify our _find_pauses and extract_tags method with this logic:
A short summary of what's happening on "_find_pauses":
And on "_extract_tags":
This is the internal structure of that Pause class (I like to save data structure classes in their own .gd as well):
Oh great, another regular expression!
The two important things to keep in mind here are:
So we now have all our pauses registered in our PauseCalculator node, and now we're gonna make the Dialogue node react to these changes.
Remember that "check_at_position()" method that we left empty a while ago inside PauseCalculator.gd? Now we're gonna modify it with the following:
Now, on the dialogue node we're gonna add another timer called PauseTimer, and we will:
The changes will be as follows:
And finally' we are gonna add this line the Type timer's timeout method (it should already be there):
All right, lets gooo! We run the scene with the same string that we used at the start of this example aaand (you know by now that this means something will not work quite as expected):
Ok so...the first one works just fine, but the second one appears off? What's happening?
Well, if we take a closer look on the amount of characters it's offset by:
Those 7 charactes come from the previous {p=0.5}, which means that all preceding tags from the current one will affect its detected position, as the RegEx check doesn't know that there's a portion of the string it doesn't have to take into account.
To fix this, we are gonna substract the length of all the tags the service finds to the left of our current tag. First, we define the following method:
Ayyy, another regex! But this one seems familiar...
Here, we are using the same expression as on the _extract_tags method to find every single tag defined between curly braces (the {%s} tags), this way we can capture our {p=%f} tags alongside any other custom tag we want to define in the future, provided that it follows this convention.
Step by step of what's happening
And we'll use this to calculate the Pause's position, meaning that this:
Will become this:
Note: this is inside the _find_pauses method, inside PauseCalculator.gd
Run the scene again, and now you should see this:
Everything seems to work fine for now, but the same problem that we saw with the previous tags will happen with previous bbcode expressions, meaning that something like this:
[wave]hello[/wave]{p=0.5} I am the text!
Will detect the pause tag 13 characters after it was truly defined (the combined length of "[wave]" and "[/wave]"), defeating the whole purpose of using a RichTextLabel over a regular Label.
We're gonna fix this issue the same way as before: detect every single bbcode tag before the current tag position, and substract their length to the resulting position. Modify the "_adjust_position" method like this:
Note: Those were the best regexes I could find to detect bbcodes, but you could define your own if they don't seem to work property for you
Now, everything should work as expected!
All right, we have our Dialogue node up and ready to go! now all we need is a way to coordinate multiple "messages" to simulate a conversation, and we're all set~. We'll be creating the DialogueManager class in this section.
Ok so, we're no longer gonna make the Dialogue auto-show any message, so we must delete the _ready function we've been using to force our messages when running the file as a standalone scene. Remember to do this, or else your text is gonna randomly change after the manager queues a set of messages.
Ok so let's briefly list what this node will be responsible of:
Which can be summarized with this diagram:
Let's start by creating a new Node, naming it DialogueManager and attaching this script to it:
We'll use the "show_messages" method to build our conversation with multiple strings, and the position will be where the dialogue will spawn on the tree. Modify it like this:
We also need to detect when the "next" key is pressed, so we can update _active_dialogue_offset and call _show_current again. This is the logic I could quickly come up with to detect when the "enter" key is pressed:
Kinda dirty, but it will have to do for now. Note that we added a "message_is_fully_visible()" method to the Dialogue node, which just compares if the number of visible characters match the max amount on the string.
Additionally, we're gonna define a "_hide()" method that will just remove the Dialogue from the tree, and notify that the conversation finished:
And finally, we add the last method we connected to on the "show_messages()" method will just emit the "message_completed" signal, that way we know when we can show/hide the "press enter" prompt:
We're almost done with our sample project, the last thing we must do is hook it up inside a test scene, and fire some dialogues!
We're just gonna create a scene with the following elements, just to showcase what the Dialogue/DialogueManager combo is capable of:
Each button will call "dialogue_manager.show_message()" with different parameters, but using basically the same logic, This is a code example on the dialogue that we showed as the "What we'll be building" gif at the beginning of this tutorial
The last three methods are just signals emitted by the manager, that we will listen to in order to show/hide the "next_label" reference.
Yeah boi, we've come full circle!!
And with that, the project is done 🙏 (in that clip, the dialogue fades in and out rather than popping in and out of existence. I won't be covering that part specifically, but it's a rather simple transformation via the Tween node animating the "modulate.a" property).
Phew! That's quite a lot of information. Obviously this is not necessarily the only way to do this (or the best for that matter), but it's the solution we came up with, and it has worked very well for us so far. Now, there's a lot of things you could expand this project with, such as:
Well, hope you could find something useful in this tutorial, or at least learn something new (I know I sure did writing this post).
Again, here's the link to the github repo, in case you want to actually see the code and run it for yourself.
And we can't end this blog post without reminding you of OUR INDIEGOGO PRELAUNCH PAGE!!!!!!!!!!
Go sign up NOW! or the geese will get you
Hope to see you soon in our following posts!