1. 从混沌到秩序:MVC架构在复杂UI系统中的工业化实践
作为一名经历过多个商业项目的开发者,我深知UI模块是最容易腐烂的代码区域。今天加个弹窗,明天改个布局,后天又来一套新手引导,三个月后打开任何一个UI脚本,里面塞满了各种业务逻辑、网络回调、动画控制,上万行代码根本不敢动。更可怕的是,不同UI之间通过FindObjectOfType互相调用,形成了一张密密麻麻的蜘蛛网。
MVC架构正是为了解决这种混乱而生的。它把UI系统拆成三个清晰的部分:Model负责数据和业务逻辑,View负责显示和动画,Controller负责协调和响应。三者各司其职,修改View不会影响Model,替换Model不会破坏View,Controller作为中间人隔离了两者的直接依赖。这套模式在Web开发领域已经被验证了几十年,在游戏UI开发中同样威力巨大。
本文将带你从零搭建一套完整的MVC UI框架,包含事件系统、窗体基类、控制类、状态类、管理类等核心组件,最后通过一个登录注册的完整案例展示它们的协同工作。这套框架已经在多个上线项目中经受过考验,支撑过百万级DAU的游戏。
2. MVC在游戏UI领域的落地思考
2.1 传统UI开发的痛点
在动手写代码之前,先看看我们想要解决的问题长什么样。一个典型的糟糕UI脚本往往长这样:
csharp复制public class LoginPanel : MonoBehaviour
{
public InputField usernameInput;
public InputField passwordInput;
public Button loginBtn;
public Button registerBtn;
public Text errorText;
private GameManager gameManager;
private NetworkManager networkManager;
private AudioManager audioManager;
private void Start()
{
// 到处找其他模块的引用
gameManager = FindObjectOfType<GameManager>();
networkManager = FindObjectOfType<NetworkManager>();
audioManager = FindObjectOfType<AudioManager>();
loginBtn.onClick.AddListener(OnLoginClick);
registerBtn.onClick.AddListener(OnRegisterClick);
}
private void OnLoginClick()
{
audioManager.PlayClick(); // UI逻辑
string username = usernameInput.text;
string password = passwordInput.text;
// 业务逻辑
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
errorText.text = "用户名密码不能为空";
return;
}
// 网络请求
StartCoroutine(LoginRequest(username, password));
}
private IEnumerator LoginRequest(string username, string password)
{
// 直接在这里写网络请求
WWWForm form = new WWWForm();
form.AddField("username", username);
form.AddField("password", password);
UnityWebRequest request = UnityWebRequest.Post("https://api.game.com/login", form);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// 处理登录成功
gameManager.OnLoginSuccess();
}
else
{
errorText.text = "网络错误";
}
}
}
这个脚本的问题一目了然:视图逻辑(按钮点击、输入框读取)、业务逻辑(数据验证)、网络逻辑(HTTP请求)全部混在一起。想换个UI框架?重写整个脚本。想改验证规则?得在一堆代码里找到那几行。想复用这个面板做其他事?没门。
2.2 MVC各层的职责边界
MVC架构通过强制分离关注点来解决上述问题:
Model(模型):负责数据和业务逻辑。它不知道也不关心数据怎么显示、用户怎么操作。典型的Model包括玩家数据、配置表、状态管理。Model变更时通过事件通知观察者,但不直接调用任何View的方法。
View(视图):负责UI的显示和输入收集。它包括面板、按钮、输入框、文本等所有可视元素。View可以持有Model的数据引用,但不应该修改Model。用户操作时,View将事件转发给Controller,不自己做业务处理。
Controller(控制器):负责协调Model和View。它监听View的事件,调用Model的接口更新数据;监听Model的变化,刷新View的显示。Controller是系统的胶水层,也是最容易变化的部分。
2.3 为什么要加事件系统
标准的MVC中,View持有Controller的引用,直接调用Controller的方法。但这样一来,View和Controller还是耦合的——换一个Controller就要改View。通过引入事件系统,View只抛出事件,不关心谁处理;Controller只监听事件,不关心谁触发的。这样View和Controller彻底解耦,可以独立替换和复用。
本章的框架将围绕事件系统展开,所有模块之间的通信都通过事件中心进行。
3. 事件系统:模块间的通信总线
3.1 事件中心的设计思路
事件中心基于观察者模式,维护一个事件名到回调函数的映射字典。任何模块都可以注册监听某个事件,任何模块也可以触发某个事件,触发时携带任意数量和类型的参数。
这种设计有几个好处:
- 全局可达:只要拿到事件中心的引用,任何地方都能收发事件
- 参数灵活:使用params object[]可以传递任意类型和数量的数据
- 动态管理:可以随时添加和移除监听,适合对象生命周期的管理
3.2 泛型事件参数:摆脱object的强制转换
基础的EventCenter使用params object[]传递参数,虽然灵活,但接收方需要进行类型转换,既麻烦又容易出错。我们可以通过泛型包装器,让事件携带强类型的数据。
csharp复制using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 事件中心:全局消息总线
/// </summary>
public static class EventCenter
{
// 无参事件的委托
public delegate void EventHandler();
// 单参数事件的泛型委托
public delegate void EventHandler<T>(T arg);
// 双参数事件的泛型委托
public delegate void EventHandler<T, U>(T arg0, U arg1);
// 存储无参事件的字典
private static Dictionary<string, EventHandler> _eventDict = new Dictionary<string, EventHandler>();
// 存储单参数事件的字典(使用object擦除类型,内部存储Delegate)
private static Dictionary<string, Delegate> _eventDictGeneric = new Dictionary<string, Delegate>();
/// <summary>
/// 添加无参事件监听
/// </summary>
public static void AddListener(string eventName, EventHandler handler)
{
if (_eventDict.ContainsKey(eventName))
{
_eventDict[eventName] += handler;
}
else
{
_eventDict.Add(eventName, handler);
}
}
/// <summary>
/// 添加单参数事件监听
/// </summary>
public static void AddListener<T>(string eventName, EventHandler<T> handler)
{
if (_eventDictGeneric.ContainsKey(eventName))
{
_eventDictGeneric[eventName] = Delegate.Combine(_eventDictGeneric[eventName], handler);
}
else
{
_eventDictGeneric.Add(eventName, handler);
}
}
/// <summary>
/// 添加双参数事件监听
/// </summary>
public static void AddListener<T, U>(string eventName, EventHandler<T, U> handler)
{
if (_eventDictGeneric.ContainsKey(eventName))
{
_eventDictGeneric[eventName] = Delegate.Combine(_eventDictGeneric[eventName], handler);
}
else
{
_eventDictGeneric.Add(eventName, handler);
}
}
/// <summary>
/// 移除无参事件监听
/// </summary>
public static void RemoveListener(string eventName, EventHandler handler)
{
if (_eventDict.ContainsKey(eventName))
{
_eventDict[eventName] -= handler;
if (_eventDict[eventName] == null)
{
_eventDict.Remove(eventName);
}
}
}
/// <summary>
/// 移除单参数事件监听
/// </summary>
public static void RemoveListener<T>(string eventName, EventHandler<T> handler)
{
if (_eventDictGeneric.ContainsKey(eventName))
{
_eventDictGeneric[eventName] = Delegate.Remove(_eventDictGeneric[eventName], handler);
if (_eventDictGeneric[eventName] == null)
{
_eventDictGeneric.Remove(eventName);
}
}
}
/// <summary>
/// 移除双参数事件监听
/// </summary>
public static void RemoveListener<T, U>(string eventName, EventHandler<T, U> handler)
{
if (_eventDictGeneric.ContainsKey(eventName))
{
_eventDictGeneric[eventName] = Delegate.Remove(_eventDictGeneric[eventName], handler);
if (_eventDictGeneric[eventName] == null)
{
_eventDictGeneric.Remove(eventName);
}
}
}
/// <summary>
/// 触发无参事件
/// </summary>
public static void TriggerEvent(string eventName)
{
if (_eventDict.TryGetValue(eventName, out EventHandler handler))
{
handler?.Invoke();
}
}
/// <summary>
/// 触发单参数事件
/// </summary>
public static void TriggerEvent<T>(string eventName, T arg)
{
if (_eventDictGeneric.TryGetValue(eventName, out Delegate del))
{
if (del is EventHandler<T> handler)
{
handler(arg);
}
}
}
/// <summary>
/// 触发双参数事件
/// </summary>
public static void TriggerEvent<T, U>(string eventName, T arg0, U arg1)
{
if (_eventDictGeneric.TryGetValue(eventName, out Delegate del))
{
if (del is EventHandler<T, U> handler)
{
handler(arg0, arg1);
}
}
}
/// <summary>
/// 清空所有事件(场景切换时调用)
/// </summary>
public static void ClearAllEvents()
{
_eventDict.Clear();
_eventDictGeneric.Clear();
}
}
这个事件中心的核心设计是:为不同参数数量的事件分别存储字典,通过泛型方法提供类型安全的接口。接收方注册时就知道参数类型,触发时直接传递强类型参数,内部用Delegate类型擦除统一存储。
3.3 事件名的定义规范
字符串事件名容易拼错,也容易冲突。商业项目中通常用常量类或枚举来统一管理:
csharp复制/// <summary>
/// UI事件定义
/// </summary>
public static class UIEvent
{
// 登录相关
public const string LOGIN_REQUEST = "LOGIN_REQUEST";
public const string LOGIN_SUCCESS = "LOGIN_SUCCESS";
public const string LOGIN_FAILED = "LOGIN_FAILED";
// 注册相关
public const string REGISTER_REQUEST = "REGISTER_REQUEST";
public const string REGISTER_SUCCESS = "REGISTER_SUCCESS";
public const string REGISTER_FAILED = "REGISTER_FAILED";
// 面板控制
public const string OPEN_PANEL = "OPEN_PANEL";
public const string CLOSE_PANEL = "CLOSE_PANEL";
public const string PANEL_OPENED = "PANEL_OPENED";
public const string PANEL_CLOSED = "PANEL_CLOSED";
}
/// <summary>
/// 游戏逻辑事件定义
/// </summary>
public static class GameEvent
{
public const string PLAYER_DATA_UPDATED = "PLAYER_DATA_UPDATED";
public const string GAME_PAUSED = "GAME_PAUSED";
public const string GAME_RESUMED = "GAME_RESUMED";
public const string SCENE_LOADED = "SCENE_LOADED";
}
3.4 生命周期管理与内存泄漏防范
事件中心的一个常见陷阱是:对象注册了事件监听,但销毁时忘记移除,导致对象一直被引用无法被GC回收。解决方案有两个:
- 在OnDestroy/OnDisable中显式移除:这是最稳妥的方式
- 使用弱引用:事件中心内部用WeakReference持有目标对象,允许GC回收,但实现较复杂
我们采用第一种方案,要求所有UI脚本在销毁时移除自己的监听。
4. 窗体基类:所有UI面板的祖宗
4.1 窗体的生命周期设计
一个典型的UI面板从创建到销毁会经历多个阶段:初始化、打开、显示、隐藏、关闭、销毁。一个好的基类应该为这些阶段提供虚方法,让子类在合适的时机插入自己的逻辑。
我们设计的生命周期包括:
- OnCreate:面板被创建时调用(仅一次)
- OnOpen:每次面板打开时调用
- OnShow:面板动画播放完毕后调用(可用于刷新数据)
- OnClose:每次面板关闭时调用
- OnDestroy:面板销毁时调用
4.2 基类实现
csharp复制using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 面板层级枚举
/// </summary>
public enum PanelLayer
{
Background, // 背景层(最底层)
Normal, // 普通层
Popup, // 弹窗层
Top, // 顶层(Toast、Loading等)
System // 系统层(最高,强制显示)
}
/// <summary>
/// 面板打开方式
/// </summary>
public enum OpenMode
{
Normal, // 正常打开
Stack, // 压入堆栈(返回时回到上一个面板)
Exclusive, // 独占模式(关闭其他同层面板)
Singleton // 单例模式(同一时间只能打开一个)
}
/// <summary>
/// 窗体基类
/// 所有UI面板都应该继承此类
/// </summary>
[RequireComponent(typeof(CanvasGroup))]
public abstract class BaseWindow : MonoBehaviour
{
[Header("基础设置")]
[SerializeField] protected PanelLayer _layer = PanelLayer.Normal;
[SerializeField] protected OpenMode _openMode = OpenMode.Normal;
[SerializeField] protected bool _isFullScreen = false;
[SerializeField] protected bool _lockInputWhenOpen = true;
[Header("动画设置")]
[SerializeField] protected float _openAnimationTime = 0.3f;
[SerializeField] protected float _closeAnimationTime = 0.2f;
// 组件引用
protected CanvasGroup _canvasGroup;
protected RectTransform _rectTransform;
// 状态
protected bool _isOpened = false;
protected bool _isAnimating = false;
// 打开时的回调
public UnityAction<BaseWindow> OnWindowOpened;
public UnityAction<BaseWindow> OnWindowClosed;
// 公共属性
public PanelLayer Layer => _layer;
public OpenMode OpenMode => _openMode;
public bool IsFullScreen => _isFullScreen;
public bool IsOpened => _isOpened;
public bool IsAnimating => _isAnimating;
protected virtual void Awake()
{
_canvasGroup = GetComponent<CanvasGroup>();
_rectTransform = GetComponent<RectTransform>();
// 初始状态为隐藏
gameObject.SetActive(false);
// 子类重写此方法进行初始化
OnCreate();
}
/// <summary>
/// 面板被创建时调用(仅一次)
/// </summary>
protected virtual void OnCreate()
{
}
/// <summary>
/// 打开面板
/// </summary>
public void Open()
{
if (_isOpened || _isAnimating)
{
return;
}
gameObject.SetActive(true);
_isOpened = true;
// 锁定输入(防止打开过程中误操作)
if (_lockInputWhenOpen)
{
_canvasGroup.interactable = false;
_canvasGroup.blocksRaycasts = false;
}
// 调用子类逻辑
OnOpen();
// 播放打开动画
PlayOpenAnimation();
}
/// <summary>
/// 子类可以重写的打开逻辑
/// </summary>
protected virtual void OnOpen()
{
}
/// <summary>
/// 播放打开动画
/// </summary>
protected virtual void PlayOpenAnimation()
{
if (_openAnimationTime <= 0)
{
// 没有动画,直接显示
OnOpenAnimationComplete();
return;
}
_isAnimating = true;
// 初始化动画状态(如alpha从0开始)
_canvasGroup.alpha = 0f;
// 使用DOTween或协程播放动画
StartCoroutine(OpenAnimationCoroutine());
}
private System.Collections.IEnumerator OpenAnimationCoroutine()
{
float elapsed = 0f;
while (elapsed < _openAnimationTime)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / _openAnimationTime);
// 缓动效果
t = Mathf.SmoothStep(0, 1, t);
_canvasGroup.alpha = t;
yield return null;
}
OnOpenAnimationComplete();
}
/// <summary>
/// 打开动画完成
/// </summary>
protected virtual void OnOpenAnimationComplete()
{
_canvasGroup.alpha = 1f;
_canvasGroup.interactable = true;
_canvasGroup.blocksRaycasts = true;
_isAnimating = false;
// 通知管理器面板已打开
EventCenter.TriggerEvent(UIEvent.PANEL_OPENED, this);
// 调用子类的显示完成逻辑
OnShow();
OnWindowOpened?.Invoke(this);
}
/// <summary>
/// 面板完全显示后调用
/// </summary>
protected virtual void OnShow()
{
}
/// <summary>
/// 关闭面板
/// </summary>
public void Close()
{
if (!_isOpened || _isAnimating)
{
return;
}
_isOpened = false;
// 锁定输入
_canvasGroup.interactable = false;
_canvasGroup.blocksRaycasts = false;
// 调用子类逻辑
OnClose();
// 播放关闭动画
PlayCloseAnimation();
}
/// <summary>
/// 子类可以重写的关闭逻辑
/// </summary>
protected virtual void OnClose()
{
}
/// <summary>
/// 播放关闭动画
/// </summary>
protected virtual void PlayCloseAnimation()
{
if (_closeAnimationTime <= 0)
{
// 没有动画,直接关闭
OnCloseAnimationComplete();
return;
}
_isAnimating = true;
// 使用协程播放动画
StartCoroutine(CloseAnimationCoroutine());
}
private System.Collections.IEnumerator CloseAnimationCoroutine()
{
float elapsed = 0f;
float startAlpha = _canvasGroup.alpha;
while (elapsed < _closeAnimationTime)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / _closeAnimationTime);
_canvasGroup.alpha = Mathf.Lerp(startAlpha, 0, t);
yield return null;
}
OnCloseAnimationComplete();
}
/// <summary>
/// 关闭动画完成
/// </summary>
protected virtual void OnCloseAnimationComplete()
{
_canvasGroup.alpha = 0f;
_isAnimating = false;
gameObject.SetActive(false);
// 通知管理器面板已关闭
EventCenter.TriggerEvent(UIEvent.PANEL_CLOSED, this);
OnWindowClosed?.Invoke(this);
}
/// <summary>
/// 强制立即关闭(不播放动画)
/// </summary>
public void CloseImmediate()
{
if (!_isOpened)
{
return;
}
_isOpened = false;
_isAnimating = false;
OnClose();
_canvasGroup.alpha = 0f;
gameObject.SetActive(false);
EventCenter.TriggerEvent(UIEvent.PANEL_CLOSED, this);
}
protected virtual void OnDestroy()
{
// 确保事件监听被移除
OnWindowClosed = null;
OnWindowOpened = null;
}
}
这个基类包含了面板的基本行为:生命周期管理、动画播放、状态维护。所有具体的UI面板都继承自它,通过重写虚方法来实现自己的逻辑。
5. 窗体子类:登录面板的实现案例
5.1 数据模型:LoginModel
首先定义登录模块的数据模型。Model只关心数据,不关心界面:
csharp复制using UnityEngine;
/// <summary>
/// 登录状态枚举
/// </summary>
public enum LoginState
{
Idle, // 空闲
LoggingIn, // 登录中
Registering, // 注册中
Success, // 成功
Failed // 失败
}
/// <summary>
/// 登录数据模型
/// 负责存储登录相关的数据,不包含任何UI逻辑
/// </summary>
public class LoginModel
{
private string _username;
private string _password;
private string _errorMessage;
private LoginState _state;
// 公开属性,提供数据访问
public string Username => _username;
public string Password => _password;
public string ErrorMessage => _errorMessage;
public LoginState State => _state;
public LoginModel()
{
Reset();
}
/// <summary>
/// 重置数据
/// </summary>
public void Reset()
{
_username = string.Empty;
_password = string.Empty;
_errorMessage = string.Empty;
_state = LoginState.Idle;
// 触发数据变更事件
EventCenter.TriggerEvent(GameEvent.PLAYER_DATA_UPDATED);
}
/// <summary>
/// 设置用户名
/// </summary>
public void SetUsername(string username)
{
_username = username;
EventCenter.TriggerEvent(GameEvent.PLAYER_DATA_UPDATED);
}
/// <summary>
/// 设置密码
/// </summary>
public void SetPassword(string password)
{
_password = password;
EventCenter.TriggerEvent(GameEvent.PLAYER_DATA_UPDATED);
}
/// <summary>
/// 设置错误信息
/// </summary>
public void SetError(string error)
{
_errorMessage = error;
_state = LoginState.Failed;
EventCenter.TriggerEvent(GameEvent.PLAYER_DATA_UPDATED);
}
/// <summary>
/// 设置登录状态
/// </summary>
public void SetState(LoginState state)
{
_state = state;
EventCenter.TriggerEvent(GameEvent.PLAYER_DATA_UPDATED);
}
/// <summary>
/// 验证输入是否有效
/// </summary>
public bool ValidateInput()
{
if (string.IsNullOrEmpty(_username) || _username.Length < 3)
{
SetError("用户名至少3个字符");
return false;
}
if (string.IsNullOrEmpty(_password) || _password.Length < 6)
{
SetError("密码至少6个字符");
return false;
}
return true;
}
}
5.2 视图:LoginView
View负责显示数据和收集用户输入。它通过事件将用户操作转发出去,不处理任何业务逻辑:
csharp复制using UnityEngine;
using UnityEngine.UI;
using TMPro;
/// <summary>
/// 登录面板视图
/// 负责UI显示和用户输入收集
/// </summary>
public class LoginView : BaseWindow
{
[Header("输入组件")]
[SerializeField] private TMP_InputField _usernameInput;
[SerializeField] private TMP_InputField _passwordInput;
[Header("按钮")]
[SerializeField] private Button _loginButton;
[SerializeField] private Button _registerButton;
[Header("显示组件")]
[SerializeField] private TextMeshProUGUI _errorText;
[SerializeField] private GameObject _loadingIndicator;
[Header("测试快捷入口")]
[SerializeField] private Button _testQuickFillButton;
protected override void OnCreate()
{
base.OnCreate();
// 注册按钮点击事件(转发给事件中心)
_loginButton.onClick.AddListener(OnLoginClick);
_registerButton.onClick.AddListener(OnRegisterClick);
if (_testQuickFillButton != null)
{
_testQuickFillButton.onClick.AddListener(OnQuickFillClick);
}
// 监听数据变更事件,刷新UI
EventCenter.AddListener(GameEvent.PLAYER_DATA_UPDATED, RefreshUI);
}
private void OnLoginClick()
{
// 收集输入数据
string username = _usernameInput.text;
string password = _passwordInput.text;
// 转发给Controller(通过事件)
EventCenter.TriggerEvent(UIEvent.LOGIN_REQUEST, username, password);
}
private void OnRegisterClick()
{
EventCenter.TriggerEvent(UIEvent.OPEN_PANEL, "RegisterPanel");
}
private void OnQuickFillClick()
{
// 测试用:快速填充
_usernameInput.text = "testuser";
_passwordInput.text = "123456";
}
/// <summary>
/// 刷新UI显示(根据Model的数据)
/// </summary>
private void RefreshUI()
{
// 这里应该从Controller获取Model,为了简化,先写死
// 实际项目中可以通过事件传递数据,或者View持有Model的引用
// 控制加载动画显示
// _loadingIndicator.SetActive(state == LoginState.LoggingIn);
// 显示错误信息
// _errorText.text = errorMessage;
// _errorText.gameObject.SetActive(!string.IsNullOrEmpty(errorMessage));
}
/// <summary>
/// 清空输入
/// </summary>
public void ClearInput()
{
_usernameInput.text = string.Empty;
_passwordInput.text = string.Empty;
}
protected override void OnDestroy()
{
base.OnDestroy();
// 移除事件监听
EventCenter.RemoveListener(GameEvent.PLAYER_DATA_UPDATED, RefreshUI);
}
}
5.3 控制器:LoginController
Controller是Model和View的桥梁,它监听View的事件,更新Model;监听Model的变化,刷新View:
csharp复制using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// 登录控制器
/// 协调Model和View的交互
/// </summary>
public class LoginController : MonoBehaviour
{
private LoginModel _model;
private LoginView _view;
private void Awake()
{
_model = new LoginModel();
// 注册事件监听
EventCenter.AddListener<string, string>(UIEvent.LOGIN_REQUEST, OnLoginRequest);
EventCenter.AddListener(UIEvent.REGISTER_SUCCESS, OnRegisterSuccess);
}
private void Start()
{
// 获取View引用(实际项目中应该通过UIManager获取)
_view = FindObjectOfType<LoginView>();
if (_view == null)
{
Debug.LogError("LoginView not found!");
}
}
/// <summary>
/// 处理登录请求
/// </summary>
private void OnLoginRequest(string username, string password)
{
// 1. 更新Model
_model.SetUsername(username);
_model.SetPassword(password);
// 2. 验证输入
if (!_model.ValidateInput())
{
// 验证失败,显示错误
// 错误信息已经在Model中了
return;
}
// 3. 开始登录流程
_model.SetState(LoginState.LoggingIn);
// 4. 发起网络请求
StartCoroutine(LoginRequestCoroutine(username, password));
}
/// <summary>
/// 模拟登录请求
/// </summary>
private IEnumerator LoginRequestCoroutine(string username, string password)
{
// 模拟网络延迟
yield return new WaitForSeconds(1f);
// 模拟登录成功(实际项目中应该是真实的网络请求)
if (username == "testuser" && password == "123456")
{
// 登录成功
_model.SetState(LoginState.Success);
// 触发成功事件
EventCenter.TriggerEvent(UIEvent.LOGIN_SUCCESS, username);
// 可以打开主界面
EventCenter.TriggerEvent(UIEvent.OPEN_PANEL, "MainPanel");
// 关闭登录面板
if (_view != null)
{
_view.Close();
}
}
else
{
// 登录失败
_model.SetError("用户名或密码错误");
// 触发失败事件
EventCenter.TriggerEvent(UIEvent.LOGIN_FAILED, "用户名或密码错误");
}
}
/// <summary>
/// 处理注册成功(来自注册面板的事件)
/// </summary>
private void OnRegisterSuccess()
{
// 注册成功后,可以自动填充账号(实际项目中可能返回账号信息)
// 这里只是示例
}
private void OnDestroy()
{
// 移除事件监听
EventCenter.RemoveListener<string, string>(UIEvent.LOGIN_REQUEST, OnLoginRequest);
EventCenter.RemoveListener(UIEvent.REGISTER_SUCCESS, OnRegisterSuccess);
}
}
这个登录案例清晰地展示了MVC的分工:LoginView负责界面和输入,LoginModel负责数据和状态,LoginController负责协调和业务逻辑。三者各司其职,任何一个都可以被替换而不影响其他两个。
6. 控制类与状态类的配合
6.1 为什么需要状态类
在复杂的UI系统中,控制器往往需要管理多种状态:登录中、注册中、等待中、完成等。如果都用if else或switch处理,代码会越来越臃肿。状态模式可以把每种状态封装成独立的类,让控制器委托给当前状态对象处理。
6.2 状态基类和具体状态实现
csharp复制/// <summary>
/// UI状态基类
/// </summary>
public abstract class UIState
{
protected UIController _controller;
public virtual void Enter(UIController controller)
{
_controller = controller;
OnEnter();
}
public abstract void OnEnter();
public abstract void OnUpdate();
public abstract void OnExit();
// 处理各种事件,子类选择性重写
public virtual void HandleLoginRequest(string username, string password) { }
public virtual void HandleRegisterRequest(string username, string password) { }
public virtual void HandleNetworkResponse(bool success, string data) { }
}
/// <summary>
/// 空闲状态
/// </summary>
public class IdleState : UIState
{
public override void OnEnter()
{
Debug.Log("进入空闲状态");
// 显示登录按钮等
}
public override void OnUpdate()
{
// 空闲状态下的更新逻辑
}
public override void OnExit()
{
Debug.Log("退出空闲状态");
}
public override void HandleLoginRequest(string username, string password)
{
// 空闲状态下收到登录请求,切换到登录中状态
LoginState loginState = new LoginState();
_controller.ChangeState(loginState);
// 让新状态处理请求
loginState.HandleLoginRequest(username, password);
}
}
/// <summary>
/// 登录中状态
/// </summary>
public class LoginState : UIState
{
private float _requestStartTime;
public override void OnEnter()
{
Debug.Log("进入登录中状态");
_requestStartTime = Time.time;
// 显示加载动画,禁用按钮等
EventCenter.TriggerEvent(UIEvent.SHOW_LOADING, true);
}
public override void OnUpdate()
{
// 可以在这里做超时检测
if (Time.time - _requestStartTime > 10f)
{
Debug.LogWarning("登录请求超时");
HandleNetworkResponse(false, "请求超时");
}
}
public override void OnExit()
{
Debug.Log("退出登录中状态");
EventCenter.TriggerEvent(UIEvent.SHOW_LOADING, false);
}
public override void HandleNetworkResponse(bool success, string data)
{
if (success)
{
// 登录成功,切换到成功状态
SuccessState successState = new SuccessState();
_controller.ChangeState(successState);
}
else
{
// 登录失败,回到空闲状态并显示错误
EventCenter.TriggerEvent(UIEvent.LOGIN_FAILED, data);
IdleState idleState = new IdleState();
_controller.ChangeState(idleState);
}
}
}
/// <summary>
/// 成功状态
/// </summary>
public class SuccessState : UIState
{
public override void OnEnter()
{
Debug.Log("进入成功状态");
// 播放成功动画,跳转场景等
}
public override void OnUpdate() { }
public override void OnExit() { }
}
6.3 带状态管理的控制器
csharp复制/// <summary>
/// 带状态管理的UI控制器基类
/// </summary>
public abstract class UIController : MonoBehaviour
{
protected UIState _currentState;
protected virtual void Start()
{
// 默认进入空闲状态
ChangeState(new IdleState());
}
protected virtual void Update()
{
_currentState?.OnUpdate();
}
/// <summary>
/// 切换状态
/// </summary>
public void ChangeState(UIState newState)
{
_currentState?.OnExit();
_currentState = newState;
_currentState?.Enter(this);
}
/// <summary>
/// 处理网络响应(由子类或事件触发)
/// </summary>
public void HandleNetworkResponse(bool success, string data)
{
_currentState?.HandleNetworkResponse(success, data);
}
}
状态模式的好处是:新增一种状态不需要修改控制器代码,只需要新建一个状态类。登录中、注册中、等待验证等不同状态的行为完全隔离,不会相互干扰。
7. 窗体管理类:统一的调度中心
7.1 为什么需要窗体管理器
如果没有统一的管理器,每个面板都要自己管理打开关闭、层级顺序、返回堆栈,会导致大量重复代码。窗体管理器集中处理这些共性问题。
管理器的职责包括:
- 注册所有面板(通过预制体加载或场景查找)
- 根据名称或类型打开/关闭面板
- 管理面板的层级(按PanelLayer排序)
- 管理面板堆栈(实现返回功能)
- 提供单例访问接口
7.2 管理器的实现
csharp复制using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 窗体管理器
/// 单例模式,统一管理所有UI面板
/// </summary>
public class UIManager : MonoBehaviour
{
private static UIManager _instance;
public static UIManager Instance
{
get
{
if (_instance == null)
{
GameObject go = new GameObject("[UIManager]");
_instance = go.AddComponent<UIManager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
// 面板层级容器
private Dictionary<PanelLayer, Transform> _