Hello all! My name's Adam, and I'm working on a game in Unity that's very much in the early stages of development. This being my first foray into the world of indie game development, I want to share what I learn as I go through the process of developing a game- not only to record the process, but also in hopes that what I've learned can be useful to others out there. Now, fair warning- while I've been an artist for a while now, I'm still very much a noob when it comes to coding and game design, so some of the methods I use to achieve my goals might not be the best or most efficient way to go about them, but I've been doing my best to learn as much as possible as I go and do things right the first time. (Though if anybody reading this sees ways that it could be improved, please feel free to chime in!) I'm not really intending this to be an in-depth tutorial, rather just an overview of the how I tackled some of the challenges that come with making a dialogue system like this. So, without further ado-
Here is my first pass at emulating (aping might be more accurate) the dialogue system of the Ace Attorney games. I took a ton of inspiration from the Ace Attorney series in coming up with the idea for the game I'm developing (Which I'm tentatively calling Psychomancer, about a fantasy therapist), and I've always loved the way that Capcom handles dialogue in those games. The two biggest things that I think make the dialogue in Ace Attorney really effective are 1. The way that the text is generated with a cadence and a variable speed to simulate speech, and 2. the way that events, animations, and sound effects are timed to the dialogue to make it feel dynamic. Those were the main points I wanted to hit when creating a dialogue system, and I think all in all the solutions I went with work pretty well.
This being my first time attempting anything like this, I started out by following this really great tutorial on creating a basic visual novel framework in Unity, which explains how to set up a basic UI, write a script to parse dialogue from a text file, display that dialogue on screen, and create buttons for choices/branching narratives. The next task was to display the text on screen one character at a time instead of all at once. When dealing with anything that needs to be time-delayed, coroutines(IEnumerators) are usually the way to go, since they can use the WaitForSeconds function to suspend execution for a specified amount of time. I created a coroutine called TextUpdate, in which I placed a for loop that takes a string containing a single line read in from the dialogue text file and runs on each individual character in that string, and during each loop we wait a certain amount of time before updating the text that's appearing on the screen. Here's a hacked-together screenshot of the basic idea (I've omitted lines in the script that aren't directly related to avoid confusion)-
From here, the next step is allowing for variable text speed, pauses, animations, basically anything that needs to be timed to the dialogue. This part is a little bit trickier, since you have to include the instructions in the actual dialogue text file if you want them to be triggered at specific points in the dialogue. Here's just a snippet of my raw text file to give you an idea of how nonsensical it looks by the end of this process-
Basically, the data between each of those weird sets of characters, or delimiters, tells the script to do something specific when it detects them. I have enclosed all text speed information between two & delimiters for instance, and each time I want to change text speed I will insert that information into that spot of the dialogue text. The <> delimiters contain rich text information, like bold, italic, and colored text, the % delimiters tell it to play an animation, and the @ delimiters tell it to play a sound effect. The script detects these characters by way of a switch construction within the previously shown for loop, for instance if dialogue[i]is & then we process the number within the ampersands with another for loop (which I put into a separate function called processDlgCommand) and update our timeInSeconds variable (which specifies the delay between each letter) to the new number, or if dialogue[i]is < then we look for the closing > and process the rich text tag, etc. One thing to keep in mind is that since we're reading in the dialogue text one character at a time, these delimiters will get printed to the screen if we don't have the computer skip ahead when it detects them, so when the command has been processed we update i (our for loop control variable- you generally don't want to mess around with these if you can help it, but in this situation it seemed like the best option) to the location of the closing delimiter, then use the 'continue' command to return to the top of the loop. There are other characters that I use in this fashion, to do things like make the text silent, insert a line break, and center the text, but the basic idea is the same. Here's the entire TextUpdate method once I was finished with it. (There are some things happening in it that I didn't really talk about, but hopefully it's readable enough that you generally understand what's going on)
One more thing I'd like to elaborate on is the word wrap functionality. You may notice in that script that I run a function called doesNextWordWrap when a space ' ' character is detected. This is because when we update text character by character like this, we run into some weird issues with text wrapping where a word near the right edge of the text box can start on an upper line, but as we add more characters to it, it automatically wraps to the line below when it gets too long. To avoid this, the doesNextWordWrap function gets the next word, uses the TextGenerationSettings class to get the size of the current text box, then compares the height of the current text vs. the height of the text with the next word added (as if the word wraps to the next line the height will be increased). If it returns true, then we add a line break.
The RemoveBetween function I'm using here uses a regEx expression to read through the text and remove any of those delimiters/commands we entered into our dialogue text, as we don't want those to factor into our word wrap calculations.
Well, that about covers it! By no means comprehensive, I know, but I wanted to at least hit on the important stuff. Next I'll be working on prototyping some of the other major game systems I'm planning to include, including a main menu and a 'notepad' mechanic. I hope this was helpful, if anyone reads this and has any questions or wants me to go more in-depth on something be sure to let me know! 'Til next time!