首页 > 代码库 > 【Unity技巧】自定义消息框(弹出框)

【Unity技巧】自定义消息框(弹出框)


写在前面



这一篇我个人认为还是很常用的,一开始也是实习的时候学到的,所以我觉得实习真的是一个快速学习工程技巧的途径。

提醒:这篇教程比较复杂,如果你不熟悉NGUI、iTween、C#的回调函数机制,那么这篇文章可能对你比较有难度,当然你可以挑战自我。

言归正传,消息框,也就是Message Box,在Windows下很常见,如下图:


在游戏里,我们也会用到这样的消息框。例如用户按了返回按钮,一般都会弹出一个确认退出的按钮。用户在执行某些重要操作时,我们总是希望再一次确认以防用户的无意操作,以此来提高用户体验。这篇教程就会详细叙述Unity中消息框的一种实现。(由于见识有限,我相信肯定还有其他的方式,可能更简单易用,如有不足欢迎指出。)




准备工作



插件和工具



这篇教程还是有点复杂的,需要各个方面的力量帮助我们:
  • 既然是界面,那么就离不开NGUI。确保你的项目里有NGUI插件,以及必要的图集来制作消息框的背景(为了省事你可以直接只用NGUI例子中的Atlas)。如果你需要中文显示,那么还需要用NGUI制作一个中文字体。网络上有很多教程。
  • 单例脚本。这里需要单例模式主要是为了实现那种无需在面板中(指的是除消息框以外的其他对象)引用任何资源,这样你看起来就像是在VS2010下调用MessageBox.Show一样。
  • iTween插件。这个插件是免费的,而且非常小巧。使用它主要是为了美化消息框的弹出效果,例如放大弹出、从上向下弹出等等。本例使用的是从上向下弹出。

准备好了这些工具后,我们需要制作本地化文字,这是为了定义消息框中的按钮文字以及弹出时显示的标题和内容文字,当然你可以略去这一步,但是这是非常不建议的,因为在真正的项目中管理好所有的文本是很重要的。本地化是NGUI的功能,经常被用于转换多种语言,例如中文、英文等。你可以在NGUI的例子中找到对应的场景和教程(Example 10 - Localization),涉及到的脚本主要是Localization.cs(统一管理所有的可用语言)、UILocalize.cs(指明一个UILabel显示的文字)。这里假设你知道这些脚本是干嘛的。


本地化文本


要实现一个本地化文本非常容易。只需要两步:
  1. 创建一个纯文本文件,例如cn.txt,里面将定义所有用到的字符串。
    Confirm = 确定
    Cancel = 取消
    
    QuitConfirmTitle = 退出确认
    QuitConfirmContent = 继续将退出游戏。\n确定退出?
    

    对于我们的弹出框,只用到上述四个文本。等号左边的名字相当于这个字符串的ID,等号右边是内容。
  2. 制作一个Localization Prefab。这点和NGUI例子很相似,就是为了方便以后修改。对于我们的教程,如果你不制作成一个Prefab也是可以的,但是还是不建议,还是那句话,这种思维还是很重要的。制作好的prefab如下:

    细心的你可能发现除了上述提到的脚本,还有一个脚本:DontDestroyOnLoad.cs。代码如下:
    using UnityEngine;
    using System.Collections;
    
    public class DontDestroyOnLoad : MonoBehaviour {
    
    	// Use this for initialization
    	void Start () {
    		DontDestroyOnLoad(this.gameObject);
    	}
    }
    

    它的作用显而易见,就是为了不让我们的本地化文本在场景切换时被销毁。这样就不用每一个场景都实例化一个Localization Prefab,而只需要在游戏的第一个场景中包含一个Localization Prefab即可。
  3. 新建一个场景,并把之前的Localization Prefab拖进去。


实现



呼呼,下面的内容比较复杂,希望你能耐心看下去。


测试功能:检测退出按钮




我们首先写一个测试脚本,它的功能就是检测用户是否按下了退出按钮,否则就会尝试调用我们的消息框(当然这里还没有定义,我只是想从最高层向底层一层一层讲解)。
下面是KeyDetecter.cs:
using UnityEngine;
using System.Collections;

public class KeyDetecter : MonoBehaviour {

	private CommonUIManager m_CommonUIManager = null;

	void Start() {
		m_CommonUIManager = Singleton.getInstance("CommonUIManager") as CommonUIManager;
	}

	// Update is called once per frame
	void Update () {
		if (Input.GetKey(KeyCode.Escape) && m_CommonUIManager != null) {		
			m_CommonUIManager.ShowMessageBox(
				Localization.instance.Get("QuitConfirmTitle"), 
				Localization.instance.Get("QuitConfirmContent"),
				MessageBox.Style.OKAndCancel,
				OnReceiveQuitConfirmResult);
		}
		
	}
	
	void OnReceiveQuitConfirmResult(MessageBox.Result result) {
		if (result == MessageBox.Result.OK) {
			Application.Quit();
		}
	}
}


上述代码很短,最重要的部分是ShowMessageBox部分。由于我们还没有实现CommonUIManager,这里你可以理解ShowMessageBox就是弹出一个消息框,它的标题是Localization.instance.Get("QuitConfirmTitle")(根据cn.txt我们知道对应的文本是“退出确认”),内容是Localization.instance.Get("QuitConfirmContent")(根据cn.txt我们知道对应文本是“继续将退出游戏。\n确定退出?”),并且它的类型是包含OK和Cancel两个按钮的标准弹出框,用户点击后的回调函数是OnReceiveQuitConfirmResult

OnReceiveQuitConfirmResult函数根据用户选择结果来判断是否真正退出游戏。



界面



最麻烦的部分来了。我们使用NGUI制作界面。用NGUI创建一个全新的UI,并重新命名。按照类似下图的组织方式创建其他界面元素:



从最上面说起。
CommonUIRoot,即之前的UI Root,的位置很重要,由于弹出框界面引入了一个新的Camera,因此为了防止它的视野范围和其他场景中已有的Camera重合,应尽量把它的位置调整到一个完全空白的位置。本例设置的位置是(0,2000,-2000)。



Camera、Anchor和Panel。确保Anchor的Side设置成Center。如果你想要自适应多种大小的屏幕,那么就需要调整Camera的Size,并向Panel添加UIStretch脚本,具体过程请Google。


Window和WindowRoot。这两个主要是为了设置弹出动画而设置的,它们原本都是空对象,而后添加了一些脚本或者动画。
Window设置如下,可以看到它天界了iTween的一个脚本,主要用户从上向下弹出的移动效果。注意,它的位置和iTween脚本中From和To参数的设置有很大关系。

这里的WindowRoot就是一个空对象,但是如果需要自定义的动画时就需要可以再通过一些技巧设置WindowRoot。这里就不讲了。

后面的元素用红色的文字和方框注释过了。其中需要解释的就是LockCollider。它的作用就是通过一个BoxCollider挡住后面所有可点击的UI,使得用户只能点击消息框上的按钮。所以它的Z坐标比其他元素更靠后,而且大小应该超过屏幕的大小。



最后,还需要设置三个消息框按钮,包括它们的文字和回调函数。
OKAndCancelButtonGroup下一共包含两个按钮。首先为CancelButton添加回调函数,这是通过把NGUI的UIButtonMessage脚本添加到CancelButton对象上实现的(即调用CommonUIRoot上脚本的OnCancel函数,当然这里我们还没有实现这个脚本):

我们希望取消按钮上的文字为“取消”,因此向CancelButton对象下面的Label添加如下脚本(通过cn.txt我们知道Cancel对应的文本是“取消”):


同理,设置另外两个按钮。OKAndCancelButtonGroup的ConfirmButton和OnlyOKButtonGroup的ConfirmButton设置相同,它们的回调函数名为OnConfirm,UILocalize脚本的Key设置为Confirm。


最后!!!真的是最后了。。。我们需要用脚本管理上面这些界面元素。建立一个新的脚本CommonUIDetail.cs:
using UnityEngine;
using System.Collections;

public class CommonUIDetail : MonoBehaviour {
	
	public TweenPosition messageBoxTween;
	public UILabel messageBoxTitle;
	public UILabel messageBoxContent;
	public GameObject[] buttonGroups = new GameObject[(int)MessageBox.Style.eNumCount];
	
	public System.Action messageBoxConfirmCallback = null;
	public System.Action messageBoxCancelCallback = null;
	
	void OnConfirm() {
		if (messageBoxConfirmCallback != null) {
			messageBoxConfirmCallback();
		}
	}
	
	void OnCancel() {
		if (messageBoxCancelCallback != null) {
			messageBoxCancelCallback();
		}
	}
}

并把该脚本添加到CommonUIRoot上,并给面板上的各个变量赋值:



调整所有UI的位置,使它们看起来像一个弹出框。

完成后,把整个CommonUIRoot及其所有子对象制作成一个Prefab,并且放在Assets/Resources文件夹下,这样才能通过Resources.Load来动态加载它。



单例脚本



还记得之前测试脚本里未实现的CommonUIManager吗?现在我们就来实现最关键的ShowMessageBox代码。CommonUIManager.cs如下:
using UnityEngine;
using System.Collections;

public class MessageBox {
	public delegate void OnReceiveMessageBoxResult(MessageBox.Result result);
	
	public enum Style {
		OnlyOK,
		OKAndCancel,
		eNumCount
	}
	
	public enum Result {
		OK,
		Cancel,
		eNumCount
	}
}

public class CommonUIManager : MonoBehaviour {
	
	public GameObject commonUIPrefab = null;
	
	public GameObject root;
	public TweenPosition messageBoxTween;
	public UILabel messageBoxTitle;
	public UILabel messageBoxContent;
	public GameObject[] buttonGroups = new GameObject[(int)MessageBox.Style.eNumCount];
	
	private MessageBox.OnReceiveMessageBoxResult messageBoxCallback = null;
	
	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
	
	}
	
	public void ShowMessageBox(string title, string content, MessageBox.Style style, 
		MessageBox.OnReceiveMessageBoxResult callback) {
		if (root == null) {
			commonUIPrefab = Resources.Load("CommonUIRoot") as GameObject;
			root = GameObject.Instantiate(commonUIPrefab) as GameObject;
			root.transform.parent = this.transform;
			
			CommonUIDetail uiDetail = root.GetComponent<CommonUIDetail>();
			messageBoxTween = uiDetail.messageBoxTween;
			messageBoxTitle = uiDetail.messageBoxTitle;
			messageBoxContent = uiDetail.messageBoxContent;
			buttonGroups[(int)MessageBox.Style.OnlyOK] = uiDetail.buttonGroups[(int)MessageBox.Style.OnlyOK];
			buttonGroups[(int)MessageBox.Style.OKAndCancel] = uiDetail.buttonGroups[(int)MessageBox.Style.OKAndCancel];
			
			uiDetail.messageBoxConfirmCallback = OnConfirm;
			uiDetail.messageBoxCancelCallback = OnCancel;
		}
		
		messageBoxTitle.text = title;
		messageBoxContent.text = content;
		messageBoxCallback = callback;
		
		switch ((int)style) {
		case (int)MessageBox.Style.OnlyOK:
			buttonGroups[(int)MessageBox.Style.OnlyOK].SetActive(true);
			buttonGroups[(int)MessageBox.Style.OKAndCancel].SetActive(false);
			break;
		case (int)MessageBox.Style.OKAndCancel:
			buttonGroups[(int)MessageBox.Style.OnlyOK].SetActive(false);
			buttonGroups[(int)MessageBox.Style.OKAndCancel].SetActive(true);
			break;
		}
		
		messageBoxTween.Play(true);
	}
	
	void OnConfirm() {
		if (messageBoxCallback != null) {
			messageBoxCallback(MessageBox.Result.OK);
			messageBoxCallback = null;
		}
		
		messageBoxTween.Play(false);
	}
	
	void OnCancel() {
		if (messageBoxCallback != null) {
			messageBoxCallback(MessageBox.Result.Cancel);
			messageBoxCallback = null;
		}
		
		messageBoxTween.Play(false);
	}
}


我只能帮你到这里了,这代码不长,相信如果你肯花时间一定可以看懂。

把之前的KeyDetector.cs代码添加到场景中的MainCamera上,运行,点击键盘上的ESC按键,应该就会弹出你的消息框了。

至此,整个过程结束。




效果



保存所有代码,此时不应该再有任何报错,如果有的话,自己找找原因吧……

下面是我制作的游戏中实现的弹出框:





结束语



啊哦,这是最近写的最复杂的一篇教程了,里面有些部分肯定说的不是非常清楚,如果实在遇到无法理解的错误(有明显编辑错误提示的,请调动您珍贵的大脑,看一下代码,去自己解决),乐意解答。


祝好运!