还记得贪吃蛇这个经典游戏吗?在诺基亚时代,在黑白像素点游戏机时代,就是这样一个简单的游戏也能让我们玩上几个小时。
在这篇文章,我们将使用HTML5来重现这个游戏,基于著名的开源HTML5游戏框架——Phaser。你将了解到游戏精灵、游戏状态,以及如何使用预加载(preload)、创建(create)与刷新(update)方法。最终效果呈现如下:
一、开发准备
首先访问Phaser官网,下载JavaScript版本的Phaser:http://www.phaser.io/download/stable,选择用于生产环境的压缩版phaser.min.js。
项目文件结构如下:
打开index.html,链接五个js文件,并添加页面标题,启动游戏时打开此文件即可:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>贪吃蛇</title>
<script src="assets/js/phaser.min.js"></script>
<script src="assets/js/menu.js"></script>
<script src="assets/js/game.js"></script>
<script src="assets/js/gameover.js"></script>
<script src="assets/js/main.js"></script>
<style> body{ padding: 0; margin: 0; } canvas{ margin: 0 auto; } </style>
</head>
<body>
</body>
</html>
二、游戏是如何组织的
基于Phaser的游戏是围绕“状态(state)”进行组织的,此处的“状态”可以看作是游戏的不同阶段,贪吃蛇游戏的状态较少,可简易的分为三个状态:
- 菜单状态,由menu.js处理,仅用于显示开始界面,点击转换到游戏状态。
- 游戏状态,由game.js处理,用于显示游戏界面、控制贪吃蛇运动,死亡后进入游戏结束状态。
- 游戏结束状态,由gameover.js处理,用于显示结束界面、最终得分,点击再次回到游戏状态。
main.js为主JavaScript文件,在其中创建游戏实例,注册各个游戏状态。
1、加载图像
到现在为止,我们仅仅预构了游戏框架,接下来我们来创建菜单状态,让它来显示游戏开始界面。
在HTML文件中我们已经引入了Phaser库,这使我们拥有了一个名为Phaser
的全局对象,通过这个对象,我们可以访问Phaser库中哪些用于构建游戏的方法和函数。
现在我们使用Phaser
对象来创建一个游戏实例,这个对象用来代表整个游戏,我们会为他添加不同的状态。
main.js
// JavaScript Document
var game;
//新建一600px宽、450px高的游戏实例
//Game对象用于管理启动、创建子系统、运行逻辑、渲染
//第三个参数表示要使用的渲染器
//第四个参数表示父级DOM元素
game = new Phaser.Game(600, 450, Phaser.AUTO, '');
//添加菜单状态
//第一个参数表示如何调用状态
//第二个参数是一个包含状态功能所需方法的对象
game.state.add('Menu', Menu);
game.state.start('Menu');
接下来初始化菜单状态对象(Menu
),在menu.js中定义一个新对象Menu
并为它添加函数。状态启动时,首先会调用preload
函数,加载游戏所需资源;加载完成后,调用create
函数,初始化游戏区域以及其他需要初始化的内容。
menu.js
// JavaScript Document
var Menu = {
preload: function () {
//加载图像以便于在其上添加游戏精灵
//第一个参数表示图像名称
//第二个参数表示文件路径
game.load.image('menu', './assets/images/menu.png');
},
create: function () {
//添加一个游戏精灵,此处添加的精灵为游戏logo
//参数以此为:X,Y,图像名称(见上)
this.add.sprite(0,0,'menu');
}
};
到此,在浏览器中打开index.html,即可看到游戏开始界面,但还无法点击。(由于浏览器的安全限制,可能无法启动游戏,那么则需要一个本地web服务器,具体参看:http://phaser.io/tutorials/getting-started/part2)
2、绘制贪吃蛇
如之前所提,Game状态才是真正的游戏状态,也是绘制贪吃蛇的位置。与Menu状态一样,我们也需要在main.js中注册Game状态:
var game;
game = new Phaser.Game(600, 450, Phaser.AUTO, '');
game.state.add('Menu', Menu);
//添加游戏状态
game.state.add('Game', Game);
game.state.start('Menu');
此外,还需要在menu.js中添加额外代码以便能够启动游戏状态。为此,我们将精灵替换为按钮,添加按钮的方法与精灵基本类似,只需提供一个点击时调用函数给它即可。以下是最终的menu.js:
// JavaScript Document
var Menu = {
preload: function () {
//加载图像以便于在其上添加游戏精灵
//第一个参数表示图像名称
//第二个参数表示文件路径
game.load.image('menu', './assets/images/menu.png');
},
create: function () {
//添加一个游戏精灵,此处添加的精灵为游戏logo
//参数以此为:X,Y,图像名称(见上)
//this.add.sprite(0,0,'menu');
//开始屏幕
//menu图像作为按钮用来启动游戏
this.add.button(0, 0, 'menu', this.startGame, this);
},
startGame: function () {
//转换状态为游戏状态
this.state.start('Game');
}
};
接下来处理Game状态,绘制贪吃蛇,结构与Menu类似:
// JavaScript Document
//设置为全局变量,以便更新功能能够随时更新这些变量
var snake, apple, squareSize, score, speed,
updateDelay, direction, new_direction,
addNew, cursors, scoreTextValue, speedTextValue,
textStyle_Key, textStyle_Value;
var Game = {
preload: function () {
//加载游戏所需资源
//贪吃蛇与食物
game.load.image('snake', './assets/images/snake.png');
game.load.image('apple', './assets/images/apple.png');
},
create: function () {
//游戏开始时初始化这些变量
snake = []; //数组作为队列使用,保存贪吃蛇的身体部分
apple = {}; //食物对象
squareSize = 15; //方块的边长,使用的图像的15*15像素
score = 0; //当前得分
speed = 0; //游戏速度
updateDelay = 0; //控制刷新速度的变量
direction = 'right'; //当前运动方向
new_direction = null; //存储新下一步方向的缓存变量
addNew = false; //食物是否被吃掉
//设置一个控制键盘输入的Phaser控制器
cursors = game.input.keyboard.createCursorKeys();
//设置游戏背景色
game.stage.backgroundColor = "#061f27";
//生成初始贪吃蛇,10个方块长
//从X=150,Y=150的位置开始添加,每次迭代增加横坐标
for (var i = 0; i < 10; i++) {
snake[i] = game.add.sprite(150 + i * squareSize, 150, 'snake');
}
//生成第一个食物
this.generateApple();
//在界面顶部添加文字
//“得分”、“速度”、“操作说明”文字样式
textStyle_Key = {
font: "bold 14px sans-serif",
fill: "#46c0f9",
align: "center"
};
//对应数值的文字样式
textStyle_Value = {
font: "bold 18px sans-serif",
fill: "#fff",
align: "center"
};
game.add.text(30, 20, "得分", textStyle_Key);
scoreTextValue = game.add.text(90, 18, score.toString(), textStyle_Value);
game.add.text(160, 20, "操作:上下左右控制方向", textStyle_Key);
game.add.text(500, 20, "速度", textStyle_Key);
speedTextValue = game.add.text(558, 18, speed.toString(), textStyle_Value);
},
update: function () {
//update()函数将会被高频率(60fps左右)调用来刷新界面
},
generateApple: function () {
//在界面中随机位置绘制食物
//X在0-585之间(39*15)
//Y在0-435之间(29*15)
//floor()方法返回小于等于x的最大整数
var randomX = Math.floor(Math.random() * 40) * squareSize,
randomY = Math.floor(Math.random() * 30) * squareSize;
apple = game.add.sprite(randomX, randomY, 'apple');
}
};
绘制结果如下(食物在游戏区域随机出现):
3、运动控制
为了能够控制蛇的运动我们需要在update
函数中添加一些代码。
首先,我们来创建一个事件监听器,监听由方向键控制的运动方向。
实际的运动要更复杂一些,由于刷新会以极快的速率被触发,所以如果每次update
被调用的时候都移动蛇的位置,那么蛇将会变得非常难以控制。为了使其能够控制,我们设置一个if条件,通过一个updateDelay计数器变量来检查是否是连续第十次调用update()
。
如果是第十次调用,我们就移除蛇身的最后一个方块(队列的第一个元素),根据运动方向给其新的坐标,并将其放置到当前蛇头的前面作为新的蛇头(队列的最后一个元素),代码如下:
update: function () {
//update()函数将会被高频率(60fps左右)调用来刷新界面
//处理方向键,并且不允许违规操作
if (cursors.right.isDown && direction != 'left') {
new_direction = 'right';
}
else if (cursors.left.isDown && direction != 'right') {
new_direction = 'left';
}
else if (cursors.up.isDown && direction != 'down') {
new_direction = 'up';
}
else if (cursors.down.isDown && direction != 'up') {
new_direction = 'down';
}
//计算当前游戏速度的公式
//得分越高,速度越快,最高为10
speed = Math.min(, Math.floor(score / ));
//更新屏幕上的数值
speedTextValue.text = '' + speed;
//初始的刷新速率为60fps左右
//需要降速以使物体可控
updateDelay++;
//只有当计数器的值等于(10-speed)时,才会触发游戏事件
//速度越快,计数值越快达到指定值
//贪吃蛇也就移动的越快
if (updateDelay % ( - speed) == ) {
//移动
var firstCell = snake[snake.length - ],
//删除数组第一项并返回该项
//即删除蛇尾
lastCell = snake.shift(),
oldLastCellx = lastCell.x,
oldLastCelly = lastCell.y;
//如果游戏者从键盘选择了新的方向
if (new_direction) {
direction = new_direction;
new_direction = null;
}
//根据方向更改最后一个单元格相对于蛇头的坐标
if (direction == 'right') {
lastCell.x = firstCell.x + ;
lastCell.y = firstCell.y;
}
else if (direction == 'left') {
lastCell.x = firstCell.x - ;
lastCell.y = firstCell.y;
}
else if (direction == 'up') {
lastCell.x = firstCell.x;
lastCell.y = firstCell.y - ;
}
else if (direction == 'down') {
lastCell.x = firstCell.x;
lastCell.y = firstCell.y + ;
}
//将最后一个单元格即蛇尾添加到队列末端即蛇头
//并将其作为新的舌头
snake.push(lastCell);
firstCell = lastCell;
//结束移动
}
}
至此已经能够控制蛇的运动。
4、碰撞检测
如何蛇能够随意运动那显然是不科学的,我们需要检测蛇什么时候与墙壁、食物、或者自身发生了碰撞,以能能够判断是否死亡,结束游戏。
通常这样的工作是通过物理引擎来完成的,Phaser框架也支持一部分。在这对于一个如此简单的游戏来说过于复杂,此处我们仅需要通过对比坐标来进行碰撞检测即可。
在update函数结束移动的位置,调用一系列方法,来比较坐标以判断是否发生了碰撞。
update:function () {
//update()函数将会被高频率(60fps左右)调用来刷新界面
...
if (updateDelay % ( - speed) == )
{
//移动
...
//结束移动
...
//如果吃掉食物则增长蛇的长度
//在原先蛇尾处添加一块即可,坐标已在上面给出
if (addNew)
{
snake.unshift(game.add.sprite(oldLastCellx, oldLastCelly, 'snake'));
addNew = false;
}
//检测是否与食物碰撞
this.appleCollision();
//检测自身是否发生碰撞
this.selfCollision(firstCell);
//检测是否与墙壁发生碰撞
this.wallCollision(firstCell);
}
},
appleCollision:function () {
//检测食物是否与蛇身有重叠
//如果食物产生在蛇身上
for (var i = ; i < snake.length; i++)
{
if (snake[i].x == apple.x && snake[i].y == apple.y)
{
//下次蛇移动时,长度加一
addNew = true;
//销毁食物
apple.destroy();
//重新生成食物
this.generateApple();
//分数加一
score++;
//刷新分数面板
scoreTextValue.text = score.toString();
}
}
},
selfCollision:function (head) {
//检测蛇头是否与蛇身发生重叠
for (var i = ; i < snake.length - ; i++)
{
if (head.x == snake[i].x && head.y == snake[i].y)
{
//游戏结束
game.state.start('Game_Over');
}
}
},
wallCollision:function (head) {
//检测蛇头是否与界面边界发生碰撞
if (head.x >= || head.x < || head.y >= || head.y < )
{
//游戏结束
game.state.start('Game_Over');
}
}
如果吃到食物,则得分加一,贪吃蛇长度加一。如果发生碰撞,则游戏结束。同样,我们需要在main.js中注册游戏结束状态Game_Over。
main.js
game.state.add('Game_Over', Game_Over);
gameover.js
// JavaScript Document
var Game_Over = {
preload: function () {
//加载游戏结束图像
game.load.image('gameover', './assets/images/gameover.png');
},
create: function () {
//创建一个按钮以重启游戏
this.add.button(, , 'gameover', this.startGame, this);
//添加文字,显示最终游戏结果
game.add.text(, , "最终得分", { font: "bold 16px sans-serif", fill: "#46c0f9", align: "center" });
game.add.text(, , score.toString(), { font: "bold 20px sans-serif", fill: "#fff", align: "center" });
game.add.text(, , "单击以重新开始游戏", { font: "bold 16px sans-serif", fill: "#46c0f9", align: "center" });
},
startGame: function () {
//回到游戏状态
this.state.start('Game');
}
};
至此,我们即完成了完整的游戏。
资料推荐
虽然这个游戏非常简单,但通过这个项目,可以了解到Phaser的多个方面,以及队列结构在此类游戏中的运用。如果对此感兴趣,可以再深入接触以下资料:
- Phaser官方文档(http://phaser.io/tutorials/making-your-first-phaser-game)
- Phaser在线编辑器(http://phaser.io/sandbox/edit/1)
- 基于Phaser的游戏实例展示(http://phaser.io/examples)