Simple Dialogue Script Development

Hi there guys, just posting here to share a dialogue script I’ve been working on. It’s fairly simple and uses just this one script and an additional serialized class in an array to prove the data for each line of dialogue.

I was wondering if anybody would be willing to offer any feedback on the code, ways to improve or possibly implement a multiple-choice answer in there.

My initial thoughts were to add a boolean to the dialogueNode script that dictates if that line of dialogue has a multiple choice. As it cycles through the dialogue nodes, if a node has a question then to set a specific number in the array as the following text box.

Current features: Change colour, Sprite, SFX, Typewriter effect for each line of text.

Desired features: Branching dialogue, Rewards

Not sure if anybody is able to offer advice on this? Feel free to use the code or to rip it to shreds, whatever you fancy :slight_smile:

My current thoughts are:

  1. Create an Array of Dialogue Arrays?
  • When you reach a dialogue branch, based on your response, the dialogue box can continue down one of two specified separate arrays of dialogue nodes.
  1. Adding a reward won’t be too hard, I can just add an Item to the dialogue node script.
    If it isn’t null then I can set the sprite to the item sprite and use some generic text for receiving the item. From there I could just use LastLine and end the dialogue or continue with subsequent dialogue nodes.

Thanks for the feedback and advice.

This is how it turned out looking in the inspector.

Any advice on the dialogue choice part would be great. Thanks :slight_smile:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections;

// My own dialogue system.
//
// Features to Add:
//
// Quest Giving / Rewards ( Need to be attached to the player inventory and Stats scriptables)
// Audio clip and animated pointer to represent moving to the next page of the dialogue.


public class DialogueSystem : Interactable
{
    [Header("Dialogue UI Elements")]
    [Tooltip("Dialogue Box Game object within the UI canvus. This is the parent to the TMPro text box and contains the BG image")]
    public GameObject dialogueBox;
    [Tooltip("TMPro text box")]
    public TextMeshProUGUI dialogueText;
    [Tooltip("Image for the character portrait sprite")]
    public Image dialoguePortrait;

    private int currentLine; // Current line progresses the script through the array.
    private int totalLines; //Total lines in determined by the dialogue lines Array.
    private bool lineComplete; // Bool to determine wether the text has finished displaying and the player can continue.
    private bool lastLine = false; //If last line is true, next click will end the dialogue and reset it.
    private string currentText = ""; //Current text is used within the coroutine for loop to create the type writer effect.


    public float delay = 0.05f; //This should be repalced with a FloatValue which change be changed to adjust in game text speed.

    [Header("Dialogue Details")]
    public List<DialogueNode> dialogueNode = new List<DialogueNode>();

    //-----------------------------------Start is called once upon creation-------------------------
    public void Start()
    {
        lineComplete = false;
        totalLines = dialogueNode.Count;
    }

    //-----------------------------------Update is called once per frame----------------------------
    void Update()
    {
        // Checks that the last line of dialogue has been reached.
        if (currentLine == totalLines - 1)
        {
            lastLine = true;
        }

        // Update function on the signpost / character / npc to check that you are in range, and the various conditions.
        // If the player goes out of range the dialogue is quit and the place in the dialogue is reset.
        if (Input.GetButtonDown("Fire1") && playerInRange)
        {
            CheckDialoguePosition();
        }
    }


    //-----------------------------------Check the position in the Dialogue--------------------------
    public void CheckDialoguePosition()
    {
        if (dialogueBox.activeInHierarchy == false && playerInRange)
        {
            dialogueBox.SetActive(true);
            ResetDialogue();
            DialogueNode thisNode = dialogueNode[currentLine];
            StartCoroutine(DisplayNextSentence(thisNode.lineOfDialogue, thisNode.dialogueSprite, thisNode.textColor, thisNode.speakerFont, delay, thisNode.speechClip));
        }

        if (lastLine == false && dialogueBox.activeInHierarchy == true && playerInRange && lineComplete)
        {
            currentLine += 1;
            DialogueNode thisNode = dialogueNode[currentLine];
            StartCoroutine(DisplayNextSentence(thisNode.lineOfDialogue, thisNode.dialogueSprite, thisNode.textColor, thisNode.speakerFont, delay, thisNode.speechClip));
        }

        if (lastLine == true && lineComplete == true && playerInRange)
        {
            EndDialogue();
        }
    }


    //-----------------------------------Change the Text and Sprite-------------------------------
    protected IEnumerator DisplayNextSentence(string _LineOfText, Sprite _Sprite, Color _textColor, TMP_FontAsset _textFont, float _delay, AudioClip _sound)
    {
        dialoguePortrait.sprite = _Sprite;
        dialogueText.font = _textFont;
        dialogueText.color = _textColor;
        dialoguePortrait.preserveAspect = true;
        lineComplete = false;

        for (int i = 0; i < _LineOfText.Length + 1; i++)
        {
            currentText = _LineOfText.Substring(0, i);
            dialogueText.GetComponent<TextMeshProUGUI>().text = currentText;
            SoundManager.instance.PlaySound(_sound);
            yield return new WaitForSeconds(_delay);

            if (i == _LineOfText.Length - 1)// Allows the player to click once the dialogue has been completed      
            {
                lineComplete = true;
                SoundManager.instance.StopSound();
                yield return new WaitUntil(() => Input.GetMouseButton(0));
            }
        }
    }

    //Closes the dialogue, resets the dialogue
    //-----------------------------------End the Dialogue-----------------------------------------
    public void EndDialogue()
    {
        ResetDialogue();
        SoundManager.instance.StopSound();
        dialogueBox.SetActive(false);
    }

    //This is used to reset the dialogue when cycling through for a second or third time.
    //-----------------------------------Reset the Dialogue---------------------------------------
    public void ResetDialogue()
    {
        currentLine = 0;
        lastLine = false;
    }

    //Context.Raise doesn't really relate to this script.
    //-----------------------------------Context Clue---------------------------------------------
    public void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player") && !other.isTrigger)
        {
            context.Raise();
            playerInRange = true;
        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        if (other.CompareTag("Player") && !other.isTrigger)
        {
            context.Raise();
            playerInRange = false;
            SoundManager.instance.StopSound();
            dialogueBox.SetActive(false);
        }
    }
}
using TMPro;
using UnityEngine;

[System.Serializable]
public class DialogueNode {
    [Header("Line of Dialogue")]
    public string lineOfDialogue;
    public Sprite dialogueSprite;
    public TMP_FontAsset speakerFont;
    public Color textColor; //Set colour of each line of dialogue text. Could be used to highlight key information or add emphasis.
    public AudioClip speechClip; //Add undertale style very short clips to be repeated for old school dialogue effect

    public bool dialogueChoice;

}

Apologies for bumping this however by the time it was approved by moderation it was at page three.

Can anybody offer any advice on implementing dialogue options or how to better go about a system such as this? Any advice on refactors would be awesome too.

All the best!

The moment a dialogue system has branching paths, the only practical way to visualize the data is a node editor. Parsing that data however, will be entirely up to you.

For my dialogue system, I went with scriptable objects as individual nodes. The nodes contain their data. Nodes have a single input(the node before), and and array of outputs(the nodes after). Starting nodes have no input. Having a single output meant the chain was just supposed to continue. Having more than one output meant it was a decision point, and options are presented to the player. Having no outputs meant it was the end of the chain.

That would be a good start to a dialogue tree IMO.

3 Likes

Thanks so much for your thoughts and input on this. In regards to a node editor, is this a UI / Unity GUI type thing similar to visual scripting or shader graph? If so, is this something I would have to build into my dialogue system or is there a general-purpose Node editor that would be suitable for a script like this?

I see how scriptable objects could be a useful way to build this. Perhaps I will make my dialogue node array into a scriptable object which is then referenced by the script. This saves building the whole thing in the inspector at least and would keep all dialogue organized.

My one concern would be the serialization of / references from the gameobject → Dialogue script to the Scriptable object as these references sometimes get lost when serializing. I’ve also been wondering if addresses can be used to create links to other game assets? This way I suppose I could create an addressable link to the dialogue scriptable, with a public string value to complete the asset path?

As you can probs tell I’m not massively experienced and havn’t yet used the addressable system

This is a node editor you can start from. I have friends who have succeeded in using this, though it requires learning its workflow. I have not personally used it, but having tried making my own, I feel building off a tool like this is easier.

With regard to referencing, sad to say I have no good answer. There are many ways of dealing with the problem, although it would depend on the situations you are creating for your game, and the game’s structure itself. I have not used addressable assets either, so I do not know if it fits the situation.

1 Like

Thanks for sending across the node editor, I will have a look into it. I can imagine it would be the best approach in the long run. Hopefully I can pick it up quite quickly :slight_smile:

You might try a jagged array for each dialogue interaction. Each main array element is the “step” in the dialogue, and the secondary element(s) are the possibilities (as strings). So if you had two interactions that were always the same (no branching), then one with two branches, it would look like

dialogue[0][0] = “Hello”; // player answer to NPC
dialogue[1][0] = “Nice to meet you”; // ditto
// now there’s a question from the NPC and a two-way branch for the player
dialogue[2][0] = “Yes, I’d like to”;
dialogue[2][1] = “No thanks”;

// now you’ll need to map the further interactions along each branch, here they are if each has another 2-branch
dialogue[3][0] = // option 1 along branch 1
dialogue[3][1] = // option 2 along branch 1
dialogue[3][2] = // option 1 along branch 2
dialogue[3][3] = // option 2 along branch 2

You’ll need to manage how the branches play out depending on the selections, and it would grow exponentially, but hopefully each interaction isn’t too complex. Mapping it on big paper or a whiteboard would make the indexing easy.

1 Like

Another thing I would look into is implementing a scriptable object, so that you can save your conversations as assets in your project folder.

2 Likes

I will try out these suggestions this evening, I’ve used scriptable objects a fair but and seems like it could be a good solution in these circumstances. I started this project last year and now I’m just refactoring all the code to make it as modular as possible with as few hard links between scripts/objects and references that can go on to disrupt other scripts when moved / removed.

In regards to the node editor, I will probably look for tutorials on Youtube to get started.

I will also try out this evening a combination of the Jagged array while using aa scriptable object to hold the dialogue details.

Will keep you guys posted, thanks again for the help! :slight_smile:

If you find creating your own too huge a hassle, see if an existing tool fits your needs. This one has gathered quite abit of attention:

If it’s more of a personal endeavor to make it yourself though, then I wish you well.

1 Like

Thank’s very much for the link :slight_smile: I shall have a look into it once I’ve finished my working hours. Any tool really to streamline the workflow / game development process is helpful. My main aim is to improve my c# programming which is why I’ve been spending a lot of time building then refactoring systems. I’m not really in any rush to push out a badly made game.

I will keep Yarn Spinner in mind however would prefer to make something myself with c# :slight_smile:

A node tool is overkill for this. You can just use GameObjects with components on them.

If you get more complicated, you can have each line display based on conditions, and each line could run some code (for quest rewards and such).

You could shift your DialogueSprite/Font onto the characters themselves, instead of being attached to the line. The line versions would just act as overrides. Then the line would have an option for Expression/Feeling (Default, Happy, Sad, Angry, Confused) and the character would just handle switching the sprite to the proper one.

Then there’s the serious/boring/making-an-actual-game part of needing to translate the lines into other languages, therefore needing to pull them from a DB…etc.

2 Likes

I like the expression/feeling option idea, very cool, could be an enum in the Inspector.
Also the DB idea for ease of populating/translating/modifying etc. So the components could just have an index or 2D index which points to the right entry.

1 Like

This is a really awesome idea, having a switch to dictate the colour and font would be super cool. I’ve seen some tutorials for text animations on youtube also when doing research into dialogue systems. Perhaps incorporating that into the enum / switch for emotion would be nice too.

I’ve been refactoring some other code this evening but nearly finished so I’m going to start playing around with these ideas.

You’ve all been super helpful so far, thanks so much!

I will post my progress here soon :slight_smile:

1 Like

I’ve yet to get a jagged array + the potential branching dialogue working however this morning I did play around a bit and put the dialogue into a scriptable object, I also created a switch really quickly and made a dropdown menu for the dialogue emotion which changed the colour and font.

Once I finish work I will get on to the multiple-choice :slight_smile:

Thanks for your help guys. Any more suggestions are always appreciated.

Some initial thoughts with the dialogue emotion enum would be to include the variable for the delay so the typewriter effect can be faster / slower. Also perhaps making a dedicated folder for the speech effects and having that automatically set by the emotion too.

6743932--777403--upload_2021-1-20_9-57-33.png

1 Like

Hey there guys!

Just another lil update with some progress. I have the code looking a bit cleaner now I think.

I implemented an enum for emotion, although no text animation as of yet.

The dialogue emotion changes Typewriter effect speed, text colour and font.

Another thing I’m going to add now is a method that automatically grabs the UI elements for the UI prefab. I will then make these hidden in the inspector so all that you need to add in the inspector is the scriptable for the dialogue sequence.

Still yet to implement multiple-choice / branching narratives which will be the next step. Gonna be challenging but good fun :slight_smile:

Any more thoughts, advice or comments on the code would be fab. Thanks very much!

6750547--778492--upload_2021-1-21_21-26-40.png

6750547--778489--upload_2021-1-21_21-26-16.png

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections;
using Sirenix.OdinInspector;

// My own dialogue system.
//
// Features to Add:
//
// Quest Giving / Rewards ( Need to be attached to the player inventory and Stats scriptables)
// Audio clip and animated pointer to represent moving to the next page of the dialogue.


public class DialogueSystemOld : Interactable
{
    [BoxGroup("Dialogue UI Elements")]
    [Tooltip("Dialogue Box Game object within the UI canvus. This is the parent to the TMPro text box and contains the BG image")]
    public GameObject dialogueBox;

    [BoxGroup("Dialogue UI Elements")]
    [Tooltip("TMPro text box")]
    public TextMeshProUGUI dialogueText;

    [BoxGroup("Dialogue UI Elements")]
    [Tooltip("Image for the character portrait sprite")]
    public Image dialoguePortrait;

    private int currentLine; // Current line progresses the script through the array.
    private string currentText = ""; //Current text is used within the coroutine for loop to create the type writer effect.

    [BoxGroup("Dialogue Scriptable")]
    public DialogueNodeScriptable dialogueNodeScriptable;


    //-----------------------------------Start is called once upon creation-------------------------
    public void Start()
    {      
        //Establishes the the last dialogue node in the list.
        int lastLine = dialogueNodeScriptable.dialogueLines.Count;
        DialogueNode lastNode = dialogueNodeScriptable.dialogueLines[lastLine - 1];
        lastNode.lastLine = true;
    }


    //-----------------------------------Update is called once per frame----------------------------
    void Update()
    {
        // Update function on the signpost / character / npc to check that you are in range, and the various conditions.
        // If the player goes out of range the dialogue is quit and the place in the dialogue is reset.
        if (Input.GetButtonDown("Fire1") && playerInRange && dialogueBox.activeInHierarchy == false)
        {
            ResetDialogue();
            CheckDialoguePosition();
        }
    }

    //--------------------------------------------------------------------------------------------------------------------------------
    //--Check the position in the Dialogue--------------------------
    public void CheckDialoguePosition()
    {
        DialogueNode thisNode = dialogueNodeScriptable.dialogueLines[currentLine];

        if (dialogueBox.activeInHierarchy == false && playerInRange)
        {
            dialogueBox.SetActive(true);
            thisNode.SetEmotion();
            currentLine += 1;
        }
        else
        {
            currentLine += 1;
            thisNode.SetEmotion();
        }
       
        StartCoroutine(DisplayNextSentence(thisNode.lineOfDialogue, thisNode.dialogueSprite, thisNode.textColor, thisNode.speakerFont, thisNode.textDelay, thisNode.speechClip, thisNode.lastLine));
    }


    //--------------------------------------------------------------------------------------------------------------------------------
    //--Display the next sentencee-------------------------------
    protected IEnumerator DisplayNextSentence(string _LineOfText, Sprite _Sprite, Color _textColor, TMP_FontAsset _textFont, float _delay, AudioClip _sound, bool _lastLine)
    {
        dialoguePortrait.sprite = _Sprite;
        dialogueText.font = _textFont;
        dialogueText.color = _textColor;
        dialoguePortrait.preserveAspect = true;

        for (int i = 0; i < _LineOfText.Length + 1; i++)
        {
            currentText = _LineOfText.Substring(0, i);
            dialogueText.GetComponent<TextMeshProUGUI>().text = currentText;
            SoundManager.instance.PlaySound(_sound);
            yield return new WaitForSeconds(_delay);

            if (i == _LineOfText.Length - 1)// Allows the player to click once the dialogue has been completed            
            {

                if (_lastLine == true)
                {
                    yield return new WaitUntil(() => Input.GetMouseButton(0));
                    EndDialogue();
                }
                else
                {
                    yield return new WaitUntil(() => Input.GetMouseButton(0));
                    CheckDialoguePosition();
                }
            }
        }
    }


    //--------------------------------------------------------------------------------------------------------------------------------
    //Closes the dialogue, resets the dialogue
    //This is used to reset the dialogue when cycling through for a second or third time.
    //--Reset the Dialogue---------------------------------------
    public void ResetDialogue()
    {
        currentLine = 0;
    }


    //--End the Dialogue-----------------------------------------
    public void EndDialogue()
    {
        ResetDialogue();
        SoundManager.instance.StopSound();
        dialogueBox.SetActive(false);
    }


    //--------------------------------------------------------------------------------------------------------------------------------
    //--On trigger enter / exit
    public void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player") && !other.isTrigger)
        {
            context.Raise();
            playerInRange = true;

        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        if (other.CompareTag("Player") && !other.isTrigger)
        {
            context.Raise();
            playerInRange = false;
            SoundManager.instance.StopSound();
            dialogueBox.SetActive(false);
        }
    }
}
using Sirenix.OdinInspector;
using TMPro;
using UnityEngine;

public enum DialogueEmotion
{
    Happy = 0,
    Sad = 1,
    Excited = 2,
    Scared = 3,
    Angry = 4,
    Neutral = 5,
}

[System.Serializable]
public class DialogueNode
{  
   
    [BoxGroup("Dialogue Node Variables")]
    [SerializeField] public DialogueEmotion dialogueEmotion;
    [BoxGroup("Dialogue Node Variables")]
    [SerializeField] public string lineOfDialogue;
    [BoxGroup("Dialogue Node Variables")]
    [SerializeField] public Sprite dialogueSprite;
    [BoxGroup("Dialogue Node Variables")]
    [SerializeField] public AudioClip speechClip; //Add undertale style very short clips to be repeated for old school dialogue effect
    [BoxGroup("Dialogue Node Variables")]
    public bool dialogueChoice;


    [HideInInspector]public float textDelay;
    [HideInInspector]public bool lastLine;
    [HideInInspector]public TMP_FontAsset speakerFont;
    [HideInInspector]public Color textColor; //Set colour of each line of dialogue text. Could be used to highlight key information or add emphasis.


    //Set the colour / fonts for the various emotions in the enum
    public void SetEmotion()
    {
        switch (dialogueEmotion)
        {
            case DialogueEmotion.Happy:
                textColor = Color.black;
                textDelay = 0.09f;
                speakerFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/rainyhearts SDF");
                break;

            case DialogueEmotion.Sad:
                textColor = Color.blue;
                textDelay = 0.1f;
                speakerFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF - Fallback");
                break;

            case DialogueEmotion.Excited:
                textDelay = 0.06f;
                textColor = Color.yellow;
                break;


            case DialogueEmotion.Angry:
                speakerFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/OpenSansLight SDF");
                textDelay = 0.07f;
                textColor = Color.red;
                break;

            case DialogueEmotion.Neutral:
                speakerFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/OpenSansLight SDF");
                textDelay = 0.08f;
                textColor = Color.black;
                break;
        }
    }
}
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Dialogue System/Dialogue Array", fileName = "New Dialogue Array")]

[System.Serializable]
public class DialogueNodeScriptable : ScriptableObject {
    public List<DialogueNode> dialogueLines;
}
1 Like