HTML5 2D平台游戏开发#4状态机

  在实现了《HTML5 2D平台游戏开发——角色动作篇之冲刺》之后,我发现随着角色动作的增加,代码中的逻辑判断越来越多,铺天盖地的if() else()语句实在让我捉襟见肘:

这还仅仅是角色只有数个动作的情况下,如果后期角色动作越来越多,那么这种编码方式不仅容易出错,而且还难以维护,我意识到自己正在朝一个错误的方向前进。在做了一番调研后,发现有限状态机(Finite-state machine,简称FSM)是解决这类问题的方案之一。不过在使用状态机之前,首先要明确都有些什么状态,状态之间是如何切换的。在稿纸上画一张草图来整理一下思路:

 

可以发现,虽然现在角色只有四种状态,但按键分支已经达到八种,而且还没有考虑到在每个状态中虽然按下按键但不改变状态的情况,比如跳跃中按下A/D键能左右移动但还是跳跃状态。

下面就到了实现状态机的阶段了。状态机首先要有一个标识当前状态的成员,另外还需要一个设置这个成员的方法:

function FSM() {
    var activeState = null;

    //@param state {Function} 每一个状态对应一个执行函数
    this.setState = function(state) {
        activeState = state;
    };
    
    this.update = function() {
        if (activeState != null) {
            activeState();
        }
    };
}

var f = new FSM();
var flag = true;

f.setState(function() {
    console.log('现在是站立状态');
});


//模拟状态切换
(function updateState() {
    if (flag) {
        f.setState(function() {
            console.log('现在是移动状态');
        });
        flag = !flag;
    } else {
        f.setState(function() {
            console.log('现在是站立状态');
        });
        flag = !flag;
    }
    f.update();
    setTimeout(updateState, 1000);
})();

不过,这个状态机在游戏中不会用到😂,这里只是用来表述一种思路。还有一种是基于堆栈的状态机,有时称之为下推自动机(Pushdown automata)

这种状态机在工作时,只有栈顶的元素处于激活状态。

一次只允许一种状态激活,这样就方便了游戏在各种状态间进行切换,同时避免了代码逻辑混乱的问题。

在update中使用条件选择语句来进入各个分支:

update(dt) {
        switch (state) {
            case STATE.IDLE:    //空闲
                this.updateIdle(dt);
                break;
            case STATE.WALKING:    //移动
                this.updateWalking(dt);
                break;
            case STATE.JUMPING:    //跳跃
                this.updateJumping(dt);
                break;
            case STATE.DASHING:    //冲刺
                this.updateDashing(dt);
                break;
            case STATE.DASHING_JUMPING:   //冲刺跳
                this.updateDashingJumping(dt);
                break;
        }
}

再次回顾一下上面的思路草图,在空闲状态,角色能过渡到的状态有跳跃、移动、冲刺,代码实现如下:

//空闲
updateIdle(dt) {
    this.speed.x = 0;    //处于静止状态,速度为0

    if (key[65]) { //向左移动
        this.speed.x -= this.speedX;
        this.direction = -1;
        this.state = STATE.WALKING; //进入移动状态

        this.play();  //播放移动状态时的动画
    }

    if (key[68]) { //向右移动
        this.speed.x += this.speedX;
        this.direction = 1;
        this.state = STATE.WALKING;//同上

        this.play();
    }

    if (key[75]) { //跳跃
        if (!this.jumping) { //这里不用判断onGround,因为处于idle状态必然是onGround
            this.state = STATE.JUMPING;//进入跳跃状态
            this.jumping = true;
            this.speed.y = this.jumpSpeed;
        }
    }

    if (key[85]) { //冲刺
        if (!this.dashing) {
            this.dashLifeTime = CONFIG.MAX_DASH_LIFE_TIME;
            this.state = STATE.DASHING;//进入冲刺状态
            this.dashing = true;
            this.speed.x += CONFIG.DASH_SPEED * this.direction;
        }
    } else {
        this.dashing = false;
    }

    this.speed.y += this.gravity;
        
    //更新位置
    this.moveX(dt);
    this.moveY(dt);

    if (this.pos.y >= 9.375) {
        this.speed.y = 0;
        this.pos.y = 9.375;
        if (!key[75]) this.jumping = false;
    }
}

在上面的代码中,如果按下了移动键,则会进入移动状态,游戏再次循环时,就会执行updateWalking方法。如法炮制,就能很轻易地实现剩余的方法。

//移动
updateWalking(dt) {
    this.state = STATE.IDLE;
    this.speed.x = 0;

    if (key[65]) {
        this.speed.x -= this.speedX;
        this.state = STATE.WALKING;
        this.direction = -1;
    }

    else if (key[68]) {
        this.speed.x += this.speedX;
        this.state = STATE.WALKING;
        this.direction = 1;
    }

    if (key[75]) {
        if (!this.jumping) {
            this.state = STATE.JUMPING;
            this.jumping = true;
            this.speed.y = this.jumpSpeed;
        }
    } else {
        this.jumping = false;
    }

    if (key[85]) { //冲刺
        if (!this.dashing) {
            this.dashLifeTime = CONFIG.MAX_DASH_LIFE_TIME;
            this.state = STATE.DASHING;
            this.dashing = true;
            this.speed.x += CONFIG.DASH_SPEED * this.direction;
        }
    } else {
        this.dashing = false;
    }

    this.moveX(dt);
    this.moveY(dt);

    if (this.state === STATE.IDLE) this.play();
}

本篇结束,有空再继续更新。

P.S.在没有使用状态机之前,我考虑的是通过记录按键的顺序与组合来实现各种动作,既繁琐又容易出错,代码感觉都看不下去了,还好悬崖勒马,才避免了许多无用功😅。

 

posted @ 2017-05-01 22:02  逐影  阅读(998)  评论(0编辑  收藏  举报