UI框架的设计是任何游戏都要做的事情,其中事件管理器(EventManager)是比较常用的UI与逻辑分离的方法,通过注册、绑定、分发事件来控制UI界面或者游戏场景的逻辑处理。之前做cocos游戏写过c++版本、lua版本的事件管理器,Unity大同小异,但是也有很多特殊的地方,这边我记录下设计过程。
特别提醒,如果习惯使用当前比较成熟的Unity MVC、SingleIoc、UIFrame等UI框架的同学就不要探究这篇笔记了,所有的UI框架都是为了让项目开发高内聚,低耦合,维护和迭代效率高为目的的,习惯用一个就可以了,如果有跟我一样非得自己写框架用的才爽的同学可以来了解下,可能里面的想法对您有用。
首先我的UI事件管理系统必须满足以下需求:
1、UI与逻辑分离
这是最基本需求,我不希望以后游戏更换UI要到处修改逻辑,我想要的是一个UI界面负责展示,一个UIManager来定义一些UI变化的逻辑,UI的变化由事件驱动,所以一个UI界面+一个UIManager+一个事件分发器就可以了,以后换UI只要改UI界面就可以了,这对比较复杂的项目非常重要,千万不要UI和逻辑揉到一起,尤其是Unity这种组件化的开发引擎,有时喜欢偷个懒,比如一个按钮Button点击触发界面A的变化,然后就把A拖拽到Button所在对象中作为组件,然后调用A的方法,这种做法一定不能有,后期维护成本会很高。
UI与逻辑分离的完美设计模式可能就是MVC了,但是要严格按照MVC设计模式做游戏我感觉不是很合适,游戏的模型太灵活多变,版本变动也很大,如果有MVC严格设计游戏UI,可能简单问题复杂化了(之前确实有同事用MVC做游戏,代码量巨大,看着头疼脑热),总之,我不选择MVC,但是上面我说的UI+UiManager+EventManager基本上也满足了UI与逻辑的分离,够用就可以了。
2、所有按钮的回调入口要统一
一个按钮写一个脚本肯定不是正常人的思路,怎么让按钮的回调入口统一呢,我写了一个TriggerEventManager模块作为EventManager的附属模块,就是一个分发事件的脚本,所有按钮只要是用到事件系统的(有些按钮可能很简单,功能是点一下隐去一个界面,直接在Inspector面板的onClik中把这个界面的Alpha改为0就好了,不需要事件系统)全部调用这个脚本接口即可,从此不需要到处找Button脚本了。
public class _TriggerEventManager : MonoBehaviour {
private _EventManager _eventManager;
void Start(){
_eventManager = GetComponent<_EventManager>() as _EventManager;
}
///
/// 无参事件
///
/// Event key.
public void TriggerEventFromNoParam(string eventKey){
_eventManager.__TriggerEvent(eventKey);
}
3、对某一个事件可以多个目标响应(类似于观察者模式)
界面A和界面B可能在我按下Button的时候都有相应,那我的EventManager中对事件的要求就是“一对多”的关系,一个事件会注册N个回调代理函数
///
/// 定义代理方法,接受层,接到消息后的回调函数
/// 回调函数的参数,可为空
///
public delegate void EventHandle(object __PARAM = null);
///
/// 定义事件字典,记录所有分发中的事件
/// 支持一个事件多个目标响应
///
public Dictionary> __EVENTDIC;
4、支持UGUI系统的onClick Inspector界面动态绑定,同时也支持代码直接调用
Unity是高度组件化的游戏编程引擎,无论我们习惯用NGUI、UGUI甚至GUI做界面,按钮的回调设计方法有很多种,直接在Inspector中绑定onClick脚本、使用sendMessage、EventLisener等等,我的想法是我的UI管理模块既要可以拖拽绑定onClick,也要可以代码直接调用,这个地方开始是比较纠结,EventManager做成单例?无法拖拽到onClick上面了,而且事件管理器我不希望一个游戏工程只有一个(原因是如果所有的界面事件都要注册到一个地方,不方便多人开发,每个人都要动这个文 件,没办法做版本控制),做成MonoBehivour挂载到场景中,还得先find它再调用它的接口,麻烦一些也耗性能。总之权衡了一下,还是选择了后者,心中还是会默念"后面如果能不继承MonoBehavior就不继承它,保证最后一次"。
综上,我做了一个UIEventSystem预设体
将UIEventSystem拖入场景负责当前场景的所有UI事件管理,也可以拖动多个,多人开发各自定义事件,也不容易冲突掉。
下面就是怎么注册和事件消息和分发消息了,比如我注册一个"startgame"的消息
随便点击一个按钮,触发开始游戏
整个注册和分发流程结束,下面我们来响应这个事件,上面说过我习惯每个UI界面写一个UIManager,用来控制UI界面或者跟UI界面相关的逻辑,那么UiManager同样也是事件的观察者(它只负责观察和响应,不能分发事件,这个很重要,各司其职才能让你的事件系统不会乱成一张网)
[MonoHeader("装备的基础界面,控制界面UI的变化")]
public class EquipBasePanelManager : MonoBehaviour {
private _EventManager _eventManager;
// Use this for initialization
void Start () {
_eventManager = GameObject.Find("UIEventSystem").GetComponent<_EventManager>() as _EventManager;
//绑定事件
_eventManager.__AttachEvent("startgame", startGameEventHandle);
}
///
/// 开始游戏
///
/// O.
public void startGameEventHandle(object o){
SceneManager.LoadScene("running");
}
}
上面是无参回调方法,后面的代码也支持有参回调,奉上所有代码
//
// _EventManager.cs
// EndlessRunner
//
// Created by jiabl on 09/27/2017.
//
//
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MiniJSONV;
///
/// 事件基类管理器
/// 所有的事件(尤其是UI交互事件都必须用管理器写事件,防止后期游戏复杂度增加维护混乱)
/// 目的:所有的界面与逻辑分离
/// 基类负责所有的事件分发处理逻辑
///
[MonoHeader("事件管理器,在当前场景生命周期有效,和_ButtonEventManager脚本一起构成由按键触发的事件控制器 的预设体")]
public class _EventManager : MonoBehaviour {
///
/// 消息列表,每一个事件对应一个枚举类型的消息,枚举类型设计为泛型,方便每个场景都有单独的事件消息层,防止多人开发冲突
///
[Header("消息类型列表,请定义不能重复的消息key")]
public string[] __EVENTMSG;
///
/// 定义代理方法,接受层,接到消息后的回调函数
/// 保留第一个消息参数,为了更方便的注册到统一入口函数中,第二个参数是回调函数的参数,可为空
///
public delegate void EventHandle(object __PARAM = null);
///
/// 定义事件字典,记录所有分发中的事件
/// 支持一个事件多个目标响应
///
public Dictionary> __EVENTDIC;
void Awake(){
__EVENTDIC = new Dictionary>();
//将事件放入字典中管理
__AddEvent();
}
///
/// 注册事件
///
private void __AddEvent(){
if(0 == __EVENTMSG.Length)
Debug.LogWarning("_EVENTMSG is empty,please define add msg key as a array!!");
foreach(string __MSG in __EVENTMSG){
__AddDelegate(__MSG);
}
}
///
/// 将事件放入字典中管理
///
/// Event key.
private void __AddDelegate(string __MSG){
if(__EVENTDIC.ContainsKey(__MSG)){
}else{
__EVENTDIC.Add(__MSG,new List());
}
}
///
/// 触发一个事件
///
/// Event key.
/// Parameter.
public void __TriggerEvent(string __MSG,object __param = null){
if(__EVENTDIC.ContainsKey(__MSG)){
foreach(EventHandle __handle in __EVENTDIC[__MSG]){
__handle(__param);
}
}else{
Debug.LogError(__MSG + " is undefine from function: _EventBaseManager::_TriigerEvent");
}
}
///
/// 事件绑定
/// 事件的接收层要调用这个方法,当然也可以使用下面的批量绑定方法,不需要每一个事件写一个函数
///
public void __AttachEvent(string __MSG, EventHandle __eventHandle){
if (__EVENTDIC.ContainsKey(__MSG)){
__EVENTDIC[__MSG].Add(__eventHandle);
}
else{
Debug.LogError(__MSG + " is undefine from function: _EventBaseManager::_AttachEvent");
}
}
///
/// 批量导入,所有的事件处理函数入口是唯一的
///
/// Event handle.
public void __BatchAttachEvent(EventHandle __eventHandle){
foreach(string __MSG in __EVENTMSG){
__AttachEvent(__MSG,__eventHandle);
}
}
///
/// 去除事件绑定
///
public void __DetachEvent(string __MSG, EventHandle __eventHandle){
if (__EVENTDIC.ContainsKey(__MSG)){
__EVENTDIC[__MSG].Remove(__eventHandle);
}
else{
Debug.LogError(__MSG + " is undefine from function: _EventBaseManager::_DetachEvent");
}
}
///
/// 销毁管理器,当用代码(单例模式)控制事件分发的时候,结束使用请销毁(因为它不会自动销毁)
///
public void __Destroy(){
MonoBehaviour.Destroy(this.gameObject);
}
}
//
// _ButtonEvent.cs
// EndlessRunner
//
// Created by jiabl on 09/28/2017.
//
//
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MiniJSONV;
[MonoHeader("分发事件管理器,可以挂在到Button的on Click中使用,也可以直接调用里面的事件分发方法"+
"示例\n void OnClick(){\n \ttriggerEventManager.TriggerEventFromNoParam(\"startgame\")\n }\n void OnClick(){\n \ttriggerEventManager.TriggerEventFromJsonParam(\"{\\\"key\\\":\\\"startgame\\\",param:\\\"1\\\"}\")\n }")]
public class _TriggerEventManager : MonoBehaviour {
private _EventManager _eventManager;
void Start(){
_eventManager = GetComponent<_EventManager>() as _EventManager;
}
///
/// 通过传递json参数,发送带有参数的事件
/// 例如:button的onClik方法参数设置:{"key":"sttttt", "param":"d" }
///
public void TriggerEventFromJsonParam(string paramsJson){
//判断是否json格式
if(GameTools.getInstance().isJson(paramsJson)){
Dictionary response = (Dictionary)Json.Deserialize(paramsJson);
if (response.ContainsKey("key")){
if(response.ContainsKey("param")){
_eventManager.__TriggerEvent((string)response["key"],(string)response["param"]);
}else{
_eventManager.__TriggerEvent((string)response["key"]);
}
}else{
Debug.Assert(true,"TriigerEventFromJsonParam Func param error: no 'key' inside the json => " + paramsJson);
}
}else{
Debug.Assert(true,"AllButtonEvent Func: TriigerEvent param is not a json string => " + paramsJson);
}
}
///
/// 无参事件
///
/// Event key.
public void TriggerEventFromNoParam(string eventKey){
_eventManager.__TriggerEvent(eventKey);
}
}