Phaser -- HTML 遊戲框架

第 4 章    坦克射擊

(1) 遊戲規格

∗ 調整坦克砲彈發射速度及砲管的角度來射擊標靶

phaserLogo

(2) 建立專案

∗ 遊戲專案

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

▸ 下載資產 assets04.zip

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

▸ 遊戲視窗 640x480

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

(3) Preload

∗ 載入資產:背景、坦克、砲管、砲彈、火焰、標靶

  preload: function() {
    game.load.image('background', 'assets/background.png');
    game.load.image('tank', 'assets/tank.png');
    game.load.image('turret', 'assets/turret.png');
    game.load.image('bullet', 'assets/bullet.png');
    game.load.image('flame', 'assets/flame.png');
    game.load.image('target', 'assets/target.png'); 
  },

(4) Create

∗ 在 create 階段:

▸ 初始設定

  create: function() {
    game.renderer.renderSession.roundPixels = true;
    game.world.setBounds(0, 0, 992, 480);
    game.physics.startSystem(Phaser.Physics.ARCADE);
    game.physics.arcade.gravity.y = 200; 
  },
.renderer.renderSession.roundPixels = true:像素顯示在整數位置 (round pixel: 整數位置),避免 Phaser 使用子像素位置 (Sub-pixel) 顯示,以免模糊
.world.setBounds(...):設定遊戲世界的邊界 (0, 0, 992, 480): 之後會設定鏡頭所看到的範圍為 640x480 (Phaser.Game(640, 480,...))
.physics.startSystem(...):使用 Arcade 物理系統
.physics.arcade.gravity.y = 200:設定重力為 200

▸ 加入背景精靈、標靶群組

  create: function() {
    ...
    game.physics.arcade.gravity.y = 200;
  
    game.add.image(0, 0, 'background');
  
    this.targets = game.add.group();
    this.targets.enableBody = true;
    this.targets.create(300, 390, 'target');
    this.targets.create(500, 390, 'target');
    this.targets.create(700, 390, 'target');
    this.targets.create(900, 390, 'target');
    this.targets.setAll('body.allowGravity', false); 
  },
.add.sprite(0, 0, 'background'):加入背景精靈,放在 (0, 0) 位置
this.targets = game.add.group():加入標靶群組
.enableBody = true:啟用標靶精靈的物理物體特性,因為要判斷碰撞
this.targets.create(...):產生四個標靶,並放置在適當位置
.setAll('body.allowGravity', false):設定不允許重力作用,否則會落下

▸ 加入砲彈精靈

  create: function() {
    ...
    this.targets.setAll('body.allowGravity', false);
  
    this.bullet = game.add.sprite(0, 0, 'bullet');
    this.bullet.exists = false;
    game.physics.arcade.enable(this.bullet); 
  },
.add.sprite(0, 0, 'bullet'):加入砲彈精靈,放在 (0, 0) 位置 (位置並不重要,因為之後會將其移至適當位置)
.bullet.exists = false:砲彈精靈預先設定為不存在:砲彈飛行中為存在, 使用者不能做任何操作,砲彈落地後為不存在,使用者才可以調整火力大小、砲管角度、或發射砲彈等
.physics.arcade.enable(this.bullet):啟用 Arcade 物理特性, 會行動的精靈需要使用物理特性

▸ 加入坦克及砲管精靈:坦克是由本體與砲管組合而成

  create: function() {
    ...
    game.physics.arcade.enable(this.bullet);
  
    this.tank = game.add.image(24, 383, 'tank');
    this.turret = game.add.sprite(this.tank.x+30, this.tank.y+14, 'turret');
  },
.add.image(24, 383, 'tank'):加入坦克本體影像,並放置在適當位置
.add.sprite(..., 'turret'):加入砲管精靈,位置在本體錨點位移 (30, 14)
phaserLogo

▸ 加入火焰精靈

  create: function() {
    ...
    this.turrent = game.add.sprite(this.tank.x+30, this.tank.y+14, 'turret');
  
    this.flame = game.add.sprite(0, 0, 'flame');
    this.flame.anchor.set(0.5);
    this.flame.visible = false; 
  },
.add.sprite(0, 0, 'flame'):加入火焰精靈,放在 (0, 0) 位置 (位置並不重要, 因為之後會將其移至適當位置)
.anchor.set(0.5):錨點為中心點
.visible = false:預設為不可見,待砲彈發射時才呈現

▸ 加入火力值及顯示文字:

  create: function() {
    ...
    this.flame.visible = false;
  
    this.power = 300;
    this.powerText = game.add.text(8, 8, 'Power: 300',
                                   {font: '18px Arial', fill: '#ffffff'});
    this.powerText.setShadow(1, 1, 'rgba(0, 0, 0, 0.8)', 1);
    this.powerText.fixedToCamera = true; 
  },
.power = 300:預設火力值 300
.add.text(...):加入文字物件,放到適當位置,設定字體
.setShadow(...):加上文字陰影:前兩個參數分別是 x 及 y 方向的陰影寬度, 第 3 個參數是陰影顏色,第 4 個參數是陰影邊緣至背景的模糊區寬度
.fixedToCamera = true:位置固定在鏡頭 (隨鏡頭移動)

▸ 設定操控鍵

  create: function() {
    ...
    this.powerText.fixedToCamera = true;
  
    this.cursors = game.input.keyboard.createCursorKeys();
    this.fireButton = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
    this.fireButton.onDown.add(this.fire, this); 
  },
.createCursorKeys():設定方向鍵
.addKey(Phaser.Keyboard.SPACEBAR):將 SPACEBAR 加入按鍵並指派為發射鍵 ( fireButton)
.onDown.add(this.fire, this):在 fireButton 信號 (Signal) 加上一個事件監聽器 (Event listener),只要此信號一發出,就執行監聽器函式
# 第一個參數:監聽器函式 (this.fire)
# 第二個參數:執行監聽器的環境 (this:亦即在 main 物件中)

▸ onDown() 的作用:設定在每次按下該按鍵時都呼叫所指定的函式 (在按鍵鬆開前只會執行一次)

this.fire() 方法

  create: function() {
    ...
  },
  
  update: function() {
    ...
  },
  
  fire: function() {
    if (this.bullet.exists) {
      return;
    }
    
    // Re-position the bullet where the turret is
    this.bullet.reset(this.turret.x, this.turret.y);
  
    // Now work out where the END of the turret is
    var p = new Phaser.Point(this.turret.x, this.turret.y);
    p.rotate(p.x, p.y, this.turret.rotation, false, 32);
  
    // Position the flame sprite there
    this.flame.x = p.x;
    this.flame.y = p.y;
    this.flame.alpha = 1;
    this.flame.visible = true; 
  },
if (this.bullet.exists):如果砲彈存在 (亦即正在飛行中), 就不允許使用者做任何操作 (直接結束函式)
.reset(...):如果砲彈不存在,則重設砲彈的位置在砲管錨點位置
✶ 計算砲口位置
# Phaser.Point(...):先產生一個點,位置在砲管錨點 (砲管底部)
# .rotate(...):旋轉 p 點後,再延伸 d 距離,就是砲口位置 (砲管長 32 px), .turret.rotation 是砲管的旋轉弧度
函式格式:
p.rotate(x, y, angle, asDegrees, distance);
p:要旋轉的點, (x, y): 旋轉軸點, angle:旋轉角度 (ө), asDegrees: 若為 true ,則 angle 值是角度, 否則是弧度, distance:延伸距離 (d)
phaserLogo
.flame.x = ..., .flame.y = ...:將火焰位置設在砲口 (p')
.alpha = 1:設為不透明 (之後會設為透明)
.visible = true:設為可見,亦即發射時可看見

▸ 火焰效果

  fire: function() {
    ...
    this.flame.visible = true;
  
    // Boom!!
    game.add.tween(this.flame).to({alpha:0}, 100, 'Linear', true);
  
    // Let camera follow the bullet
    game.camera.follow(this.bullet);
  
    // The launch trajectory is based on the angle of the turret and the power
    game.physics.arcade.velocityFromRotation(this.turret.rotation, this.power,
                                             this.bullet.body.velocity); 
  }
.add.tween(...).to(...): 將火焰物件從目前狀態轉到最終狀態,.to(...) 格式:
to(properties, duration, ease, autoStart, delay, ...)
# properties:指定需轉變的狀態,例如{ alpha:0 }: 最終狀態變成透明,或 {x:500}:最終 x 位置移到 500
# duration:狀態轉換的時間 (毫秒,ms),例如 100: 0.1 秒轉換完成
# ease:狀態轉換的模式,例如' Linear ': 線性轉換 (以線性方式從不透明狀態轉到透明狀態), 其他轉換方式可參考這裡, 函式與字串對應參考這裡
# autoStart:是否自動開始
# delay:延遲時間 (等候多少時間後,才開始轉變狀態)
.camera.follow(...):鏡頭追隨砲彈
.arcade.velocityFromRotation(...):利用 Arcade 物理特性來設定砲彈的速度 (產生符合物理現象的運動軌跡),格式如下:
velocityFromRotation(rotation, speed, point)
velocityFromRotation(rotation, speed, obj.body.velocity)
# rotation:發射的角度 (弧度),例如 this.turret.rotation: 砲管的角度
# speed:發射的速度,例如 this.power: 使用者所設定的發射速度
# obj.body.velocity:設定物件物理物體的速度,例如 this.bullet.body.velocity:砲彈的速度,設定完成後, 砲彈就會依照符合物理物體的飛行軌跡運動

(5) Update

∗ 基本演算法架構

  update: function() {
    //  If the bullet is in flight we don't let user control anything
    if (this.bullet.exists) {
      // If bullet touches the ground, remove it
      // Else if bullet hits targets, run callback function
    }
    else {
      // Let user adjust the bullet speed or turret's angle
      // Update the speed text
    }
  },

▸ 如果砲彈存在 (亦即在飛行中,使用者不能做任何操作)

✶ 如果砲彈著地,將其刪除
✶ 否則,偵測砲彈是否擊中標靶,並執行回呼函式處理碰撞問題

▸ 如果炮彈不存在

✶ 允許使用者調整砲彈發射速度或砲管角度
✶ 更新畫面上的速度文字

∗砲彈存在:偵測砲彈是否著地,若未著地則偵測是否擊中標靶

  update: function() {
    //  If the bullet is in flight we don't let user control anything
    if (this.bullet.exists) {
       if (this.bullet.y > 424) {  // Check to see if it's fallen too low
        this.removeBullet();
      }
      else {  //  Bullet vs. the Targets
        this.physics.arcade.overlap(this.bullet, this.targets,
                                    this.hitTarget, null, this);
      }
    }
    else {
      // ...
      // ...
    }
  },

if (this.bullet.y>424):偵測砲彈是否著地,標靶底部 (390 +標靶高度 (34) = 424)

✶ 若已著地,撰寫新方法 .removeBullet() 來刪除砲彈,並將鏡頭移回
✶ 若尚未著地:使用碰撞偵測,並由回呼函式 this.hitTarget() 來處理
this.physics.arcade.overlap(this.bullet, this.targets, this.hitTarget, null, this);

∗.removeBullet() 方法

  update: function() {
    ...
  },
  
  fire: function() {
    ...
  },
  
  removeBullet: function() {
    this.bullet.kill();
    game.camera.follow();
    game.add.tween(game.camera).to({x:0}, 1000, 'Quint', true, 1000);
  }, 

.kill():刪除砲彈

.camera.follow():解除鏡頭追蹤,原先設定追蹤砲彈, 沒有參數的 follow() 函式表示不追蹤任何物件

.tween(...).to(...):利用 tween 將鏡頭移回 x=0 的位置(狀態轉變)

{x:0}:最終位置狀態
✶ 第 1 個 1000:一秒內移回
Quint:以五次方曲線轉變方式移動位置
true:自動開始
✶ 第 2 個 1000:延遲一秒後再開始轉變狀態

∗.hitTarget()方法

  ...
  
  removeBullet: function() {
    ...
  },
  
  hitTarget: function(bullet, target) {
    target.kill();
    this.removeBullet();
    if (this.targets.total == 0) {
      game.state.start('main');
    }
  }, 

.kill():刪除標靶

.removeBullet():刪除砲彈 this

if (this.targets.total == 0) ...:如果標靶全部打完,重啟遊戲

∗ 砲彈不存在

▸ 調整砲彈速度並更新火力文字

  update: function() {
    if (this.bullet.exists) {
      ...
    }
    else {
      //  Allow user to set the power between 100 and 600
      if (this.cursors.left.isDown && this.power>200) {
        this.power -= 2;
      }
      else if (this.cursors.right.isDown && this.power<600) {
        this.power += 2;
      } 
    }
  },
✶ 每按一次左鍵或右鍵分別減或加 2 (範圍 200 ~ 600)

▸ 調整砲管角度

  update: function() {
    if (this.bullet.exists) {
      ...
    }
    else {
      //  Allow user to set the power between 100 and 600
      if (this.cursors.left.isDown && this.power>200) {
        ...
      }
      else if {
        ...
      }
      
      //  Allow to set the angle between -90 (straight up) and 0 (facing right)
      if (this.cursors.up.isDown && this.turret.angle>-90) {
        this.turret.angle--;
      }
      else if (this.cursors.down.isDown && this.turret.angle<0) {
        this.turret.angle++;
      }
      
      //  Update the text
      this.powerText.text = 'Power: ' + this.power; 
    }
  },
✶ 每按一次向下或向上鍵分別加或減 1 度 (角度,範圍 0 ~ -90)
.turret.rotation 會自動轉成弧度值
✶ 最後,更新火力文字
✶ 測試

(6) 進階功能

∗ 加上砲彈轟炸痕跡

▸ 在遊戲中放置一個點陣圖資料物件 (BitmapData object),該物件包含一個 HTML Canvas (畫布) 元素,可在其上繪製圖形

▸ 可先在點陣圖上繪製某個景觀,當砲彈擊中景觀某處時,在點陣圖的該處繪製一個圖形,象徵被砲彈轟炸的痕跡

▸ 點陣圖亦可應用在需要有動態的外表或紋理 (Texture) 的精靈

▸ 因將使用 Canvas 元素,需將 Phaser 顯示畫布的技術改為 Canvas:

var game = new Phaser.Game(640, 480,  Phaser.CANVAS , 'gameDiv');
game.state.add('main', main);
game.state.start('main');

∗Preload:加上 land.png 資產

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

▸ land.png 為景觀影像,含有透明色版 (Alpha channel),非景觀區都是透明

∗ Create:

▸ 加上點陣圖資料物件

  create: function() {
    ...
    this.fireButton.onDown.add(this.fire, this);
    
    this.land = game.add.bitmapData(992, 480);
    this.land.draw('land');
    this.land.update();
    this.land.addToWorld(); 
  },
.bitmapData():在遊戲中加入點陣圖資料物件, 並指派給 this.land 屬性
.draw(...):在點陣圖上繪製 land 圖形
.update():更新 this.land, 繪製結果才會顯示在畫布上
.addToWorld():將 this.land 物件加入遊戲世界 (因為沒有給參數所以位置在 (0, 0) )

▸ 再增加一個標靶,並改變各標靶的位置

  create: function() {
    ...
    this.targets.create( 284, 378 , 'target');
    this.targets.create( 456, 153 , 'target');
    this.targets.create( 545, 305 , 'target');
    this.targets.create( 726, 391 , 'target');
    this.targets.create(972, 74, 'target'); 
    ...
  },

∗Update:加上偵測砲彈與景觀的關係

  update: function() {
    //  If the bullet is in flight we don't let them control anything
    if (this.bullet.exists) {
      ...
    }
    else { 
      this.physics.arcade.overlap(this.bullet, this.targets,
                                  this.hitTarget, null, this);
      this.bulletVsLand(); 
    }
    else {
      ...
    }
  },

▸ 將原先的程式碼改為兩個指令:

.arcade.overlap(...):偵測砲彈與標靶的關係
.bulletVsLand():砲彈與景觀的關係由撰寫新方法 bulletVsLand() 來處理

∗ .bulletVsLand() 方法:

▸ 偵測砲彈是否應該刪除

var main = {
  ...
  
  bulletVsLand: function() {
    if (this.bullet.x>game.world.width || this.bullet.y>420) {
      this.removeBullet();
      return;
    }
  },
 
};
if (...):偵測砲彈是否超出遊戲世界的右方 (可以超出上方) 或落地
.removeBullet():,如果是,則將砲彈刪除,並結束函式

▸ 砲彈擊中景觀時,炸掉一塊圓形區域

  bulletVsLand: function() {
    if (this.bullet.x>game.world.width || this.bullet.y>420) {
      ...
    }
    
    var x = Math.floor(this.bullet.x);
    var y = Math.floor(this.bullet.y);
    var rgba = this.land.getPixel(x, y);
    if (rgba.a > 0) {    // Not transparent
      this.land.blendDestinationOut();
      this.land.circle(x, y, 16);
      this.land.blendReset();
      this.land.update();
      this.removeBullet(); 
    }
  
  },
Math.floor(...):將目前砲彈位置轉為整數 (x, y), 以便取出該點像素的值
.getPixel(...):取出點陣圖在該點的像素值,並指派給 rgba 變數
if (rgba.a>0):若 rgba 的透明色版值大於 0 (不透明,亦即砲彈擊中景觀),刪除一個圓形區域
# .blendDestinationOut():設定繪製圖形時,新圖形與目前畫布內容的混和模式為 destination-out,亦即顯示畫布與新繪製圖形不重疊的區域 (也就是將目前畫布刪除掉新繪製圖形的區域) → 各種混和模式
# .land.circle(...):以砲彈擊中點為中心畫一個半徑 16 的圓,因混和模式設定為 destination-out,因此會在點陣圖中將此圓的區域刪除
# .land.blendReset():重設混和模式
# .land.update():更新點陣圖 (繪製的結果才會顯現)
# .removeBullet():將砲彈刪除

▸ 擊中標靶時,加入爆炸效果

✶ Create:利用 Phaser 的 emitter (發射器) 物件,製作爆炸效果
  create: function() {
    ...
    this.land.addToWorld();
    
    this.emitter = game.add.emitter();
    this.emitter.makeParticles('flame');
    this.emitter.setXSpeed(-120, 120);
    this.emitter.setYSpeed(-100,-200);
    this.emitter.setRotation(); 
  
  },
.add.emitter():加入發射器
.makeParticles(...):製作發射粒子,亦使用 flame 資產
.setXSpeed(...):設定粒子 x 方向的隨機速度,範圍為 -120 ~ 120 (左右方向)
.setYSpeed(...):設定粒子 y 方向的隨機速度,範圍為 -100 ~ -200 (僅向上)
.setRotation():設定粒子旋轉角度範圍,沒有參數表示不旋轉

hitTarget() 方法:加入爆炸效果,如果標靶打完,延遲 2 秒看完爆炸效果,再重啟遊戲

  hitTarget: function(bullet, target) { 
    this.emitter.at(target);
    this.emitter.explode(2000, 10); 
  
    target.kill();
    this.removeBullet(true);

    if (this.targets.total == 0) {
      setTimeout(function() {
        game.state.start('main');
      }, 2000);
    } 
  },
.at(...):設定發射器在 target 精靈的位置
explode(...):爆炸,持續時間 2000 毫秒 (2 秒),一次發射 10 個粒子
.removeBullet(true):刪除砲彈,增加一個參數來控制鏡頭回復的等候時間 (擊中標靶有爆炸效果,擊中地面則沒有)
if (this.targets.total==0):如果標靶全部打完
setTimeout(...):等候 2 秒再重啟遊戲

▸ 修改 removeBullet():如果有爆炸效果,延遲 2 秒才回復鏡頭,否則僅延遲 1 秒

  removeBullet: function(hasExploded) {
    this.bullet.kill();
    game.camera.follow();
    var delay = 1000;
    if (hasExploded) {
      delay = 2000;
    }
    game.add.tween(game.camera).to({x:0}, 1000, 'Quint', true, delay); 
  },
✶ 增加參數 hasExploded
✶ 如果 hasExploded 為 true,鏡頭回復前等候 2 秒 (看完爆炸效果),否則只等候 1 秒

(7) 練習

上一章       下一章