Unity3d 中 UI 开发的 MVC 模式

转载自 http://blog.csdn.net/jjiss318/article/details/44220081

原文:http://engineering.socialpoint.es/MVC-pattern-unity3d-ui.html

动机

        和游戏开发的其他模块类似,UI 一般需要通过多次迭代开发,直到用户体验近似 OK。另外至关重要的是, 我们想尽快加速迭代的过程。使用 MVC 模式来进行设计,已经被业界证明了是可以解耦屏幕上的显示,如何控制用户的输入对显示的改变,以及如何根据应用的状态进行改变。MVC 模式提供了以下好处:

(1) 可以修改 UI 的外观,而不用修改一行代码

(2) 在不同的组件里面可以共享同一套逻辑代码,用来创建复杂的视图;

(3) 可以用很小的代价来改变 UI 的实现,比如正在使用 NGUI , 但将来会切换成 UGUI


代码示例

这里提供了一个 MVC 在使用 NGUI 开发的例子,需要注意的是,我们不能在代码中提供 NGUI 库,毕竟它是收费的,所以想运行起来的话,需要自己导入 NGUI 库。接下来的内容都会以这个例子的代码作为基准,所以可以在阅读时可以先把代码下载下来。


总体概述

这是一张简图,从宏观的角度描述了代码中 MVC 模式的不同部分。

        

Model

  传统的 MVC 中的 Model,我们都很熟悉:

        (1) 不保存任何 View 的数据或者 View 的状态

        (2) 只能被 Controller 或者其他 Model 访问

        (3) 会触发事件来通知外部系统进行处理和变更

这里的 Model 是使用 Plain Old C# Objects(POCOs) 来实现的,就是不依赖于任何外部库的 C# 代码。这是例子中对于PlayerModel 的链接地址,它表示了 Player 的数据,包括 HitPoints,XP 和 level,使用两个属性来进行访问。我们可以增加 XP 点,而 Model 则会 raise 一个 XPGained 事件。当获得升级的经验时,Level 属性会更新,并 raise 一个 LevelUp 事件。


 View

概念上的 view 一般就是在屏幕上渲染的东西。View 的职责包括:

   (1) 处理用户绘制元素的 reference,包括纹理,特效等

(2) 播放动画

(3) 布局

(4) 接受用户输入

在代码这个特定例子中,View 的实现使用了 NGUI,所以它只是 Unity 工程中的一个 Prefab。但这个实现细节需要解耦。想了解更多学术上的知识的话,还可以看下 Passive View。View 不知道工程中其他部分的任何事情,无论是数据还是逻辑。这样其他的代码必选显式地告诉 View 显式什么,播放什么动画等等。


Controller

Controller 是连接 Model 和 View 的桥梁。它会保存 View 的状态,并且根据外部事件来更新 View 的状态:

(1) 持有 View 所需要的应用状态

(2) 控制 View 的流程

(3) 根据状态 show/hides/activates/deactivates/updates View 或者 View 的某些部分。如 controller 可临时将攻击按钮 Distable 掉,因为此时攻击处于冷却状态,冷却状态一过,controller 会 re-enable 这个按钮。

(4) load/Instantiate 需要的 assets,比如显示 particles, 动态改变 sprites 等

   (5) 处理用户在 View 中触发的事件,比如用户按下了一个按钮;处理 Model 触发的事件,比如 player 获得了      XP 并触发了升级,所以 controller 就更新了 View 中的 Level Number

这就是 MVC 模式中定义的三个基本元素。但在这个例子中,我们加入了另外一个中间层来进一步解耦 NGUI 的 View 实现,称之为:


ViewPresenter

一个 ViewPresenter 位于 View 和 Controller 之间,作为一个接口存在,暴露了对于一个 View 来说是普适的操作集合,无论 View 本身是基于什么库来实现的(NGUI,UGUI 等)

比如一个游戏中的按钮,一般来说都有以下功能集:

(1) 设置按钮 label 中的文字

(2) 修改按钮的背景图

(3) enable/disable 用户输入

(4) 当用户点击按钮时,进行 Notify

这些都是与 UI 具体实现无关的操作,在任何一个 UI toolkit 中都能找到这些操作。ViewPresenter 实现为一个 MonoBehaviour 被 Attach 到 NGUI View 的 Prefab 上,所以可以在 Controller 中通过 GameObject.GetComponent 来得到它来进行功能调用。由于 ViewPresenter 是游戏代码和 UI 代码的桥梁,所以它不能完全独立于屏幕渲染的底层实现。在这个例子中,ViewPresenter 需要持有 NGUI Widgets(比如 UIButton,UILabel 等)的引用,用来于它们进行交互。其实 ViewPresenter 实际上是一个适配器模式 (Adapter pattern) 的实现:  我们额外创建了定制的接口来对应用进行访问。这些引用必须在 Inspector 或者其他代码中进行设置。

幸运的是,这个设置过程可以部分自动化,可以参看 ViewPresenter.AutoPopulateDeclaredVidgets(),虽然 ViewPresenter 和某个特定的 UI 系统有耦合,但使用创建用于 Controller 的接口得到了一个好处:如果需要更换 GUI 库,只需要修改修改 ViewPresenter 的实现,而不需要修改 ViewPresenter 的接口以及 controller 的任何逻辑。

之所以将其称之为 ViewPresenter 是因为它与 Model-View-presenter 模式中的 Presenter 有些类似,只不过 Presenter 可以访问 Model,但 ViewPresenter 不可以。

ViewPreseter 可以持有一些 player 标识的状态,比如 View 存储了不同的颜色来提示 heath point( 以健康度作为基准来提示满血,充裕,虚弱之类的),这些值可以暴露出来作为公共属性,运行在运行时通过 inspector 来进行实时修改。但无论如何,ViewPresenter 不能持有应用程序逻辑的任何状态,而逻辑由 controller 管理,ViewPresenter 根本不应该知道什么样的 heath level 是表示 low。

比如,如果扩展 PlayerController 来处理 Hit points,可以增加一个方法来改变 label 的颜色,当处于 low health 的时候:

public class PlayerController
{   // ...
    void UpdateHitPointsUI()
    {
        if (Player.HasLowHitPoints)
        {
            HitPointsViewLabel.ShowLowHealthColor();
        }
        else
        {
            HitPointsViewLabel.ShowNormalHealthColor();
        }
    }

}

           如果你想创建一个非常特定或者复杂的 UI,并在工程中进行重用的话,这个方法可能有些过度设计了。在这个 Sample 代码中,我们很轻松地进行了如此处理:controller 只需要修改 ViewPresenter 中的一个 UnityEngine.Color 类型的属性。

Handling UI events

NGUI 提供了一个设计良好的时间系统,在任何定义了某事件 hander 的 MonoBehaviour 中都可以触发这个事件。这样就解耦了触发事件的 MonoBehaviour 和处理这个事件的 MonoBehaviour。然后,强大的功能增加了结构混乱的机会,因为可以使用任何 MonoBehaviour 作为事件的 handler,很随手的就把 scene 中的某个 monoBehaviour 拖放到 inspector 的 handler 上了,这样当创建一个包含了若干个控件的复杂 View 时,就容易得到一个难以 track 的依赖,这个依赖图的复杂度可以非常快速地增长。

为了防止代码中混乱的蔓延,我们遵守了一个非常容易的原则:所有的 View 的 UI 事件只能被 attatch 到这个 View 的 ViewPresenter 来进行处理。ViewPresenter 会捕获 NGUI 的事件,并 Raise 一个 .net 的 Event 作为回应。其他的代码只订阅那个. net 时间。这么做是因为需要将 UI 事件的具体实现与 NGUI 解耦,因为这样做我们使用代码中的事件,而不是 inspector 的事件。在我们的观点中,这是一个更安全的方法:可以很容易地在 IDE 中搜索处理这个 Event 的代码;而且更加类型安全:(如果你删除了一个处理这个 Event 的 MonoBehavior,你只会发现控件在 Play Mode 中停止工作了), 可以允许设置 Event 上的各个参数。当然我们需要在 View Presenter 中封装 NGUI 的 Event,但我们的使之自动化:看看这个代码 ButtonViewPresenter.wireUIEvents()


创建复杂的 View

现在已经有一些模块可以支持建造了,通过组合来创建一些更复杂的 View 变得容易:由若干个 UI prefab 来组合成一个新的 View, 并为这个组合出来的 view 创建一个新的 ViewPresenter, 将子 View 中的 ViewPresenter 暴露出来,这样就可以在 controller 中进行访问了。


这个组合出来的新 ViewPresenter,可以在这里找到。

结语

欢迎告知使用这种方法的感受,并欢迎告知你们在游戏 UI 开发中的独特方法,欢迎各种反馈。

另外,如果不使用 NGUI,而是使用 Unity3D 的 GUI,那么实现 ViewPresenter 的 OnGUI 方法为 Unity3D 自带的 GUI API 将会是一个不错的体验。

你可能感兴趣的:(Unity,unity3d,mvc,ui)