本項目使用H5 canvas繪製飛機大戰遊戲,使用ES6 class面向對象開發,目的在於練習js面向對象開發的能力,熟練使用canvas api。git
飛機大戰遊戲,使用canvas繪製,程序僅做練習用,實現主要功能,沒有實現遊戲開始界面、結束判斷等,效果以下圖github
遊戲全部實體的基類(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
用於控制遊戲中的粒子效果,繼承自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();
}
複製代碼
遊戲中的子彈類,負責渲染遊戲中的子彈,繼承自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();
}
複製代碼
遊戲中的敵機類,負責渲染遊戲中的敵機,繼承自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;
複製代碼
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()中遍歷便可。
而對於邊界檢測和傷害檢測則本質是一個區間判斷,不一樣的是,邊界檢測是限制在一個區間,也就是若是不在這個區間須要限制(或者說是更正);而傷害檢測則是若是在這個區間那麼就觸發對應的事件,如我方子彈與敵機碰撞,則敵機炸燬,子彈銷燬。
下面是圖示,其實簡單的碰撞檢測都是這樣子的,好比打磚塊
場景類這裏抽出了一個基類,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();
}
複製代碼
負責管理全部的場景,
constructor() {
this.manager = []
this.init()
}
add() {...} // 添加場景對manager
remove() {...} // 對manager場景移除
static freeze() {...} // 對manager場景凍結
static unfreeze() {...} // 對manager場景解凍
bindEvent() {...} // 一些事件響應
複製代碼
這個東西在這次案例並沒太多體現,是我在層疊消融遊戲案例中抽離出來的,運用在多場景切換中,好比一個是遊戲遊玩模式,一個是遊戲自定義編輯模式,是這種狀況下使用的。
github:canvas飛機大戰(今天github山不去,代碼沒提交上去)
---------------------完結---------------------