Unity3d UGUI 通用Confirm确认对话框实现 Inventory Pro学习总结
背景
在 Winform 开发中,使用 MessageBox 对话框十分便捷,甚至有人封装了可选择各种图标和带隐藏详情的 MessageBox。然而在 Unity3d UGui 中,所有 UI 都需自行实现。不过,借助各种插件,Inventory Pro 中的对话框方案提供了一种通用且可复用的解决方案。
个人想法
若要自行实现通用对话框,需解决以下几个方面的问题:窗体显示控制、窗体 UI 布局、窗体文字显示、窗体事件回调、窗体显示动画控制、窗体显示声音控制以及窗体与其他窗体的关系。虽然这些功能看似简单,但涉及的知识面广泛,自行开发并非易事,因此不建议重复造轮子。
插件实现的效果
简单的确认对话框提示
当执行扔物品操作时,会弹出确认对话框,提示用户是否确认该操作。
稍复杂的购买物品对话框
在购买物品时,会显示包含购买物品、物品数量及金额的对话框。
简单确认对话框的使用
- 设计对话框:使用 UGUI 设计自定义对话框,包含基本元素,如标题(Title)、描述(description)和两个按钮(two buttons)。
- 添加拖拽功能:为对话框绑定 Draggable Window(Script),使其具备拖拽能力。
- 定义动画效果:添加 Animator 组件,为对话框显示设置动画效果。
- 实现打开关闭及音效动画:添加 UI Windows(script),实现对话框的打开、关闭功能,同时支持声音和动画效果。
- 设置对话框属性:添加 Confirmation Dialog(script),为对话框赋予事件回调、模态对话框属性以及文字绑定等固有属性。
通过以上步骤,简单的对话框即可完成。这充分展示了绑定技术、组件技术、UI 解耦和框架的强大功能。
复杂对话框的使用
Item Int Val Dialog(script) 是 ConfirmDialog 类的子类,了解这一点后,复杂对话框的使用就变得容易理解,此处不再详细阐述。
分析
确定功能需求后,实现这些功能可能需要运用一些设计模式和经验。下面通过类图进行详细分析。
类图分析
根据前文的脑图和类图,我们逐个分析相关类。InventoryUIDialogBase 是一个抽象类,也是与 UI 绑定的主体,该类没有无用的属性。需要重点关注的字段和属性包括:UIWindow 类是通用的窗口显示和动画控制组件,InventoryMessage 是字符串 Message 的封装类。
具体功能实现分析
1. 窗体 UI 布局
UI 布局通过 Unity3d UGUI 的拖拽方式进行设计,这种方式简单易行,实现了 UI 与逻辑的分离。
2. 窗体文字显示
窗体文字显示通过后台与 UI 进行绑定,利用 Unity3d 的组件设计时绑定技术(类似于 WPF 中 MVVM 的绑定)。关键在于文字信息,实际上 Dialog 类并不关心具体显示的字符串内容,而是使用 Inventory Pro 提供的 Message 类进行封装。这样做的目的是为了实现国际化以及文字性扩展,如颜色和字体显示方案。
InventoryLangDataBase 类对所有消息体文字进行集中处理,并且本身是一个 Asset,具有集中管理和支持国际化文字的优点。由于 Unity3d UGUI 支持文字颜色和字体的格式化操作,因此可以扩展添加带有颜色和字体大小的文字重载。
3. 窗体显示控制、动画控制和声音控制
- 显示控制:利用 Unity3d 平台的组件化功能,通过 UIWindow 专门进行控制。UIWindow 类必须加载 Animator 动画类。
- 动画控制:主体 DialogBase 在设计时绑定动画效果,由 UIWindow 类在控制显示和关闭时播放动画,同时使用了协程。
- 声音控制:通过全局类静态方法 InventoryUIUtility.AudioPlayOneShot 播放声音。
4. 窗体与其他窗体的关系
此功能类似于网页中的遮罩或 Winform 里的模态(ModelDialog)对话框,由于没有现成的实现,需要自行编写代码。主要通过 CanvasGroup 插件控制 UGUI 的事件处理。
5. 窗体事件回调
窗体事件回调由 Dialog 子类处理,在重载的 ShowDialog 方法中添加委托的事件回调函数,并通过代码绑定(使用 onClick.AddListener,而非 UI 手动可视化绑定)按钮事件。这种通过代码定义显示委托的方式具有很强的灵活性,相比匿名委托、泛型委托(Action 或 Func)和 Lambda 表达式,代码可读性更高。
其他问题
对话框触发显示的实现
对话框的触发显示机制是一个疑问。实际上,Inevntory Pro 有一个全局 setting 类,需要进行一些配置,其中包括将窗体元素与 SettingManger 脚本进行绑定,SettingManger 是一个单例全局类。
显示对话框的代码
显示对话框的代码中,ShowDialog 方法的两个按钮事件回调函数使用 Lambda 表达式,非常直观。
总结与思考
框架的优势
优秀的框架使开发变得简单,易于扩展。通过上述分析和示例可以看出,很容易扩展出类似 Confirm 对话框的功能,并且遵循对修改封闭、对新增开放的原则。
小功能蕴含的知识
一个看似普通的小功能,实际上涵盖了 Unity3d 的许多知识。不断重复和学习这些功能,有助于将 Unity3d 技术牢记于心。
UI 系统的通用性
所有 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>
// 此处原文似乎未完整,保持原样
public abstract class InventoryUIDialogBase : MonoBehaviour
{
// 可根据实际情况补充更多内容
}
}