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

相關文章
相關標籤/搜索