模型视图控制器(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.