码迷,mamicode.com
首页 > 编程语言 > 详细

Unity 之 Redux 模式 (Flux)

时间:2016-12-06 14:24:20      阅读:575      评论:0      收藏:0      [点我收藏+]

标签:pat   日期   序列化   style   speed   mpm   protected   any   在家   

作者:软件猫

日期: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。

首先创建如下脚本文件:

文件名 描述
IAction Action 接口
IReducer Reducer 接口
Store  
State State 数据的根
ViewProvider PlayerViewProvider 的基类
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)

标签:pat   日期   序列化   style   speed   mpm   protected   any   在家   

原文地址:http://www.cnblogs.com/softcat/p/6135195.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!