在不使用播放器首选项等的情况下进行场景之间对话的最简单方法

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

我一直在为我的游戏开发对话系统,我想知道是否有人知道如何在不同场景之间保持该系统。我知道你可以使用诸如 Player Prefs 之类的东西,但一方面,我不明白它,并且根据研究,人们通常不推荐它来存储大型复杂的东西。我设法通过使用 dontDestroy 来接近这样做,就像使用角色一样,但是,它不能完全工作,因为切换到下一行文本的按钮当然与我为我创建的单例一起中断了系统。对我来说最好的方法是什么?

这是我的所有代码,以备不时之需:

制作脚本对象:

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;

}

系统主要代码(单例切换场景中断):

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);
    }

}

对话激活:

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();
    }
}

转到下一行对话:

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

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

这样的怎么样?

这个想法与您正在做的非常相似,只是做了一些调整:

  • 我将活动对话框存储在可编写脚本的对象 (DialogueSystem) 中,以便它可以在场景之间持续存在。每次加载新场景时,我都会检查是否有活动对话框,如果有,我会在 Start().
  • 中显示对话框弹出窗口
  • 虽然您从当前对话框中删除了当前向玩家显示的对话部分,但我不会删除当前部分,直到玩家点击下一部分。这是必要的,因为如果您移动到新场景,您可能需要重新显示相同的部分。

确保创建 DialogueSystem 脚本对象的实例并将其分配给 MainDialogueActivationMainDialogManager

MainDialogActiviation 中有一些测试代码,因此您可以按一个键来启动新对话或在场景之间切换。

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>();
}

这是我的对话框预制件的样子

每个场景都需要一个具有 MainDialogActiviationMainDialogManager 的对象(大概是一个预制件,以便于添加到每个场景)。我的看起来像这样:

这可能有点不受欢迎,但使用 Singleton 是可以的。只是MonoBehaviour单例比较棘手,可以使用Object.DontDestroyOnLoad(instance)。但是事情变得很难看,因为当场景改变时它不会被破坏(好)但是如果你回到场景它会加载另一个(坏)。有几种方法可以解决这个问题,比如如果已经有一个实例或有一个子场景,让对象自行销毁。

我建议不要使用 MonoBehaviour 单例并使用 ScriptableObject 单例。您可以通过将资产放在资源文件夹中并像这样使用 Resource.Load 来延迟实例化。

public class ScriptableSingleton<T> : ScriptableObject where T : ScriptableSingleton<T> {

    private static string ResourcePath {
        get {
            return typeof(T).Name;
        }
    }

    public static T Instance {
        get {
            if (instance == null) {
                instance = Resources.Load(ResourcePath) as T;
            }
            return instance;
        }
    }

    private static T instance;
}

使用这段代码,您可以创建一个单例 class 比如说 DialogueManager,您可以为其创建一个 DialogueManager.asset 并将其放入 "Resources" 文件夹中。