Phaser -- HTML 遊戲框架

第 7 章    槍戰對決

(1) 遊戲規格

∗ 兩名西部牛仔槍戰對決,遊戲流程:

▸ 一開始,走 10 步

▸ 等待一個隨機時間,然後裁判喊「拔槍」

▸ 雙方拔槍射擊

▸ 如果你先開槍,你贏;如果他先開槍,他贏

shootout

(2) 建立專案

∗ 遊戲專案

▸ 建立專案目錄結構,遊戲名稱:shootout

▸ 下載資產 assets07.zip

▸ 建立 index.html 及初始 main.js

▸ 遊戲視窗 640x480

▸ 接下來修改 main.js 開始建構遊戲

(3) 遊戲狀態規劃

∗ 建立 main.js,內容如下:

var Boot = {

  preload: function() {
  },

  create: function() {
  },

};

var Preloader = {

  preload: function() {
  },

  update: function() {
  },

};

var Shootout = {

  create: function() {
  },

};

var game = new Phaser.Game(640, 480, Phaser.AUTO, 'gameDiv');
game.state.add('Boot', Boot);
game.state.add('Preloader', Preloader);
game.state.add('Shootout', Shootout);
game.state.start('Boot');

▸ 建立三個狀態 (State):

Boot:開機狀態,只有 preloadcreate 兩個標準屬性
Preloader:載入狀態,包含 preloadupdate 兩個標準屬性,此狀態目的在於顯示媒體檔案載入的進度
Shootout:射擊狀態,只有 create 一個標準屬性

▸ 最後將三個狀態加入遊戲中,然後從 Boot 狀態開始

(4) Boot 狀態

∗ 在 Boot 加入以下程式:

var Boot = {

  preload: function() {
    game.load.image('preload', 'assets/preload.png'); 
  },

  create: function() {
    game.input.maxPointers = 1;
    game.state.start('Preloader'); 
  },
};

▸ Preload

✶ 載入 preload.png 進度條影像

▸ Create:

game.input.maxPointers = 1:在同一時間最多可用一個指標 (Pointer):例如滑鼠指標或觸控點
✶ 接著進入 Proloader 狀態

(5) Preloader 狀態

∗ 在 Preloader 加入以下程式:

var Preloader = {
  ready: false, 

  preload: function() {
    this.preloadBar = game.add.sprite(120, 260, 'preload');
    game.load.setPreloadSprite(this.preloadBar);
    // Load images
    game.load.image('standoff', 'assets/standoff.png');
    game.load.image('win', 'assets/win.png');
    game.load.image('lose', 'assets/lose.png');
    // Load audio
    game.load.audio('casing', 'assets/casing.mp3');
    game.load.audio('fire', 'assets/fire.mp3');
    game.load.audio('reload', 'assets/reload.mp3');
    game.load.audio('splat', 'assets/splat.mp3');
    game.load.audio('walk', 'assets/walk.mp3'); 
  },

};

▸ 設定 ready 屬性初始值為 false:用來判斷音訊是否解碼完畢

▸ Preload:

game.add.sprite():將 preload 精靈加入遊戲置於適當位置,並指派給屬性 preloadBar
game.load.setPreloadSprinte():依據 preload() 執行的比例來顯示進度
game.load.image():載入背景影像
# standoff:兩人對峙
# win:你贏
# lose:你輸
game.load.audio():載入音訊
# casing:彈殼掉落聲
# fire:開槍聲
# reload:裝填聲
# splat:槍管聲
# walk:走路聲
✶ 註:在 preload: function() {...} 裡所列的載入資料並不會立即載入,這些指令只是列出需載入的清單,在 preload() 執行完畢才會真正載入。因此如果捨去 Boot 狀態,而將 game.load.image('preload', ...) 移至 Preloader.preload() (如下列程式),在執行到 game.add.sprite(..., 'preload') 指令時,Phaser 會找不到 preload 鍵值。故 Boot 狀態必須保留
var Preloader = {

  preload: function() {
    game.load.image('preload', 'assets/preload.png');
    this.preloadBar = game.add.sprite(120, 260, 'preload');
    ...
  },

  update: function() {
  },

};

∗ Update:

var Preloader = {
  ...

  update: function() {
    //  Make sure all mp3s have been decoded before starting the game
    if (this.ready) {
      return;
    }
    if (game.cache.isSoundDecoded('casing') &&
        game.cache.isSoundDecoded('fire') &&
        game.cache.isSoundDecoded('reload') &&
        game.cache.isSoundDecoded('splat') &&
        game.cache.isSoundDecoded('walk')) {
      this.ready = true;
      game.state.start('Shootout');
    } 
  },

};
✶ 如果 this.read 屬性為 false 才判斷各個音訊解碼是否完成,之後將 this.ready 設為 true,再進入 Shootout 狀態
game.cache.isSoundDecoded(...):檢查音訊是否已解碼完成

(6) Shoutout 狀態

∗ 在 Shootout 設定變數:

var Shootout = {
  INTRO: 0,
  PACING: 1,
  SHOOTING: 2,
  OUTCOME: 3,

  pace: 1,
  shotAt: 0,
  reactionTimeMin: 100,
  reactionTimeMax: 500,

  cowboyTimer: null,

  introText: '點擊後開始走10步\n等裁判說「拔槍」後\n點擊開槍\n', 

  create: function() {
  },

};

▸ 設定槍戰對決的四個階段:

INTRO:簡介階段
PACING:步行階段
SHOOTING:開槍階段
OUTCOME:顯示槍戰結果階段

pace:步行數從 1 開始

shotAt:被打中的時間,初始值設為 0

reactionTimeMin, reactionTimeMax:拔槍的反應時間為 0.1 ~ 0.5 秒,對手在此時間範圍內隨時會開槍

cowboyTimer:牛仔開槍的計時器

introText:簡介文字

∗ Create:

var Shootout = {
  ...
  introText: '點擊後開始走10步\n等裁判說「拔槍」後\n點擊開槍\n',

  create: function() {
    this.background = game.add.image(0, 0, 'standoff');
    this.helpText = game.add.text(460, 400, this.introText,
                                  {fontSize: '16px', fill: '#fff'});
    this.helpText.align = 'right';
    this.paceText = game.add.text(480, 390, '1',
                                  {fontSize: '64px', fill: '#fff'})
    this.paceText.visible = false;
    this.drawText = game.add.text(440, 400, '拔槍!',
                                  {fontSize: '64px', fill: '#fff'})
    this.drawText.visible = false;
    this.mode = this.INTRO;
    game.input.onDown.add(this.onDown, this); 
  },

};

background:背景影像,兩人對峙影像 (standoff)

helpText:簡介文字,靠右對齊

paceText:步行文字,初始設定隱藏

drawText:拔槍文字,初始設定隱藏

this.mode:初始從「簡介」階段開始

game.input.onDown.add(this.onDown, this):如果使用者按下滑鼠,執行 onDown()函式

∗ onDown():按下指標

var Shootout = {
  ...
  create: function() {
    ...
  },

  onDown: function() {
    if (game.time.time-this.shotAt<250) {
      return;
    }

    if (this.mode==this.INTRO) {
      this.start();
    }
    else if (this.mode==this.PACING) {
      this.cheat();
    }
    else if (this.mode==this.SHOOTING) {
      this.shoot();
    }
    else if (this.mode==this.OUTCOME) {
      this.restart();
    }
  }, 

};

game.time.time-this.shotAt:如果目前時間減去被射擊時間小於 0.25 秒 (亦即連續開槍),結束函式

▸ 判斷目前槍戰階段:

✶ 簡介:執行 start() 函式,開始
✶ 步行:執行 cheat() 函式,作弊
✶ 開槍:執行 shoot() 函式,開槍
✶ 結果:執行 restart() 函式,重新開始

∗ start():開始

var Shootout = {
  ...
  onDown: function() {
    ...
  },

  start: function() {
    this.mode = this.PACING;
    this.helpText.visible = false;
    this.paceText.visible = true;
    this.paceTimer = game.time.create(false);
    this.paceTimer.repeat(500, 10, this.stepAway, this);
    this.paceTimer.start();
    this.sound.play('walk');
  }, 

};

this.mode = this.PACING:進入步行階段

this.helpText.visible = false:隱藏簡介文字

this.paceText.visible = true:顯示步行文字

this.paceTimer = game.time.create(false ):產生一個計時物件, false:該時間物件相關的事件都結束後,不要刪除此物件

this.paceTimer.repeat(...):在 paceTimer 計時過程中,每 0.5 秒執行一次 this.stepAway(),共執行 10 次

this.paceTimer.start():開始計時

this.sound.play('walk'):播放步行聲音

∗ stepAway():步行

var Shootout = {
  ...
  start: function() {
    ...
  },

  stepAway: function() {
    this.pace++;
    if (this.pace<=10) {
      this.paceText.text = this.pace;
    }
    else {
      this.paceText.visible = false;
      var delay = game.rnd.between(500, 3000);
      game.time.events.add(delay, this.startDraw, this);
    }
  },

};

this.pace++:步行數加 1

if (this.pace<=10):如果步行數小於等於 10,繼續計數

this.paceText.text = this.pace:顯示目前步行數文字

▸ 否則,準備顯示拔槍文字:

this.paceText.visible = false:隱藏步行文字
delay = game.rnd.between(500, 3000):在 500 與 3000 (亦即 0.5~3 秒) 之間隨機挑選一個數字
game.time.events.add(...):在延遲 delay 時間後,加入一個事件:執行 this.startDraw() → 拔槍

∗ startDraw():拔槍

var Shootout = {
  ...
  stepAway: function() {
    ...
  },

  startDraw: function() {
    this.mode = this.SHOOTING;
    this.drawText.visible = true;

    var reactionTime = game.rnd.between(this.reactionTimeMin,
                                        this.reactionTimeMax);
    this.cowboyTimer = game.time.create(false);
    this.cowboyTimer.add(reactionTime, this.heShoots, this);
    this.cowboyTimer.start();
  }, 

};

this.mode = this.SHOOTING:進入開槍階段

this.drawText.visible = true:顯示拔槍文字

reactionTime ...:在最小與最大反應時間範圍內,挑選一個隨機數字

this.cowboyTimer = ...:產生一個牛仔計時物件,不自動刪除

this.cowboyTimer.add(...):在延遲反應時間後,在牛仔計時物件加入一個事件:執行 this.heShoots() → 他開槍

this.cowboyTimer.start():開始計時

∗ heShoots():他開槍

var Shootout = {
  ...
  startDraw: function() {
    ...
  },

  heShoots: function() {
    game.sound.play('fire');
    this.youLose();
  }, 

};

game.sound.play('fire'):播放開槍聲

this.youLose():執行你輸了函式

∗ youLose():你輸了

var Shootout = {
  ...
  heShoots: function() {
    ...
  },

  youLose: function() {
    this.mode = this.OUTCOME;
    this.drawText.visible = false;
    game.sound.play('casing');
    game.time.events.add(500, this.playSplat, this);
    this.helpText.text = '他開槍\n你死了\n下輩子拔槍快一點';
    this.helpText.visible = true;
    this.background.loadTexture('lose');
    this.shotAt = game.time.time;
    this.cowboyTimer.stop();
  }, 

};

this.mode = this.OUTCOME():進入結果階段

this.drawText.visible = false:隱藏拔槍文字

game.sound.play('casing'):播放彈殼掉落聲

game.time.events.add(...):延遲 0.5 秒執行 this.playSplat() → 播放聲音

this.helpText ...:設定簡介文字,並呈現

this.background.loadTexture('lose'):顯示你輸了背景影像

this.shotAt = ...:設定被射擊時間為現在

this.cowboyTimer.stop():停止牛仔計時

∗ playSplat():播放槍套管聲音

var Shootout = {
  ...
  youLose: function() {
    ...
  },

  playSplat: function() {
    game.sound.play('splat');
  }, 

};

game.sound.play('splat'):播放槍套管聲音

∗ cheat():作弊

var Shootout = {
  ...
  playSplat: function() {
    ...
  },

  cheat: function() {
    this.youWin(true);
  }, 

};

▸ 執行 this.youWin() 函式,參數 true:作弊 (false:沒有作弊)

∗ youWin():你贏了

var Shootout = {
  ...
  cheat: function() {
    ...
  },

  youWin: function(cowardly) {
    this.mode = this.OUTCOME;
    this.drawText.visible = false;
    this.paceText.visible = false;
    game.sound.play('fire');
    game.sound.play('casing');

    game.time.events.add(500, this.playSplat, this);

    if (cowardly) {
      this.helpText.text = '你先開槍\n你射死他了\n但你真是個小人'
    }
    else {
      this.helpText.text = '他拔槍\n但你先開槍\n你射死他了,好槍法'
    }
    this.helpText.visible = true;
    this.background.loadTexture('win');

    this.paceTimer.stop();
    this.shotAt = game.time.time;

    if (this.cowboyTimer) {
      this.cowboyTimer.stop();
    }
  },

};

this.mode = this.OUTCOME:進入結果階段

this.drawText.visible = false:隱藏拔槍文字

this.paceText.visible = false:隱藏步行文字

game.sound.play('fire'):播放開槍聲音

game.sound.play('casing'):播放彈殼掉落聲音

game.time.events.add(500, this.playSplat, this):延遲 0.5 秒後,在遊戲計時上加入一個事件:執行 this.playSplat() → 播放聲音

if (cowardly) ...:如果作弊,顯示作弊文字,否則顯示未作弊文字

this.helpText.visible = true:顯示簡介文字

this.background.loadTexture('win'):載入你贏背景影像

this.paceTimer.stop():停止步行計時

this.shotAt = game.time.time:設定被射擊時間:現在

if (this.cowboyTimer) ...:如果牛仔計時還存在,停止計時

∗ shoot():開槍

var Shootout = {
  ...
  youWin: function() {
    ...
  },

  shoot: function() {
    if (this.cowboyTimer.running) {
      this.youWin(false);
    }
  }, 

};

if (this.cowboyTimer.running) ...:執行 this.youWin() → 你贏了,沒有作弊

∗ restart():重新開始

var Shootout = {
  ...
  shoot: function() {
    ...
  },

  restart: function () {
    this.mode = this.INTRO;
    this.background.loadTexture('standoff');

    this.cowboyTimer = null;

    this.pace = 1;
    this.paceTimer = null;
    this.paceText.text = '1';

    this.shotAt = 0;
    this.helpText.text = this.introText;
    game.sound.play('reload');
  }, 

};

this.mode = this.INTRO:進入簡介階段

this.background...:設定背景為兩人對峙影像

this.cowboyTimer = null:刪除牛仔計時

this.pace = 1:步行從 1 開始

this.paceTimer = null:刪除步行計時

this.paceText.text = '1':步行文字設為 1

this.shotAt = 0:設定被射擊時間為 0

this.helpText...:設定簡介文字

game.sound.play('reload'):播放裝填子彈聲音

(7) 練習

上一章       下一章