淘先锋技术网

首页 1 2 3 4 5 6 7

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

在这篇文章,我们将使用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(, 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的多个方面,以及队列结构在此类游戏中的运用。如果对此感兴趣,可以再深入接触以下资料:

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