Unity 之 Redux 模式(第一篇)—— 人物移動

做者:軟件貓html

日期:2016年12月6日node

轉載請註明出處:http://www.cnblogs.com/softcat/p/6135195.html網絡

 

在朋友的慫恿下,終於開始學 Unity 了,因而有了這篇文章。架構

 

本文用一個控制小人移動的示例,講述如何在 Unity 中實現 Redux 架構。框架

關於 Flux、單向數據流是什麼,請參考 阮一峯 大大的文章 http://www.ruanyifeng.com/blog/2016/01/flux.html編輯器

 

Redux 是什麼鬼

 

Reflux是根據 Facebook 提出的 Flux 建立的 node.js 單向數據流類庫。它實現了一個簡化版的 Flux 單向數據流。ide

以下圖所示:函數

 小明(User)在家打遊戲,邊看着屏幕,邊用鍵盤鼠標控制遊戲中的人物。post

 

屏幕後面有個 ViewProvider(固然,小明才無論這個)。性能

ViewProvider 負責兩個事情:

一、每一幀渲染前,根據數據(State)更新 GameObject 中的參數。

二、獲取鍵盤鼠標的輸入,而後向 Store 發 Action,告訴 Store,小明按了鍵盤⬆️鍵

別的事情它就無論了。它不能親自去修改 State 數據。

 

Store 也負責兩件事情:

一、保存遊戲的數據,這裏咱們叫 State。

二、建了一個處理管道,裏面丟了一堆 Reducer。Action 來了之後,會丟進這個管道里。管道中的 Reducer 會判斷這個 Action 本身是否關心,若是關心,則處理 Action 中承載的數據,並更新 State

 

它們兩各司其職,並造成了一個單項數據流。

 

每一個遊戲一般只有一個 Store,集中管理遊戲數據,方便 Load & Save。

Store 中的 State 是一個很大的數據樹,保存了遊戲中全部的數據。

一般建議這個樹是扁平化的,通常只有兩三層。這樣在序列化和反序列化的時候能夠獲得更好的性能。

 

Unity 中的 GameObject 一般會對應一到多個 ViewProvider。

每一個 ViewProvider 一般都會發出 Action。

每一個 Action 都有對應的一到多個 Reducer 來處理數據。

 

實踐1: 用常規的方式實現一個能夠控制走動的小人

 

一、建立一個 Unity 2D 項目。

二、將下面的小人做爲 Sprite 資源拖入 Project。

三、將小人從 Project 中拖入 Scene,並重命名爲 Player。

四、設置 Position 爲 0,0,0。

五、設置 Rotation 爲 0,0,90,讓小人面向上方。

六、選中 Player,點擊菜單 Component -> Physics 2D -> Rigidbody 2D,爲小人添加剛體組件。

七、建立以下腳本,並拖放到 Player 上。這段腳本用於處理 Player

using UnityEngine;
using System.Collections;

public class PlayerMovement : MonoBehaviour
{
    [SerializeField]
    float speed = 3f;

    Rigidbody2D rigid;

    float ax, ay;

    void Start ()
    {
        rigid = GetComponent<Rigidbody2D> ();
    }

    void FixedUpdate ()
    {
        getInput ();
        rotate ();
        move ();
    }

    // 獲取搖桿輸入
    void getInput ()
    {
        ax = Input.GetAxis ("Horizontal");
        ay = Input.GetAxis ("Vertical");
    }

    // 處理旋轉
    void rotate ()
    {
        if (ax == 0 && ay == 0)
            return;

        float r = Mathf.Atan2 (ay, ax) * Mathf.Rad2Deg;

        rigid.MoveRotation (r);
    }

    // 處理移動
    void move ()
    {
        Vector2 m = new Vector2 (ax, ay);
        m = Vector2.ClampMagnitude (m, 1);

        Vector2 dest = (Vector2)transform.position + m;
        Vector2 p = Vector2.MoveTowards (transform.position, dest, speed * Time.fixedDeltaTime);

        rigid.MovePosition (p);
    }

}

咱們設置了一個 speed 參數,用於設置小人行走的速度。

咱們建立了 FixedUpdate 方法,接受搖桿輸入數據,而後分別處理小人的轉向和移動。

完成後點擊 Play ,小人能夠在 Game 視圖中經過方向鍵控制移動。

 

實踐2: 實現Redux模式

 

如今,咱們來實現 Redux。

首先建立以下腳本文件:

文件名 描述
IAction.cs Action 接口
IReducer.cs Reducer 接口
Store.cs 存放 State,構建 Reducer 管道
State.cs State 數據的根
ViewProvider.cs PlayerViewProvider 的基類
PlayerActions.cs 存放多個 Player 相關的 Action
PlayerReducers.cs 存放多個 Player 相關的 Reducer
PlayerState.cs 保存和 Player 相關的 State
PlayerViewProvider.cs 繼承 ViewProvider,實現 Action 和 Render

 

文件建好後,咱們直接上代碼:

 

一、IAction.cs

public interface IAction
{

}

這個比較簡單,一個空接口。用於識別 Action 而已。

 

二、IReducer.cs

public interface IReducer
{
    State Reduce (State state, IAction action);
}

建立了一個接口,聲明瞭 Reduce 方法。在 Store 管道中,循環調用全部的 Reducer,並執行這個方法。

方法傳入當前的 State 和要處理的 Action。Reducer 判斷若是是本身的 Action,則處理數據,並修改 State,而後將 State 返回。

注意:在 Redux 模式中,一般建議 State 是一個不變量,Reducer 並不直接修改它,而是建立一個修改過的 State 的副本,而後將其返回。

使用不變量有不少好處,好比咱們能夠輕鬆實現一個 Do - Undo 的功能。不過遊戲裏這個功能大多時候不太有用(特例:紙牌)

可是在遊戲開發中,因爲考慮到性能問題,這裏仍是捨棄了這個特性。

 

三、Store.cs

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

public class Store : MonoBehaviour
{
    // 保存 State 數據
    public static State State { get; private set; }

    // Reducer 列表
    static List<IReducer> reducerList;

    // 靜態構造函數
    static Store ()
    {
        State = new State ();

        // 反射獲取項目中全部繼承 IReducer 的類,生成實例,並加入 reducerList 列表
        reducerList = AppDomain.CurrentDomain.GetAssemblies ()
            .SelectMany (a => a.GetTypes ().Where (t => t.GetInterfaces ().Contains (typeof(IReducer))))
            .Select (t => Activator.CreateInstance (t) as IReducer)
            .ToList ();
    }

    // ViewProvider 調用 Dispatch 方法,傳入 Action
    // 循環調用全部的 Reducer,傳入當前的 State 與 Action
    // 將 Reducer 返回的 State 保存
    public static void Dispatch (IAction action)
    {
        foreach (IReducer reducer in reducerList) {
            State = reducer.Reduce (State, action);
        }
    }

    // 狀態改變事件
    public static Action<State> StateChanged;
    public static Action<State> FixedStateChanged;

    // FixedUpdate 時執行,監測 State 是否變動,並拋出 FixedStateChanged 事件
    void FixedUpdate ()
    {
        StartCoroutine (AfterFixedUpdate ());
    }

    IEnumerator AfterFixedUpdate ()
    {
        yield return new WaitForFixedUpdate ();

        if (!State.IsFixedStateChanged)
            yield break;

        State.IsFixedStateChanged = false;

        if (FixedStateChanged != null)
            FixedStateChanged (State);
    }

    // LateUpdate 時執行,監測 State 是否變動,並拋出 StateChanged 事件
    void LateUpdate ()
    {
        if (!State.IsStateChanged)
            return;

        State.IsStateChanged = false;

        if (StateChanged != null)
            StateChanged (State);
    }

}

Store 負責下面的事情:

 

a、保存 State

b、建立 Reducer 管道,用於處理 Action

c、在每個固定幀,全部的 GameObject 執行完 FixedUpdate 後,執行 AfterFixedUpdate,拋出 FixedStateChanged 事件。

詳見 Unity 之 AfterFixedUpdate,在全部 GameObject FixedUpdate 後執行

 

d、在 LateUpdate 時,拋出 StateChanged 事件。

 

因爲物理引擎須要使用固定幀率的 FixedUpdate,這裏把 FixedStateChanged 和 StateChanged 分開,分別拋出事件。

 

四、State.cs

// State 根。用於存放其餘模塊定義的 State。
public class State
{
    // 變動標記。Reducer 若是更改了 State 中的數據,須要將此值設置爲 True。
    public bool IsStateChanged { get; set; }

    // 物理引擎的數據變動單獨記錄
    public bool IsFixedStateChanged { get; set; }

    // Player 模塊定義的 State
    public Player.PlayerState Player { get; private set; }

    public State ()
    {
        Player = new Player.PlayerState ();
    }
}

IsStateChanged 會被 Reducer 修改成 True。Store 會經過 IsChanged 觸發 OnStateChanged 事件,並通知 ViewProvider。

一樣,IsFixedStateChanged = true 會觸發 OnFixedStateChanged 事件。

 

五、ViewProvider.cs

using UnityEngine;

// 繼承了 MonoBehaviour,可用於附加到 GameObject 上
public class ViewProvider : MonoBehaviour
{
    // 註冊 StateChanged 和 FixedStateChanged 事件
    protected virtual void Awake ()
    {
        Store.StateChanged += OnStateChanged;
        Store.FixedStateChanged += OnFixedStateChanged;
    }

    // 註銷 StateChanged 和 FixedStateChanged 事件
    protected virtual void OnDestroy ()
    {
        Store.StateChanged -= OnStateChanged;
        Store.FixedStateChanged -= OnFixedStateChanged;
    }

    // 處理狀態變動
    protected virtual void OnStateChanged (State state)
    {
        
    }

    // 處理物理引擎相關狀態變動
    protected virtual void OnFixedStateChanged (State state)
    {

    }

}

ViewProvider 基類。註冊/註銷 OnStateChanged 和 OnFixedStateChanged 事件。子類能夠 override 這兩個方法,實現相應的遊戲數據變動。

 

1-5 咱們把框架搭好了,下面開始實現 PlayerMovement 。

 

六、PlayerActions.cs

using UnityEngine;

namespace Player
{
    // Player 初始化,設置座標、旋轉角度與移動速度
    public class InitAction : IAction
    {
        public Vector2 position { get; set; }

        public float rotation { get; set; }

        public float speed { get; set; }
    }

    // 移動軸
    public class AxisAction : IAction
    {
        public float x { get; set; }

        public float y { get; set; }
    }
}

兩個 Action

 

七、PlayerReducers.cs

using UnityEngine;

namespace Player
{
    // 處理初始化過程
    public class InitReducer : IReducer
    {
        public State Reduce (State state, IAction action)
        {
            // 檢測 action 類型是否是本身想要的,若是不是,則說明本身不須要作什麼,直接返回 state 便可。
            if (!(action is InitAction))
                return state;

            InitAction a = action as InitAction;

            // 初始化 PlayerState
            state.Player.Position = a.position;
            state.Player.Rotation = a.rotation;
            state.Player.Speed = a.speed;

            return state;
        }
    }

    // 處理搖桿數據
    public class AxisReducer : IReducer
    {
        public State Reduce (State state, IAction action)
        {
            // 檢測 action 類型是否是本身想要的,若是不是,則說明本身不須要作什麼,直接返回 state 便可。
            if (!(action is AxisAction))
                return state;
            
            AxisAction a = action as AxisAction;

            // 若是搖桿在 0 點,則不須要處理數據,直接返回 state。
            if (a.x == 0 && a.y == 0)
                return state;

            // 根據 action 傳入的搖桿數據修改 state
            float speed = state.Player.Speed;
            Vector2 position = state.Player.Position;

            // 旋轉
            state.Player.Rotation = Mathf.Atan2 (a.y, a.x) * Mathf.Rad2Deg;

            // 位移
            Vector2 m = new Vector2 (a.x, a.y);
            m = Vector2.ClampMagnitude (m, 1);

            Vector2 dest = position + m;
            state.Player.Position = Vector2.MoveTowards (position, dest, speed * Time.fixedDeltaTime);

            // 每次修改 state 以後,須要告訴 state 已經被修改過了
            state.IsFixedStateChanged = true;

            return state;
        }
    }

}

InitReducer:讀取了遊戲的初始化數據,並傳給State。它並不知道初始化數據是從哪裏來的(也許是某個xml,或者來自網絡),只管本身執行初始化動做。

AxisReducer:咱們把 PlayerMovement 中的代碼搬了過來。

 

八、PlayerState.cs

using UnityEngine;

namespace Player
{
    public class PlayerState
    {
        // 玩家座標
        public Vector2 Position { get; set; }

        // 玩家面向的方向
        public float Rotation { get; set; }

        // 移動速度
        public float Speed { get; set; }
    }
}

這個文件寫好後,在 State 中加入 PlayerState 類型的屬性,並在 State 構造函數中初始化。

 

九、PlayerViewProvider.cs

using UnityEngine;

namespace Player
{
    public class PlayerViewProvider: ViewProvider
    {
        [SerializeField]
        float speed = 3f;

        Rigidbody2D rigid = null;

        void Start ()
        {
            rigid = GetComponent<Rigidbody2D> ();

            // 執行初始化
            Store.Dispatch (new InitAction () {
                position = transform.position,
                rotation = transform.rotation.eulerAngles.z,
                speed = this.speed,
            });
        }

        void FixedUpdate ()
        {
            // 獲取軸數據,並傳遞 Action
            float ax = Input.GetAxis ("Horizontal");
            float ay = Input.GetAxis ("Vertical");

            if (ax != 0 || ay != 0) {
                Store.Dispatch (new AxisAction () { x = ax, y = ay });
            }
        }
            
        protected override void OnFixedStateChanged (State state)
        {
            if (rigid != null) {
                // 剛體旋轉和移動
                rigid.MoveRotation (state.Player.Rotation);
                rigid.MovePosition (state.Player.Position);
            }
        }

    }
}

最終,咱們經過 PlayerViewProvider 將上面全部的代碼連起來。

在 Start 時初始化數據,這裏咱們是直接取的 Unity 編輯器中的數據。真實遊戲數據會來自網絡或遊戲存檔。

在 FixedUpdate 時獲取移動軸數據,而後執行 Action。

在 OnFixedStateChanged 中改變剛體數據。

 

腳本寫好後,咱們建立一個空 GameObject,重命名爲 Store,拖入 Store 腳本。

而後把 PlayerViewProvider 拖到 Player 這個 GameObject 上,並關掉實踐1中的 PlayerMovement。

執行遊戲!大功告成!

 

重要!這一篇旨在說明 Redux 模式。實際開發中,Rigidbody2D.MovePosition 會根據碰撞物來決定最終的 Position 和 Rotation。在下一篇,咱們會針對這個問題進行改造。

相關文章
相關標籤/搜索