首页 > 代码库 > Unity 之 Redux 模式 (Flux)

Unity 之 Redux 模式 (Flux)

作者:软件猫

日期:2016年12月6日

转载请注明出处: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 单向数据流。

如下图所示:

技术分享

(图片来自网络,侵删)

小明(User)在家打游戏,边看着屏幕,边用键盘鼠标控制游戏中的人物。

 

屏幕后面有个 ViewProvider(当然,小明才不管这个)。

ViewProvider 负责两个事情:

1、每一帧渲染前,根据数据(State)更新 GameObject 中的参数。

2、获取键盘鼠标的输入,然后向 Store 发 Action,告诉 Store,小明按了键盘??键

别的事情它就不管了。它不能亲自去修改 State 数据。

 

Store 也负责两件事情:

1、保存游戏的数据,这里我们叫 State。

2、建了一个处理管道,里面丢了一堆 Reducer。Action 来了以后,会丢进这个管道里。管道中的 Reducer 会判断这个 Action 自己是否关心,如果关心,则处理 Action 中承载的数据,并更新 State

 

它们两各司其职,并形成了一个单项数据流。

 

每个游戏通常只有一个 Store,集中管理游戏数据,方便 Load & Save。

Store 中的 State 是一个很大的数据树,保存了游戏中所有的数据。

通常建议这个树是扁平化的,一般只有两三层。这样在序列化和反序列化的时候可以得到更好的性能。

 

Unity 中的 GameObject 通常会对应一到多个 ViewProvider。

每个 ViewProvider 通常都会发出 Action。

每个 Action 都有对应的一到多个 Reducer 来处理数据。

 

实践1: 实现一个可以控制走动的小人

 

1、创建一个 Unity 2D 项目。

2、将下面的小人作为 Sprite 资源拖入 Project。
技术分享

3、将小人从 Project 中拖入 Scene,并重命名为 Player。

4、设置 Position 为 0,0,0。

5、设置 Rotation 为 0,0,90,让小人面向上方。

6、选中 Player,点击菜单 Component -> Physics 2D -> Rigidbody 2D,为小人添加刚体组件。

7、创建如下脚本,并拖放到 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。

首先创建如下脚本文件:

文件名描述
IActionAction 接口
IReducerReducer 接口
Store 
StateState 数据的根
ViewProviderPlayerViewProvider 的基类
PlayerActions存放多个 Player 相关的 Action
PlayerReducers存放多个 Player 相关的 Reducer
PlayerState保存和 Player 相关的 State
PlayerViewProvider继承 ViewProvider,实现 Action 和 Render

 

文件建好后,我们直接上代码:

 

1、IAction

public interface IAction{}

这个比较简单,一个空接口。用于识别 Action 而已。

 

2、IReducer

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

创建了一个接口,声明了 Reduce 方法。在 Store 管道中,循环调用所有的 Reducer,并执行这个方法。

方法传入当前的 State 和要处理的 Action。Reducer 判断如果是自己的 Action,则处理数据,并修改 State,然后将 State 返回。

注意:在 Redux 模式中,通常建议 State 是一个不变量,Reducer 不能修改它,而是创建一个修改过的 State 的副本,然后将其返回。

使用不变量有很多好处,比如我们可以轻松实现一个 Do - Undo 的功能。不过游戏里这个功能大多时候不太有用(特例:纸牌)

但是在 Unity 中,由于考虑到性能问题,这里还是舍弃了这个特性。

 

3、Store

using UnityEngine;using System;using System.Collections.Generic;using System.Linq;using System.Reflection;public class Store{    // 保存 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);        }    }}

Store 负责两件事情:1、保存 State,2、创建 Reducer 管道,用于处理 Action

 

4、State

// State 根。用于存放其他模块定义的 State。public class State{    // 变更标记。Reducer 如果更改了 State 中的数据,需要将此值设置为 True。    public bool IsChanged { get; set; }    // Player 模块定义的 State    public Player.PlayerState Player { get; private set; }    public State ()    {        Player = new Player.PlayerState ();    }}

注意:IsChanged 会被 Reducer 修改为 True,然后被 ViewProvider 执行渲染前更新游戏参数,并将其修改为 False,这是唯一一处会被 ViewProvider 修改的 State。

 

5、ViewProvider

using UnityEngine;// 继承了 MonoBehaviour,可用于附加到 GameObject 上public class ViewProvider : MonoBehaviour{    // 定义一个 LateUpdate 虚方法,它会在 LateUpdate 时被调用    protected virtual void LateUpdate ()    {        // 判断 State 是否被改变,如果被改变,则调用 OnStateChanged        if (Store.State.IsChanged) {            Store.State.IsChanged = false;            OnStateChanged (Store.State);        }    }    // 虚方法,需要在子类中实现 GameObject 的数据更新    protected virtual void OnStateChanged (State state)    {            }}

ViewProvider 基类。这里取了一个巧,我们并没有象 Redux 模块那样通过事件传递状态变更的消息,而是通过在 LateUpdate 时读取 State.IsChanged 来判断是否需要执行 OnStateChanged 方法。

得益于 Unity 的生命周期,在每一帧执行过程中,总是先执行 Update,然后执行 LateUpdate,再然后开始执行渲染过程。

我们在 Update 时调用 Action 然后通过 Reducer 修改 State。然后在 Late Update 的最后一步执行 OnStateChanged。

注意:ViewProvider 的子类中,override LateUpdate 时需要把 base.LateUpdate 放在最后执行。

 

说一下 FixedUpdate:

我们通过实验得知 FixedUpdate 其实是和 Update 在同一个线程上执行的,这样我们就不用担心 State.IsChanged 状态的线程同步问题。

然后,FixedUpdate 通常用于处理物理引擎相关的代码,执行完毕后改变游戏参数。

但是直到渲染过程执行时,这些参数才会体现出用途。

所以我们没必要在 FixedUpdate 后立即设置游戏参数,而是延迟到 LateUpdate 时再设置。

(如果上面的想法不正确,请轻喷,谢谢!)

 

1-5 我们把框架搭好了,下面开始实现 PlayerMovement 。

 

6、PlayerActions

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

 

7、PlayerReducers

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.IsChanged = true;            return state;        }    }}

InitReducer:读取了游戏的初始化数据,并传给State。它并不知道初始化数据是从哪里来的(也许是某个xml,或者来自网络),只管自己执行初始化动作。

AxisReducer:我们把 PlayerMovement 中的代码搬了过来。

 

8、PlayerState

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 构造函数中初始化。

 

9、PlayerViewProvider

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 OnStateChanged (State state)        {            if (rigid != null) {                // 刚体旋转和移动                rigid.MoveRotation (state.Player.Rotation);                rigid.MovePosition (state.Player.Position);            }        }    }}

最终,我们通过 PlayerViewProvider 将上面所有的代码连起来。

在 Start 时初始化数据,这里我们是直接取的 Unity 编辑器中的数据。真实游戏数据会来自网络或游戏存档。

在 FixedUpdate 时获取移动轴数据,然后执行 Action。

在 OnStateChanged 中改变刚体数据。

 

最后,我们需要把 PlayerViewProvider 拖到 Player 这个 GameObject 上,然后关掉实践1中的 PlayerMovement。

执行游戏!大功告成!

Unity 之 Redux 模式 (Flux)