目录
一、MVC介绍
- model:数据层。界面展示的数据(需要进行初始化、更新、保存、事件通知等操作),单例模式,不必继承MonoBehaviour。
- view:界面层。寻找UI,提供更新UI控件的方法,供controller调用。要挂在预制体上。
(1)MVC是对M和V层进行了解耦,但没有完全解耦,从而诞生了MVP:对M和V层完全解耦。
(2)MVC里view还是具备直接得到model的权限的,MVP框架是完全把视图跟模型分离了。 - controller:处理业务逻辑。控制View的显隐及更新,事件监听及取消。要挂在预制体上。
(1)注意:物体隐藏时,OnDestroy不会被调用。
(2)一个view对应一个controller。
二、搭建UI界面
搭建如下的UI界面,并制作成预制体,并放于Resources\Prefabs目录。
三、代码实现
创建三个脚本:Model.cs、View.cs、Controller.cs。并将View.cs、Controller.cs挂于预制体上。
1.Model层
cs
using System;
using UnityEngine;
public class Model
{
// 单例
private static Model instance;
public static Model Instance
{
get
{
if (instance == null)
{
instance = new Model();
instance.Init();
}
return instance;
}
}
// 数据
private int coinCount;
public int CoinCount
{
get { return coinCount; }
}
// 数据操作:初始化、更新、保存
private void Init()
{
coinCount = PlayerPrefs.GetInt("CoinCount", 0);
}
public void UpdateCoinCount()
{
++coinCount;
Save();
}
private void Save()
{
PlayerPrefs.SetInt("CoinCount", coinCount);
CallEvent();
}
// 事件:用于通知Controller进行更新View
private event Action<Model> ModelEvent;
public void AddEventListener(Action<Model> function)
{
ModelEvent += function;
}
public void RemoveEventListener(Action<Model> function)
{
ModelEvent -= function;
}
private void CallEvent()
{
ModelEvent?.Invoke(this);
}
}
2.View层
cs
using UnityEngine;
using UnityEngine.UI;
public class View : MonoBehaviour
{
//提供UI控件
public Text coinText;
public Button addCoinButton;
//提供更新UI控件的方法
public void UpdateCoin(Model model)
{
coinText.text = model.CoinCount.ToString();
}
}
3.Controller层
一个UI预制体对应一个View、一个Controller。
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Controller : MonoBehaviour
{
//controller控制view
private View view;
private static Controller controller;
public static Controller Instance
{
get
{
return controller;
}
}
//1.面板显示隐藏
public static void Show()
{
if (controller == null)
{
var temp = Resources.Load<GameObject>("Prefabs/UIPanel");
var go = Instantiate(temp);
go.transform.parent = GameObject.Find("Canvas").transform;
go.transform.localPosition = Vector3.zero;
go.transform.localScale = Vector3.one;
controller = go.GetComponent<Controller>();
}
controller.gameObject.SetActive(true);
}
public static void Hide()
{
if (controller != null)
{
controller.gameObject.SetActive(false);
}
}
void Start()
{
view = GetComponent<View>();
//2.首次更新面板数据
view.UpdateCoin(Model.Instance);
//3.事件监听,更新数据
view.addCoinButton.onClick.AddListener(() =>
{
Model.Instance.UpdateCoinCount();
});
Model.Instance.AddEventListener(UpdateCoin);
}
private void UpdateCoin(Model model)
{
//2.更新面板数据
view.UpdateCoin(model);
}
private void OnDestroy()
{
Model.Instance.RemoveEventListener(UpdateCoin);
}
}
四、MVC框架测试
cs
using UnityEngine;
public class TestMVC : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Controller.Show();
}
if (Input.GetMouseButtonDown(1))
{
Controller.Hide();
}
}
}
五、知识补充
cs
private event Action<Model> ModelEvent;
public void AddEventListener(Action<Model> function)
{
ModelEvent += function;
}
public void RemoveEventListener(Action<Model> function)
{
ModelEvent -= function;
}
private void CallEvent()
{
ModelEvent?.Invoke(this);
}
在C#中,event关键字用于声明一个事件,它本质上是一种特殊的多播委托。`ModelEvent`在这里是一个事件,当你对其使用`+=`操作符时,你实际上是将一个回调方法添加到委托的调用列表中。如果你注册了多个回调,那么这些回调方法就会被添加到`ModelEvent`的调用链中。
当调用`ModelEvent?.Invoke(this);`时,以下是在底层发生的事情:
- 空值检查:`?.`是C# 6.0中引入的空条件运算符。它在尝试调用方法或访问成员之前会检查左侧的对象是否为`null`。如果`ModelEvent`不为`null`,那么调用继续;如果为`null`,那么调用就不会执行,整个表达式的结果也是`null`。
- Invoke方法:`Invoke`是`MulticastDelegate`类的一个方法,它负责同步地调用委托链中的每个回调方法。`this`关键字是传递给每个回调方法的参数,表示事件的发起者。
- 多播委托的调用链:`ModelEvent`作为一个多播委托,可以持有对多个方法的引用。当你调用`Invoke`时,它按照注册的顺序调用这些方法。如果调用链中的任何一个方法抛出异常,那么后续的方法调用将不会执行,除非你捕获并处理了这个异常。
- **线程安全性:**虽然`?.`操作符提供了对`null`的检查,但这并不保证线程安全性。如果在检查`ModelEvent`之后和调用`Invoke`之前,另一个线程将`ModelEvent`设置为`null`,那么仍然会抛出`NullReferenceException`。在多线程环境中,通常需要额外的同步机制来确保线程安全。
- **事件的触发:**如果`ModelEvent`不为空,`Invoke`会触发事件,即调用所有注册的回调方法。这些方法将按照它们被添加到委托中的顺序被调用。
在C#中,事件的使用是一种很好的设计模式,它允许对象通知其他对象发生了某些事情,而不需要知道这些对象是谁或者它们要做什么。这有助于保持代码的解耦和灵活性。