Phaser -- HTML 遊戲框架

第 10 章    打磚塊

(1) 遊戲規格

∗ 打磚塊( Brick Breaker )

▸ 頂端有四列磚塊,底端有橫桿

▸ 球在磚塊與橫桿之間來回彈跳

brickBreaker

(2) 建立專案

∗ 遊戲專案

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

▸ 下載資產 assets10.zip

▸ 建立初始 index.html

▸ 遊戲視窗 640x480

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

(3) Preload

∗ 載入影像與音訊資產

  preload: function() {
    // Images
    game.load.image('paddle', 'assets/paddle.png')
    game.load.image('greenBrick', 'assets/greenBrick.png');
    game.load.image('purpleBrick', 'assets/purpleBrick.png');
    game.load.image('redBrick', 'assets/redBrick.png');
    game.load.image('yellowBrick', 'assets/yellowBrick.png');
    game.load.image('ball', 'assets/ball.png');
    game.load.image('blueBG', 'assets/blueBG.png');
    game.load.image('blackBG', 'assets/blackBG.png');
    
    // Audio
    game.load.audio('sfxHitBrick', 'assets/fx_hit_brick.wav');
    game.load.audio('sfxHitPaddle', 'assets/fx_hit_paddle.wav');
    game.load.audio('bgmMusic', 'assets/bgm_electric_air.ogg');
  },

(4) 各種設定

∗ 設定音效、啟用碰撞系統、建立背景、及鍵盤輸入

  create: function() {
    // Audio
    this.sfxHitBrick = game.add.audio('sfxHitBrick');
    this.sfxHitPaddle = game.add.audio('sfxHitPaddle');
    this.bgmMusic = game.add.audio('bgmMusic');
    this.bgmMusic.loop = true;
    this.bgmMusic.play();
  
    // Physics system
    game.physics.startSystem(Phaser.Physics.ARCADE);
  
    // Tilesprite: blue background   
    this.background = game.add.tileSprite(0, 0, game.world.width,
                                         game.world.height, 'blueBG');
  
    // Keyboard
    this.cursors = game.input.keyboard.createCursorKeys();
    
  },

game.add.audio(...):加入各種音效

.loop = true:不斷播放

.play():開始播放

startSystem(...):啟動 Arcade 碰撞系統

game.add.tileSprite(...):以 blugBG 磚塊拼貼背景

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

(5) 製作磚塊

∗ 在 Create 階段:

  create: function() {
    ... 
    // Keyboard
    this.cursors = game.input.keyboard.createCursorKeys();
    
    // Bricks
    this.numCols = 10;
    this.numRows = 4;
    this.bricks = game.add.group();
    this.bricks.enableBody = true;
    this.bricks.bodyType = Phaser.Physics.ARCADE;
  
    var brickAssets = [
      'greenBrick',
      'purpleBrick',
      'redBrick',
      'yellowBrick',
    ];
  
    for (var i=0; i<this.numRows; i++) {
      var brickAsset = brickAssets[i];
      for (var j=0; j<this.numCols; j++) {
        var brick = this.bricks.create(0, 0, brickAsset);
        brick.x = brick.width*j;
        brick.y = brick.height*i;
      }
    }
    this.bricks.setAll('body.immovable', true);
  },

numCols, numRows :磚塊的陣列 ( 10 x 4 )

this.bricks = game.add.group() :產生磚塊群組

bricks.enableBody = true :設定磚塊群組有物理物體特性

bricks.bodyType = ...ARCADE :設定磚塊群組有 Arcade 物理系統

var brickAssets = [ ... :製作資產字串陣列,方便後續設定磚塊位置時取用

for (var i ...) ... for (var j ...) ... :產生磚塊群組成員,分別放置在適當位置

bricks.setAll('body.immovable', true) :設定磚塊碰撞時不會移動

∗ 結果:

brickBreakerResult

(6) 製作橫桿及下方黑色橫條背景

∗ 製作橫桿

▸ 在 Create 階段:

  create: function() {
    ...
    this.bricks.setAll('body.immovable', true);
  
    // Paddle
    this.paddleVelX = 500/1000;
    this.paddlePrevX = game.input.x;
    this.paddle = game.add.sprite(0, 0, 'paddle');
    this.paddle.anchor.setTo(0.5, 1);
    this.paddleHalf = this.paddle.width/2;
    game.physics.arcade.enable(this.paddle);
    this.paddle.body.immovable = true;
  
    this.resetPaddleBall(); 
    
  },
paddleVelX = 500/1000x 方向速度為每秒 500 個單位距離 (之後會以標準計時計算)
paddlePrevX = game.input.x:設定先前的 x 位置為目前滑鼠的 x 位置,以便之後判斷先前位置與滑鼠位置,如果有差異, 就移至滑鼠位置 ( 亦即由滑鼠控制橫桿位置 )
game.add.sprite(...):加入橫桿精靈
.anchor.setTo(0.5, 1):橫桿中心點之 x 方向在中間, y 方向在下方
this.paddleHalf:一半寬度 ( 預備將球至於橫桿中央點 )
game.physics.arcade.enable(...):啟用碰撞特性 ( 橫桿與球之碰撞 )
body.immovable = true:橫桿與球碰撞後保持不動
this.resetPaddleBall():重設橫桿及球的函式 ( 目前僅設定橫桿 )
  update: function() {
  },
  
  resetPaddleBall: function() {
    this.paddle.x = game.world.centerX;
    this.paddle.y = game.world.height - this.paddle.height;
  },
# paddle.x = game.world.centerX:橫桿的 x 座標設為遊戲世界 x 座標的中心
# paddle.y = game.world.height - this.paddle.height:設定適當的 y 座標
brickBreakerResult

▸ 在 Update 階段:偵測滑鼠指標及鍵盤方向鍵,兩者均可移動橫桿

  update: function() {
    if (this.paddlePrevX != game.input.x) {
      this.paddle.x = game.input.x;
    }
    else if (this.cursors.left.isDown) {
      this.paddle.x -= this.paddleVelX*game.time.physicsElapsedMS;
    }
    else if (this.cursors.right.isDown) {
      this.paddle.x += this.paddleVelX*game.time.physicsElapsedMS;
    }
  
    this.paddlePrevX = game.input.x;
  
    if (this.paddle.x-this.paddleHalf < 0) {
      this.paddle.x = this.paddleHalf;
    }
    if (this.paddle.x+this.paddleHalf > game.world.width) {
      this.paddle.x = game.world.width - this.paddleHalf;
    }
  },
if (this.paddlePrevX != game.inputx):橫桿先前的位置不是目前指標的位置, 表示滑鼠移動了,橫桿也要跟著移動,亦即將橫桿位置移到滑鼠位置
else if (this.cursors.left.isDown ...):如果鍵盤左鍵按下,橫桿向左移動, 移動距離為速度乘上 game.time.physicsElapsedMS
# game.time.physicsElapsedMS:物理系統的標準時段,使用此數值可以將整個遊戲的各種計時標準化, 此值為 1000/FPS (Frames per second:每秒播放畫面幀數,預設為 60),因此,1000/FPS = 16.67 ms
else if (!this.cursors.left.isDown ...):如果鍵盤右鍵按下,橫桿向右移動,距離同上
this.paddlePrevX = game.input.x:更新橫桿的「先前」位置 (如果滑鼠沒有移動, 橫桿先前位置就等於滑鼠位置,不影響橫桿目前位置)
✶ 最後判斷如果橫桿位置超出左右邊界,將其定在左右邊界
→ 測試:使用滑鼠及鍵盤左右方向鍵均可控制橫桿

∗ 製作下方黑色橫條背景

▸ 在 Create 階段:

✶ 因之後在 this.resetPaddleBall() 函式裡將加入球的資訊,故需往下移,因此, 在 this.resetPaddleBall() 之前加入以下
  create: function() {
    ...
    this.paddle.body.immovable = true;
  
    // Tilesprite: bottom black background
    var blackLine = game.add.tileSprite(0, 0, game.world.width,
                                        this.paddle.height, 'blackBG');
    blackLine.anchor.set(0, 1);
    blackLine.y = game.world.height; 
  
    this.resetPaddleBall();
    
  },
* game.add.tileSprite(...):以 blackBG 磚塊拼貼橫條背景, 高度等於橫桿高度
* blackLine.anchor.set(0, 1):中心點之 x 方向在左邊, y 方向在下方
* blackLine.y = game.world.height:設定垂直方向位置等於世界底端

(7) 製作球

∗ Create 階段:在 this.resetPaddleBall() 之前加入以下

  create: function() {
    ...
    blackLine.y = game.world.height;
  
    // Ball
    this.ball = game.add.sprite(100, 100, 'ball');
    game.physics.arcade.enable(this.ball);    
    this.ball.body.enable = true;
    this.ball.anchor.setTo(0.5, 1);
    this.ball.body.bounce.set(1);
    this.ball.body.collideWorldBounds = true;
    game.physics.arcade.checkCollision.down = false;
    this.ball.initVelX = 200;
    this.ball.initVelY = -300;
    game.input.onDown.add(this.shootBall, this);
  
    this.resetPaddleBall();
  
  },

add.sprite():加入球精靈 (位置不拘,之後會在 this.setPaddle() 函式設定)

arcade.enable(...):啟用球精靈的 Arcade 碰撞系統

this.ball.body.enable = true:啟用物理物體特性

anchor.setTo(0.5, 1):中心點之 x 方向在中間,y 方向在下方

bounce.set(1):物理物體的彈性為完全彈性

collideWorldBounds = true:設定球會碰撞遊戲世界的邊界

checkCollision.down = false:不偵測世界邊界下方的碰撞 (亦即,物件可移出下邊界, 所以球可以落出下邊界)

initVelX, initVelY:設定球的 xy 方向的初始速度

game.input.onDown.add(this.shootBall, this):如果按下滑鼠鍵可發射球

∗ resetPaddleBall():設定球未發射前,與橫桿相同位置

  resetPaddleBall: function() {
    ...
    this.paddle.y = game.world.height - this.paddle.height;
    this.ball.x = this.paddle.x;
    this.ball.y = this.paddle.y - this.paddle.height;
    this.ball.body.velocity.set(0);
    this.ball.isShot = false;
  },

this.ball.x = ..., this.ball.y = ...:重設橫桿與球時,球固定在橫桿上

this.ball.body.velocity.set(0):速度設為 0,防止球一開始就墜落

isShot = false:設定球尚未發射

∗ shooBall():發球

  resetPaddleBall: function() {
    ...
  },

  shootBall: function() {
    if (this.ball.isShot) {
      return;
    }
    var velX = this.ball.initVelX;
    var velY = this.ball.initVelY;
    var rand = Math.floor(Math.random()*2);
    if (rand % 2) {
      velX *= -1;
    }
    this.ball.isShot = true;
    this.ball.body.velocity.set(velX, velY);
    this.sfxHitPaddle.play();
  },

}; 

if (this.ball.isShot) ... :如果球已發射,就不再發射

velX, velY:球速變數

Math.floor(Math.random()*2):產生 0 或 1 的隨機整數數值

if (rand % 2) ...:一半的機率球往右跑,一半的機率球往左跑

isShot = true:設定已發球

velocity.set(...):設定球速

this.sfxHitPaddle.play():發出與橫桿碰撞聲音

∗ Update:偵測碰撞

  update: function() {
    ...
  
    if (this.paddle.x+this.paddleHalf > game.world.width) {
      this.paddle.x = game.world.width - this.paddleHalf;
    }
  
    game.physics.arcade.collide(this.ball, this.paddle);
    game.physics.arcade.collide(this.ball, this.bricks);
  
    if (!this.ball.isShot) {
      this.ball.x = this.paddle.x;
    }
    if (game.input.keyboard.isDown(Phaser.Keyboard.SPACEBAR)) {
      this.shootBall();
    }
  },

game.physics.arcade.collide(...):設定球與橫桿及磚塊會發生碰撞

if (!this.ball.isShot) { ... }:球未發射前,固定在橫桿上

if ( ... SPACEBAR)):如果使用者按下空間棒,呼叫 shootBall() 函式射球

∗ 測試:確認發球與碰撞效果正確

(8) 生命數與碰撞磚塊點數

∗ Create:加入生命數量及點數

  create: function() {
    ... 
    this.resetPaddle();
  
    // Lives
    var txtConfig = {
      fontSize: '15px',
      fill: '#ffffff',
    };
    this.lifeText = '生命數:';    
    this.lives = 3;
    this.txtLives = game.add.text(0, 0, this.lifeText+this.lives, txtConfig);
    this.txtLives.align = 'left';
    this.txtLives.anchor.set(0, 1);
    this.txtLives.y = game.world.height;
    
    // Points
    this.pointText = ' 點';
    this.points = 0;
    this.txtPoints = game.add.text(0, 0, this.points+this.pointText, txtConfig);
    this.txtPoints.align = 'right';
    this.txtPoints.anchor.set(1);
    this.txtPoints.x = game.world.width;
    this.txtPoints.y = game.world.height;
  
  },

txtConfig:文字樣式

this.lifeText:生命數文字

this.lives = 3:3 個生命 ( 亦即 3 個球 )

this.txtLives:文字物件 ( 並放置在適當位置 )

this.pointText:點數文字

this.points = 0:點數從 0 開始

this.txtPoints:文字物件 ( 並放置在適當位置 )

(9) 球與橫桿碰撞

∗ Update:修改碰撞程式,加入函式處理球與橫桿的碰撞,當撞擊發出聲音

  update: function() {
    ...
    game.physics.arcade.collide(this.ball, this.paddle, this.hitPaddle, null, this);
    game.physics.arcade.collide(this.ball, this.bricks);
    ...
  },

∗ hitPaddle():

  shootBall: function() {
    ...
  },
  
  hitPaddle: function() {
    this.sfxHitPaddle.play();
  },

sfxHitPaddle.play():碰撞時發出聲音

(10) 球與磚塊碰撞

∗ Update:修改碰撞程式,加入函式處理球與磚塊的碰撞,當球撞擊磚塊時,刪除磚塊

  update: function() {
    ...
    game.physics.arcade.collide(this.ball, this.paddle, this.hitPaddle, null, this);
    game.physics.arcade.collide(this.ball, this.bricks, this.removeBrick, null, this);
    ...
  },

▸ 加入 removeBrick() 函式參數,當碰撞時呼叫該函式

∗ removeBrick():

  hitPaddle: function(ball, paddle) {
    ...
  },
  
  removeBrick: function(ball, brick) {
    brick.kill();
    this.points += 10;
    this.txtPoints.text = this.points + this.pointText;
    this.sfxHitBrick.play();
  },

brick.kill():碰撞後刪除磚塊

this.points += 10:點數加 10

this.txtPoints.text = ...:修改文字內容

this.sfxHitBrick.play():播放碰撞聲音

(11) 扣減生命

∗ 當球落出下邊界,扣減生命

Create:在 this.resetPaddleBall() 指令之前加入以下

  create: function() {
    ...
    game.input.onDown.add(this.shootBall, this);
  
    this.ball.checkWorldBounds = true;
    this.ball.events.onOutOfBounds.add(this.loseLife, this);
  
    this.resetPaddleBall();
    ...
  
  },

this.ball.checkWorldBounds = true:需檢查與邊界碰撞

.events.onOutOfBounds.add(this.loseLife, this):球出界時呼叫 loseLife() 函式

∗ loseLife():

  removeBrick: function(ball, brick) {
    ...
  },
  
  loseLife: function() {
    this.lives--;
    this.txtLives.text = this.lifeText + this.lives;
    this.resetPaddleBall();
  },

(12) 練習

上一章       下一章