Canvas 飛機大戰遊戲案例

引言

本項目使用H5 canvas繪製飛機大戰遊戲,使用ES6 class面向對象開發,目的在於練習js面向對象開發的能力,熟練使用canvas api。git

1、案例演示

飛機大戰遊戲,使用canvas繪製,程序僅做練習用,實現主要功能,沒有實現遊戲開始界面、結束判斷等,效果以下圖github


2、知識點

  • canvas的基本使用
  • 動畫requestAnimationFrame的基本使用
  • 碰撞、邊界檢測
  • js面向對象編程(OOP)
  • 遊戲按鍵事件處理

3、案例中的對象

  • Stage 舞臺
  • SceneBase 場景,場景中的基類
  • GameScene 遊戲場景
  • Sprite 精靈類,遊戲中的基類
  • Enemy 敵機類
  • Player 玩家戰機類
  • Bullet 子彈類
  • Particle 粒子類

4、實現思路

1. Sprite類

遊戲全部實體的基類(Player | Enemy | Bullet | Particle均繼承此類)算法

抽象出公共的屬性方法,以下:編程

constructor() {
  this.unit = 10;	// 最小單元
  this.vx = 0.2;	// x方向速度
  this.vy = 0.2;	// y方向速度
  this.sizeX = 2;
  this.sizeY = 2;
  this.posX = 0;	// x座標
  this.posY = 0;	// y座標
  this.color = '#369';
  this.alive = true;	// 是否存活
  this.birth = new Date();
}
getWidth() {...}	// 獲取對象真實大小
render() {...}	// 把此對象繪製在canvas上
複製代碼

這裏用unit定義了遊戲的最小單元,由於這個遊戲都是正方形,我不想每次都寫20、50、100像素,這樣看着數字太大了,定義一個最小單元,用getWidth()經過計算獲取實際的大小,即unit * sizecanvas

2. Particle類

用於控制遊戲中的粒子效果,繼承自Sprite類,擴展方法:api

this.life	// 對象生命,用於控制什麼時候銷燬對象

update() {	// 更新對象狀態並調用父類的render方法
  if (new Date() - this.birth > this.life) {
  	this.alive = false;
  }
  this.posX += this.vx;
  this.posY += this.vy;
  this.render();
}
複製代碼

3. Bullet類

遊戲中的子彈類,負責渲染遊戲中的子彈,繼承自Sprite類,擴展方法:bash

constructor(scene) {
  super();
  this.scene = scene;
  this.vx = 0;
  this.vy = -1;
  this.state = new Date();
  this.particleCB = 500;
  this.type = 0;  // 區分子彈類型 0 enemy, 1 player
}
collide() {...}	// 碰撞檢測
createParticle() {...}	// 子彈播放的例子效果
update() {...}	// 更新對象狀態並調用父類的render方法
複製代碼

子彈類中的碰撞檢測僅負責判斷與場景的碰撞dom

// 傳入場景寬高
collide(w, h) {
  if (this.posX < 0 - this.getWidth() ||
    this.posX > w + this.getWidth() ||
    this.posY < 0 - this.getWidth() ||
    this.posY > h + this.getWidth()) {
    this.alive = false;
  }
}
複製代碼

子彈運行過程會有粒子效果,這個功能在createParticle()實現ide

let p = new Particle({...});	// new一個實例
this.scene.particles.push(p);	// 把粒子加入場景,統一處理
update() { // 更新對象狀態
  if (new Date() - this.state >= this.particleCB) {
    this.createParticle();
    this.state = new Date();
  }
  this.posX += this.vx;
  this.posY += this.vy;
  this.render();
}
複製代碼

4. Enemy類

遊戲中的敵機類,負責渲染遊戲中的敵機,繼承自Sprite類,擴展方法:函數

constructor(scene) {
  super()
  this.scene = scene
  this.dx = .005	// x方向的加速度
  this.dy = 0	// y方向的加速度
  this.changeCB = 500	// 敵機改變方向的CB
  this.canChange = true
  this.init()
}
init() {
  this.bulletCB = 2000	// 敵機發射子彈的CB
  this.attackTime = -1	// 能夠攻擊的時間,小於0便可攻擊
  this.boomConfig = new Map([	// 配置敵機被擊中後的爆炸效果
    [2, [1.5, 1, 0.5]],	// 敵機大小爲2,爆炸後分紅1.5 1 0.5粒子大小
    [3, [2, 2, 1]]			// 敵機大小爲3,爆炸後分紅3 3 1粒子大小
  ])
}
collide() {...}	// 碰撞檢測
attack() {...}	// 發射子彈
boom() {...}	// 敵機中彈
changeX() {...}	// 敵機AI
changeY() {...}	// 敵機AI
change() {...}	// 敵機AI
update() {...}	// 更新對象狀態並調用父類的render方法
複製代碼
boomConfig中之因此那樣配置,是由於在場景類中初始化敵機只有三種大小,這裏沒有考慮那麼靈活,能夠在類中加入一個minSize和maxSize控制生成敵機答大小,這裏就不作那麼複雜了。
// GameScene -> createEnemy()
e.sizeX = Math.random() * 3 + 1 | 0;
複製代碼

5. Player類

Player類則相對複雜不少,有Enemy類的屬性方法外,還需註冊鍵盤事件,(Enemy和Player類這裏還能夠抽出一個基類,這裏就不搞的那麼複雜了)

init() {
    this.keydowns = {};
    this.actions = {};
    this.bulletCB = 800;
    this.attackTime = -1;
    this.registAction('a', this.moveLeft.bind(this));
    this.registAction('d', this.moveRight.bind(this));
    this.registAction('w', this.moveUp.bind(this));
    this.registAction('s', this.moveDown.bind(this));
    this.bindEvent();
  }
  registAction(key, callback) {
    this.actions[key] = callback;
  }
  bindEvent() {
    window.addEventListener('keydown', ev => {
      this.keydowns[ev.key] = true;
    })
    window.addEventListener('keyup', ev => {
      this.keydowns[ev.key] = false;
    })
  }
  update() {
    this.edge();	// 邊界檢測,限制玩家在場景內
    this.damage();	// 傷害檢測,子彈、敵機
    let actions = Object.keys(this.actions);
    actions.forEach(k => this.keydowns[k] && this.actions[k]())
    this.attack();
    this.render();
  }  
複製代碼

按鍵響應經過把按鍵狀態和回調函數存在對象的方式,使用registAction註冊相應事件,這樣代碼看上去會簡潔不少,僅需在update()中遍歷便可。

而對於邊界檢測和傷害檢測則本質是一個區間判斷,不一樣的是,邊界檢測是限制在一個區間,也就是若是不在這個區間須要限制(或者說是更正);而傷害檢測則是若是在這個區間那麼就觸發對應的事件,如我方子彈與敵機碰撞,則敵機炸燬,子彈銷燬。

下面是圖示,其實簡單的碰撞檢測都是這樣子的,好比打磚塊

6. Scene類

場景類這裏抽出了一個基類,SceneBase,當時考慮後期擴展會有遊戲主場景,開始場景,設置場景等,因此在這裏抽離了一層。

SceneBase類也沒定義多少內容,有兩個靜態屬性,WIDTH和HEIGHT,也就是場景大小,像這類屬性不必放在一個個實例中,全部實例共用一個便可,這類屬性通常能夠放在靜態屬性中。

真正是GameScene類在負責整個遊戲的運做,它繼承自SceneBase類,也是遊戲的引擎。

init方法則是初始化了遊戲的各個參數,

init() {
    super.init();
    this.setEnemyCD = 3000;	// 敵機出現的CB
    this.createEnemyTime = -1
    this.bullets = [];	// 場景中的全部子彈
    this.enemys = [];	// 場景中的全部敵人
    this.particles = [];	// 場景中的粒子
    this.initPlayer();
    this.initEnemy();
    this.warn = false;	// 玩家是否被擊中
    this.killCount = 0;
  }
複製代碼

update方法則是統一處理場景全部實體的更新

update() {
    super.update();
    this.initEnemy();
    if (this.warn) {
      this.warning();
      this.warn = false;
    } else {
      ctx.clearRect(0, 0, SceneBase.WIDTH, SceneBase.HEIGHT);
    }
    this.player.update();
    this.updateBullet();
    this.updateParticle();
    this.updateEnemy();
    this.updateInfo();
  }
複製代碼

7. Stage類

負責管理全部的場景,

constructor() {
    this.manager = []
    this.init()
  }
  add() {...}	// 添加場景對manager
  remove() {...}	// 對manager場景移除
  static freeze() {...} // 對manager場景凍結
  static unfreeze() {...} // 對manager場景解凍
  bindEvent() {...} // 一些事件響應
複製代碼

這個東西在這次案例並沒太多體現,是我在層疊消融遊戲案例中抽離出來的,運用在多場景切換中,好比一個是遊戲遊玩模式,一個是遊戲自定義編輯模式,是這種狀況下使用的。

您能夠查看關於 層疊消融遊戲 檢測與目標圖形匹配的算法

github:canvas飛機大戰(今天github山不去,代碼沒提交上去)

---------------------完結---------------------

相關文章
相關標籤/搜索