Phaser-游戲之旅


雖然這個小游戲邏輯不是很復雜,但為了熟悉Phaser這個游戲框架的使用方法所以就選擇了它。

另外第一次在項目中嘗試使用ES6,之后利用babel進行轉換。

自動化構建:gulp(其他文件復制和解析) + webpack(負責js的模塊打包) + browser-sync(實時預覽);

剛開始拿到項目的交互后,對游戲功能進行了分析,然后將整個游戲大致分”游戲啟動前、加載、游戲、結束“4個場景。確定場景后,考慮實現的方式。我選擇webpack + gulp來打包我的代碼,
我的工程目錄大致如下所示:

    文件目錄如下:
    .
    ├── 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中。

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的各種對象、方法我就不過多描述了,文檔比我寫的詳細。主要寫寫我怎么構建這個游戲的吧,哈哈哈~~

游戲啟動場景

該場景繼承於Phaser.State對象,這樣便於切換和構建畫面。主要功能對游戲進行適配以及開啟游戲的物理引擎。如果加載場景中需要圖片可以在這個場景中進行下一場景需要的圖片。

注: 游戲中所有場景繼承於Phaser.State對象,Phaser.State通常會有preload、create、update、render方法。

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然后切換場景。

export default class Boot extends Phaser.State {

    preload () {
        //...
        //加載游戲所需要的資源
    }

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

Phaser給我們提供了各種資源加載的方式,這里列一下我在游戲中加載的資源類型:

單個圖加載

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM