MVP Pattern

MVC模式(Model-View-Controller):MVC模式在開發UI中是一種非常受歡迎的設計模式。

  • MVC主要想法為:將程式劃分為模型(Model)、視圖(View)和控制器(Controller
    • Model:主要負責管理資料邏輯。Model拿到資料後將資料交給View
      • 在此可以直接與資料庫、API 或其他資料來源交互。
      • 在此不執行遊戲邏輯或是執行運算。
    • View:負責顯示資料。
      • 它從Model中獲取資料,並將這些資料呈現給使用者。View僅關注如何顯示資料。
    • Controller:充當ModelView之間的溝通的橋樑。
      • 它接收使用者的輸入,呼叫Model來處理這些輸入,然後選擇適當的View來顯示處理結果。
      • 可以把它想像為大腦,它處理遊戲資料並在執行期間計算資料是如何改變的。
  • MVC模式符合單一職責原則,每個部分都只負責自己的部分。
  • 在Unity中,UI Toolkit或是Unity UI很自然地負責擔任View
  • 缺點
    • MVC模式中三者相互依賴,一但更新了其中一個,另外兩個也須跟著修改。
    • Controller的程式碼會隨著功能的添加越來越臃腫。
graph TD
    A[ User ] -->| Interacts with | B[ View ]
    B -->| Sends user input to | C[ Controller ]
    C -->| Updates | D[ Model ]
    D -->| Notifies changes to | B[ View ]
    B -->| Displays data from | D[ Model ]

MVP模式(Model-View-Presenter):MVP是MVC的一種變體

  • MVP將Controller改爲Presenter,並改變了通信方向。當Model拿到資料之後,不直接給View更新,而是交給Presenter,之後Presenter再把資料交給ViewView再更新畫面。
    • View:只負責收到使用者回饋,之後呼叫Presenter拿取資料,並在接收到資料時,更新畫面。
    • Model:被動的接收到Presenter命令,拿取資料,並回傳給Presenter
    • PresenterModelView之間的的橋樑,與ViewModel溝通。
  • 從三者相互依賴變成都只依賴Presenter
    • M <=> P <=> V 之間雙向通信但ViewModel不通信
  • Presenter的程式碼會隨著功能的添加越來越臃腫。
graph TD
    A[ User ] -->| Interacts with  | B[ View ]
    B -->| Sends user input to | C[ Presenter ]
    C -->| Updates | D[ Model ]
    D -->| Notifies changes to | C[ Presenter ]
    C -->| Updates | B[ View ]
    B -->| Displays data from | C[ Presenter ]

使用MVP模式製作生命條UI:
Health類

  • 在此Health類的身份是Model,保存真正的生命資料
  • 含有一個HealthChanged event,每當有更改生命值的動作時,都會呼叫這個event。
  • Health類只負責增加,減少生命值,符合符合單一職責原則
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class Health : MonoBehaviour
    {
    public event Action HealthChanged;

    private const int minHealth = 0;
    private const int maxHealth = 100;
    private int currentHealth;

    public int CurrentHealth
    {
    get => currentHealth;
    set => currentHealth = value;
    }

    public int MinHealth => minHealth;
    public int MaxHealth => maxHealth;

    public void Increment(int amount)
    {
    currentHealth += amount;
    currentHealth = Mathf.Clamp(currentHealth, minHealth, maxHealth);
    UpdateHealth();
    }

    public void Decrement(int amount)
    {
    currentHealth -= amount;
    currentHealth = Mathf.Clamp(currentHealth, minHealth, maxHealth);
    UpdateHealth();
    }

    public void Restore()
    {
    currentHealth = maxHealth;
    UpdateHealth();
    }

    private void UpdateHealth()
    {
    HealthChanged?.Invoke();
    }
    }

HealthPresenter類

  • HealthPresenter類含有Health類的依賴,用來操控Health
  • 其他物件不會直接操控Health類,而是透過HealthPresenter類暴露的DamageHeal以及Reset來操控
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    public class HealthPresenter : MonoBehaviour
    {
    // 在MVP中的Model
    [SerializeField] private Health health;

    // 在MVP中的View
    [SerializeField] private Slider healthSlider;

    private void Start()
    {
    if (health != null)
    {
    health.HealthChanged += OnHealthChanged;
    }
    UpdateView();
    }

    private void OnDestroy()
    {
    if (health != null)
    {
    health.HealthChanged -= OnHealthChanged;
    }
    }

    public void Damage(int amount)
    {
    health?.Decrement(amount);
    }

    public void Heal(int amount)
    {
    health?.Increment(amount);
    }

    public void Reset()
    {
    health?.Restore();
    }

    public void UpdateView()
    {
    if (health == null) return;

    if (healthSlider != null && health.MaxHealth != 0)
    {
    healthSlider.value = (float)health.CurrentHealth / health.MaxHealth;
    }
    }

    private void OnHealthChanged()
    {
    UpdateView();
    }
    }

ClickDamage類:為使用HealthPresenter的類

  • 不直接操控 Model (Health)與 View (healthSlider)而是透過HealthPresenter
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    [RequireComponent(typeof(HealthPresenter), typeof(Collider))]
    public class ClickDamage : MonoBehaviour
    {
    private Collider collider;
    private HealthPresenter healthPresenter;
    [SerializeField] private LayerMask layerToClick;
    [SerializeField] private int damageValue = 10;

    private void Start()
    {
    collider = GetComponent<Collider>();
    healthPresenter = GetComponent<HealthPresenter>();
    }

    private void Update()
    {
    if (Input.GetMouseButtonDown(0))
    {
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    if (Physics.Raycast(ray, Mathf.Infinity, layerToClick))
    {
    healthPresenter?.Damage(damageValue);
    }
    }
    }
    }

Reference: https://github.com/Unity-Technologies/game-programming-patterns-demo/tree/main/Assets/12%20MVP
https://github.com/push-pop/Unity-MVVM/blob/main/Docs/Architechture.md

評論