适用于 Unity 的 MVP 架构

发布: (2026年2月1日 GMT+8 06:07)
9 min read
原文: Dev.to

Source: Dev.to
Note:不是 一本严格的 MVP 手册,也不是经典 MVP 在 Unity 中的一对一完美映射。这里展示的是一种适用于我们游戏的改编方案,它更像是一个拥有大量菜单和众多小游戏的应用程序。它可能完全适配你的项目,也可能不完全适配,但希望能为你提供有价值的思路。

产品

没有“一种架构统治一切”。

我们的应用 Magrid 是面向 3 – 10 岁儿童的学习解决方案(如果你有孩子,或者恰好是个 5 岁的小朋友,快去看看吧)。它旨在实现两个主要目标:

  • 语言无关 – 世界各地的儿童都可以使用。
  • 包容性 – 包含对有残障的儿童的支持。

技术视角

  • 许多菜单(登录/注册、学生管理、绩效评审、课程管理等)
  • 许多小游戏(模式识别、视觉感知、数字比较等)

主要的技术关注点是 关注点分离,既包括菜单逻辑,也包括小游戏本身。

架构概览

该架构包含两类实体:

  1. MVP 模块
  2. 系统

MVP 模块

一个 MVP 模块由 三个组件 构成:

组件职责
View(继承自 MonoBehaviour处理用户输入,显示数据,直接与 Presenter 交互
Presenter(普通 C# 类)包含所有业务逻辑,从 Model 获取数据,仅通过接口更新 View
Model(普通 C# 类)存储模块所需的数据,为了清晰而独立分离

流程

  1. View 是 MVP 模块的入口点。
  2. View 创建其 Presenter(们)。
  3. 为了复用,一个 View 可以拥有多个 Presenter。
  4. View 将自身的接口传递给 Presenter。
  5. Presenter 在需要时创建其 Model。
  6. 模块准备就绪。

所有业务逻辑都位于 Presenter 中。设想将 View 替换为命令行实现——应用仍能正常工作,因为 Presenter 不使用 Unity API,仅通过接口进行通信。

简单代码示例

// View ---------------------------------------------------------
public class FooView : BaseScreen, IFooView
{
    [SerializeField] private TMP_Text exposedInExpectorExample;

    private FooPresenter _presenter;

    private void Start()
    {
        _presenter = new FooPresenter(this);
    }

    public void SetName(string barName)
    {
        exposedInExpectorExample.text = barName;
    }
}

// View interface ------------------------------------------------
public interface IFooView : BaseMvpView
{
    void SetName(string barName);
}

// Presenter ----------------------------------------------------
public class FooPresenter : BaseMvpPresenter
{
    private readonly IFooView _view;
    private readonly FooModel _model;

    public FooPresenter(IFooView view)
    {
        _view  = view;
        _model = new FooModel();
    }
}

// Model ---------------------------------------------------------
public class FooModel : BaseMvpModel
{
    public string BarName { get; set; }
}

说明

  • BaseScreen(View 的基类)是一个负责画布管理(打开、关闭等)的 MonoBehaviour
  • BaseMvpView 是一个通用接口,视图接口继承自它:
public interface BaseMvpView where T : BaseMvpPresenter { }
  • BaseMvpModel 是一个简单接口:
public interface BaseMvpModel
{
    int Version { get; }
}

Version 字段用于在更新后加载持久化数据时进行数据迁移。每个模块可以自行管理迁移逻辑。

系统

系统本质上是 没有 View 的 MVP 模块。它们的目的是为所有 MVP 模块提供服务。

  • 所有系统由单个 MonoBehaviour(称为 AppManager)实例化,MVP 可以访问它(Unity 版单例)。
  • 由于 IL2CPP 与 Zenject 的兼容性问题,我们实现了一个简易的依赖管理器。

特性

  • 每个系统 公开一个接口
  • 所有系统都可以在测试中 被 mock
  • 系统在应用加载期间 完成初始化

示例系统

系统职责
Profile System持久化数据存储
API System与服务器通信
Purchase System应用内购买
Analytics System分析提供商及事件(GameAnalytics、Firebase 等)
UI System屏幕、弹窗、导航、返回操作

PlayInfo – 中介者

PlayInfo 处理 MVP 模块需要通信的少数特殊情况。它包含:

  • 一组模块可以订阅的 events
  • 一些 shared variables

最初它主要用于帮助从遗留代码库过渡,在那个代码库中所有东西都通过静态方法相互访问。PlayInfo 充当了重构后的 MVP 模块与遗留代码之间的中介。最终我们保留了一些事件,因为它们被证明非常有用。

测试策略

我们使用两种类型的测试:

  1. 单元测试 – 覆盖 Presenter 和系统。它们的依赖最少,便于进行 Mock。
  2. 集成 / UI 测试 – 模拟用户交互并测试多个模块的协同工作。可以使用 UnityTest 和反射来模拟用户交互并检查 UI(这值得单独写篇文章)。

隔离

  • 每个模块都通过 接口(以及少数情况下的 事件)实现完全隔离。
  • 这使得重构/更改更加安全,并减少意外的副作用。

典型模块组成

  • View
  • View Interface
  • Presenter
  • Model

Unity 边缘情况

归根结底,这仍然是一个游戏。有时你需要使用 Unity 的 API,例如 协程(Coroutines)物理(Physics)

  • 要启动协程,请使用 AppManager(MonoBehaviour 单例)。
// 示例:从 Presenter 启动协程
public class FooPresenter : BaseMvpPresenter
{
    private readonly IFooView _view;
    private readonly FooModel _model;
    private readonly AppManager _appManager;

    public FooPresenter(IFooView view, AppManager appManager)
    {
        _view        = view;
        _model       = new FooModel();
        _appManager  = appManager;
    }

    public void DoSomethingAsync()
    {
        _appManager.StartCoroutine(DoWork());
    }

    private IEnumerator DoWork()
    {
        // 这里写 Unity‑specific 逻辑
        yield return new WaitForSeconds(1f);
        _view.SetName("Done");
    }
}

其他 Unity‑specific 的需求(物理、动画事件等)也采用类似方式处理:Presenter 将调用委托给系统或 AppManager,从而保持 Presenter 本身不直接依赖 Unity。

TL;DR

  • MVP 为我们提供可测试、解耦的 UI 逻辑。
  • 系统(Systems) 提供共享服务,避免污染 View 层。
  • PlayInfo 是用于旧式通信的轻量级中介。
  • AppManager 是唯一需要 MonoBehaviour 的 Unity 入口点。

欢迎根据自己的项目自行调整这些思路——目标是保持关注点分离、便于测试,同时在需要时仍能利用 Unity 强大的运行时特性。

在 MVP 架构中避免重复

这些情况对我们来说很少见。虽然我宁愿每天选择 可维护性 而不是 可复用性,但重复代码仍然会带来伤害。

我们解决此问题的方式

  • 构建通用 UI 组件
    示例:在多个视图中复用的 学生资料 UI 元素。

    • 无需重复相同的 UI 代码。
    • 只需更改一次组件(或其预制件),即可更新所有使用它的屏幕。
  • 将无状态逻辑放入工具类
    示例:可以从任何模块调用的电子邮件验证正则表达式。

  • 对每个视图使用多个 presenter 并注入适当的接口(接口分离)
    示例:“联系支持”逻辑已经在 设置 中实现。 在 商店 界面中复用该 presenter,而不是重复代码。

  • 当情况不符合上述两种情况时,将其视为系统
    提供一个 MVP 模块可以使用的服务。

为什么 MVP(或 MVVM)适用于 Unity 项目

即使这些模式最初并非为游戏设计,它们在 菜单繁多、逻辑驱动的 Unity 项目 中表现出色。采用 MVP 为我们带来了:

  • 可测试性 – presenter 可以在隔离环境下进行单元测试。
  • 关注点分离 – UI、业务逻辑和数据被清晰划分。
  • 可维护性 – 一层的更改很少会波及其他层。

有反馈吗?

如果你有问题或更好的改进想法,请告诉我。 :D

Back to Blog

相关文章

阅读更多 »