Phaser -- HTML 遊戲框架

第 9 章    空戰奇兵

(1) 遊戲規格

* 空戰奇兵

▸ 敵方多架次戰機不斷下降攻擊

▸ 我方單機發射機槍回擊

airCombat

(2) 建立專案

* 遊戲專案

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

▸ 下載資產 assets09.zip

▸ 建立初始 index.html

▸ 遊戲視窗 800x600

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

(3) Preload

* 載入資產:影像及精靈表

  preload: function() {
    game.load.image('sea', 'assets/sea.png');
    game.load.image('bullet', 'assets/bullet.png');
    game.load.spritesheet('enemy', 'assets/enemy.png', 32, 32);
    game.load.spritesheet('explosion', 'assets/explosion.png', 32, 32);
    game.load.spritesheet('player', 'assets/player.png', 64, 64);
  },

game.load.image('sea', ...) :載入海平面磚塊影像

game.load.image('bullet', ...) :載入子彈影像

game.load.spritesheet('enemy', ...) :載入敵機精靈表

game.load.spritesheet('explosion', ...) :載入爆炸精靈表

game.load.spritesheet('bullet', ...) :載入我方戰機精靈表

(4) Create

* 製作海平面及設定我方戰機特性

  create: function() {
    
    game.physics.startSystem(Phaser.Physics.ARCADE);
  
    this.sea = game.add.tileSprite(0, 0, 800, 600, 'sea');
  
    this.player = game.add.sprite(400, 550, 'player');
    this.player.anchor.setTo(0.5);
    this.player.animations.add('fly', [0, 1, 2], 20, true);
    this.player.play('fly');
    this.physics.enable(this.player, Phaser.Physics.ARCADE);
    this.player.speed = 300;
    this.player.body.collideWorldBounds = true;
    this.player.body.setSize(20, 20, 0, -5);
  
  },

game.physics.startSystem(Phaser.Physics.ARCADE): 啟用 ARCADE 物理碰撞系統

game.add.tileSprite(x, y, width, height, key): 利用磚塊建立一片圖 (或背景), (x, y) 為圖的左上角位置, (width, height) 為圖的寬高度, Phaser 自動已磚塊填滿

game.add.sprite(...) :加入我方戰機精靈

anchor.setTo(0.5) :設定戰機錨點為中心點

.animations.add('fly', [0, 1, 2], 20, true) :設定動畫,名稱 fly ,使用 0~2 畫面,每秒跑 20 個畫面, true 表示不斷重複

.play(...) :播放動畫

.enable(..., Phaser.Physics.ARCADE) :啟用 ARCADE 物理系統 (亦可使用 .game.physics.arcade.enable(...) 指令)

speed = 30 :設定速度

.body.collideWorldBounds = true :會碰撞世界邊界

.body.setSize(...) :設定物理物體尺寸

* 設定敵機特性

  create: function() {
    ...
    this.player.body.setSize(20, 20, 0, -5);
    
    this.enemyPool = game.add.group();
    this.enemyPool.enableBody = true;
    this.enemyPool.createMultiple(50, 'enemy');
    this.enemyPool.setAll('anchor.x', 0.5);
    this.enemyPool.setAll('anchor.y', 0.5);
    this.enemyPool.setAll('outOfBoundsKill', true);
    this.enemyPool.setAll('checkWorldBounds', true);
    this.enemyPool.forEach(function(enemy) {
      enemy.animations.add('fly',[0, 1, 2], 20, true);
    });
    this.nextEnemyAt = 0;
    this.enemyDelay = 1000;
    
  },

game.add.group() :設定敵機群組並指派給 this.enemyPool 變數 (戰機群)

.enableBody = true :設定敵機群組有物理物體特性

.createMultiple(50, 'enemy'):一次產生 50 個敵機

.setAll('anchor.x', 0.5) :所有成員 x 方向錨點都在中心點

.setAll('anchor.y', 0.5) :所有成員 y 方向錨點都在中心點

.setAll('outOfBoundsKill', true) :所有成員離開世界邊界就殺死

.setAll('checkWorldBounds', true) :所有成員都需判斷是否在世界邊界

.forEach(function(enemy) { … } :每個成員設定動畫

this.nextEnemyAt = 0 :下一個敵機出現的時間 (會與現在時間比較,因此設定為 0 就會立即出現)

this.enemyDelay = 1000 :兩架敵機先後出現之間的延遲為 1 秒鐘 (亦即 1 秒鐘出現一架敵機)

* 設定子彈特性

  create: function() {
    ...
    this.enemyDelay = 1000;
    
    this.bulletPool = game.add.group();
    this.bulletPool.enableBody = true;
    this.bulletPool.createMultiple(100, 'bullet');
    this.bulletPool.setAll('anchor.x', 0.5);
    this.bulletPool.setAll('anchor.y', 0.5);
    
    this.nextShotAt = 0;
    this.shotDelay = 100;
    
  },

game.add.group() :設定子彈群組並指派給 this.bulletPool 變數 (子彈群)

.enableBody = true :設定敵機群組有物理物體特性

.createMultiple(100, 'bullet') :一次產生 100 發子彈

.setAll('anchor.x', 0.5) :所有成員 x 方向錨點都在中心點

.setAll('anchor.y', 0.5) :所有成員 y 方向錨點都在中心點

this.nextShotAt = 0 :下一個子彈可以發射的時間 (會與現在時間比較,因此設定為 0 就可以立即發射)

this.shotDelay = 100 :發射子彈之間的延遲為 0.1 秒鐘 (亦即 0.1 秒鐘可以發射一發子彈)

* 設定鍵盤及說明文字

  create: function() {
    ...
    this.shotDelay = 100;
    
    this.cursors = game.input.keyboard.createCursorKeys();
    
    this.instructions = game.add.text(400, 500, '使用方向鍵或滑鼠拖曳來移動飛機,按Z鍵或點滑鼠鍵來發射子彈\n', {font: '20px monospace', fill: '#fff', align: 'center'});
    this.instructions.anchor.setTo(0.5);
    this.instExpire = game.time.now + 5000;
    
  },

game.input.keyboard.createCursorKeys() :設定輸入鍵盤

game.add.text(...) :加入說明文字

.anchor.setTo(0.5) :錨點在中心點

this.instExpire = game.time.now + 5000 :文字顯示 5 秒後消失

(5) Update

* 海平面持續下降,使戰機有前進的感覺

  update: function() {
    this.sea.tilePosition.y += 2;
  
  },

.tilePosition.y += 2y 方向位置每次增加 2

* 處理碰撞、敵機出現模式、及控制我機等

▸ 敵機與子彈碰撞,我機與敵機碰撞

  update: function() {
    this.sea.tilePosition.y += 2;
  
    game.physics.arcade.overlap(this.bulletPool, this.enemyPool,
                                this.enemyHit, null, this);
  
    game.physics.arcade.overlap(this.player, this.enemyPool,
                                this.playerHit, null, this);
  },
game.physics.arcade.overlap(..., this.enemyHit, ...)this.enemyHit() 處理子彈與敵機碰撞
game.physics.arcade.overlap(..., this.playerHit, ...)this.playerHit() 處理我機與敵機碰撞

▸ 敵機出現方式

  update: function() {
    ...
    game.physics.arcade.overlap(this.player, this.enemyPool,
                                       this.playerHit, null, this);
  
    if (this.nextEnemyAt<game.time.now && this.enemyPool.countDead()>0) {
      this.nextEnemyAt = game.time.now + this.enemyDelay;
      var enemy = this.enemyPool.getFirstExists(false);
      enemy.reset(game.rnd.integerInRange(20, 780), 0);
      enemy.body.velocity.y = game.rnd.integerInRange(30, 60);
      enemy.play('fly');
    }
    
  },
if (this.nextEnemyAt<game.time.now && this.enemyPool.countDead()>0): 如果下一個敵機出現時間小於現在時間 (該出現了) ,而且還有活著的敵機
game.time.now + this.enemyDelay: 設定下一個敵機出現的時間是現在時間加上延遲時間
.getFirstExists(false) :群組在成立時,成員預設值為死亡, 此函式參數若為 true 則取第一個活著的成員,否則取第一個死亡的成員
enemy.reset(game.rnd.integerInRange(20, 780), 0): 重新設定位置,x 方向為 20~780 之間的隨機整數, y 方向為 0 (頂端) 。設定位置的另一個作用是將該成員設定為活著。 敵機若離開世界,就會設定為死亡,若設定其位置就會設定為活著,因此,在頁面上看到的敵機就是活著的, 沒看到的就是死亡的,所以前一個指令 (getFirstExists(false) 要從死亡的敵機中選出第一個讓其復活
.velocity.y = game.rnd.integerInRange(30, 60): 設定 y 方向速度為 30~60 之間的整數
.play('fly') :播放飛翔動畫

▸ 我機控制方式

  update: function() {
    ...
    if (this.nextEnemyAt<game.time.now && this.enemyPool.countDead()>0) {
      ...
    }
    
    this.player.body.velocity.x = 0;
    this.player.body.velocity.y = 0;
    
    if (this.cursors.left.isDown) {
      this.player.body.velocity.x = -this.player.speed;
    }
    else if (this.cursors.right.isDown) {
      this.player.body.velocity.x = this.player.speed;
    }
    
    if (this.cursors.up.isDown) {
      this.player.body.velocity.y = -this.player.speed;
    }
    else if (this.cursors.down.isDown) {
      this.player.body.velocity.y = this.player.speed;
    }
    
    if (game.input.activePointer.isDown &&
        game.physics.arcade.distanceToPointer(this.player)>15) {
      game.physics.arcade.moveToPointer(this.player, this.player.speed);
    }
    
    if (game.input.keyboard.isDown(Phaser.Keyboard.Z) ||
        game.input.activePointer.isDown) {
      this.fire();
    }
    
    if (this.instructions.exists && game.time.now>this.instExpire) {
      this.instructions.destroy();
    }
    
  },
.body.velocity.x = 0, .body.velocity.y = 0 :我機每次重速度 0 (靜止) 開始
.left.isDown {...} :若左鍵按下,左移
.right.isDown {...} :若右鍵按下,右移
.up.isDown {...} :若上鍵按下,上移
.down.isDown {...} :若下鍵按下,下移
.activePointer.isDown && .distanceToPointer(this.player)>15: 如果滑鼠 (或手機觸點) 按下,而且按下點和我機距離大於 15 ,則我機移向觸點
.keyboard.isDown(Phaser.Keyboard.Z) || .activePointer.isDown : 按下 z 鍵或者手機觸點,就執行 this.fire() 發射函式
.exists && game.time.now>this.instExpire :說明文字時間到,刪除

enemyHit() 函式屬性:子彈擊中敵機

  update: function() {
    ...
  },
  
  enemyHit: function(bullet, enemy) {
    bullet.kill();
    enemy.kill();
  
    var explosion = game.add.sprite(enemy.x, enemy.y, 'explosion');
    explosion.anchor.setTo(0.5);
    explosion.animations.add('boom');
    explosion.play('boom', 15, false, true);
  },
✶ 敵機與子彈碰撞:刪除敵機與子彈,加上爆炸效果( 15 :播放 15 次 ???false :不要重複播放, true :播放後刪除)

playerHit() 函式 :敵機與我機碰撞

  enemyHit: function(bullet, enemy) { 
    ...
  },
  
  playerHit: function(player, enemy) { 
    enemy.kill;
    var explosion = game.add.sprite(player.x, player.y, 'explosion');
    explosion.anchor.setTo(0.5);
    explosion.animations.add('boom');
    explosion.play('boom', 15, false, true);
    player.kill();
  },
✶ 敵機與我機碰撞:刪除敵機,加上爆炸效果,最後刪除我機

▸ 發射子彈

  playerHit: function(player, enemy) { 
    ...
  },
  
  fire: function() { 
    if (!this.player.alive || this.nextShotAt>game.time.now) {
      return;
    }
    if (this.bulletPool.countDead()==0) {
      return;
    }
    this.nextShotAt = game.time.now + this.shotDelay;
    
    var bullet = this.bulletPool.getFirstExists(false);
    bullet.reset(this.player.x, this.player.y-20);
    bullet.body.velocity.y = -500;
  },
!this.player.alive || this.nextShotAt>game.time.now: 如果我機已陣亡,或者尚未到達能夠發射下一發子彈,直接回復不處理
this.bulletPool.countDead()==0 :群組成員在成立時預設為死亡,因此,如果 countDead()==0 表示已無子彈
this.nextShotAt = game.time.now + this.shotDelay: 設定下一發子彈能夠發射的時間
.getFirstExists(false) :取得第一個死亡的成員
.reset(...) :設定位置,此指令的另一個作用是將成員改為活著
.body.velocity.y = ... :設定子彈的 y 方向速度

上一章       下一章