H5游戏开发的一乘轻骑---Phaser

6,138 阅读9分钟

快速入门(普通版)

Phaser 是一款非常优秀的 HTML5 游戏框架,致力于发展 PC 端和移动端的 HTML5 游戏,是一款不可多得的神器。

本篇文章将带你快速入门游戏的开发(移动端),适合想要了解游戏开发或者想投入游戏开发的人群,毫无疑问,Phaser 是一个值得你学习的游戏框架。

特别注意

Phaser 虽然是一款非常优秀的HTML5游戏框架,但是也有它软肋的地方,现在请注意下面两个 Phaser 处理不了或者处理起来很棘手的地方:

1. 3D。如果你想学做一个很酷的3D动画,可以学习 Three.js 

2. 视频(移动端)。如果你在移动端页面上需要嵌入视频,请使用原生HTML5标签 video,如果需要对 video 有更好的扩展,可以参考 Video.js 

接下来你可以先打开我们这篇文章所讲的 案例 (服务器渣渣,可能要等好久好久哈哈哈),看完这个案例,你可能会说,这有什么的,我用Jquery的动画都做得出来,哈哈,以前我也对一个大神说过类似的话,然后人家很清楚地跟我说拿 Phaser 的动画跟 Jquery 的动画比简直是在侮辱 Phaser。

好了,话就说到这里,我个人觉得太复杂的案例反而会让人理不清思路,便有违我们的快速入门的宗旨,相信看完这篇文章之后,你可以打开游戏开发的篇章,自由的描绘喜欢的世界。

代码地址

说在前面

首先,为了能正常运行我们的案例,你需要一个本地服务器。你可能会有疑问,为什么我们不能直接把 index.html  拖进浏览器中并运行呢?

因为这与用于访问文件的协议有关。当你在网络上请求任何东西时,你需要使用HTTP,而服务器可以保证你只能访问你想要的文件。但是当你直接把 html 文件拖到浏览器上直接运行时,它是通过本地文件系统(file://)加载的,由于本地文件系统没有域的概念,没有服务器级别的安全,只是一个原始文件系统。

您真的希望JavaScript能够从文件系统中的任何地方加载文件吗?答案当然是不行。你的计算机当然也不会同意。

而 Phaser 需要加载资源:图像,音频文件,JSON数据,或者其他Javascript文件。为了做到这些,它就需要运行在服务器下。

如果你对如何配置一个本地服务器来运行 html 文件感到毫无头绪,并且这也不是我们这篇文章的重点,那我可以介绍一个很简单的方式,你可以尝试进行下列操作:

1. 打开命令行,输入以下命令回车: npm install puer -g (如果没有 npm 请自行百度安装)2. puer 安装完成后,在需要搭载在本地服务器的案例根文件夹下调用命令行,输入以下命令回车:puer -p 9999 如下:


3.如果puer运行成功则如下并自动打开浏览器:


如下案例已经搭载在地址为 http://localhost:9999/ 上了,点击 index.html 就可以把 Phaser 运行起来啦


开始第一步

首先创建我们的目录结构如下:


img文件夹下的图片可以自行从 案例代码地址 上拷贝,然后你会发现,咦,GitHub上怎么多了一张 sprites.png 图片和一个 sprites.xml 文件,这个后面将会讲到,现在我们只需要准备现阶段需要的,js/libs文件下的 jquery.min.jsphaser.min.js 请自行准备或从 案例代码地址 上拷贝,然后其它文件我们需要保证空白以便我们后面迅速填充内容。

第二步

首先当然是向我们的 index.html 快速填充内容啦,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Phaser快速入门(普通版)</title>
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
</head>
<style>
    html,
    body {
        width: 100%;
        height: 100%;
        font-family: "Helvetica Neue", Helvetica, STHeiTi, sans-serif;
        overflow: hidden;
        background: #fff;
        margin: 0;
        padding: 0;
    }
    .main {
        width: 100%;
        height: 100%;
        position: absolute;
        top: 0;
        left: 0;
    }
    .main img {
        width: 100%;
        height: 100%;
    }
</style>
<body>
    <!--H5页面所有动画的canvas载体-->
    <div id="game-container"></div>
    <!--预加载背景,防止预加载的资源加载太久以致屏幕长时间空白-->
    <div class="main">
        <img src="img/bg.png" alt="">
    </div>
</body>
<script src="js/libs/jquery.min.js"></script>
<script src="js/states/utils.js"></script>
<script src="js/libs/phaser.min.js"></script>
<script src="js/states/boot.js"></script>
<script src="js/states/preload.js"></script>
<script src="js/states/state1.js"></script>
<script src="js/states/state2.js"></script>
<script src="js/app.js"></script>
</html>

好了,内容填充完毕,在这个文件里面首先我们案例所有动画都是搭载在id为 game-container 的元素上,然后class为 main 的元素是预加载背景,看了注释对它的用意还不是很清楚的话可以不用深究,后面自然就清楚了。

然后你还会发现我们导入了一个 jquery.min.js ,你可能会有这样的疑问,这是要干嘛?难道在使用 Phaser 的时候还需要先导入 Jquery 吗?答案肯定不是,导入 Jquery 只是为了在后面证明一些东西。utils.js 是封装了一些我们在后面经常用到的操作(满屏效果,缩放图片)

接下来我们把重点放在我们这个案例运行起来的机制,先来看看我们的5个核心文件:

<script src="js/states/boot.js"></script>
<script src="js/states/preload.js"></script>
<script src="js/states/state1.js"></script>
<script src="js/states/state2.js"></script>
<script src="js/app.js"></script>

这5个文件的执行顺序依次是:

app.js -> boot.js -> preload.js -> state1.js -> state2.js

app.js 里面会执行 boot.jspreload.jsstate1.jsstate2.js 这4个场景,因此 app.js 需要在导入 boot.jspreload.jsstate1.jsstate2.js 这4个文件之后导入。接下来逐一解析这5个文件。

app.js

这个文件是我们 Phaser 执行的第一个文件,主要进行一些游戏开发方面的配置,包括设置我们资源的路径,挂载元素,添加场景,启动场景等。

具体代码如下:

(function() {
    'use strict'

    // 设置资源目录(项目根目录)
    var baseURI ='../..'

    //将图片目录放在内存中,方便全局调用
    localStorage.baseURI = baseURI

    //设置$('#game-container')的高度等于屏幕的高度(这里用原生js代码书写)
    document.getElementById('game-container').style.height = document.body.clientHeight + 'px'

    //获取屏幕的缩放比
    var Ratio = window.devicePixelRatio

    //获取屏幕的宽和高
    var w = document.documentElement.clientWidth || document.body.clientWidth
    var h = document.documentElement.clientHeight || document.body.clientHeight

    //因为我们在index.html设置了禁止缩放的meta头
    //<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    //所以当我们将屏幕的宽和高直接传入Phaser.Game对象时,浏览器会自动将canvas按屏幕的缩放比缩放,也就是当你的屏幕缩放比是2时,canvas宽高你设置成屏幕的宽高时实际看到的却只有一半,因此相应我们需要将画布放大Ratio倍
    var ww = Ratio * w
    var hh = Ratio * h

    //前两个参数是Phaser要创建的canvas元素的宽高,第三个参数是游戏渲染的引擎,这里让Phaser自己识别设置即可,第四个参数是游戏挂载在哪个元素上
    var game = new Phaser.Game(ww, hh, Phaser.AUTO, 'game-container')

    //添加场景
    game.state.add('Boot', Boot)
    game.state.add('Preload', Preload)
    game.state.add('State1', State1)
    game.state.add('State2', State2)

    //启动场景
    game.state.start('Boot')

})(window)

上面的每一行代码都已经加上相应的注释,如果有空的话可以看一下并理解清楚,现在我们只需知道 app.js 只是用来配置并且启动我们的第一个场景(boot.js)就行。

在说我们的场景之前,我们先来了解什么是场景,场景是我们 Phaser 游戏开发的主体,我们所有看到的游戏动画都是在场景里实现,场景可以是一个js自定义对象,也可以是一个函数,只要存在preload、create、update这三个方法中的任意一个,就是一个合法的场景(总共5个方法,另外两个是init和render方法)。

init方法:一些场景的初始化代码可以写在这个方法里,最先执行。

preload方法:用来加载资源的,如果没有init方法则它会最先执行。

create方法:初始化以及构建场景,会等到preload方法里加载的资源全部加载完成后执行。

update方法:更新函数,它会在游戏的每一帧都执行,一般是1/60秒执行一次。

render方法:在游戏的每一渲染周期都会调用,用来做一些自定义的渲染工作。

boot.js

这个场景不会向用户展示,只是为了加载下个场景(即预加载场景preload.js)所需的资源,因此资源不能加载太多,太多的话则屏幕会长时间黑屏(黑屏其实就是这个场景加载资源的过程,因为这是第一个场景,没有上个场景帮你加载好的资源用来构建界面,所以也就没有可以友好显示给用户的界面),而这个问题我们也在index.html 用 $('.main') 解决,即先展示 $('.main') 这个元素展现出来的界面(界面背景需要跟预加载场景背景一样),以便这个场景加载太久有个良好的用户体验,而当这个场景加载完后 $('.main') 也可以实现跟预加载场景的良好衔接。

具体代码如下:

var Boot = function(game) {
    var baseURI = localStorage.baseURI
    this.init = function() {
        //game.device.desktop判断是WAP端还是PC端,true为PC端,false为WAP端
        if (!game.device.desktop) {
            //设置游戏背景色
            game.stage.backgroundColor = '#282C34';
            //鼠标指针对象,由于WAP端没有鼠标,因此设置为1(即为null)
            game.input.maxPointers = 1;
            //缩放控制,这里将画布(canvas)拉伸至填满父容器(即#game-container),不保持比例
            game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
            //启用时,显示画布(canvas)将在父容器中水平对齐
            game.scale.pageAlignHorizontally = true;
            //启用时,显示画布(canvas)将在父容器中垂直对齐
            game.scale.pageAlignVertically = true;
            //强制游戏只能以一个方向运行,这里设置游戏仅能在纵向模式运行(true),无法再横向模式运行(false)
            game.scale.forceOrientation(false, true);
            //当forceOrientation设置只能纵向模式运行,把手机横向摆放就会调用enterIncorrectOrientation这个方法
            game.scale.enterIncorrectOrientation.add(enterIncorrectOrientation, this);
            //当forceOrientation设置只能纵向模式运行,把手机横向摆放就会调用enterIncorrectOrientation这个方法,重新将手机纵向摆放就会调用leaveIncorrectOrientation这个方法
            game.scale.leaveIncorrectOrientation.add(leaveIncorrectOrientation, this);
        }else{
            game.scale.pageAlignHorizontally = true
            game.scale.pageAlignVertically = true
            //防止浏览器失去焦点后动画暂停,如果考虑到计算机性能可以设置为false
            game.stage.disableVisibilityChange = true;
        }

    }
    this.preload = function() {
        //设置图片支持跨域请求
        game.load.crossOrigin = true
        //加载预加载界面所需的资源,可以看到图片跟$('.main')界面一致,第一个参数是创建sprite对象时所需要的资源标识,第二个参数是资源所在路径
        game.load.image('start_bg', baseURI + '/img/bg.png')

    }
    this.create = function() {
        //跳转到下一个场景
        game.state.start('Preload');
    }
    function enterIncorrectOrientation() {
        alert('请将手机纵向摆放');
    }
    function leaveIncorrectOrientation() {
        alert('已经将手机纵向摆放');
    }
}

上面的每一行代码已经都加上相应的注释,如果有空的话也可以看一下并理解清楚,现在我们只需知道 boot.js 只是用来设置我们所有场景并且加载我们的第二个场景(预加载场景preload.js)所需的资源就行。

preload.js

这个场景是预加载场景,会加载后面所有场景的资源,由于加载的资源比较多,所以在这个场景我们需要以一种友好的方式展现我们资源加载的过程,用到的是上个场景已经帮我们加载完成的 start_bg 资源,和 Phaser 自身的文本对象(显示加载进度)。

preload.js 特别注意我们需要加载一个帧图片(实现动图效果)

//加载帧图片,第一个参数是创建sprite对象时所需要的资源标识,第二个参数是图片所在路径,第三个参数是标识图片xml文件
game.load.atlasXML('sprites', baseURI + '/img/sprites.png',baseURI + '/img/sprites.xml')

加载帧图片我们需要一张包含所有动作的整体图片,还需要一个配置xml文件。那么问题来了,我们为什么需要两个文件呢,为什么不直接把帧图片做成动图然后展示,这个问题其实很简单,首先直接一张动图远远比一张静态图片大的多(xml文件太小忽略不计),而且我们也无法在代码中控制动图的切换速度。

好吧,现在按我们的想法来,如果我们有两张需要在 Phaser 做成动图的图片,比如下面:


然后我们要怎么把它们合成我们想要的一张整体图和一个配置xml文件呢?这个时候我们就需要一个软件来帮我们处理了,这里我来介绍 Shoebox :


官网地址

下载安装完成后,我们打开 Shoebox ,将需要合成动图的各帧图片一起移到 Sprite Sheet ,如图松手放开:


点击 Save ,就已经生成了一张帧图片和一个xml文件:


xml文件主要记载我们帧图片包含的每张动作图片的位置和名称信息,当然,这不是我们关注的重点。

具体代码如下:

var Preload = function(game) {
    var baseURI = localStorage.baseURI
    var tool = new utils()
    var w = null
    var h = null
    var start_bg = null
    var loading = null
    this.init = function() {
        //获取画布的宽高,即Ratio倍屏幕宽高
        w = game.width
        h = game.height

        //动画组,方便统一处理多个对象的动画
        group=game.add.group()

        //将$('.main')元素隐藏
        $('.main').hide()

        //由于start_bg跟$('.mian')界面一致,用户是看不出$('.mian')隐藏start_bg显示这个过程的
        //创建sprite对象,第一个参数是画布x坐标(距离画布左边缘多远),第二个参数是画布y坐标(距离画布上边缘多远),第三个参数是构建对象的资源
        start_bg = game.add.sprite(0, 0, 'start_bg')
        //将start_bg宽高设置全屏
        tool.setFull(start_bg)

        //创建文本对象,前面两个参数跟sprite等同,第三个参数是文本内容,第四个参数是文本样式
        loading=game.add.text(w*.5, h*.5, '0%',
            {
                fontSize:60,
                fill:'#ffffff'
            })
        //锚点位置(相对自身),第一个参数是相对自身左移多少(.5是左移自身宽度的50%),第二个参数是相对自身上移多少(.5是上移自身高度的50%)
        loading.anchor.set(.5, .5)
        //loading最后的位置是相对画布居中

        //loading的补间动画,from(从怎样的状态转变到默认状态),to(从默认状态转变到怎样的状态),这里用的是from
        //第一个参数:一个js对象,包含着需要进行动画的属性,{ alpha: 0 }表示透明度为0
        //第二个参数:动画的持续时间
        //第三个参数:动画过程函数,默认为匀速动画Phaser.Easing.Linear.None
        //第四个参数:是否自动开始
        //第五个参数:动画开始前的延迟时间,单位是毫秒
        //第六个参数:动画重复的次数,如果需要动画永远循环,则把该值设为 Number.MAX_VALUE
        //第七个参数:是否自动反转
        game.add.tween(loading).from({ alpha: 0 }, 500, null, true, 0, 0, false)

    }
    this.preload = function() {
        game.load.crossOrigin = true

        //加载帧图片,第一个参数是创建sprite对象时所需要的资源标识,第二个参数是图片所在路径,第三个参数是标识图片xml文件
        game.load.atlasXML('sprites', baseURI + '/img/sprites.png',baseURI + '/img/sprites.xml')
        
        game.load.image('next', baseURI + '/img/next.png')
        game.load.image('img2_1', baseURI + '/img/2_1.jpg')
        game.load.image('img2_2', baseURI + '/img/2_2.jpg')

        //这个方法是文件加载过程,返回的progeress是完成的进度,0~100
        game.load.onFileComplete.add(function(progeress) {
            loading.setText(progeress + '%')
        })

        //所有文件都完成加载时会调用这个方法,我们可以在调用这个方法的时候跳转到下一个场景,效果等同于在create方法执行game.state.start('State1')
        game.load.onLoadComplete.add(function() {
            game.state.start('State1')
        })

    }
    this.create = function() {
       //game.state.start('State1')    }
    this.update = function() {}
}

上面的代码我们要特别注意 $('.main').hide() ,这也是我们在前面导入 jquery.min.js 的要证明的东西,就是你可以在 Phaser 里面写 Jquery 的任何代码。

如果上面两个场景(boot.jspreload.js)你已经完全理解和摸透了话,那下面这两个场景(state1.jsstate2.js)估计你能够很快速就看完。

state1.js

这个场景主要应用到 Phaser 的动图,事件还有动画组的运用。在这里细心的小伙伴可以看到我们把本来写在create方法的初始化以及构建场景的代码写在了preload方法里,就我的经验,只要一个场景不需要预加载资源,preload方法和create方法的效用并没有差别。

具体代码如下:

var State1 = function(game) {

    var baseURI = localStorage.baseURI

    var tool = new utils()
    var w = null
    var h = null
    
    var next=null
    var sprites=null
    var group=null

    this.preload = function() {

      w = game.width
      h = game.height

      //动画组,方便统一处理多个对象的动画
      group=game.add.group()

      //第四个参数是没有加入动画时静态展示第几帧图片
      sprites=game.add.sprite(w*.5,h*.6,'sprites',0)
      tool.setSize(sprites,'width',w*.5)
      sprites.anchor.set(.5,1)
      //给sprite对象添加一个新动画,第一个参数是动画名称
      sprites.animations.add('run')
      //播放动画,第一个参数是动画名称,第二个参数是播放的速率,第三个参数是是否循环
      sprites.animations.play('run',4,true)
      
      
      //将start_bg加入动画组
      group.add(sprites)

      next=game.add.sprite(w*.5,h*.8,'next')
      tool.setSize(next,'width',w*.3)
      next.anchor.set(.5,.5)
      game.add.tween(next).to({ width:next.width+20,height:next.height+20 }, 500, Phaser.Easing.Linear.In, true, 0, -1, true)

      group.add(next)

      //默认情况下,游戏对象不会处理任何事件,所以我们需要让它可以处理事件
      next.inputEnabled = true

      //当对next对象点击然后手指放开的时候触发
      next.events.onInputUp.add(function() {

        //onComplete方法是补间动画完成后的回调,我们可以在跳转到下一个场景的时候做一些用户体验比较良好的当前场景的退场动画(这里的退场动画类似淡出效果)
        game.add.tween(group).to({ alpha: 0 }, 500, Phaser.Easing.Linear.In, true, 0, 0, false).onComplete.add(function() {
            game.state.start('State2')
        })
        
      })

    }

}

state2.js

这个场景只是为了承接上个场景 next 按钮的点击跳转。

var State2 = function(game) {
  
    var w = null
    var h = null

    var img2_1=null
    var img2_2=null

    this.preload = function() {
      w = game.width
      h = game.height

      img2_1=game.add.sprite(0,h*.5,'img2_1')
      img2_1.width=w
      img2_1.height=h*.5
      img2_1.anchor.set(0,1)
      game.add.tween(img2_1).to({ y:h }, 2000, Phaser.Easing.Linear.In, true, 500, -1, true)

      img2_2=game.add.sprite(0,h*.5,'img2_2')
      img2_2.width=w
      img2_2.height=h*.5
      game.add.tween(img2_2).to({ y:0 }, 2000, Phaser.Easing.Linear.In, true, 500, -1, true)
    }
    this.create = function() {}
}

好了,state2.js 就不做过多的赘言了,看到了这里相信你早已知道这个场景是怎样的一种动画效果了。

煮了一天的鸡汤

好了,到这里我们就好好结束吧,Phaser 还有太多有趣的地方等着你去发掘,如果想要了解更多可以直接去 官网 ,里面有很多有趣的小例子。

在这里特别鸣谢眯眼猫的黎世灿大神带我疯狂入坑,并且对这篇文章的技术支持。

在这里祝贺大家双12快乐,事业进步!