Phaser -- HTML 遊戲框架

第 5 章    迷宮飆車

(1) 遊戲規格

∗ 迷宮飆車

▸ 建立一個迷宮,裡面有馬路及圍牆

▸ 汽車在迷宮中行駛,使用者可以控制轉彎

maze

(2) 建立專案

∗ 遊戲專案

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

▸ 下載資產 assets05.zip

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

▸ 遊戲視窗 640x480

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

(3) Preload

∗ 載入資產:地圖資料、磚塊影像、汽車影像

  preload: function() {
    game.load.image('car', 'assets/car.png');
    game.load.image('mazeTiles', 'assets/tiles.png');
    game.load.tilemap('map', 'assets/maze.json', null, Phaser.Tilemap.TILED_JSON);
  },

game.load.image('car', ...):載入汽車影像

game.load.tilemap(...):載入地圖資料

✶ 參數1:設定地圖資產鍵名稱
✶ 參數2:地圖資料檔案(JSON 或 CSV格式)
✶ 參數3:如果參數 2 為地圖檔案,參數3就給 null;如果地圖資料先前已經載入或產生 (可透過第三方來源或執行程式產生),就放在參數 3 ,而參數 2 就給 null
✶ 參數4:若資料格式為 JSON,就給 Phaser.Tilemap.TILED_JSON;若為 CSV 格式則給 Phaser.Tilemap.CSV

▸ 地圖資料包含:

✶ 圖層資料:在每個網格座標指定磚塊編號
✶ 磚塊的寬高 (單位:像素)
✶ 圖層的寬高 (單位:磚塊)

game.load.image('mazeTiles', ...):載入磚塊影像

✶ 磚塊影像包含各種磚塊樣式,如圖 1
✶ 在地圖資料中利用序號來選用某個磚塊,序號從 1 開始,由左至右、由上至下計數。例如:使用圖 1 之磚塊影像及圖 2 的地圖資料, 會產生圖 3 之磚塊地圖:
maze1圖 1

20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20,  1, 20,  1,  1,  1,  1,  1,  1, 20,  1,  1,  1,  1,  1,  1,  1,  1,  1, 20,
20,  1, 20,  1, 20, 20, 20, 20, 20, 20,  1, 20, 20, 20, 20, 20, 20, 20,  1, 20,
20,  1, 20,  1, 20,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 20,  1, 20,  1, 20,
20,  1, 20,  1, 20,  1, 20, 20, 20, 20, 20,  1, 20, 20,  1, 20,  1, 20,  1, 20
圖 2

maze2圖 3

(4) Create

∗ 設定各個變數值

  create: function() {
    this.safeTileIndex = 1;
    this.gridsize = 32;
    
    // Half the grid size
    this.gridsize2 = this.gridsize/2;
  
    this.speed = 150;
    this.threshold = 6;
  
    this.gridPos = new Phaser.Point();
    this.turnPoint = new Phaser.Point();
  
    this.fourTiles = [null, null, null, null, null];
    this.opposites = [Phaser.NONE, Phaser.RIGHT, Phaser.LEFT,
                      Phaser.DOWN, Phaser.UP];
    this.currentDir = Phaser.UP;
  
    game.physics.startSystem(Phaser.Physics.ARCADE);
  },

.safeTileIndex = 1:安全磚塊為 1 號磚塊 (亦即可在其上行駛者)

.gridsize:網格尺寸為 32 (亦即磚塊尺寸)

.speed:汽車速度

.threshold = 6:位置視為相同的門檻,汽車到達可轉彎點而且使用者按下方向鍵才會轉彎,但在高速行駛的狀態下, 使用者按下方向鍵時,汽車可能距離該點很近,但並非完全在該點,為使汽車能順利轉彎,汽車位置與轉彎點距離相差在 6 以內都算是在相同位置

.gridPos:汽車位置的網格座標 (汽車位置是像素座標)

.turnPoint:轉彎點 (在磚塊中央)

.fourTiles:用來記錄汽車目前位置的上下左右磚塊網格座標 (1 左,2 右,3 上,4 下),第 0 個元素不使用

.opposites:記錄反方向,左變右 (1: Phaser.RIGHT )、右變左 (2: Phaser.LEFT )、上變下 (3: Phaser.DOWN )、下變上 (4: Phaser.UP ),第 0 個元素不使用

✶ 利用 Phaser 所提供的方向常數: Phaser.None: 0, Phaser.LEFT: 1, Phaser.RIGHT : 2, Phaser.UP: 3, Phaser.DOWN: 4
✶ 將方向常數帶入 .opposite[] ,即可得到反方向,例如 .opposite[Phaser.LEFT] 會得到 Phaser.RIGHT

.currentDir:現在的汽車方向 (初始值設為 Phaser.UP,向上)

.physics.startSystem(...):啟用 Arcade 碰撞系統

∗ 加入磚塊地圖

  create: function() {
    ...
    game.physics.startSystem(Phaser.Physics.ARCADE);
  
    this.map = game.add.tilemap('map');
    this.map.addTilesetImage('tiles', 'mazeTiles');
    this.layer = this.map.createLayer('Tile Layer 1');
    this.map.setCollision(20, true, this.layer);
  },

game.add.tilemap('map'):將地圖資料加入遊戲

.addTilessetImage('tiles', 'tiles'):將磚塊影像加入地圖資料,組成磚塊地圖

✶ 第1個參數來自於在 JSON 檔案裡所設定的 tilesets 的名稱
"tilesets":[
        ...
        "name":"tiles ",
✶ 第2個參數是磚塊影像資產鍵

.createLayer('The Layer 1'):在磚塊地圖中產生圖層,使用在 JSON 檔案裡所設定的 layers 的名稱

"layers":[
        ...
        "name":"Tile Layer 1 ",
✶ 地圖資料可以包含許多圖層,可以變換使用 (例如 maze.json 有兩個圖層)

.setCollision(20, true, this.layer):設定圖層磚塊的碰撞效應

✶ 第 1 個參數:有碰撞效應的磚塊編號,其效果就像圍牆,汽車無法穿過,沒有碰撞效應的磚塊就像道路,汽車可在其上行駛
→ 如果要設定多種磚塊,可利用陣列資料,例如 3、5、20 號磚塊有碰撞效應: .setCollision([3, 5, 20], true, this.layer)
✶ 第 2 個參數:是否啟用碰撞效果 ( true:啟用, false:停用)
✶ 第 3 個參數:所使用的圖層

∗ 產生汽車精靈及設定方向鍵等

  create: function() {
    ...
    this.map.setCollision(20, true, this.layer);
  
    this.car = game.add.sprite(48, 48, 'car');
    this.car.anchor.set(0.5);
    game.physics.arcade.enable(this.car);
    this.cursors = game.input.keyboard.createCursorKeys();

    this.move(Phaser.DOWN);
  },

game.add.sprite(..., 'car'):在遊戲中加入汽車精靈,擬置於左上角的道路上, 因磚塊及汽車尺寸都是 32x32,汽車錨點之後會設在中心點 (16),因此位置應該在 48 (32+16 = 48)

carDirection

.anchor.set(0.5):設定汽車錨點為中心點

.arcade.enable(...):汽車啟用 Arcade 物理特性 (如此才有碰撞效果)

.createCursorKeys():設定方向鍵

.move(...):新增 move() 方法,讓汽車在一開始時就向下移動

  update: function() {
  
  },
  
  move: function(to) {
    var speed = this.speed;
  
    if (to==Phaser.LEFT || to==Phaser.UP) {
      speed = -speed;
    }

    if (to==Phaser.LEFT || to==Phaser.RIGHT) {
      this.car.body.velocity.x = speed;
    }
    else {
      this.car.body.velocity.y = speed;
    }
    
    this.currentDir = to;
  },
to:傳入參數,汽車的前進方向
var speed = this.speed:宣告變數 speed 來調變汽車速度, this.speed 維持不變
✶ 第 1 個if:如果汽車朝左或朝上前進,則速度設定為負值
✶ 第 2 個if:如果汽車朝左或朝右前進,設定 x 方向速度,否則設定 y 方向速度
.currentDir = to:將目前方向設為前進方向

(5) Update

∗ 如何知道汽車可以轉彎?何時可轉彎?

1. 將汽車位置的像素座標轉為網格座標

2. 找出汽車周圍的四個磚塊,確認哪個磚塊是路

3. 偵測使用者按下方向鍵時,汽車是在本磚塊的中心點 *附近* 才允許轉彎

∗ 像素座標轉為網格座標

  update: function() {
    game.physics.arcade.collide(this.car, this.layer);
  
    this.gridPos.x = Math.floor(this.car.x/this.gridsize);
    this.gridPos.y = Math.floor(this.car.y/this.gridsize);
  },

.physics.arcade.collide(...):設定汽車與圖層碰撞效應

this.gridPos.x = ...; this.gridPos.y = ...; :將像素座標轉為網格座標

✶ 例如網格尺寸 32,像素點 (166, 70) → (5, 2)

▸ 汽車周圍上下左右的 4 個磚塊

  update: function() {
    ...
    this.gridPos.y = Math.floor(this.car.y/this.gridsize);
  
    var i = this.layer.index;
    var x = this.gridPos.x;
    var y = this.gridPos.y;
    this.fourTiles[Phaser.LEFT] = this.map.getTileLeft(i, x, y);
    this.fourTiles[Phaser.RIGHT] = this.map.getTileRight(i, x, y);
    this.fourTiles[Phaser.UP] = this.map.getTileAbove(i, x, y);
    this.fourTiles[Phaser.DOWN] = this.map.getTileBelow(i, x, y);
  },

i:圖層編號

x, y:汽車的的網格座標

.getTileLeft(...), .getTileRight(...), .getTileAbove(...), .getTileBelow(...) :計算汽車周圍上下左右四個磚塊,並儲存在 this.fourTiles 陣列中

▸ 可以利用 this.fourTiles[...].index 讀取磚塊的編號,以判斷是路還是牆

∗ 當使用者按下方向鍵,執行汽車轉彎

  update: function() {
    ...
    this.fourTiles[Phaser.DOWN] = this.map.getTileBelow(i, x, y);
  
    if (this.cursors.left.isDown) {
      this.turn(Phaser.LEFT);
    }
    else if (this.cursors.right.isDown) {
      this.turn(Phaser.RIGHT);
    }
    else if (this.cursors.up.isDown) {
      this.turn(Phaser.UP);
    }
    else if (this.cursors.down.isDown) {
      this.turn(Phaser.DOWN);
    } 
  },

▸ 如果使用按了左/右/上/下鍵 → 呼叫 turn() 方法處理汽車轉彎問題:

  move: function(to) {
    ...
  },
  
  turn: function(to) {
    if (this.currentDir==to || this.fourTiles[to]==null ||
        this.fourTiles[to].index!==this.safeTileIndex) {
      return;
    }
    
    if (this.currentDir == this.opposites[to]) {
      this.move(to);
      return;
    }
    this.turnPoint.x = this.gridPos.x*this.gridsize + this.gridsize/2;
    this.turnPoint.y = this.gridPos.y*this.gridsize + this.gridsize/2;
  
    var cx = Math.floor(this.car.x);
    var cy = Math.floor(this.car.y);
    if (Math.abs(cx - this.turnPoint.x)>this.threshold ||
        Math.abs(cy - this.turnPoint.y)>this.threshold) {
      return;
    }
  
    this.car.x = this.turnPoint.x;
    this.car.y = this.turnPoint.y;
    this.car.body.reset(this.turnPoint.x, this.turnPoint.y);
    this.move(to);
  },

to:使用者要轉的方向

▸ 第 1 個 if:如果有以下狀況之一,不做任何事

this.currentDir===to:目前方向就是使用者要轉的方向
this.fourTiles[to]===null:無此方向 (多加保險的指令)
this.fourTiles[to].index!==this.safeTileIndex:要轉的方向撞牆

▸ 第 2 個 if:如果目前方向和轉彎方向相反 (亦即回轉),呼叫 .move(...) 直接回轉

▸ 否則 (亦即往左或右轉):設定轉彎點為汽車網格座標的中心點 (像素座標) ,並比較汽車位置與轉彎點的距離

this.turnPoint...:轉彎點的位置 → 磚塊中心點
cx, cy:汽車位置的整數值
✶ 如果汽車的位置等於轉彎點即可轉向前進,但汽車可能速度過快、或者使用者按鍵速度不同等,汽車位置完全等於轉彎點位置的機率不高。 因此,使用約略值判斷汽車是否在轉彎點,亦即如果汽車位置和轉彎位置相差在 this.threshold 以外,即結束函式 (亦即不轉彎)
✶ 汽車可以轉彎:
- 將汽車移至轉彎點,即磚塊中央
- 亦將汽車物理物體位置重設為轉彎點 (如此才不會「卡」到牆角)
- 呼叫 .move() 方法,讓汽車轉向

(6) 汽車轉向

▸ 汽車在轉向時,利用動畫將車頭轉向前進方向

▸ 首先在 create 裡定義各個方向的旋轉角度

  create: function(to) {

    ...
    this.cursors = game.input.keyboard.createCursorKeys();

    // Car turning angles
    this.turnAngles = [];
    this.turnAngles[Phaser.LEFT] = 270;
    this.turnAngles[Phaser.RIGHT] = 90;
    this.turnAngles[Phaser.UP] = 0;
    this.turnAngles[Phaser.DOWN] = 180;
    
    this.move(Phaser.DOWN);
  },
✶ 利用陣列儲存旋轉角度,以上方為基準並依順時針方向,則向左為旋轉 270 度,向右為旋轉 90 度,向上為旋轉 0 度,向下為旋轉 180 度

▸ 在 move 函式中,利用 tween 旋轉汽車:

  move: function(to) {
    var speed = this.speed;

    // Rotate the car
    game.add.tween(this.car).to({angle:this.getAngle(to)},
                                this.speed, "Linear", true);

    if (to==Phaser.LEFT || to==Phaser.UP) {
      speed = -speed;
    }
    ...
✶ 為使汽車轉向看來正常,需決定應該順時針或逆時針轉向,以旋轉角度少者為主,因此呼叫 getAngle() 函式來決定旋轉角度

▸ 決定旋轉角度

  move: function(to) {
    ...
  },

  // Get the correct turning angle
  getAngle: function(to) {
    // Try only the following line:
    // return this.turnAngles[to];
    var carDirection = game.math.radToDeg(this.car.rotation); 
    var toDirection = this.turnAngles[to];
    var toDirectionNeg = toDirection - 360;
    var diffTurnRight = game.math.difference(toDirection, carDirection);
    var diffTurnLeft = game.math.difference(toDirectionNeg, carDirection);
    if (diffTurnRight < diffTurnLeft) {
      return toDirection;
    }
    return toDirectionNeg;
  },
  
  turn: function(to) {
    ...
  },
  
  ...
var carDirection = ...:將汽車目前弧度方向轉為角度
var toDirection = ...:將要轉的方向
var toDirectionNeg = ...:將要轉的方向的負角度 (亦即反向旋轉)
var diffTurnRight = ...:利用 game.math.difference() 計算如果要右轉將轉幾度
var diffTurnLeft = ...:計算如果要左轉將轉幾度
✶ 最後:如果右轉角度較小,則回覆要轉的方向,否則回覆要轉的方向的負角度

(7) 練習

上一章       下一章