Hello and welcome back! It’s been silent around here for the last 2 weeks, because I’ve been bedridden with fever and stuff like that. But no more! I’m back on the keyboard and wrote up a tutorial on making Multiple-Choice Dialog in FlashPunk. This feature is of course implemented right into my JRPG Engine, that shouldn’t be a surprise to you. It’s pretty basic. The dialog texts are written and stored in XML files as assets. The engine allows for multiple dialog options and can make NPCs answer to the chosen line of the player. You’ll find out more in great detail throughout the article. The source code can be gotten from here. You can play the demo here.
0. Intro and explanation what this dialog feature does
The dialog feature makes it possible for the player to approach an NPC and initiate conversation. Upon pressing the action button (Spacebar) while properly facing the NPC, the NPC can say something to the player (if there is any dialog data in the npc_data.xml). This text is displayed in a textbox, with word wrapping. If the text is too much to fit into a single textbox, the text is subdivided into “pages”, which are basically subsets of the entire current text, each fitting into a single page, so that they can be displayed properly. When pressing the action button, the next page gets displayed until there is no more text. Then it is the player’s turn and the player’s own dialogbox displays all possible answers as a list. The player can change his/her selection of what to say next, by pressing up or down. As opposed to displaying it all in pages, when there is more text than space, the dialogbox scrolls to properly display the current selection.
1. Structure of Dialog Input in XML files
The actual strings of the conversations are stored in the npc_data.xml and player_data.xml for NPCs and Player respectively. Each dialog partner (Zelda and Link for example) stores their own portion of the conversation. Zelda’s dialog with Link is stored within a dialog tag like this:
and here is the player’s part:
As you can see, the attributes of the dialog tag indicate the other party, in this case “player”, which tells us this is going to be a conversation between Zelda and our player character. I’m hoping that I can somehow implement NPC to NPC dialog later on, for a cool eavesdropping feature. That’d be neat, eh? So anyways…the other attribute is the index of the dialog FOR THAT PARTNER. Which means that the next dialog with the player must be of index 1, but if there was a dialog with Peach, Zelda’s mother (I’m making this up, so bear with me), its index would be 0, and any consecutive dialog with Peach would increment on that. The code would still work if you didn’t adhere to this, but I think it’s better to keep track of things like this nonetheless.
The dialog tag contains line tags within it. You see, it’s basically like this. When the player presses action to initiate conversation, the NPC says the first line, which is always its 0th line of the current dialog with that partner (the player). After saying that line, it’s the player’s turn to give a response, but because there can be several answers, each being on the same station/level of the conversation though, lines actually consist of “versions”, which are actual strings of dialog text. That’s why the first line of the NPC has only one version, since it isn’t a “dynamic” response to something the player said. Now when the player is at his 0th line and choses the second version in it, the NPC is going to answer with the second version of its 1st line (remember it’s zero based). So this is pretty much it on the data storage side of things.
2. How are dialogs stored in Player and NPC class
So, it’s basically dialogs contain lines which contain strings, right? And players and NPCs can have multiple dialogs. The datastructures are pretty simple actually, for example here we have Dialog.as, which is in the utility folder:
You see there is an array for the lines. Lines have their own class, Line.as:
And versions is an array of simple strings. In the Player.as and NPC.as classes, there is a new property for each called dialogs, which is an array for objects of type Dialog. There are also 2 new dictionaries that help us with our dialog management, and the player has a new property called dialogPartner of type string, which is used when starting up a conversation, but I’ll get back to that later. The first dictionary dialogsInTotal uses the names of partners, such as “Zelda” or “Peach” as keys, and the values are ints that tell us how many conversations there are in total with that partner. The second dictionary dialogCounters also uses the dialog partner name as key, and the value is an int that tells us which dialog with that partner is next, so if there are 4 conversations in total with Zelda and the player, then dialogsInTotal has the value of 4, and if the first two have been gone over, then the dialogCounter would be 2. If the counter is equal to the total, that means that all conversations are gone over. I hope this makes some sense.
3. Reading dialog data from XML
The dialog tags from the data xmls are read into the dialogs arrays of the NPC and Player instances in the Dataloader.as class, which as you can see also loads in the other data for NPCs, Player and Maps from the XMLs. The dialog tags are simple opened, iterated through and the collected data is pushed into the next highest datastructure. Versions go into Lines and they go into Dialogs which go into dialog arrays. For the player:
and for NPCs
4. Initiating conversation
How is conversation actually initiated? Well, since it’s the player’s action, the code is in the Player.as class’ update function. It’s the second large if-else clause mumbojumbo there. So we first check if the action button was pressed (Spacebar), but then also we check if the Game’s gameMode is set to DIALOG_MODE and if dialogEndedThisFrame property is false. The gameMode is pretty self-explanatory. When in conversation, this property is set to DIALOG_MODE and back to NORMAL_MODE when exiting conversation. dialogEndedThisFrame isn’t so easy to explain but it isn’t that big of a deal. Thing is, when pressing the action button at the end of a conversation to end it (namely switching inDialog to false), in that same frame, the Player.as class’ update function again checks if the action button was pressed, resulting in jumping straight into the next conversation. For that reason, I added this flag that simply tells the function “Hey dude, chill. The conversation JUST ended with that last action button press, so don’t be fooled by inDialog being false. You can start checking on this button next frame, ok?” 😀
So we check the direction the player is facing by clumsily using the curAnimation var (I gotta improve this code later…). If in that direction there is an NPC 3 pixels away (using the collision detection method), then set the dialogPartner property to that NPCs name and set Game.gameMode to Game.DIALOG_MODE. We did the first step towards talking to other people!
5. How the Game.as class works with this stuff
So there is this kinda new notion of “modes”. Dialog-mode, Worldmap-mode, Normal-mode and more to come. We want everything to freeze in dialog mode. We stop all NPCs from walking around, we stop the time from being updated and we don’t check if the player left the map or something. You also might’ve seen that in the Player’s update method, we don’t listen for movement keys if the game is in dialog mode. The Game.as class implements the dialog handling code in two places: the update method and the processGeneralInput method.
In the update method, we check if we’re in dialog. The game mode can be set to dialog mode, as a reminder, in the Player.as class’ update function, so the Game.as class always has to check if the player did that. If that’s the case, we check if the new flag dialogModeInitiated is false. At start-up this flag is set to false, and so since the flag tells us that dialog mode is NOT initiated, we go ahead and invoke the new function for initiating the dialog mode.
Before declaring that dialog mode is officially initiated by setting the flag to true, we first check if there is any dialog between the player and the NPC we’re trying to talk to that hasn’t been done yet. If that is the case, the dialogManager’s setCurrentDialogWithPlayer returns true and it also charges its own currentDialog property to the correct dialog, by reading in the dialogs properties of both NPC and Player. This is explained in greater detailer later. We set the dialogModeInitiated flag to true and set the npcDialogBox to visible. The classes NPCDialogBox and PlayerDialogBox are explained properly in a later paragraph, but for now you just picture it as the entity (even though its technically a set of entities) that displays dialog text. The first combination of those dialog boxes and the dialog manager is shown right away when the npcDialogBox sets its text by requesting it from the dialogManager calling the function nextNPCLine. The parameter isn’t important to understand right now. Just know it must be zero at this point. And in the end we have the else, in case there wasn’t a valid dialog to be had with the npc, so we switch back to normal mode. In the processGeneralInput method, we listen for the input from the player to advance the dialog. The action button does several things depending on the current state of the conversation. If it’s the NPC’s turn to display new text, pressing action displays it. Pressing it again switches the turn to the player and the player’s dialog options display. If the NPC has more text in one line than can be displayed on a single page (or text box), it shows the next page of text from the NPC until all pages are finished, and then the player’s turn can begin. When it is the player’s turn, pressing action selects a dialog option. Pressing up and down during the player’s turn scrolls the list of options if there is more than fits into the box. All this code is here:
We check for the 3 buttons: action, scroll up and scroll down. When pressing action, we first check if there is another page in the npcDialogBox to display by checking if the current page is the last one. If it isn’t, display the next page. The next clause would be the NPC’s turn, at which we give the npcDialogBox the next line from the dialogManager, supplying the function the chosen dialog option of the player (0 would be the default, so we gave it in the initiation). To remind you, the NPC’s version of the current line is determined by the version of the player’s line immediately before. The last clause is the player’s turn. If the player’s dialog box hasn’t been invoked (for example in “dialogs” where the NPC just says one thing to you), it is now time to show it. Instead of setting the line of the player, like we do with the NPC, we set the lineVersions. We require all version of the current line, because we need to show them to the player as dialog options. It should be obvious. In case the dialog has ended and we end up in the else clause, we set the game mode back to normal, set the dialogHasEndedThisFrame flag to true, set the dialog boxes to invisible and the dialogModeInitiated flag back to false. The selection_up and selection_down presses each call the playerDialogBox’s respective function, which will be explained later. It is to note that the turn is checked to be the NPC’s. This might look weird, but each time the dialog manager calls a nextNPCLine or nextPlayerLineVersions, the turn is switched automatically, so when we’re actually still in the player’s turn, the dialog manager already has the property switched.
Okay, this is a pretty important class and you should know a good deal about it already. The dialog manager is what connects the Game.as class’ dialog boxes for NPC and Player with their dialog datastructures. Its job is ensuring that each dialog box gets fed the correct text for display and that the correct dialog is selected in the first place. The basic way a conversation works (at least in my game ;P), is turn-based. Link walks up to Zelda and presses action. She says something, Link then potentially gets to choose between one of several answers, chooses one, and Zelda says something according to what Link said. It’s not the kind of deep encapsulated dialog tree system you’d find when playing something like Dragon Age or so, but hey, it’s enough for the moment. One problem right now is that there is no backtracking yet, to try different dialog options to sort of get all possible info from the conversation. Anyway, the dialog manager manages dialog by keeping track of who’s turn it is right now, and feeding the text that is to be displayed in the Player’s or NPC’s respective dialog box. It also keeps track of the current “line”. You’ll remember from the XML structure that lines tell us how far into the conversation we are at the moment. The first part of DialogManager.as that I’ll explain more elaborately is the function setCurrentDialogWithPlayer.
The parameters are an instance of the player and the NPC the player is initiating conversation with. It is checked whether the instance is null, in which case we return false. Then currentDialog, the property of type Array is emptied/initialized. We check whether the player’s dialogCounters dictionary contains an entry for the NPC, and whether that counter (if existant) has reached the last dialog by comparing the values in dialogCounters and dialogsInTotal. If this clause was satisfied, it would mean that for example from 5 possible dialogs, all 5 have already been finished. If all those exit clauses do not apply, there must be a dialog with this NPC. We create the helper variables d and i. d is of type Dialog, and i is just a loop index. Then we create the array dialogsWithGivenNPC and we fill it as the name suggests, with all dialog instances of the player that are connected to the given NPC. Then the same procedure, but from the NPC’s side of things is done. currentDialog then gets the 2 halves of the dialog. The correct element is found by looking up the dialogCounters for the NPC and Player. We set the currentTurn to be the NPC’s, and the currentLine to 0. And to round things up, the dialogCounters are iterated by 1. The next important function is nextNPCLine:
It’s pretty simple. We take the index of the version that the player chose in his/her line before this turn as parameter, and use it to find the version of the current line of the NPC half of the current dialog. Remember that the currentDialog has only 2 elements, and both are dialog instances. After this, the turn is set to be the player’s and the NPC’s line is returned. The player’s respective function is nextPlayerLineVersions:
Because we want to present the player with all the dialog options of the current line, we return the versions array. The currentTurn is set to be the NPC’s and also we iterate the currentLine property. The last function dialogHasEnded is pretty easy to understand:
7. PlayerDialogBox and NPCDialogBox explanation
The dialog boxes aren’t entities themselves, but they bundle the entities for the dialog boxes as properties. The first would be the background entity which is an instance of TextBox. Then there is an array of DisplayText instances, called displayTexts. This array contains an instance for each row that is shown within the dialog box. Therefore, the number of DisplayTexts is proportional to the scale in height of the text box. The NPCDialogBox and PlayerDialogBox have different uses and therefore different code apart from this basic similarity. I’ll start with the NPCDialogBox.
…and here’s the constructor of NPCDialogBox
The NPCDialogBox receives in its constructor the position coordinates, and scale values for height and width. _x and _y are directly used to position the box, and all 4 parameters are handed to the TextBox constructor at instantiation of the textBox property. The property “charsPerRow” needs further explanation. charsPerRow means “characters per row”, which defines basically the maximum number of letters, numbers or other characters that fit into one row of text in the dialog box. We do this because we need to clip text to fit into rows if we don’t want it to print out of bounds of the dialog box. This value is gotten by multiplying it with the horizontal scale factor and a multiplier that works out well. I chose 46 purely based on trial and error, there is no rocket science behind it. Then we have the property “rowsPerPage”, which is as you probably have guessed by now the number of rows that can be displayed at once within the dialog box. It is proportional to the vertical scale factor. After that we call the initDisplayTexts function
We simply make as many DisplayTexts as we have rows per page. The positioning parameters are also calculated by trial and error and also has a slight dependency on the horizontal scale factor. But it’s nothing dramatic. The next function is the setter function “line”:
So this function has the task of taking the string given from the dialog manager and store it in the pages array. The pages array contains arrays that contain strings. It’s like this: a page is the container for rows. A row is an array of words. When the function takes the simple string of text as parameter, it is split by the spaces. Then the resulting array is stored as the variable “words”. It is looped over and the charCount variable get incremented by the length of each word and the spaces. Each time the charCount reaches the maximum, the portion of the text since the last clip is pushed into the pages array, thus essentially becoming a page. The j variable gets updated to mark the beginning of the next portion, the charCount is reset. If the loop reaches the end of the words array, the last portion of the array gets pushed as-is into pages. After this is done, we call nextPage().
In nextPage we start off by resetting the display texts, and declaring the helper variables. It loops over the contents of the current page, with a charCount keeping track of the word lengths much like in the line setter function. When the starting and ending index that mark the words for a row are found, the subset is joined by spaces and set as the text for the current display text. What this does is it finds which words are next in line to fill into a row of text and then joins those words and set it as the display text’s text property. In the end, the remainder portion of the page is straight joined and displayed. Splitting the initial simple string of text into single strings and sticking them into an array enables us to get word wrapping. Sadly, the code necessary here is very convoluted and hard to read/debug/update, because of all the index counter juggling. One last interesting part of the class is the visible getter and setter. I wanted the dialog box to behave like an entity, where you can make it visible and invisible easily. So there is a boolean flag called visibility that simply says true when all the property entities are visible and false when they are invisible. The setter speaks for itself, I think. The PlayerDialogBox has the task of presenting the player with all dialog options he/she can choose from. Pressing up and down changes the current selection and can scroll the list to make sure it is fully displayed. Pressing action selects the current dialog option. Any single option must be at maximum (maxRows * charsPerRow) in character length. Anything above can’t be displayed at once.
and here is the first interesting function:
The constructor works exactly like the one in NPCDialogBox. Instead of the setter “line” method, we have the setter “lineVersions” method, which takes an array as parameter. The array contains strings, which are the text for a dialog option each. The class’ dialogOptions property is charged and the displayTextFeed array is initiated. This array has the task of containing instances of the new class DisplayTextFeed. A DisplayTextFeed represents a single row in the list – its text and the dialog option’s index number (starting at 1). At the end of this method, all dialog options are fit into these display text feeds, word wrapped and properly indented/numbered. The two properties startDisplayTextFeedIndex and endDisplayTextFeedIndex tell us where the subset of displayTextFeed is, that is currently displayed in the display texts.
The dCounter variable is a incrementing integer and marks the current version number of the dialog option. For example, if the first dialog option is “Good to see you too!”, then the dCounter is the prefix number that is added: “1. Good to see you too!”. It is also used to be the version value of any DisplayTextFeed instances for this dialog option. We loop through all dialog options, cut them in pieces by the space characters and fit them into rows, much like in NPCDialogBox. When a row has been defined, its text is joined back together and used to create a DisplayTextFeed instance, along with the dCounter. This instance is pushed into the displayTextFeeds array for later use. The class has the property chosenVersion of type integer that indicates the current selected dialog option. A value of 1 for example means the first dialog option, since it starts at 1. After all this is done we call populateDisplayTexts and then highlightChosenVersion.
populateDisplayTexts takes the startDisplayTextFeedIndex and endDisplayTextFeedIndex to find the subset in displayTextFeeds that is to be shown in the display texts. It checks in the loop whether the current row is the first row of a version, by looking if the first character is a number prefix. If it is, nothing special happens, but if it isn’t, the row gets an indentation. This isn’t necessary but I find it prettier this way.
The highlightChosenVersion method loops over the display texts and checks whether a row is part of the current chosen version, in which case the display text gets a red font color, and white if it isn’t. The last part that is a bit more complex is the selection functionality. The selectionUp and selectionDown functions change the current chosen version, and highlight the corresponding display texts. If need be, the subset of displayTextFeeds is adjusted, too, in order to properly show all the rows belonging to the selected version.
In changeSelection we first get the startIndex and endIndex of the portion of displayTextFeeds that marks the selected version. Then it is checked whether there are more rows than possible to display at once, in which case only the first rows are displayed. If that isn’t the case and an upper part (when going to an earlier version) or lower part (when going to a later version) is not visible, the startDisplayTextFeedIndex and endDisplayTextFeedIndex variables are updated accordingly. If none of those cases apply, the highlight is simply updated to properly highlight the new selection.
So this is it for now. There have been some other changes in the code, but those are mostly just refactor changes. The dialog feature is by far not complete yet, but at this stage of development, I can work well with the feature as it is. The only necessary addition I’d need to make is to allow for going back to previous lines to try different dialog options.
Anyways, I hope you liked this tutorial, and hopefully it was useful and understandable. These articles seem to get larger and larger each time, and my explanation skills probably aren’t that great. So if you have questions or feedback to give, please do so. I’m pondering on what to do next. I think next up is gonna be a menu system with inventory and stats and stuff of that nature. It should be a hell of a lot of work, hehe. If I’m not getting into any other illnesses etc, I should be able to work on this from now on pretty steadily. So, until next time and see ya!