Phaser-游戏之旅

虽然这个小游戏逻辑不是很复杂,但为了熟悉Phaser这个游戏框架的使用方法因此就选择了它。css

另外第一次在项目中尝试使用ES6,以后利用babel进行转换。html

自动化构建:gulp(其余文件复制和解析) + webpack(负责js的模块打包) + browser-sync(实时预览);webpack

刚开始拿到项目的交互后,对游戏功能进行了分析,而后将整个游戏大体分”游戏启动前、加载、游戏、结束“4个场景。肯定场景后,考虑实现的方式。我选择webpack + gulp来打包个人代码,
个人工程目录大体以下所示:ios

文件目录以下:
    .
    ├── src
    │   ├── img     //存放图片资源
    │   ├── js      
    │   │   ├── app      //一些本身写的库
    │   │   ├── lib      //第三方库
    │   │   ├── prefabs  //存放游戏元件
    │   │   ├── states   //存放游戏场景
    │   │   │   ├── boot.js 
    │   │   │   ├── preload.js 
    │   │   │   ├── play.js 
    │   │   │   └── over.js  
    │   │   └── index.js //程序入口
    │   ├── css
    │   │   └── style.less
    │   └── media   //存放媒体文件
    ├── index.html
    ├── gulpfile.js  
    └── webpack.config.js

程序入口

主要是利用es6的class建立一个游戏对象并继承于Phaser.Game,而后将全部的场景添加到Phaser.state中。git

class Game extends Phaser.Game { // 子类继承父类Phaser.Game
    constructor () {  //构造函数
        
        super(width, height, Phaser.CANVAS|Phaser.webgl|Phaser.auto, elementName, null);  //经过super来调用父类(Phaser.Game)构造数
        
        this.state.add('Boot', Boot, true); //添加场景
        this.state.add('Preload', Preload, true);
        this.state.add('Play', Play, true);
        this.state.add('Over', Over, true);
        this.state.start('Boot'); //启动
    }
}

注:关于Phaser的各类对象、方法我就不过多描述了,文档比我写的详细。主要写写我怎么构建这个游戏的吧,哈哈哈~~es6

游戏启动场景

该场景继承于Phaser.State对象,这样便于切换和构建画面。主要功能对游戏进行适配以及开启游戏的物理引擎。若是加载场景中须要图片能够在这个场景中进行下一场景须要的图片。github

注: 游戏中全部场景继承于Phaser.State对象,Phaser.State一般会有preload、create、update、render方法。web

export default class Boot extends Phaser.State {

    //先预紧力。一般状况下,你会使用这个来装载你的游戏资产(或当前状态所需的)
    preload () {}

    //建立被称为一次预载完成,这包括从装载的任何资产的装载。
    create () {
        //show_all规模的模式,展现了整个游戏的同时保持比例看
        this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
        this.scale.pageAlignHorizontally = true; //当启用显示画布将水平排列的
        this.scale.pageAlignVertically = true; //当启用显示画布将垂直对齐的

        //物理系统启动:phaser.physics.arcade,phaser.physics.p2js,phaser.physics.ninja或相位。物理。Box2D。
        this.game.physics.startSystem(Phaser.Physics.ARCADE);
        this.state.start('Preload');
    }
}

加载场景

在preload方法中对游戏的资源进行加载,加载完成以后进入create而后切换场景。npm

export default class Boot extends Phaser.State {

    preload () {
        //...
        //加载游戏所须要的资源
    }

    create () {
        this.state.start('Play');
    }
}

Phaser给咱们提供了各类资源加载的方式,这里列一下我在游戏中加载的资源类型:json

单个图加载

image(key, src); //key: 在游戏中使用时的名称、src: 图片地址 

this.load.image('bg', imgPath + 'bg.jpg');

雪碧图加载

spritesheet(key, src, 图片单帧的宽, 图片单帧的高, 帧数, margin, spacing); 另外两个参数我使用的默认值

this.load.spritesheet('master',  imgPath + 'master.png', 280, 542, 14);

单个音频加载

image(key, src); //key: 在游戏中使用时的名称、src: 音频地址 

this.load.audio('bgMusic',[mediaPath + 'bg.mp3']);

雪碧音加载

audiosprite(key, urls, jsonURL, jsonData)  //jsonURL:若是经过数据直接设置设为空, jsonData:json数据(能够本身去生成,见音频处理)

this.load.audiosprite('music', mediaPath + 'audio.mp3', null, audioJSON);

游戏场景

游戏的核心都在这一块,先罗列一下我须要实现的功能:

  1. 背景、云、建筑、地板的移动。

  2. 点击start按钮倒数

  3. 三我的物的自动跑。

  4. 障碍物的生成与移动。

  5. 能量的生成与移动。

  6. 点击jump按钮主人物跳起。

  7. 两个npc遇到障碍物自动跳起。

  8. 吃到能量后能量条的变化。

  9. 剩余生命数的显示。

  10. replay功能。

下面来看一下怎么具体来实现着一些功能:

首先我将游戏的进行拆分,把全部的元素都写成单独的一个元件,而后将这些元件合起来。大体分为如下(其实还能够细分):

import TopBar from '../prefabs/TopBar';  //顶部
import Person from '../prefabs/Master'; //主人物
import Enemys from  '../prefabs/Enemys'; //两个npc
import Obstacles from  '../prefabs/Obstacles'; //障碍物 
import Bullet from '../prefabs/Bullet'; //子弹(功能目前去掉了)
import Energies from  '../prefabs/Energies'; //能量 
import Death from  '../prefabs/Death'; //死亡画面

准备好以后来实现我须要的功能。

元件的移动

Phaser提供了一个TileSprite的对象给咱们使用,咱们把须要自动移动的元件利用TileSprite添加到场景中去,下面以云为例:

//定义一个移动的基准速度,而后经过这个速度去实现不一样速度的移动

this.gameSpeed = 300; 

//云
this.cloud = new Phaser.TileSprite(this.game, 0, 132, this.game.width, 408, 'cloud'); //添加到场景中

//TileSprite(game, x|坐标, y|坐标, width|宽, height|高, key|图片名, frame|指定帧数,默认第一帧)

this.cloud.fixedToCamera = true; //固定

this.cloud.autoScroll(-this.gameSpeed / 8 , 0); // 元件移动

注:移动主要靠autoScroll()来进行自动移动。 中止移动stopScroll();

其余的元件移动方法跟这个同样的操做,只是速度不一样而已。

人物自动跑

这个其实不用考虑,只要一直运行人物跑的动画,而后背景和地板等移动,这样人物就跑起来了。全部首先要作的是将人物添加动画并绘制在场景中。

  • 先用Sprite构建一我的物对象:
export default class Person extends Phaser.Sprite {

    constructor ({game, x, y, asset, frame, floor}) {

        super(game, x, y, asset, 0);
    
        //... 人物的初始化设置
    }
}
  • 而后添加animations()添加须要的动画。我把人物动画大体分红’初始化、跑、跳、死亡、经过‘代码以下:
//参数: 使用时候的name、 动画运行的帧、time、重复运行
 this.animations.add('init',[0], 10, false);
 this.animations.add('run',[1,2,3,4,5,6], 20, true);
 this.animations.add('jump', [7], 10, false);

 //外部使用: obj.animations.play('run');
  • 要让人物在地板上跑,这里要用到碰撞检测,Phaser提供了检测的方法,咱们添加上就能够。首先开启人物与地板的物理系统,而后利用碰撞检测
    的方法检测人物是否落在地板上,代码大体以下:
this.game.physics.arcade.enable(人物对象); //开启人的物理系统
this.body.gravity.y = 1600; //设置人物的重力

this.game.physics.enable(地面对象); //开启地面物理系统
this.floor.body.immovable = true; //这里须要将地面设置为固定不动

this.game.physics.arcade.collide(人物对象, 地面对象,callback); //在update方法里用collide去实时检测这两个元件是否有接触

注: 另外能够用 人物对象.body.setSize(130, 522, 75, 0);去设置元件的碰撞范围,这里要让人物看起来跑在地面是上因此须要对地面进行接触面的设置。

点击start按钮倒数

  • 这个功能比较简单,在绘制按钮的时候刚开始我是用两个图去绘制两个按钮,而后我发现Button这个对象能够去设置当前显示帧数,因此后面我将两个按钮
    合成一张图,而后去改变显示的帧数,刚设置完的时候,出现了jump按钮一直显示第一帧的状况,由于Button它有几种状态,然而我只设置了一种,
    其它的状态都被设置成了默认的。设置代码基本以下:
startBtn = new Phaser.Button(game, x, y, 'btn', null, null, 0, 0);

jumpBtn = new Phaser.Button(game, x, y, 'btn', null, null, 1, 1);

//这里设置第一个null,当按钮按下时的callback。第二个null,callback的上下文环境。
  • 按钮设置完成以后就是添加时间和倒数的功能了,Phaser添加事件比较简单,代码以下:
startBtn.inputEnabled = true;
startBtn.input.pixelPerfectClick = true; //精确点击
startBtn.events.onInputDown.addOnce(function(){}, this);

注:这里用addOnced的缘由是个人开始按钮只点击一次,其余的用add添加便可。

  • 倒数功能直接用setInterval实现便可,主要是利用loadTexture去改变每次显示的帧数来达到数字的切换。

障碍物、能量的生成与移动

首先分析简单分析障碍物与能量有哪些对外的方法“修改图片、设置速度、中止移动、隐藏、重置位置”。接下了就是实现着一些方法。以前想着障碍物会无限循环的出来,这个点想了
比较久,由于若是每次都去建立一个新的障碍物,那么假设有100个障碍物这样就会建立100次,这样资源就会出现浪费,也会出现性能上的问题。由于Phaser中提供kill()
reset()方法,因此能够利用一下。大体就是假设建立5个障碍物对象,每次当障碍物移出左边屏幕的时候,将它kill掉而后用reset去重置当前这个障碍物的位置,这样
场景中永远都只有这几个在重复利用了。大体实现代码以下所示:

this.createMultiple(num, asset, 0, false); //建立num个贴图为asset的元件

//添加每一个元件的信息
let obstacle;
for(var i = 0; i< this.num; i++){
    let EnergyX = (i * this.distance) + (this.distance * this.distanceThan[i]) + 110;
    let EnergyY = Math.floor(this.game.height-295 -140);
    
    //设置元件的物理属性、触碰大小、动画、基点位置等。
    //..
}

this.lastObstacle = obstacle; //保存最后一个信息


//在update中判断是否移出屏幕将其kill,而后重置对象
updata() {
    this.forEach((obstacle)=>{
        if (obstacle.body.right <= 0) {
            obstacle.kill();
            //..
        }
    },this);

    this.forEachDead((obstacle)=>{
        obstacle.reset(x,y);
        //...
        this.lastObstacle = obstacle;
    },this);
}

注:forEachDead循环死亡对象。

最后由于障碍物时固定的因此我把这一部分功能剔除掉了,在这里还有一个就是因为能量的个数只有3个,因此我用了个投机取巧的办法去让这个障碍物与能量对应起来。
就是用两个数组,去固定相应位置。

人物的跳起

人物跳起的核心就是去改变人物的重力velocity.y代码以下所示:

jumpEvent () {
    
    if(this.isMasterJump) return;

    this.master.body.velocity.y = -700;
    
    //播放跳起动画...
}

//接下来只要在update中检测人物与地面再次接触便可
updata () {
    this.game.physics.arcade.collide(this.master, this.floor, ()=>{
        //人物跳起落地
        if(!this.isDown && this.master.body.touching.down) {
            this.isMasterJump = false;
            this.isDown = true;
            this.master.animations.play('run');
        }
    }, null, this);
}

注: obj.body.touching.down这个属性当有检测多个碰撞是都会触发。

主人物的跳起功能完成,接下来就是NPC的自动跳起,大体的思路就是获得障碍的位置,而后根据位置去执行NPC的动画。其中利用forEachExists去实时检测障碍物的位置
这个方法会返回当前元件的信息,里面包含位置信息。代码大体以下:

updata () {
    this.obstacles.forEachExists(this.checkObstacle,this); // 检测柱子位置
}

checkObstacle () {
    this.enemys.enemy1.checkJump(obstacle);
    this.enemys.enemy1.checkDown();
    //..其余操做
}

//检测是否跳起
checkJump (obstacle) {
    if(!this.jump && obstacle.x - this.x < 57 && obstacle.x - this.x > 0){
        //..
    };
}

//检测是否落地
checkDown () {
    if(!this.isDown && this.body.touching.down && this.jump) {
        //..
    };
}

注: 这里关闭NPC与障碍物得碰撞检测,否则当NPC碰到障碍物body.touching.down=true这个结果不是咱们想要的。

能量条、生命数的显示与变化

首先用Sprite对象绘制出生命图形以及能量条,而后对外暴露出“更新、显示、隐藏”等方法,这里能量条的变化利用crop()配合Rectangle()获得须要显示的地方。代码以下:

//建立能量条
this.energyBg = new Phaser.Sprite(this.game, 0, 33, 'energyBar', 0);
this.energyCover = new Phaser.Sprite(this.game, 0, 33, 'energyBar', 1);

//建立3条生命
for (var i = 0; i< 3; i++) {
    var x = (i * 43)+3;
    var key = 0;
    if(i >= this.life) {key = 1;} //若是有死亡显示的图形
    let sprite = new Phaser.Sprite(this.game, x, 33, 'heart',key);
    sprite.animations.add('death',[1], 10, false);
    this.heartGroup.add(sprite);
}

//更新能量条
updateEnergy () {
    let distance = this.energyBg.width * (3-this.score) / 3;

    this.energyCover.x = distance;

    this.energyCover.crop(new Phaser.Rectangle(distance, 0, this.energyBg.width * (this.score / 3), 35)); //裁切一个矩形区域

    this.energyCover.updateCrop(); //更新
}

replay功能

这里个人作法比较粗暴,直接state.start('Play')

至此游戏的大致功能都实现了,剩下的就是结束场景而后就是调试与测试了。

结束场景

最后就是游戏结束以后会跳转到这个场景,以后的逻辑能够在create中编写。

export default class Over extends Phaser.State {
    
    preload () {}
    
    create () {
        //...经过逻辑
    }
}

音频处理

由于用到了雪碧音,若是本身去合成雪碧音的换修改和替换起来会比较麻烦因此在npm找了个合成雪碧音的工具:audiosprite。

因而就写了个简单的音频合成代码:

var audiosprite = require('audiosprite')

var files = ['file1.mp3', 'file2.mp3'];

var opts = {
    output: 'audio',
    format: 'jukebox',
    export: 'mp3',
    loop: 'false'
}
audiosprite(files, opts, function(err, obj) {
    if (err) return console.error(err)

    console.log(JSON.stringify(obj, null, 2))
})

输出json格式:

{
  "resources": [
    "audio.mp3"
  ],
  "spritemap": {
    "file1": {
      "start": 0,
      "end": 1.2026984126984126,
      "loop": false
    },
    "file2": {
      "start": 3,
      "end": 4.202698412698412,
      "loop": false
    }
  }
}

以后把这个json数据复制到音频加载那里就能够了,

重点是音频修改起来方便只要运行一下这个js,而后替换下json数据就能够了。

最后再说点吧,虽然这个小游戏比较简单,可是让我用另一种思惟去思考问题。代码方面写法比较粗糙,还要去写更多的练习去磨练本身。期待下次本身的进步吧!

文章中Phaser的各种方法我就没细说了,具体使用看文档吧,Phaser的话demo超多,文档写的也比较详细了,帮了我很多忙了。 文章中不配游戏截图由于我太懒了~~~

附上Phaser文档http://phaser.io/docs/2.6.2/index

源码地址:https://github.com/flowers1225/Phaser-game

相关文章
相关标签/搜索