Unity應用架構設計(1)—— MVVM 模式的設計和實施(Part 1)

木宛城主發表於2019-02-26

初識 MVVM

談起 MVVM 設計模式,可能第一映像你會想到 WPF/Sliverlight,他們提供了的資料繫結(Data Binding),命令(Command)等功能,這讓 MVVM 模式得到很好的實現。MVVM 設計模式顧名思義,通過分離關注點,各司其職。通過 Data Binding 可達到資料的雙向繫結,而命令 Command 更是將傳統的 Code Behind 事件獨立到 ViewModel 中。

MVVM 設計模式在 WPF 中的實現

在WPF中,你會像如下這樣去定義一個專門管理檢視 View 的 ViewModel:

public class SongViewModel : INotifyPropertyChanged
{
    #region Construction
    /// Constructs the default instance of a SongViewModel
    public SongViewModel()
    {
        _song = new Song { ArtistName = "Unknown", SongTitle = "Unknown" };
    }
    #endregion

    #region Members
    Song _song;
    #endregion

    #region Properties
    public Song Song
    {
        get
        {
            return _song;
        }
        set
        {
            _song = value;
        }
    }

    public string ArtistName
    {
        get { return Song.ArtistName; }
        set
        {
            if (Song.ArtistName != value)
            {
                Song.ArtistName = value;
                RaisePropertyChanged("ArtistName");
            }
        }
    }
    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

    #region Methods

    private void RaisePropertyChanged(string propertyName)
    {
        // take a copy to prevent thread issues
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    #endregion
}複製程式碼

同時在 View 中你需要使用 Binding 將 ViewModel 的屬性繫結和控制元件的內容相繫結:

 <TextBox Content="{Binding ArtistName}" />複製程式碼

值得注意的是,要實現 View 和 ViewModel 雙向繫結,我們的 ViewModel 必須實現 INotifyPropertyChanged 介面,由於 WPF Framework 讓控制元件監聽了 PropertyChanged 事件,當屬性值發生時,觸發 PropertyChanged 事件,所以控制元件就能自動獲取到最新的值。反之,當控制元件的值發生改變時,例如 TextBox 觸發 OnTextChanged 事件,自動將最新的值同步到 ViewModel 相應的屬性中。

MVP & MVVM

Unity與 WPF/Sliverlight 不同,它沒有提供類似的 Data Binding,也沒有像 XAML 一樣的檢視語法,那麼怎樣才能在 Unity 3D 中去實現 MVVM 呢?

在 ASP.NET WebForm 時代,那時還沒有 ASP.Net MVC 。我們為了讓 UI 表現層分離,常常會使用 MVP 設計模式,以下是我在幾年前畫的一張老圖:

MVP 設計模式核心就是,通過定義一個 View,將 UI 抽象出來,它不必關心資料的具體來源,也不必關心點選按鈕之後業務邏輯的實現,它只關注 UI 互動。這就是典型的分離關注點。

其實這就是我今天想講的主題,既然 Unity 3D 沒有提供資料繫結,那麼我們也可以參考之前 MVP 的設計理念:

將 UI 抽象成獨立的一個個 View,將面向 Component 開發轉換為面向 View 開發,每一個 View 都有獨立的 ViewModel 進行管理,如下所示:

由於 Unity沒有 XAML,也沒有 Data Binding 技術,故只能在抽象出來的 View 中去實現類似於 WPF 的 Data Binding,Converter,Command 等。

值得注意的是,MVP 設計模式中資料的繫結是通過將具體的 View 例項傳遞到 Presenter 中完成的,而 MVVM 是以資料改變引發的事件中完成資料更新的。

MVVM 設計模式在 Unity 中的設計與實現

再回顧一下 WPF 中 ViewModel 的寫法。 ViewModel 提供了 View 需要的資料,並且 ViewModel 實現 INotifyPropertyChanged 介面 ,當資料更改時,觸發了 PropertyChanged 事件,由於控制元件也監聽了此事件,在事件的響應函式裡實現資料的更新。

瞭解了之後,我們要考慮怎樣在 Unity中去實現它。假設我們需要完成如下的一個功能,並且是使用 MVVM 設計思想實現:

首先,我們要定義一個 View,這個 View 是對 UI 元素的一個抽象,到底要抽象哪些 UI 元素呢?就這個例子而言,InputField,Label,Slider,Toggle,Button 是需要被抽象出來的。

public class SetupView
{
    public InputField nameInputField;
    public Text nameMessageText;
    public InputField jobInputField;
    public Text jobMessageText;

    public InputField atkInputField;
    public Text atkMessageText;

    public Slider successRateSlider;
    public Text successRateMessageText;

    public Toggle joinToggle;
    public Button joinInButton;
    public Button waitButton;
}複製程式碼

可以看到,這是一個很簡單的 View。接著我們需要定義一個專門用來管理 View 的 ViewModel,它以屬性的形式提供資料,以方法的形式提供行為。

值得注意的是,ViewModel 中的屬性不是特殊的屬性,它必須具備當資料更改時通知訂閱者這個功能,怎麼通知訂閱者?當然是事件,故我把此屬性稱為 BindableProperty 屬性。

public class BindableProperty<T>
{
    public delegate void ValueChangedHandler(T oldValue, T newValue);

    public ValueChangedHandler OnValueChanged;

    private T _value;
    public T Value
    {
        get
        {
            return _value;
        }
        set
        {
            if (!object.Equals(_value, value))
            {
                T old = _value;
                _value = value;
                ValueChanged(old, _value);
            }
        }
    }

    private void ValueChanged(T oldValue, T newValue)
    {
        if (OnValueChanged != null)
        {
            OnValueChanged(oldValue, newValue);
        }
    }

    public override string ToString()
    {
        return (Value != null ? Value.ToString() : "null");
    }
}複製程式碼

接著,我們再定義一個 ViewModel,它為 View 提供了資料和行為:

 public class SetupViewModel : ViewModel
{
    public BindableProperty<string> Name = new BindableProperty<string>();
    public BindableProperty<string> Job = new BindableProperty<string>();
    public BindableProperty<int> ATK = new BindableProperty<int>();
    public BindableProperty<float> SuccessRate = new BindableProperty<float>();
    public BindableProperty<State> State = new BindableProperty<State>();
}複製程式碼

有了 View 與 ViewModel 之後,我們需要考慮:

  • 怎樣為 View 指定一個 ViewModel
  • 當 ViewModel 屬性值改變時,怎樣訂閱觸發的 OnValueChanged 事件,從而達到 View 的資料更新

基於以上兩點,我們可以定義一個通用的 View,將它命名為 UnityGuiView

public interface IView
{
    ViewModel BindingContext { get; set; }
}複製程式碼
public class UnityGuiView:MonoBehaviour,IView
{
    public readonly BindableProperty<ViewModel> ViewModelProperty = new BindableProperty<ViewModel>();
    public ViewModel BindingContext
    {
        get { return ViewModelProperty.Value; }
        set { ViewModelProperty.Value = value; }
    }

    protected virtual void OnBindingContextChanged(ViewModel oldViewModel, ViewModel newViewModel)
    {
    }

    public UnityGuiView()
    {
        this.ViewModelProperty.OnValueChanged += OnBindingContextChanged;
    }

}複製程式碼
  • 上述程式碼中,提供一個 BindingContext 上下文屬性,類似於 WPF 中的 DataContext。 BindingContext 屬性我們不能將它視為一個簡單的屬性 ,它是上述定義過的 BindableProperty 型別屬性。那麼當為一個 View 的 BindingContext 指定 ViewModel 例項時,初始化時,勢必會觸發 OnValueChanged 事件。
  • 在響應函式 OnBindingContextChanged 中 ,我們可以在此對 ViewModel 中事件進行監聽,從而達到資料的更新。當然這是一個虛方法,你需要在子類 View 中 Override。

所以修改定義過的 SetupView,繼承自 UnityGuiView:

public class SetupView:UnityGuiView
{
   ...省略部分程式碼

   public SetupViewModel ViewModel { get { return (SetupViewModel)BindingContext; } }

   protected override void OnBindingContextChanged(ViewModel oldViewModel, ViewModel newViewModel)
    {

        base.OnBindingContextChanged(oldViewModel, newViewModel);

        SetupViewModel oldVm = oldViewModel as SetupViewModel;
        if (oldVm != null)
        {
            oldVm.Name.OnValueChanged -= NameValueChanged;
            ...
        }
        if (ViewModel!=null)
        {
            ViewModel.Name.OnValueChanged += NameValueChanged;
            ...
        }
        UpdateControls();
    }

    private void NameValueChanged(string oldvalue, string newvalue)
    {
        nameMessageText.text = newvalue.ToString();
    }
}複製程式碼

由於子類 Override 了 OnBindingContextChanged 方法,故它會對 ViewModel 的屬性值改變事件進行監聽,當觸發時,將最新的資料同步到 UI 中。

同理,考慮到雙向繫結,你也可以在 View 中定義一個 OnTextBoxValueChanged 響應函式,當文字框中的資料改變時,在響應函式中就資料同步到 ViewModel 中。在這我就不累述了。

最後,在 Unity 3D 中將 SetupView 附加到 相應的 GameObject上:

最後在攝像機上加一段指令碼,很簡單,傳入 SetupView 物件併為其繫結 ViewModel:

public SetupView setupView;
void Start()
{
    //繫結上下文
    setupView.BindingContext=new SetupViewModel();
}複製程式碼

小結

這是一個非常簡單的 MVVM 框架,也證明了在 Unity中實現 MVVM 設計模式的可能性。
原始碼託管在Github上,點選此瞭解

歡迎關注我的公眾號:

相關文章