背景
曾幾何時,在Winform中,使用MessageBox對話框是如此happy,后來還有人封裝了可以選擇各種圖標和帶隱藏詳情的MessageBox,現在Unity3d UGui就沒有了這樣的好事情了,所有的UI都需要自己來搞定了,幸好還有各種插件,Inventory Pro中的對話框方案不失一種通用,可復用的方案。
YY(自己的想法)
所謂通用對話框,如果是自己實現的話有以下幾點需要解決,窗體顯示控制,窗體UI布局,窗體文字顯示,窗體事件回調,窗體顯示動畫控制,窗體顯示聲音控制,窗體與其他窗體的關系,功能雖然小涉及的方面和知識卻不少,自己做真的很不容易,所以別再自己造輪子了。
插件實現的效果
簡單的確認對話框提示
當扔物品的時候會提示是否確認對話框。
稍微復雜一些的購買物品對話框
當購買物品時會顯示出一個購買的物品,物品數量金額的對話框
簡單確認對話框的使用
1、使用UGUI來設計一個自己使用的對話框,基本幾個元素Title,description ,two buttons;
2、給對話框綁定Draggable Window(Script)使其具有拖拽功能
3、添加Animator,定義對話框顯示的時候具有動畫效果
4、添加UI Windows(script)使其具有打開關閉,聲音,動畫的效果
5、Confirmation Dialog(script)使其具有事件回調,model對話框的屬性,文字綁定等對話框固有的屬性
至此簡單的對話框就做好了,這里我們充分見識了綁定技術、組件技術、UI解耦和框架的強大威力
復雜對話框的使用
這里只要知道Item Int Val Dialog(scirpt)其實是ConfirmDialog類的一個子類,剩下的東西就很自然了,這里不詳細展開了。
分析
功能需求確定了,如何實現這些功能可能就需要用到一些模式,以及一些經驗了,先看一下類圖
根據前一節的腦圖,類圖我們逐個分析,InventoryUIDialogBase 是一個抽象類,也是與UI進行綁定的主體,其沒有一個無用的屬性,這里重點關注幾個字段和屬性,UIWindow類是通用的窗口顯示和動畫控制組件,InventoryMessage是字符串Message的封裝類。
1)窗體UI布局
UI布局是通過Unity3d UGUI拖拽的方式設計上去的,這個很簡單,首先做到了UI分離
2)窗體文字顯示
窗體文字的顯示首先是通過后台與UI做的綁定,這里使用Unity3d的組件設計時綁定技術(這里做過WPF的同學有是否有印象MVVM中的綁定),這里關鍵是文字信息,實際發現其實Dialog類並不關心顯示的什么string,而是Inventory Pro提供的(類圖中的Message類)一層封裝后得到的結果,這里為什么要單獨拿出來實際是為了做國際化以及一些文字性的擴展,比如顏色,字體顯示的方案。
InventoryLangDataBase類對於所有的消息體文字進行了集中處理,而且本身也是Asset,這里有兩種好處一種就是可以集中管理,一種就是為國際化文字。
因為Unity3d UGUI可以做文字顏色和字體的格式化操作,這里完全可以擴展添加有顏色和字體大小的文字重載
3)窗體顯示控制,窗體顯示動畫控制,窗體顯示聲音控制
窗體顯示的控制,完全利用Unity3d平台的組件化功能,通過UIWindow專門拿出來控制,這里看到UIWinow類是必須加載Animator動畫類的
窗體的動畫控制,由主體DialogBase進行設計時的動畫效果綁定,由UIWindow類在控制顯示和關閉時進行動畫的Play,這里還用到了協程
窗體顯示聲音控制,由全局類靜態方法 InventoryUIUtility.AudioPlayOneShot 來播放即可
3)窗體與其他窗體的關系
這個功能類似於網頁中的遮罩或者winform里的模態(ModelDialog)對話框,這里沒有現成的東西可以使用只能自己寫了,這里如何關閉UGUI的事件處理主要是通過CanvasGroup這個插件來控制
4) 窗體事件回調
窗體中的事件回調交給了Dialog子類來處理,具體是在重載的ShowDialog方法中添加了委托的事件回調函數,然后通過代碼綁定的方式(這里是onClick.AddListener,而不是UI手動可視化綁定)進行了按鈕事件的綁定,這里有很大的靈活性。我比較喜歡這種通過代碼定義顯示委托的方式,來完成事件的回調(c++系可能叫做函數指針),同比匿名委托,泛型委托(Action或者Func),Lambda表達式,代碼可讀性更強
其它
這里留了一個小疑問,對話框的觸發顯示是如何實現的,我們的(MessageBox.Show)在哪里呢?
看過前面的文章的同學應該知道,Inevntory Pro有一個全局setting類,需要進行一些配置,其中就需要窗體元素與SettingManger腳本進行綁定,而SettingManger是一個單列全局類
最后是如何顯示對話框的代碼了,看到ShowDialog方法了嗎,兩個按鈕的事件回調函數 Lambda表達式特別顯眼
寫在最后
分析總結完畢后有一些想法
1、好的框架使開發變得的easy,擴展很方便,通過以上的分析和例子看的出來很容易就能擴展出來一些簡單的類似Confirm對話框,而且是對修改封閉,對新增開放的;
2、一個司空見慣的小功能,如果做好了完全可以覆蓋到Unity3d的許多知識,剩下的只是不斷進行這樣的重復,重建你的神經網絡即可,總有一天Unity3d的技術就這樣印在你的大腦之中;
3、如果你真的看懂了本文,分析一下其實所有的UI系統都是相通的只是API和使用的技術不同而已,只是有些API封裝的死,有些封裝的松散一些。換句話說如果你自己在某種UI體系中完成一種自己的實現,換到另一個UI體系一樣可以實現的;
4、微軟體系如Winform過渡的封裝是否是好事情?有些時候是好事情,有些時候就未必。根據手上的資源合理的選擇技術才是根本;
5、關於使用輪子和造輪子的糾結,這也是一組矛盾,不造輪子就不能深刻的體會技術,造輪子需要大量的時間可造出來未必有已經造好的輪子設計的好,你會選擇哪一種呢?
本文首發於蠻牛,次發於博客園,特此說明
核心代碼
UIWindow
using System; using UnityEngine; using System.Collections; using UnityEngine.EventSystems; using System.Collections.Generic; namespace Devdog.InventorySystem { /// <summary> /// Any window that you want to hide or show through key combination or a helper (UIShowWindow for example) /// </summary> [RequireComponent(typeof(Animator))] [AddComponentMenu("InventorySystem/UI Helpers/UIWindow")] public partial class UIWindow : MonoBehaviour { public delegate void WindowShow(); public delegate void WindowHide(); #region Variables /// <summary> /// Should the window be hidden when the game starts? /// </summary> [Header("Behavior")] public bool hideOnStart = true; /// <summary> /// Keys to toggle this window /// </summary> public KeyCode[] keyCombination; /// <summary> /// The animation played when showing the window, if null the item will be shown without animation. /// </summary> [Header("Audio & Visuals")] public AnimationClip showAnimation; /// <summary> /// The animation played when hiding the window, if null the item will be hidden without animation. /// </summary> public AnimationClip hideAnimation; public AudioClip showAudioClip; public AudioClip hideAudioClip; /// <summary> /// The animator in case the user wants to play an animation. /// </summary> public Animator animator { get; set; } protected RectTransform rectTransform { get; set; } [NonSerialized] private bool _isVisible = false; /// <summary> /// Is the window visible or not? Used for toggling. /// </summary> public bool isVisible { get { return _isVisible; } protected set { _isVisible = value; } } private IEnumerator showCoroutine; private IEnumerator hideCoroutine; /// <summary> /// All the pages of this window /// </summary> [HideInInspector] private List<UIWindowPage> pages = new List<UIWindowPage>(); public UIWindowPage defaultPage { get; private set; } #endregion #region Events /// <summary> /// Event is fired when the window is hidden. /// </summary> public event WindowHide OnHide; /// <summary> /// Event is fired when the window becomes visible. /// </summary> public event WindowShow OnShow; #endregion public void AddPage(UIWindowPage page) { pages.Add(page); if (page.isDefaultPage) defaultPage = page; } public void RemovePage(UIWindowPage page) { pages.Remove(page); } public virtual void Awake() { animator = GetComponent<Animator>(); if (animator == null) animator = gameObject.AddComponent<Animator>(); rectTransform = GetComponent<RectTransform>(); if (hideOnStart) HideFirst(); else { isVisible = true; } } public virtual void Update() { if (keyCombination.Length == 0) return; bool allDown = true; foreach (var key in keyCombination) { if (Input.GetKeyDown(key) == false) { allDown = false; } } if (allDown) Toggle(); } #region Usefull UI reflection functions /// <summary> /// One of our children pages has been shown /// </summary> public void NotifyPageShown(UIWindowPage page) { foreach (var item in pages) { if (item.isVisible && item != page) item.Hide(); } } protected virtual void SetChildrenActive(bool active) { foreach (Transform t in transform) { t.gameObject.SetActive(active); } var img = gameObject.GetComponent<UnityEngine.UI.Image>(); if(img != null) img.enabled = active; } public virtual void Toggle() { if (isVisible) Hide(); else Show(); } public virtual void Show() { if (isVisible) return; isVisible = true; animator.enabled = true; SetChildrenActive(true); if (showAnimation != null) { animator.Play(showAnimation.name); if (showCoroutine != null) { StopCoroutine(showCoroutine); } showCoroutine = _Show(showAnimation); StartCoroutine(showCoroutine); } // Show pages foreach (var page in pages) { if (page.isDefaultPage) page.Show(); else if (page.isVisible) page.Hide(); } if (showAudioClip != null) InventoryUIUtility.AudioPlayOneShot(showAudioClip); if (OnShow != null) OnShow(); } public virtual void HideFirst() { isVisible = false; animator.enabled = false; SetChildrenActive(false); rectTransform.anchoredPosition = Vector2.zero; } public virtual void Hide() { if (isVisible == false) return; isVisible = false; if (OnHide != null) OnHide(); if (hideAudioClip != null) InventoryUIUtility.AudioPlayOneShot(hideAudioClip); if (hideAnimation != null) { animator.enabled = true; animator.Play(hideAnimation.name); if (hideCoroutine != null) { StopCoroutine(hideCoroutine); } hideCoroutine = _Hide(hideAnimation); StartCoroutine(hideCoroutine); } else { animator.enabled = false; SetChildrenActive(false); } } /// <summary> /// Hides object after animation is completed. /// </summary> /// <param name="animation"></param> /// <returns></returns> protected virtual IEnumerator _Hide(AnimationClip animation) { yield return new WaitForSeconds(animation.length + 0.1f); // Maybe it got visible in the time we played the animation? if (isVisible == false) { SetChildrenActive(false); animator.enabled = false; } } /// <summary> /// Hides object after animation is completed. /// </summary> /// <param name="animation"></param> /// <returns></returns> protected virtual IEnumerator _Show(AnimationClip animation) { yield return new WaitForSeconds(animation.length + 0.1f); if (isVisible) animator.enabled = false; } #endregion } }
InventoryUIDialogBase
using UnityEngine; using System.Collections; using Devdog.InventorySystem.Dialogs; using UnityEngine.UI; namespace Devdog.InventorySystem.Dialogs { public delegate void InventoryUIDialogCallback(InventoryUIDialogBase dialog); /// <summary> /// The abstract base class used to create all dialogs. If you want to create your own dialog, extend from this class. /// </summary> [RequireComponent(typeof(Animator))] [RequireComponent(typeof(UIWindow))] public abstract partial class InventoryUIDialogBase : MonoBehaviour { [Header("UI")] public Text titleText; public Text descriptionText; public UnityEngine.UI.Button yesButton; public UnityEngine.UI.Button noButton; /// <summary> /// The item that should be selected by default when the dialog opens. /// </summary> [Header("Behavior")] public Selectable selectOnOpenDialog; /// <summary> /// Disables the items defined in InventorySettingsManager.disabledWhileDialogActive if set to true. /// </summary> public bool disableElementsWhileActive = true; protected CanvasGroup canvasGroup { get; set; } protected Animator animator { get; set; } public UIWindow window { get; protected set; } public virtual void Awake() { canvasGroup = GetComponent<CanvasGroup>(); if (canvasGroup == null) canvasGroup = gameObject.AddComponent<CanvasGroup>(); animator = GetComponent<Animator>(); window = GetComponent<UIWindow>(); window.OnShow += () => { SetEnabledWhileActive(false); // Disable other UI elements if (selectOnOpenDialog != null) selectOnOpenDialog.Select(); }; window.OnHide += () => { SetEnabledWhileActive(true); // Enable other UI elements }; } public void Toggle() { window.Toggle(); if(window.isVisible) SetEnabledWhileActive(false); // Disable other UI elements else SetEnabledWhileActive(true); // Enable other UI elements } /// <summary> /// Disables elements of the UI when a dialog is active. Useful to block user actions while presented with a dialog. /// </summary> /// <param name="enabled">Should the items be disabled?</param> protected virtual void SetEnabledWhileActive(bool enabled) { if (disableElementsWhileActive == false) return; foreach (var item in InventorySettingsManager.instance.disabledWhileDialogActive) { var group = item.gameObject.GetComponent<CanvasGroup>(); if (group == null) group = item.gameObject.AddComponent<CanvasGroup>(); group.blocksRaycasts = enabled; group.interactable = enabled; } } } }
ConfirmationDialog
using UnityEngine; using System.Collections; using UnityEngine.UI; namespace Devdog.InventorySystem.Dialogs { public partial class ConfirmationDialog : InventoryUIDialogBase { /// <summary> /// Show this dialog. /// <b>Don't forget to call dialog.Hide(); when you want to hide it, this is not done auto. just in case you want to animate it instead of hide it.</b> /// </summary> /// <param name="title">Title of the dialog.</param> /// <param name="description">The description of the dialog.</param> /// <param name="yes">The name of the yes button.</param> /// <param name="no">The name of the no button.</param> /// <param name="yesCallback"></param> /// <param name="noCallback"></param> public virtual void ShowDialog(string title, string description, string yes, string no, InventoryUIDialogCallback yesCallback, InventoryUIDialogCallback noCallback) { SetEnabledWhileActive(false); window.Show(); // Have to show it first, otherwise we can't use the elements, as they're disabled. titleText.text = title; descriptionText.text = description; yesButton.GetComponentInChildren<Text>().text = yes; noButton.GetComponentInChildren<Text>().text = no; yesButton.onClick.RemoveAllListeners(); yesButton.onClick.AddListener(() => { if (window.isVisible == false) return; SetEnabledWhileActive(true); yesCallback(this); window.Hide(); }); noButton.onClick.RemoveAllListeners(); noButton.onClick.AddListener(() => { if (window.isVisible == false) return; SetEnabledWhileActive(true); noCallback(this); window.Hide(); }); } /// <summary> /// Show the dialog. /// <b>Don't forget to call dialog.Hide(); when you want to hide it, this is not done auto. just in case you want to animate it instead of hide it.</b> /// </summary> /// <param name="title">The title of the dialog. Note that {0} is the item ID and {1} is the item name.</param> /// <param name="description">The description of the dialog. Note that {0} is the item ID and {1} is the item name.</param> /// <param name="yes">The name of the yes button.</param> /// <param name="no">The name of the no button.</param> /// <param name="item"> /// You can add an item, if you're confirming something for that item. This allows you to use {0} for the title and {1} for the description inside the title and description variables of the dialog. /// An example: /// /// ShowDialog("Are you sure you want to drop {0}?", "{0} sure seems valuable..", ...etc..); /// This will show the item name at location {0} and the description at location {1}. /// </param> /// <param name="yesCallback"></param> /// <param name="noCallback"></param> public virtual void ShowDialog(string title, string description, string yes, string no, InventoryItemBase item, InventoryUIDialogCallback yesCallback, InventoryUIDialogCallback noCallback) { ShowDialog(string.Format(string.Format(title, item.name, item.description)), string.Format(description, item.name, item.description), yes, no, yesCallback, noCallback); } } }