Redux Hero Part 4:每个英雄都需要一个大反派(一种有趣的方式介绍 redux-saga)

栏目: IOS · Android · 发布时间: 5年前

内容简介:翻译自 Redux Hero 系列文章第 4 篇,原文链接请戳我。当你想到像但是为了让英雄轻松些,英雄是不会在每一次前进操作的时候都会碰到大怪兽的,大怪兽会随机地分布在大地图上的某个角落但不会出现在所有角落(否则英雄会忙不过来的)。所以这里的关键在于,

翻译自 Redux Hero 系列文章第 4 篇,原文链接请戳我。

当你想到像 《勇者斗恶龙》(Dragon Warrior)《最终幻想》(Final Fantasy) 这样经典的 RPG 游戏时,你就会发现这些类型的游戏内容是在一张大地图上面四处游荡然后与遇到的怪物展开战斗。

但是为了让英雄轻松些,英雄是不会在每一次前进操作的时候都会碰到大怪兽的,大怪兽会随机地分布在大地图上的某个角落但不会出现在所有角落(否则英雄会忙不过来的)。所以这里的关键在于, 随机性(Randomness)

但是在 Redux 的世界里,所有的东西都应该是纯函数(确定性和无副作用),所以随机性在哪里呢?

然后这里还有一个更大的问题: 我们连贯的应用逻辑(主角出生,闲逛地图,打怪兽,被怪兽打,扣血,主角领盒饭)一般应该放在哪里?(where does my application logic belong in general) 似乎我们更通常的做法是把这些连贯的应用逻辑分散到不同的角落里(逛地图的操作属于一个 redux 模块文件,主角状态和血量属于另外一个 redux 模块文件)。

幸运的是,这时候 redux-saga 出现了,提供了一种有效的解决方案。

首先,为我们这个英雄打怪兽领便当的过程定义伪代码:

loop while player is still alive
    wait for player to move
    are we in a safe place?
    randomly decide if there is a monster
    fight the monster
end loop
复制代码

当主角还活着

让我们来完善我们的第一行代码:

export function* gameSaga() {
    // 一直循环,只要主角血槽还没空
    let playerAlive = true;
    while (playerAlive) {
        // 活着的时候,你能做很多事情。
        // 所以要珍惜活着的每一份每一秒。
    }
}
复制代码

redux-saga 依赖 生成器(generator) ,我们这里不会对生成器进行详细介绍(因为我们的故事是英雄打怪兽)。想了解关于生成器的更多东西,请戳function on MDN 和 ES6 Generators by David Walsh

等待主角移动

然后我们来为主角移动创造一些 action 吧:我们会有一个 MOVE 的 action 类型和一个 move() 的 action 类型构造函数。我们键盘上的一些按键会派发这些 types 。

const Actions = {
    MOVE: 'MOVE',
    // ...
}
const move = ({ x, y }) => ({
    type: Actions.MOVE,
    payload: { x, y }
})
复制代码

一旦 dispatch ,action 就会被中间件拦住(这里我们的 redux-saga 就是其中之一的中间件)。这时候 redux-saga 把 action 壁咚了之后,就可以 猥琐欲为 了。

export function* gameSaga() {
    let playerAlive = true;
    while (playerAlive) {
        // 等待主角移动
        yield take(Actions.MOVE)
        // 只有当主角移动了才会进入到下一行来
    }
}
复制代码

take会阻塞 saga ,直到指定的 action 被 dispatch。但是注意,这里的阻塞不会阻塞 ui 或页面操作,一个 saga 其实很像 一个后台自动运行着的进程

主角是否安全(没碰到怪兽)

我们不希望在有公主的城堡房间里都会碰到怪兽忙着打架,这样主线故事就不浪漫了。

export function* gameSaga() {
    let playerAlive = true;
    while (playerAlive) {
        yield take(Actions.MOVE);
        
        // 主角是否安全
        const location = yield select(getLocation);
        if (location.safe) continue;
    }
}
复制代码

select允许我们从 store 中拿取 state。这里的 getLocation 是一个选择器,接收 state 作为它的参数:

export const getLocation = state => {
    const { x, y } = state.hero.position;
    return worldMap[ y, x ];
}
复制代码

redux-saga 代替我们访问 store ,并用 getState 获取状态传递给我们的选择器(selector)。

随机决定某个位置是否有怪兽

我们不能把 Math.random() 的调用放到 reducer 里,因为Redux 三大原则 之一是函数必须是纯的(没有副作用,禁止直接修改应用状态,而是返回一个新的状态)(只依赖于输入参数,同样的参数永远返回相同的结果,不管何时何地调用这个纯函数)。显然 Math.random() 是不纯的。

我们先来看一个坏的例子:

export const reducer = (state = {}, action) => {
    switch (action.type) {
        case Action.MOVE:
            const monsterProbability = Math.random(); // BAD!!
            if (monsterProbability > location.encounterThreshold) {
                // 我们的主角遇到了一只怪兽
            }
            return newState;
    }
}
复制代码

上面那个是坏例子,然而我们却可以在 saga 里面这样干:

export function* gameSaga() {
    let playerAlive = true;
    while (playerAlive) {
        yield take(Actions.MOVE);
        
        const location = yield select(getLocation);
        if (location.safe) continue;
        
        // 随机决定是否会遇到怪兽
        const monsterProbability = yield call(Math.random);
        if (monsterProbability < location.encounterThreshold) continue;
        // 我们的主角在这里遇到了一只怪兽
    }
}
复制代码

因为我们的 random 不是直接在 redux 内部使用的,所以并不会破坏 redux 的原则和纯函数性质。

打怪兽 !!

因为战斗的过程会比较复杂,所以我们创建另外的单独的 saga 来处理战斗过程。fightSaga 最终会返回一个布尔值(如果主角活下来了就返回 true ,否则返回 false):

export function* gameSaga() {
    let playerAlive = true;
    while (playerAlive) {
        yield take(Actions.MOVE);
        
        const location = yield select(getLocation);
        if (location.safe) continue;
        
        const monsterProbability = yield  (Math.random);
        if (monsterProbability < location.encounterThreshold) continue;
        
        // 打怪兽
        playerAlive = yield call(fightSaga);
    }
}
复制代码

下面给出 fightSaga 的伪代码实现:

begin loop
   monster's turn to attack
   is player dead? return false
   player fight options
   is monster dead? return true
end loop
复制代码

然后下面就是正式的 JavaScript 实现:

export function* fightSaga() {
    const monster = yield select(getMonster);
    
    while (true) {
        // 怪兽发起攻击 !!
        yield call(monsterAttackSaga, monster);
        
        // 主角死了没死 ??
        const playerHealth = yield select(getHealth);
        if (playerHealth <= 0) return false;
        
        // 主角发起攻击 !!
        yield call(playerFightOptionsSaga);
        
        // 怪兽死了没死 ??
        const monsterHealth = yield select(getMonsterHealth);
        if (monsterHealth <= 0) return true;
    }
}
复制代码

防止 saga 函数代码量过大时很重要的,我们可以把上面所有我们还没有实现的 saga 全部内联写到 fightSaga 里面,但是这样会使可阅读性下降,并且细分 saga 也更便于以后测试。所以这里 fightSaga 只负责声明顺序,怪兽攻击然后主角攻击,直到最后怪兽没了或者主角领便当了。

接下来我们来实现 怪兽攻击函数 monsterAttackSaga主角攻击函数 playerFightOptionsSaga

轮到怪兽攻击了

让我们思考一下游戏体验。对于玩家来说,如果怪兽在主角进行 move 移动操作后马上对他执行攻击(每走一步都会马上被挨揍,管你走没走出攻击区域),这会对玩家造成巨大的心灵创伤。所以我们会加入延迟,在主角移动之后的一段时间内,怪兽不会瞬间攻击我们的主角。

export function* monsterAttackSaga(monster) {
    // 等待一小段时间延迟
    yield call(delay, 1000);
    
    // 随机产生伤害数值
    let damage = monster.strength;
    const critProbablity = yield call(Math.random);
    if (critProbability >= monster.critThreshold) damage *= 2;
    
    // 华丽丽的攻击前预备动作
    yield put(animateMonsterAttack(damage));
    yield call(delay, 1000);
    
    // 攻击 !!
    yield put(takeDamage(damage));
}
复制代码

put是 redux-saga 用来*派发 action(dispatch action)*的。animateMonsterAttack() 会返回 { type:.. , payload: .. } 的 action 对象。

主角战斗函数

主角的战斗函数要比怪兽复杂些,因为主角不单单只是进行攻击,他是主角他还可以做其他事情(比如中途吻一下公主或者露出悲伤抑郁的神情,或者正常一点的就是喝药补血和放大招)。

wait for player to select an action
if attack, run the attack sequence
if potion, run the heal sequence
if run away, run the escape sequence
复制代码
export function* playerFightOptionsSaga() {
    // 等待玩家选择一个动作执行
    const { attack, heal, escape } = yield race({
        attack: take(Actions.ATTACK),
        heal: take(Actions.DRINK_POTION),
        escape: take(Actions.RUN_AWAY)
    });
    
    if (attack) yield call(playerAttackSaga);
    if (heal) yield call(playerHealSaga);
    if (escape) yield call(playerEscapeSaga);
}
复制代码

我们可以往 race 里面放任何东西 —— 甚至是执行另外一个 saga 函数 —— race 会取第一个执行完毕的函数结果返回,其他未执行完成的函数就会被取消掉。下面我们演示如何使用 race 来使玩家读存档:

export function* metaSaga() {
    // 等待静态资源(assets)加载
    // 展示片头动画
    // 等待玩家点击开始游戏
    
    // 开始游戏的同时,监听玩家读取存档操作
    while (true) {
        yield race({
            play: call(gameSaga),
            load: take(Actions.LOAD_GAME)
        })
    }
}
复制代码

LOAD_GAME action 会将 state 还原成初始状态 initialState,然后 saga 就会拦截到 LOAD_GAME 事件。从而使 load 执行完毕(resolve)而使得 play 执行中断(reject)(可以把 saga 的 race 想象成 Promise.race)。最终 redux-saga 就会中断掉游戏 gameSaga 的进行。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript: The Definitive Guide, 5th Edition

JavaScript: The Definitive Guide, 5th Edition

David Flanagan / O'Reilly Media / 2006-08-01 / USD 49.99

This Fifth Edition is completely revised and expanded to cover JavaScript as it is used in today's Web 2.0 applications. This book is both an example-driven programmer's guide and a keep-on-your-desk ......一起来看看 《JavaScript: The Definitive Guide, 5th Edition》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具