关于MonoBehaviour的单例通用规则

长久以来,对于基于MonoBehaviour的单例总是心有梗结,总觉得用得很忐忑,今天,终于有时间思考和总结了下,理清了想通了。代码和注释如下:

其中GameLogic是我们自己的控制游戏生命周期的管理类,当游戏进行中,GameLogic.AddComponent总是成功的添加组件到一个标记为DontDestroyOnLoad的对象上,这样保证单例的生命期。

当游戏结束后,开始数据清理时,如果调用 GameLogic.AddComponent就会返回null,这样保证在游戏结束的清理工作中已销毁置空的单例不会因为误引用而【死而复生】

class UICanvasManager : MonoBehaviour
    {
        /*** 基于MonoBehaviour的单例规则,保证单例的唯一性和安全性
         * 1,不要手动在编辑器中挂给对象,防止挂重
         * 2,使用如下方式通过GameLogic生成单例,生命周期易控制
         * 3,不要在AWake或其它任何地方给_instance赋值,保证单例唯一,不变。考虑以下【危险案例】
         */

        /*** 【危险案例】
         * 先通过UICanvasManager.Instance获取了一个指向对象A的单例,进行一些操作后,A中有了一些数据,
         * 后面不小心通过AddComponent添加了一个UICanvasManager组件,此时单例指向了对象B
         * 这时候有一些游戏数据在对象A中,有一些数据在对象B中,后果是B中有些你以为已经初始化了的数据却为空
         */

        protected static UICanvasManager _instance;
        public static UICanvasManager Instance
        {
            get
            {
                if(_instance == null)
                {
                    _instance = GameLogic.AddComponent<UICanvasManager>();
                }
                return _instance;
            }//get
        }//Instance
}

 

using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [System.Serializable] public class DialogueOption { public string text; // 选项文本 public string[] response; // 选项对应的回应对话 public UnityEvent action; // 选择选项时触发的事件 } public class DialogueSystem : MonoBehaviour { /// <summary> /// 获取对话系统,方便在其他脚本中调用。 /// </summary> private static DialogueSystem _instance; public static DialogueSystem Instance { get { if (_instance == null) { // 从Resources文件夹加载对话系统预制体 GameObject obj = Resources.Load<GameObject>("DialogueSystem/DialogueCanvas"); _instance = Instantiate(obj).GetComponent<DialogueSystem>(); DontDestroyOnLoad(_instance.gameObject); } return _instance; } } /* --------------------------------------------------- 脚本生命周期 --------------------------------------------------- */ /// <summary> /// 脚本开始时调用 /// </summary> void Start() { // 注册继续按钮的点击事件 continueButton.onClick.AddListener(ShowNextMessage); foreach (var btn in optionButtons) { btn.gameObject.SetActive(false); } optionsPanel.SetActive(false); } /// <summary> /// 脚本销毁时调用 /// </summary> void OnDestroy() { // 注销继续按钮的点击事件 continueButton.onClick.RemoveListener(ShowNextMessage); } /* ---------------------------------------------------- 对话框组件 --------------------------------------------------- */ /// <summary> /// npc名称文字组件。 /// </summary> public Text nameText; /// <summary> /// 对话内容文字组件。 /// </summary> public Text dialogueText; /// <summary> /// 继续对话提示文字组件。(可根据对话内容进行改变。如当对话不是最后一句时,显示“继续”,是最后一句时显示“完成”。) /// </summary> public Text continueText; /// <summary> /// 继续按钮组件 /// 用于点击继续对话或结束对话。 /// </summary> public Button continueButton; /// <summary> /// 立绘图像组件。 /// </summary> /// 选项按钮组 // 当前显示的选项 public Image avatarImage; public Button[] optionButtons; // 预先配置好的选项按钮 public GameObject optionsPanel; // 选项面板 private List<DialogueOption> currentOptions = new List<DialogueOption>(); /* ---------------------------------------------------- 对话框属性 --------------------------------------------------- */ /// <summary> /// 对话内容数组 /// 用于存储对话的多条消息。 /// </summary> private string[] messages; /// <summary> /// 当前对话消息索引 /// 用于跟踪当前显示的对话消息。 /// </summary> private int currentMessageIndex = 0; /// <summary> /// 用于对话结束时的回调 /// </summary> private Action onDialogueComplete; /// <summary> /// 显示对话框 /// </summary> /// <param name="name">npc名称</param> /// <param name="messages">对话内容数组</param> /// <param name="avatarPath">立绘位置</param> /// <summary> /// 显示带选项的对话 /// </summary> public void ShowDialogue(string name, string[] msgs, string avatarPath, List<DialogueOption> options = null) { // 设置基本对话信息 nameText.text = name; avatarImage.sprite = Resources.Load<Sprite>(avatarPath); messages = msgs; currentMessageIndex = 0; // 存储选项供对话结束后使用 currentOptions = options; // 显示第一条消息 ShowNextMessage(); } /// <summary> /// 隐藏对话框 /// </summary> public void HideDialogue() { gameObject.SetActive(false); // 重置对话内容和索引 messages = null; currentMessageIndex = 0; } /// <summary> /// 显示下一条对话信息 /// </summary> private void ShowNextMessage() { // 隐藏所有选项按钮 HideAllOptions(); if (currentMessageIndex < messages.Length) { dialogueText.text = messages[currentMessageIndex]; continueText.text = (currentMessageIndex < messages.Length - 1) ? "继续" : "继续"; currentMessageIndex++; } else { // 对话结束,显示选项 ShowOptions(); } } private void ShowOptions() { if (currentOptions != null && currentOptions.Count > 0) { dialogueText.text = "你现在要干什么呢"; continueButton.gameObject.SetActive(false); optionsPanel.SetActive(true); // 激活并设置选项按钮 int optionCount = Mathf.Min(currentOptions.Count, optionButtons.Length); for (int i = 0; i < optionCount; i++) { // 关键修复:创建局部变量捕获当前选项 int index = i; // 创建局部变量 DialogueOption option = currentOptions[index]; // 获取当前选项 optionButtons[i].gameObject.SetActive(true); optionButtons[i].GetComponentInChildren<Text>().text = option.text; optionButtons[i].onClick.RemoveAllListeners(); optionButtons[i].onClick.AddListener(() => OnOptionSelected(option)); } } else { // 没有选项时直接结束对话 HideDialogue(); } } /// <summary> /// 隐藏所有选项按钮 /// </summary> private void HideAllOptions() { optionsPanel.SetActive(false); foreach (var btn in optionButtons) { btn.gameObject.SetActive(false); } } /// <summary> /// 选项选择处理 /// </summary> private void OnOptionSelected(DialogueOption option) { // 触发选项事件 option.action?.Invoke(); if (option.response != null && option.response.Length > 0) { // 显示选项的回应对话 ShowDialogue(nameText.text, option.response, avatarImage.sprite.name); } else { // 没有后续对话则结束 HideDialogue(); } } } using System.Collections.Generic; using UnityEngine; public class OpenDialogueExample : MonoBehaviour { /// <summary> /// 在脚本开始时打开对话框并显示内容。 /// </summary> // 将显示内容传入并打开对话框 public List<DialogueOption> options; // 在Inspector中配置选项 void Start() { // 初始对话 string[] initialDialogue = { "你好,我是星之卡比", "有什么需要帮忙的吗?" }; // 显示带选项的对话 DialogueSystem.Instance.ShowDialogue( "星之卡比", initialDialogue, "Avatars/Kirby", options ); } /// <summary> /// 对话结束时的回调函数。 /// 这里可以添加对话结束后的逻辑处理,如触发事件或更新UI等。 /// </summary> } 我为你提供两个代码,我希望让OpenDialogueExample基本为string[] initialDialogue = { "你好,我是星之卡比", "有什么需要帮忙的吗?" };为这个文本格式,然后DialogueSystem来调用OpenDialogueExample中的文本,希望减少代码量
最新发布
08-15
在Unity游戏引擎中,正确实现和使用Monobehaviour模式(也称为“静态化”或“Singleton”)是为了在整个游戏中只创建并管理一个实,这对于资源管理、全局访问等场景非常有用。以下是实现步骤: 1. **设计基础类**: 创建一个继承自`MonoBehaviour`的类,如`MySingleton`,并在其中声明一个静态变量来存储唯一的实。 ```csharp public class MySingleton : MonoBehaviour { private static MySingleton _instance; public static MySingleton Instance { get { return _instance; } private set { _instance = value; } } // 其他成员函数... } ``` 2. **延迟初始化**: 为了避免在脚本加载时就创建实,应在需要的地方获取。通常在`Awake()`或`OnEnable()`方法中检查实是否已存在,如果不存在则创建。 ```csharp void Awake() { if (_instance == null) { if (FindObjectOfType<MySingleton>() != null) Destroy(gameObject); else Instance = this; } else { if (this != _instance) Destroy(gameObject); // 如果有其他实,销毁当前实 } } ``` 3. **避免公共属性**: 为了防止意外创建多个实,应尽量减少`Instance`字段的公开性,使其只能通过`GetInstance()`静态方法获取。 4. **使用工厂方法获取**: 你可以封装这个过程,提供一个友好的静态获取方法,比如: ```csharp public static MySingleton GetInstance() { if (_instance == null) Awake(); // 确保初始化 return _instance; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值