Safety Protocol’s core revolves around a narrative dialoguebetween the player and a survivor on the VASA space station. For this purpose, I created this dialogue system. I had never made a dialogue system before but I really enjoy games with an intuitive and immersive storyline where the player feels part of the story; I had ample experience using dialogue systems implemented in other games. Based on that experience, I had a clear vision of what I wanted my own dialogue system to be like.
Giving the player options to choose from felt the most vital to help the player feel like they’re part of the game. Further, each choice should trigger a unique response, which in turn can trigger further options and responses, or simply reroute back to the main story. With this vision in mind, I started looking for ways on how to create a dialogue system.
I looked for some tutorial videos to find inspiration. Surprisingly, I didn’t find anything that really matched my vision.
I ended up following a basic system using queues to add and remove messages. This didn’t really work for what I had in mind, so I ended up just analyzing how that system worked and used that new knowledge to make my own system from scratch. I hashed out the concept on a piece of paper and started thinking about what kind of code structure and what kind of variable types would help me achieve my goal.
Out of this 7 week project, I spent almost four weeks working on and continuously improving upon this system. Part of that was working closely with our game designers, making sure the system did what they wanted it to and having it be user friendly for them to implement their narrative story on.
In the end I feel like I made a very well written, well functioning and easy-to-use dialogue system which can be tailored to fit in many different kind of games, either as it is now or as a good structural base upon which to build even more functionality. Of course, a few things could be improved upon, though I am still quite proud of what I’ve made thus far.
The Sequence
While activity diagrams are not commonly used within game programming, the way this dialogue system is built fits very well with their purpose. (This is a general overview, some steps are skipped.)
Written version:
After initiating the conversation, the system goes to the next dialogue index, checks if the end of the dialogue has been reached and then either initiates the game ending or otherwise prints the message stored at the index. That message then might trigger a set of options which in turn triggers specific responses. It might otherwise signal that it’s the end of the current conversation and thus pause the system until it’s reactivated through game progression. If neither of those trigger, the system simply goes to the next dialogue index instead. This sequence is repeated until either a new dialogue set is initiated, or the game ends.
How it’s implemented
To use this system [in Unity] you should apply the Dialogue Manager script to one or several Dialogue Manager objects in your game. They then implement dialogue through structs turned into scriptable objects, which store all the data in a safe way to prevent potential data loss for the game designers.
For Safety Protocol, we only needed one dialogue manager which was shared across all the consoles in the game so they all share the same dialogue. With some light code tweaking, you could also use the same system for NPCs or other settings where you would want several separate dialogues in the game. It’s made to be customizable and user friendly!
The Code
The Safety Protocol project, especially writing this dialogue system, helped me finally develop my own style of coding. It is probably a style many other people use as well but as someone quite new to the field who has written her fair share of messy incoherent code, I was thrilled to develop my coding style through this project.
I want my code to be as easy to read and easy to follow as possible; Every functionality should have its own designated method. Even just one sentence, for example setting a variable, should in my way of coding be its own method called something like “SetVariable”. This way, if another programmer were to look at my code, they should be able to read through it easily and get a good grasp of what’s going on, without actually needing to go through most of the more “codey” lines. It does make the script deceptively lengthy but each individual method is very easy to read. (Feel free to click the gif on the right to see how it’s structured.)
This way of coding also makes adjustments and additions to the code very simple. For example, in the 5th week of our 7 week project, the game designers came to me with a request to change the way the dialogue worked when implementing the game’s endings; They wanted a more extensive ending dialogue to occur in one of the endings, while the other ending dialogue simply ended the game. They also wanted to add a new option button functionality for the new extensive ending.
At first I was a bit aghast with sudden changes so late in the project but with how my code was organized it was actually pretty easy to add this new functionality and make it work properly. I must hand it to our designers, the new ending functionality was brilliant and we had a lot of positive feedback from players experiencing that ending. (Go check it out for yourselves! Can you pick the right ending? 😉 )
With that said… there are cases where I have to choose between extra optimization and more legible code. For instance, in this project I use structs to store the dialogues, options and responses. Technically, the dialogues and responses function in the same way and could be merged into one thing. Though for legibility’s sake, I opted for keeping them separated. They do the same thing but that doesn’t necessarily mean they Are the same thing. Thus, to help keep a clear vision while coding and to follow the code’s structure easier, I kept the separation of “dialogue” and “response”.
Some might disagree with this (I could keep them separated in the main script of the code while still only using one struct in the struct script, for example) and I completely understand why! I hope my reasoning still resonates with those who would do it differently.
using System.Collections.Generic;
using TMPro;
using UnityEngine.UI;
public enum UserName
{
AI,
Survivor,
EmptyLine
}
[System.Serializable]
public struct Dialogue
{
public UserName username;
public string dialogue;
public bool triggersOption;
public bool endsCurrentConversation;
public List
options, int index)
{
if (options[index].fakeOption == true)
{
_optionButtons[index].isFake = true;
}
}
private bool IsButtonFake(OptionButton optionButton)
{
if (optionButton.isFake == true)
{
optionButton.button.interactable = false;
return true;
}
return false;
}
private void ResetButtonBools(int index)
{
_optionButtons[index].button.interactable = true;
_optionButtons[index].isFake = false;
}
private void SetupButtonClickListener()
{
for (int i = 0; i < _optionButtons.Count; i++)
{
if (_optionButtons == null)
{
Debug.LogWarning($"Please assign option buttons. Missing: OptionButton{i}");
continue;
}
int j = i; // variable capturing.
_optionButtons[i].button.onClick.AddListener(delegate { ClickedButton(j); });
}
}
private void ClickedButton(int buttonIndex)
{
_optionIndex = buttonIndex;
if (IsButtonFake(_optionButtons[_optionIndex]) == false)
{
SetCurrentOptionStruct();
DeactivateOptionPanel();
StartTypingSelection();
CheckEndingBool();
}
}
private void CheckEndingBool()
{
if (_currentOptionStruct.triggerGoodEnding == true)
{
_goodEnding = true;
InitiateEndingDialogue();
}
else if (_currentOptionStruct.triggerBadEnding == true)
{
_badEnding = true;
InitiateEndingDialogue();
}
}
private void InitiateEndingDialogue()
{
if (_goodEnding == true)
{
//reserved for code to initiate good ending dialogue if needed.
}
if (_badEnding == true)
{
_dialogueManager.SetCurrentDialogue(_dialogueManager.badEndingDialogue.dialogues);
EndingHandlerConsole.OnStartGlitch(false);
EndingHandlerConsole.OnStartPoisoning();
}
}
private void InitiateGameEnding()
{
if (_goodEnding == true)
{
//good ending
EndingHandlerConsole.OnStartGoodEndingSequence();
}
else if (_badEnding == true)
{
//bad ending
EndingHandlerConsole.OnStartBadEndingSequence();
}
else
{
// when the dialogue ends in good ending scene.
EndingHandlerConsole.OnStartTriggerCreditInGoodEndingScene();
}
}
private void SetConversationInProgress(bool inProgress)
{
_dialogueManager.ConversationInProgress = inProgress;
}
private float SetMessageDelay(string sentence)
{
float _tempDelay = 0;
foreach (char letter in sentence.ToCharArray())
{
_tempDelay += _currentMessageDelayAddition;
}
return _currentMessageDelayDefault + _tempDelay;
}
private string GetUserNameWithColor(UserName username)
{
string name = "";
string hexadecimalColor = "";
if(username == UserName.AI)
{
name = _dialogueManager.AiName;
hexadecimalColor = _dialogueManager.AiNameColor;
}
if (username == UserName.Survivor)
{
name = _dialogueManager.SurvivorName;
hexadecimalColor = _dialogueManager.SurvivorNameColor;
}
return $"{name}";
}
private void GetManagers()
{
_manager = GameObject.FindGameObjectWithTag("Manager");
if (_manager == null)
{
Debug.LogWarning("Can't find a gameobject that has 'Manager' tag.");
}
_dialogueManager = _manager.GetComponentInChildren();
if (_dialogueManager == null)
{
Debug.LogWarning("Can't find DialogueManager.");
}
}
private void GetConsoleUI()
{
_consoleUI = _manager.GetComponentInChildren();
if (_consoleUI == null)
{
Debug.LogWarning("Can't find ConsoleUI.");
}
else
{
_mainDialogueText = _consoleUI.MainDialogueText;
}
}
IEnumerator InitiateConversation()
{
PrintFullDialogue();
if (_dialogueManager.conversationPaused == false)
{
SetConversationInProgress(true);
yield return new WaitForSeconds(_currentOptionDisplayDelay);
PrintDialogue("--------- New Message ---------");
StartCoroutine(HandleNextMainDialogue());
StartCoroutine(BlinkingCursor());
}
}
IEnumerator TypeSelection()
{
_consoleUI.OptionOutputText.text = "";
_sentence = _optionButtons[_optionIndex].text.text;
_isTypingText = true;
foreach (char letter in _sentence.ToCharArray())
{
_consoleUI.OptionOutputText.text += letter;
yield return new WaitForSeconds(_currentTypingDelay);
}
_isTypingText = false;
StopTypingSound();
_consoleUI.OptionOutputText.text = "";
PrintDialogue("> " + _sentence);
AddToFullDialogue("> " + _sentence);
StartCoroutine(HandleResponse());
}
IEnumerator HandleResponse()
{
yield return new WaitForSeconds(SetMessageDelay(_sentence));
for (int i = 0; i < _currentOptionStruct.responses.Count; i++)
{
SetConversationInProgress(true);
SetResponseOutput(i);
PrintDialogue(_outputText);
AddToFullDialogue(_outputText);
if (_currentOptionStruct.responses[i].triggersOption == true)
{
StoreCurrentResponseStruct(i);
yield return new WaitForSeconds(_currentOptionDisplayDelay);
DisplayOptions(_currentResponseStruct);
yield break;
}
if (_currentOptionStruct.responses[i].endsCurrentConversation)
{
DialogueManager.OnPause();
SetConversationInProgress(false);
yield break;
}
yield return new WaitForSeconds(SetMessageDelay(_outputText));
}
// after all the responses are printed
StartCoroutine(HandleNextMainDialogue());
}
IEnumerator HandleNextMainDialogue()
{
while (true)
{
_dialogueManager.MainIndex++;
if (_dialogueManager.currentDialogue.Count == _dialogueManager.MainIndex)
{
InitiateGameEnding();
}
if (_dialogueManager.currentDialogue.Count > _dialogueManager.MainIndex)
{
StoreCurrentMainDialogue();
SetMainDialogueOutput();
PrintDialogue(_outputText);
AddToFullDialogue(_outputText);
if (_currentMainDialogue.triggersOption == true)
{
yield return new WaitForSeconds(_currentOptionDisplayDelay);
DisplayOptions(_currentMainDialogue);
yield break;
}
if (_currentMainDialogue.endsCurrentConversation)
{
DialogueManager.OnPause();
SetConversationInProgress(false);
yield break;
}
yield return new WaitForSeconds(SetMessageDelay(_outputText));
}
else
{
yield break;
}
}
}
IEnumerator BlinkingCursor()
{
bool show = false;
while (_dialogueManager.ConversationInProgress)
{
if (!_isTypingText)
{
show = !show;
if (show)
{
_consoleUI.OptionOutputText.text = "|";
}
else
{
_consoleUI.OptionOutputText.text = "";
}
}
yield return new WaitForSeconds(.5f);
}
_consoleUI.OptionOutputText.text = "";
}
}
The Future Expansion
For this game it wasn’t really needed but there is one functionality I would love to expand this system with in the future, namely to connect past options with future dialogues and events. For example, store the option selected by the player somewhere and then revisit it at a different point in time, further into the game. Tying together game events in this way would make for an even more immersive and dynamic player experience. This is definitely something I want to explore in the future.