模型檢視控制器(MVC)設計模式

模型檢視控制器是一種非常常見的設計模式,已經存在了相當長的一段時間。這種模式側重於通過將類分成功能部分來減少義大利麵條程式碼。最近我一直在 Unity 中嘗試這種設計模式,並希望列出一個基本的例子。

MVC 設計由三個核心部分組成:模型,檢視和控制器。

模型: 模型是表示物件資料部分的類。這可以是玩家,庫存或整個級別。如果程式設計正確,你應該能夠使用此指令碼並在 Unity 之外使用它。

請注意有關模型的一些事項:

  • 它不應該繼承 Monobehaviour
  • 它不應包含 Unity 特定的可移植性程式碼
  • 由於我們避免使用 Unity API 呼叫,這可能會妨礙 Model 類中的隱式轉換器(需要解決方法)

Player.cs

using System;

public class Player
{
    public delegate void PositionEvent(Vector3 position);
    public event PositionEvent OnPositionChanged;

    public Vector3 position 
    {
        get 
        {
            return _position;
        }
        set 
        {
            if (_position != value) {
                _position = value;
                if (OnPositionChanged != null) {
                    OnPositionChanged(value);
                }
            }
        }
    }
    private Vector3 _position;
}

Vector3.cs

與我們的資料模型一起使用的自定義 Vector3 類。

using System;

public class Vector3
{
    public float x;
    public float y;
    public float z;

    public Vector3(float x, float y, float z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

檢視: 檢視是表示與模型關聯的檢視部分的類。這是從 Monobehaviour 派生的合適類。這應包含直接與 Unity 特定 API 互動的程式碼,包括 OnCollisinEnterStartUpdate 等…

  • 通常繼承自 Monobehaviour
  • 包含 Unity 特定程式碼

PlayerView.cs

using UnityEngine;

public class PlayerView : Monobehaviour
{
    public void SetPosition(Vector3 position)
    {
        transform.position = position;
    }
}

控制器: 控制器是一個將模型和檢視繫結在一起的類。控制器使模型和檢視保持同步以及驅動器互動。控制器可以偵聽來自任一夥伴的事件並相應地更新。

  • Binds both the Model and View by syncing state
  • Can drive interaction between partners
  • Controllers may or may not be portable (You might have to use Unity code here)
  • If you decide to not make your controller portable, consider making it a Monobehaviour to help with editor inspecting

PlayerController.cs

using System;

public class PlayerController
{
    public Player model { get; private set; }
    public PlayerView view { get; private set; }

    public PlayerController(Player model, PlayerView view)
    {
        this.model = model;
        this.view = view;

        this.model.OnPositionChanged += OnPositionChanged;
    }

    private void OnPositionChanged(Vector3 position)
    {
        // Sync
        Vector3 pos = this.model.position;

        // Unity call required here! (we lost portability)
        this.view.SetPosition(new UnityEngine.Vector3(pos.x, pos.y, pos.z));
    }
    
    // Calling this will fire the OnPositionChanged event 
    private void SetPosition(Vector3 position)
    {
        this.model.position = position;
    }
}

Final Usage

Now that we have all of the main pieces, we can create a factory that will generate all three parts.

PlayerFactory.cs

using System;

public class PlayerFactory
{
    public PlayerController controller { get; private set; }
    public Player model { get; private set; }
    public PlayerView view { get; private set; }

    public void Load()
    {
        // Put the Player prefab inside the 'Resources' folder
        // Make sure it has the 'PlayerView' Component attached
        GameObject prefab = Resources.Load<GameObject>("Player");
        GameObject instance = GameObject.Instantiate<GameObject>(prefab);
        this.model = new Player();
        this.view = instance.GetComponent<PlayerView>();
        this.controller = new PlayerController(model, view);
    }
}

And finally we can call the factory from a manager…

Manager.cs

using UnityEngine;

public class Manager : Monobehaviour
{
    [ContextMenu("Load Player")]
    private void LoadPlayer()
    {
        new PlayerFactory().Load();
    }
}

Attach the Manager script to an empty GameObject in the scene, right click the component and select “Load Player”.

For more complex logic you can introduce inheritance with abstract base classes and interfaces for an improved architecture.