Search code examples
unity-game-enginescene

Simplest way to carry dialogue between scenes without use of Player Prefs, etc


I have been working on a dialogue system for my game and I was wondering if anyone knows how to keep the system between different scenes. I know you can use things such as Player Prefs but for one, I do not understand it and upon research, people do not generally recommend it for storing large complicated things. I managed to get close to doing so by using dontDestroy just as you would with a character, however, it did not work completely as the button to switch to the next line of text, of course, broke along with the singleton I created for my system. What would be the best way for me to go about this?

Here is all of my code just in case it is needed:

Making the scriptable object:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "New Dialogue", menuName = "Dialogues")]
public class Dialogue : ScriptableObject
{
    [System.Serializable]
    public class Info
    {
        public string myName;
        public Sprite portrait;
        [TextArea(4, 8)]
        public string mytext;
    }
    [Header("Insert Dialogue Info Below")]
    public Info[] dialogueInfoSection;

}

Main code for system (sigleton breaks here while switching scenes):

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

public class MainDialogueManager : MonoBehaviour
{
    public static MainDialogueManager instance;

    private void Awake()
    {
        if(instance != null)
        {
            Debug.LogWarning("FIX THIS" + gameObject.name);
        }
        else
        {
            instance = this;
        }
    }

    public GameObject DialogueBoX;

    public Text dialogueNameofChar;
    public Text characterSays;
    public Image characterPortrait;
    private float textDelay = 0.005f;

    public Queue<Dialogue.Info> dialogueInfoSection = new Queue<Dialogue.Info>();

    public void EnqueueDialogue(Dialogue db)
    {
        DialogueBoX.SetActive(true);
        dialogueInfoSection.Clear();

        foreach(Dialogue.Info info in db.dialogueInfoSection)
        {
            dialogueInfoSection.Enqueue(info);
        }

        DequeueDialogue();
    }

    public void DequeueDialogue()
    {
        if (dialogueInfoSection.Count==0)
        {
            ReachedEndOfDialogue();
            return; /////
        }
        Dialogue.Info info = dialogueInfoSection.Dequeue();

        dialogueNameofChar.text = info.myName;
        characterSays.text = info.mytext;
        characterPortrait.sprite = info.portrait;

        StartCoroutine(TypeText(info));
    }

    IEnumerator TypeText(Dialogue.Info info)
    {
        characterSays.text= "";
        foreach(char c in info.mytext.ToCharArray())
        {
            yield return new WaitForSeconds(textDelay);
            characterSays.text += c;
            yield return null;
        }
    }

    public void ReachedEndOfDialogue()
    {
        DialogueBoX.SetActive(false);
    }

}

Dialogue Activation:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MainDialogueActivation : MonoBehaviour
{
    public Dialogue dialogue;

    public void startActivationofDialogue()
    {
        MainDialogueManager.instance.EnqueueDialogue(dialogue);
    }
    private void Start()
    {
        startActivationofDialogue();
    }
}

Go to next dialogue line:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MainDialogueButtons : MonoBehaviour
{
   public void GoToNextDialogueLine()
    {
        MainDialogueManager.instance.DequeueDialogue();
    }
}

Solution

  • How about something like this?

    The idea is pretty similar to what you're doing, with a few tweaks:

    • I'm storing the active dialog in a scriptable object (DialogueSystem) so that it can persist between scenes. Each time I load a new scene, I check if there's an active dialog, and if I so I show the dialog popup in Start().
    • Whereas you remove the dialog section that you're currently showing to the player from the current dialog, I don't remove the current section until the player clicks to the next section. That's necessary because you may need to re-show the same section if you move to a new scene.

    Make sure to create an instance of the DialogueSystem scriptable object and assign it to MainDialogueActivation and MainDialogManager

    MainDialogActiviation has some testing code in it so you can hit a key to start a new dialog or switch between scenes.

    MainDialogueActiviation.cs

    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class MainDialogueActivation : MonoBehaviour
    {
        public Dialogue dialogue;
    
        // This scriptable object stores the active dialog so that you
        // can persist it between scenes
        public DialogueSystem dialogSystem;
    
        private void Start()
        {
            // If we had an active dialog from the previous scene, resume that dialog
            if (dialogSystem?.dialogInfoSections.Count > 0)
            {
                GetComponent<MainDialogueManager>().ShowDialog();
            }
        }
    
        private void Update()
        {
            // Pressing D queues and shows a new dialog
            if (Input.GetKeyDown(KeyCode.D))
            {
                GetComponent<MainDialogueManager>().EnqueueDialogue(this.dialogue);
            }
    
            // Pressing C ends the current dialog
            if (Input.GetKeyDown(KeyCode.C))
            {
                this.dialogSystem.dialogInfoSections.Clear();
                GetComponent<MainDialogueManager>().ReachedEndOfDialogue();
            }
    
            // Pressing S swaps between two scenes so you can see the dialog
            // persisting
            if (Input.GetKeyDown(KeyCode.S))
            {
                if (SceneManager.GetActiveScene().name == "Scene 1")
                {
                    SceneManager.LoadScene("Scene 2");
                }
                else if (SceneManager.GetActiveScene().name == "Scene 2")
                {
                    SceneManager.LoadScene("Scene 1");
                }
            }
        }
    }
    

    MainDialogueManager.cs

    using System.Collections;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class MainDialogueManager : MonoBehaviour
    {
        // This scriptable object stores the active dialog
        public DialogueSystem dialogSystem;
    
        public GameObject DialogueBox;
    
        public Text dialogueNameofChar;
        public Text characterSays;
        public Image characterPortrait;
        private float textDelay = 0.005f;
    
        // The game object for the dialog box that is instantiated in this
        // scene
        private GameObject dialogBoxGameObject;
    
        /// <summary>
        ///     Shows the dialog window for the dialog that is in this object's
        ///     dialogSystem property. 
        /// </summary>
        public void ShowDialog()
        {
            // Instantiate the dialog box prefab
            this.dialogBoxGameObject = Instantiate(this.DialogueBox);
    
            // I'd recommend putting a script on your "dialog box" prefab to
            // handle this stuff, so that this script doesn't need to get a
            // reference to each text element within the dialog prefab.  But
            // this is just a quick and dirty example for this answer
            this.dialogueNameofChar = GameObject.Find("Character Name").GetComponent<Text>();
            this.characterSays = GameObject.Find("Character Text").GetComponent<Text>();
            this.characterPortrait = GameObject.Find("Character Image").GetComponent<Image>();
    
            // If you have multiple response options, you'd wire them up here.
            // Again; I recommend putting this into a script on your dialog box
            GameObject.Find("Response Button 1").GetComponent<Button>().onClick.AddListener(ShowNextDialogSection);
            GameObject.Find("Response Button 2").GetComponent<Button>().onClick.AddListener(ShowNextDialogSection);
    
            ShowDialogSection(this.dialogSystem.dialogInfoSections.Peek());
        }
    
        /// <summary>
        ///     Puts a dialog into this object's dialogSystem property and
        ///     opens a dialog window that will show that dialog.
        /// </summary>
        public void EnqueueDialogue(Dialogue db)
        {
            foreach (Dialogue.Info info in db.dialogueInfoSection)
            {
                this.dialogSystem.dialogInfoSections.Enqueue(info);
            }
            ShowDialog();
        }
    
        /// <summary>
        ///     Removes the dialog section at the head of the dialog queue, 
        ///     and shows the following dialog statement to the player.  This
        ///     is a difference in the overall logic, because now the dialog
        ///     section at the head of the queue is the dialog that's currently
        ///     being show, rather than the previous one that was shown
        /// </summary>
        public void ShowNextDialogSection()
        {
            this.dialogSystem.dialogInfoSections.Dequeue();
            if (this.dialogSystem.dialogInfoSections.Count == 0)
            {
                ReachedEndOfDialogue();
                return;
            }
    
            Dialogue.Info dialogSection = this.dialogSystem.dialogInfoSections.Peek();
            ShowDialogSection(dialogSection);
        }
    
        /// <summary>
        ///     Shows the specified dialog statement to the player.
        /// </summary>
        public void ShowDialogSection(Dialogue.Info dialogSection)
        {
            dialogueNameofChar.text = dialogSection.myName;
            characterSays.text = dialogSection.mytext;
            characterPortrait.sprite = dialogSection.portrait;
    
            StartCoroutine(TypeText(dialogSection));
        }
    
        IEnumerator TypeText(Dialogue.Info info)
        {
            characterSays.text = "";
            foreach (char c in info.mytext.ToCharArray())
            {
                yield return new WaitForSeconds(textDelay);
                characterSays.text += c;
                yield return null;
            }
        }
    
        public void ReachedEndOfDialogue()
        {
            // Destroy the dialog box
            Destroy(this.dialogBoxGameObject);
        }
    
    }    
    

    DialogSystem.cs

    using System.Collections.Generic;
    using UnityEngine;
    
    [CreateAssetMenu(menuName = "Dialogues/Dialog System")]
    public class DialogueSystem : ScriptableObject
    {
        public Queue<Dialogue.Info> dialogInfoSections = new Queue<Dialogue.Info>();
    }
    

    Here's what my dialog box prefab looks like

    enter image description here

    Every scene needs an object (presumably a prefab to make it easy to add to every scene) that has MainDialogActiviation and MainDialogManager on it. Mine looks like this:

    enter image description here