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