Creating a custom interface for the new dialogue system


As you maybe already know, the default Dialogue system’s interface looks like this:

Well, what if you don’t like that look? In the old system, you’d be out of luck. However, with this system, you can customize the user interface to look however you want. Let’s begin by explaining what an “Interface” is in the context of the new dialogue system.

There are two main components of the system that you as a developer don’t interact with: the client and the server. The server validates all conditions, executes all actions, and communicates with the client. The client serves as a mediator between the server and the interface, which is a completely developer-controlled component of the system. You can find the default interface and an interface template in ReplicatedStorage.RobloxDialogue.Interfaces.


Feel free to explore the Default interface to see how it works. There’s quite a bit in there to make it cross-platform compatible that makes the code a bit of a difficult read, but it’s probably still worthwhile. If you look in InterfaceTemplate.Interface, you can see the code that your Interface must implement. I’ll list it here for convenience:

local Interface = {}

--* called by the client immediately after it is initialized.
--* used to pass communication functions into the interface.
--* the current list of functions is as follows:
--*--* Call this function when the user manually ends
--*--* any dialogue, so that the server can respond
--*--* accordingly and not send false timeout signals
function Interface.Initialize(clientFunctions)
	Interface.ClientFunctions = clientFunctions

--* called when the client discovers a Dialogue. The Interface
--* should provide some way for the user to interact with the
--* dialogue in question. When they interact with it, call the
--* callback and the client will initialize the dialogue
function Interface.RegisterDialogue(dialogue, startDialogueCallback)

--* called when the client discovers that a dialogue has been
--* removed from the game. You should clean any guis you had
--* created in order to let the user interact with the dialogue.
function Interface.UnregisterDialogue(dialogue)

--* called when the dialogue has a TriggerDistance and the player
--* walks in range of it. Here so you can manage your interaction
--* buttons, etc. Call the callback to start the dialogue.
function Interface.Triggered(dialogue, startDialogueCallback)

--* called when the server notifies that the client attempted
--* to start a dialogue that was too far away (ConversationDistance).
function Interface.RangeWarned()

--* called when the player took too long to choose an option.
function Interface.TimedOut()

--* called when the player gets too far away from the dialogue
--* that they are currently speaking with.
function Interface.WalkedAway()

--* called when the conversation finishes under normal circumstances
--* with prompt is whether or not the dialogue finished with a prompt
--* as opposed to a response. useful if you want to show the prompt
--* for some time but want to end immediately conversations that
--* end with a response
function Interface.Finished(withPrompt)

--* the bread and butter of the interface, the interface should present
--* the player with the prompt and responses. The dialogueFolder is the
--* folder representing the dialogue, the prompTable is a table in the
--* following format:
--* {
--* 	Line = "The prompt string.",
--* 	Data = [the data from the prompt node],
--* }
--* and the responseTables is a list of tables in the following format:
--* {
--* 	Line = "Some string to show.",
--* 	Callback = [function to call if the player chooses this response],
--*		Data = [the data from the response node],
--* }
--* if a player chooses the response, call the callback.
function Interface.PromptShown(dialogueFolder, promptTable, responseTables)

--* very similar to PromptShown, except there are no responses.
--* that means there's a prompt following this prompt. the only
--* responsibility of this function is to call the clalback in
--* order to acknowledge that the chain should proceed. this
--* allows you to use your own timing scheme for how the chain
--* should proceed, with a continue button, arbitrary timing, etc.
--* promptTable is in the following format:
--* {
--* 	Line = "The prompt string.",
--* 	Data = [the data from the prompt node],
--* }
function Interface.PromptChained(dialogueFolder, promptTable, callback)

return Interface

As you can see, the code is somewhat self-documented. But I’ll try to give a more thorough explanation by using the default interface as an example and providing theoretical examples also.

First up is Initialize. This function provides your interface with a list of functions that your interface may need to call on the client. Currently, the only one that exists is UserEnded. In the case of the default interface, this function is called when the user either clicks out of the UI or clicks the red ‘X’ button at the top-right of the UI. Basically, it’s a way for your interface to let the server know that the user wanted out of the dialogue. You need not use this! If you want certain dialogues to be inescapable, for example, you don’t have to call this.

RegisterDialogue and UnregisterDialogue go hand in hand. Whenever the client detects a new RobloxDialogue in the game, it calls this RegisterDialogue in your interface, telling you which dialogue needs to be registered and providing a callback function. The default interface’s RegisterDialogue creates a BillboardGui above the RobloxDialogue’s host, and calls the callback when the user clicks on that BillboardGui. When the client detects that a RobloxDialogue has been destroyed, it calls UnregisterDialogue. The default interface then removes that BillboardGui, cleaning up after itself. One theoretical use of these functions is creating an interface which presents a button prompt upon walking up to RobloxDialogues, such as ‘press F to interact.’

Triggered is an odd function that you may not even have to use. The client will call this function on RobloxDialogues that have a trigger distance when your character walks within that trigger distance. The default interface uses this function to make the associated BillboardGui disappear as expected, then calls the callback function to start the dialogue. This could be used theoretically in a pokemon-style “eye contact” battle, where perhaps all dialogues that are triggered produce a surprised exclamation point above the NPC’s head, differentiating it from normal interactions.

RangeWarned fires when the client tries to start a conversation, but the server responds by saying that you’re too far away. This emulates the original dialogue system, where clicking on a dialogue that’s too far away presents the user with a message saying “you are too far away to chat!” The default interface these days just does nothing but make the BillboardGui reappear.

The next two are pretty straightforward. TimedOut and WalkedAway are called by the server (via the client) to let you know that the conversation ended because it either timed out (the player didn’t choose a response in time) or walked away (they left the conversation range of the RobloxDialogue). The default interface uses these to just end the conversation, but you could use them to do something like this:

Finished is up next. This is called by the server (via the client) to let you know that the conversation ended under normal, expected circumstances (that is, a prompt with no responses was reached, or a response with no prompts was reached). It also gives you a boolean to let you know if the conversation ended with a prompt, so that you can potentially react differently. In the case of the default interface, when a conversation ends with a prompt, it shows the prompt for a time with no responses so that the user can read it, even though the conversation is, from the server’s perspective, over.

PromptShown is the most complicated, most central function in your interface. When this is called, you should somehow show the prompt to the user and somehow present and let him choose a response from those listed (if there are any). The default interface shows the prompt in a text box and presents the user with a grid of options. When you want to choose a response, call the callback function associated with the responseTable and the client will handle the rest. This is where your creativity can really go wild – this function could cinematically place your camera based on some information saved in promptTable.Data, or maybe just emulate the old billboard-based dialogue system.

PromptChained is very similar to PromptShown. However, in this case, you have no responses. This is called when a prompt leads directly to another prompt. In the case of the default interface, this presents the user with a “next” button, allowing them to choose when to proceed to the next prompt in the chain. However, you could theoretically wait a certain amount of time and proceed automatically, or press the ‘A’ button on a gamepad like in Legend of Zelda. All you have to do is call the callback.

So, how do you get the dialogue system to use your interface? Well, that’s simple. There’s an object value in RobloxDialogue called ClientInterface. Set the value of that object value to the folder containing your interface. Make sure that the ModuleScript is called Interface precisely, or it will not work.

If you have any questions, please let me know! I’m excited to see what kind of interfaces you come up with. To finish, here’s an example of a customized interface from our very own @GollyGreg: