使用Phaser框架构建你的第一个H5游戏

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/sinat_32582203/article/details/73303153

还记得贪吃蛇这个经典游戏吗?在诺基亚时代,在黑白像素点游戏机时代,就是这样一个简单的游戏也能让我们玩上几个小时。

在这篇文章,我们将使用HTML5来重现这个游戏,基于著名的开源HTML5游戏框架——Phaser。你将了解到游戏精灵、游戏状态,以及如何使用预加载(preload)、创建(create)与刷新(update)方法。最终效果呈现如下:

H5贪吃蛇

一、开发准备

首先访问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(10, Math.floor(score / 5));

	//更新屏幕上的数值
	speedTextValue.text = '' + speed;

	//初始的刷新速率为60fps左右
	//需要降速以使物体可控
	updateDelay++;

	//只有当计数器的值等于(10-speed)时,才会触发游戏事件
	//速度越快,计数值越快达到指定值
	//贪吃蛇也就移动的越快
	if (updateDelay % (10 - speed) == 0) {
		//移动
		var firstCell = snake[snake.length - 1],
			//删除数组第一项并返回该项
			//即删除蛇尾
			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 + 15;
			lastCell.y = firstCell.y;
		}
		else if (direction == 'left') {
			lastCell.x = firstCell.x - 15;
			lastCell.y = firstCell.y;
		}
		else if (direction == 'up') {
			lastCell.x = firstCell.x;
			lastCell.y = firstCell.y - 15;
		}
		else if (direction == 'down') {
			lastCell.x = firstCell.x;
			lastCell.y = firstCell.y + 15;
		}

		//将最后一个单元格即蛇尾添加到队列末端即蛇头
		//并将其作为新的舌头
		snake.push(lastCell);
		firstCell = lastCell;
		//结束移动
	}
}

至此已经能够控制蛇的运动。

4、碰撞检测

如何蛇能够随意运动那显然是不科学的,我们需要检测蛇什么时候与墙壁、食物、或者自身发生了碰撞,以能能够判断是否死亡,结束游戏。

通常这样的工作是通过物理引擎来完成的,Phaser框架也支持一部分。在这对于一个如此简单的游戏来说过于复杂,此处我们仅需要通过对比坐标来进行碰撞检测即可。

在update函数结束移动的位置,调用一系列方法,来比较坐标以判断是否发生了碰撞。

update:function ()
{
    //update()函数将会被高频率(60fps左右)调用来刷新界面
    ...
    if (updateDelay % (10 - speed) == 0)
    {
        //移动
        ...
        //结束移动
        ...
        //如果吃掉食物则增长蛇的长度
        //在原先蛇尾处添加一块即可,坐标已在上面给出
        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 = 0; 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 = 0; i < snake.length - 1; i++)
    {
        if (head.x == snake[i].x && head.y == snake[i].y)
        {
            //游戏结束
            game.state.start('Game_Over');
        }
    }
},

wallCollision:function (head)
{
    //检测蛇头是否与界面边界发生碰撞
    if (head.x >= 600 || head.x < 0 || head.y >= 450 || head.y < 0)
    {
        //游戏结束
        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(0, 0, 'gameover', this.startGame, this);

		//添加文字,显示最终游戏结果
		game.add.text(235, 310, "最终得分", { font: "bold 16px sans-serif", fill: "#46c0f9", align: "center" });
		game.add.text(350, 308, score.toString(), { font: "bold 20px sans-serif", fill: "#fff", align: "center" });
		game.add.text(235, 350, "单击以重新开始游戏", { font: "bold 16px sans-serif", fill: "#46c0f9", align: "center" });
	},

	startGame: function () {
		//回到游戏状态
		this.state.start('Game');
	}
};

至此,我们即完成了完整的游戏。

资料推荐

虽然这个游戏非常简单,但通过这个项目,可以了解到Phaser的多个方面,以及队列结构在此类游戏中的运用。如果对此感兴趣,可以再深入接触以下资料:

完整项目地址:https://github.com/zhangrj/Snake_Game

展开阅读全文

没有更多推荐了,返回首页