曾幾什麼時候,在Winform中,使用MessageBox對話框是如此happy,後來還有人封裝了能夠選擇各類圖標和帶隱藏詳情的MessageBox,如今Unity3d UGui就沒有了這樣的好事情了,全部的UI都須要本身來搞定了,幸虧還有各類插件,Inventory Pro中的對話框方案不失一種通用,可複用的方案。html
所謂通用對話框,若是是本身實現的話有如下幾點須要解決,窗體顯示控制,窗體UI佈局,窗體文字顯示,窗體事件回調,窗體顯示動畫控制,窗體顯示聲音控制,窗體與其餘窗體的關係,功能雖然小涉及的方面和知識卻很多,本身作真的很不容易,因此別再本身造輪子了。c++
簡單的確認對話框提示canvas
當扔物品的時候會提示是否確認對話框。網絡
稍微複雜一些的購買物品對話框app
當購買物品時會顯示出一個購買的物品,物品數量金額的對話框框架
簡單確認對話框的使用ide
一、使用UGUI來設計一個本身使用的對話框,基本幾個元素Title,description ,two buttons;函數
二、給對話框綁定Draggable Window(Script)使其具備拖拽功能組件化
三、添加Animator,定義對話框顯示的時候具備動畫效果佈局
四、添加UI Windows(script)使其具備打開關閉,聲音,動畫的效果
五、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表達式特別顯眼
分析總結完畢後有一些想法
一、好的框架使開發變得的easy,擴展很方便,經過以上的分析和例子看的出來很容易就能擴展出來一些簡單的相似Confirm對話框,並且是對修改封閉,對新增開放的;
二、一個司空見慣的小功能,若是作好了徹底能夠覆蓋到Unity3d的許多知識,剩下的只是不斷進行這樣的重複,重建你的神經網絡便可,總有一天Unity3d的技術就這樣印在你的大腦之中;
三、若是你真的看懂了本文,分析一下其實全部的UI系統都是相通的只是API和使用的技術不一樣而已,只是有些API封裝的死,有些封裝的鬆散一些。換句話說若是你本身在某種UI體系中完成一種本身的實現,換到另外一個UI體系同樣能夠實現的;
四、微軟體系如Winform過渡的封裝是不是好事情?有些時候是好事情,有些時候就未必。根據手上的資源合理的選擇技術纔是根本;
五、關於使用輪子和造輪子的糾結,這也是一組矛盾,不造輪子就不能深入的體會技術,造輪子須要大量的時間可造出來未必有已經造好的輪子設計的好,你會選擇哪種呢?
本文首發於蠻牛,次發於博客園,特此說明
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); } } }