17-回合制游戏的流程和状态机
9.3.8 回合制游戏的流程和状态机
游戏的流程和逻辑将被分割为 16 个独立状态。整体游戏通过定时器以每秒40帧运行。
switchGameState(GAME_STATE_INIT);
var FRAME_RATE = 40;
var intervalTime = 1000/FRAME_RATE;
setInterval(runGame, intervalTime )
与其他游戏(例如第 8 章或本章之前的游戏)类似,使用一个函数引用状态机来执行当前的游戏状态。使用switchGameState()函数切换游戏状态。首先简要讨论这个函数,然后介绍其他的游戏函数。
提示
此处不会罗列每一行代码,也不会剖析细节。请读者将本节视作代码的使用指南。游戏的全部代码将在本章结尾展示(见例9-3)。目前,读者已经接触了大部分创建游戏逻辑的代码和概念。接下来的章节将介绍新的概念和代码。
1.GAME_STATE_INIT
这个状态用于加载游戏所需的资源,仅需要为微型坦克迷宫加载一个图片表,不需要加载声音。
在初始化加载之后,该状态将向状态机发送GAME_STATE_WAIT_FOR_LOAD状态,直到加载事件被触发。
2.GAME_STATE_WAIT_FOR_LOAD
这个状态只是确保在GAME_STATE_INIT中所有的资源都被正确加载。此后,将向状态机发送GAME_STATE_TITLE状态。
3.GAME_STATE_TITLE
这个状态将显示标题屏幕,然后等待用户按下空格键。用户按下空格键之后,将向状态机发送GAME_STATE_NEW_GAME状态。
4.GAME_STATE_NEW_GAME
这个状态会重置所有的游戏数组和对象,然后调用createPlayField()函数。在createPlayField()函数中,将为新游戏创建playField数组和enemy数组,并为player对象设置新的起始位置。此操作结束后将调用renderPlayField()函数一次,用于在游戏屏幕上显示初始化的游戏板。
以上操作完成后,状态机将准备进入真正的游戏循环,通过向游戏状态机发送GAME_STATE_WAIT_FOR_PLAYER_MOVE状态实现。
5.GAME_STATE_WAIT_FOR_PLAYER_MOVE
这个状态等待用户按下4个方向键中的一个。当用户按下方向键之后,switch语句将检测用户按下了哪个方向键。根据被按下的方向键调用checkBounds()。
提示
这个状态中包含了一些用于区块移动逻辑的新代码,是之前在本书中没有介绍过的。这些概念的更多细节请读者参考9.3.9节”。
checkBounds()函数接受以下3个参数:
(1)玩家当前所在行的增加量;
(2)玩家当前所在列的增加量;
(3)待检测的对象(玩家或者一辆敌方坦克)。
此函数的唯一目的是检查待检测的对象是否可以在指定的方向上移动。在本游戏中,只有移动到屏幕外面是不合法的。在类似的Pac-Man的游戏中,此函数还将继续检查,以确保目的地不是墙体区块。本章游戏没有进行类似的检查,因为允许player和enemy对象能够失误撞到墙上,并被摧毁。
如果玩家在用户按下的方向可以合法移动,就调用setPlayerDestination()函数。这个函数只是简单地根据新区块的位置设置player.destinationX和player.destinationY属性。
checkBounds()函数设置player.nextRow和player.nextCol属性。setPlayerDestination()函数将player.nextRow和player.nextCol乘以区块大小(32)得到player.destinationX和player.destinationY属性的值。这两个属性用于将玩家移动到新位置。
最后,将GAME_STATE_ANIMATE_PLAYER设置为当前游戏状态。
6.GAME_STATE_ANIMATE_PLAYER
这个状态函数会将玩家移动到它的destinationX和destinationY属性所指定的位置。由于这是一个回合制游戏,因此在移动发生时没有做任何处理。
在每一次迭代中,player.currentTile将增加1。这样将使用playerTiles数组中的下一个图片渲染该区块。当destinationX和destinationY等于player的x和y属性的值时,移动动画将停止。游戏状态将变为GAME_STATE_EVALUATE_PLAYER_MOVE状态。
7.GAME_STATE_EVALUATE_PLAYER_MOVE
现在,玩家已经移动到下一个区块。player.row 和 player.col 属性将被分别设置为player.nextRow和player.nextCol的值。
接下来,如果玩家在目标区块上,就将player.win属性设为true。如果玩家在一个墙体区块上,就将player.hit设置为true。
然后遍历所有enemy对象,看看有没有敌方坦克与玩家位于相同的区块上。如果有,则玩家和敌方坦克的hit属性都要被设为true。
接下来,将游戏转换到GAME_STATE_ENEMY_MOVE状态。
8.GAME_STATE_ENEMY_MOVE
这个状态使用自制的人工智能追踪算法为每个敌方坦克选择一个移动方向。9.3.11 节将对算法进行讨论。为了实现这个操作,程序会遍历所有坦克,并分别进行逻辑 设置。
这个函数首先使用一个基于区块的运算来判断玩家位置与一辆敌方坦克的关系。然后,根据函数运算结果创建一组用于测试的方向。函数将这些方向以字符串的形式存储在名为directionsToTest的变量中。
接下来,函数使用chanceRandomMovement的值(25%)来确定函数是使用刚刚创建的方向列表决定前进方向,还是随机选择前进方向。
不论是何种情况,函数都必须检查所有可行的前进方向(不论是在directionsToMove中的方向,还是随机移动的4个方向),从中选择第一个不会让坦克移动到屏幕外边的方向。
当函数选定移动方向之后,函数将像player对象那样,通过使用相同的区块大小乘以x和y属性的方式设置敌方坦克的destinationX和destinationY的值。
最后,函数将游戏状态设为GAME_STATE_ANIMATE_ENEMY状态。
9.GAME_STATE_ANIMATE_ENEMY
与GAME_STATE_ANIMATE_PLAYER状态类似,这个状态将坦克用动画方式移动到destinationX和destinationY所代表的新位置。在这个状态中,必须为每一辆敌方坦克进行该操作。所以,使用enemyMoveCompleteCount变量来保存有多少敌方坦克已经完成移动。
当所有敌方坦克都完成移动时,游戏状态将切换到GAME_STATE_EVALUATE_OUTCOME状态。
10.GAME_STATE_EVALUATE_ENEMY_MOVE
与GAME_STATE_EVALUATE_PLAYER_MOVE状态类似,这个状态将检查每辆坦克的位置,以判断将摧毁哪辆坦克。
如果一辆坦克与玩家、墙体或另一辆坦克占用了相同的区块,该坦克则“将被摧毁”。如果玩家和敌方坦克占用了同一个区块,玩家也“将被摧毁”。通过将敌方坦克或玩家的hit属性设为true表示“将被摧毁”的状态。
然后,游戏进入GAME_STATE_EVALUATE_OUTCOME状态。
11.GAME_STATE_EVALUATE_OUTCOME
这个状态函数将遍历每一辆敌方坦克和玩家坦克来判断其hit属性是否被设为true。如果是,则该坦克的dead属性将被设为true,然后调用createExplode()函数并传入对象实例(玩家或敌方坦克)来创建一个爆炸效果。如果是敌方坦克,将从enemy数组中移除死掉的敌方坦克。
接下来,游戏状态将变为GAME_STATE_ANIMATE_EXPLODE状态。
12.GAME_STATE_ANIMATE_EXPLODE
如果explosions数组的长度大于0,那么这个状态函数将遍历数组中的每一个实例,并使用explodeTiles数组播放爆炸动画。在动画播放完成之后,从explosions数组中移除每一个爆炸实例。当explosions数组长度为0时,游戏将进入GAME_STATE_CHECKFOR GAME_OVER状态。
13.GAME_STATE_CHECK_FOR_GAME_OVER
这个状态首先检查玩家是否已经死了,然后检查玩家是否获胜。这意味着,如果一辆敌方坦克与玩家同时抵达目标区块,玩家将不能获胜。
如果玩家失败,游戏状态将变为GAME_STATE_PLAYER_LOSE状态;如果玩家获胜,游戏状态将变为GAME_STATE_PLAYER_WIN状态。如果两种情况都没发生,游戏将进入GAME_STATE_WAIT_FOR_PLAYER_MOVE状态。这将重新开始游戏循环,玩家将开始下一回合。
14.GAME_STATE_PLAYER_WIN
如果玩家获胜,将在下一轮游戏中增加maxEnemy变量。系统还会将玩家的得分与当前会话中的最高分进行比较,判断玩家是否得到了新的最高分。在这个状态中,将等待用户按下空格键,然后游戏进入GAME_STATE_NEW_GAME状态。
15.GAME_STATE_PLAYER_LOSE
将玩家的得分与当前会话中的最高分进行比较,以判断玩家是否获得了新的最高分。在这个状态中,将等待用户按下空格键,然后游戏进入GAME_STATE_NEW_GAME状态。