Phaser官方簡介
Phaser是一個HTML5游戲框架,它的目的是輔助開發者真正快速地制作強大的、跨瀏覽器的HTML5游戲。 做這個框架,主要是想發掘現代瀏覽器(兼及桌面和移動兩類系統)的優點。對瀏覽器的唯一要求是,支持畫布(canvas)標簽。
游戲鏈接
在線體驗:http://hymhub.gitee.io/plane-game-phaser/(ps: 沒做資源加載動畫,點擊開始游戲出現黑屏請耐心等待)
源碼地址:https://gitee.com/hymhub/plane-game-phaser
游戲說明
- 操作
PC端:鍵盤上下左右控制我方飛機移動,也可以鼠標左鍵按住飛機拖動
移動端:按住飛機拖動 - 道具
連發道具:吃到后增加一發子彈,上限9發,單發有效期12秒
炸彈道具:吃到后消滅全屏內敵方飛機
代碼
搭建開發環境
使用 Phase 必須使用服務器方式啟動,出於安全性考慮,Phaser 不能通過 file:// 方式加載本地資源,如果是 vscode 可以裝一個 Live Server 插件

隨后鼠標右鍵 html 文件點擊 Open with Live Server 即可

也可直接使用 vue/react 腳手架或是其他工具(例如 nginx、tomcat、WAMP Server、XAMPP)
創建初始化游戲場景
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>plane-game-phaser</title>
<style>
body {
margin: 0;
width: 100vw;
height: 100vh;
text-align: center;
}
</style>
<script type="text/javascript" src="./js/phaser.js"></script>
<!-- phaser3 框架代碼可以從上面的項目源碼中獲取,也可以使用網絡地址: -->
<!-- <script src="https://cdn.jsdelivr.net/npm/phaser@3.15.1/dist/phaser-arcade-physics.min.js"></script> -->
</head>
<body>
<script type="text/javascript" src="./js/index.js"></script>
</body>
</html>
js:
// 獲取屏幕寬度
let viewWidth = document.body.clientWidth > 420 ? 420 : document.body.clientWidth
// 獲取屏幕高度
let viewHeight = document.body.clientHeight > 812 ? 812 : document.body.clientHeight
// 獲取 dpr 設置分辨率
const DPR = window.devicePixelRatio
// 創建場景,場景1(初始化游戲)
class InitScene extends Phaser.Scene {
constructor() {
super({ key: 'InitScene' })
}
// 開始按鈕
startBtn = null
preload() {
// 加載資源,游戲圖片資源可以從上面的項目源碼中獲取
this.load.image('initBG', 'assets/imgs/startBG.png')
this.load.image('startBtn', 'assets/imgs/start_btn.png')
}
create() {
// 設置縮放讓背景拉伸鋪滿全屏 ,也可使用 setDisplaySize(viewWidth, viewHeight)
this.add.image(viewWidth / 2, viewHeight / 2, 'initBG').setScale(viewWidth / 320, viewHeight / 568)
this.startBtn = this.add.sprite(viewWidth / 2, viewHeight / 2 + 140, 'startBtn').setInteractive().setScale(.5)
// 綁定開始按鈕事件
this.startBtn.on('pointerup', function () {
game.scene.start('GameScene') // 啟動游戲中場景,后面會創建
game.scene.sleep('InitScene') // 使當前場景睡眠
})
}
update() {}
}
const config = {
type: Phaser.AUTO, // Phaser 檢測瀏覽器支持情況自行選擇使用 webGL 還是 Canvas 進行繪制
width: viewWidth,
height: viewHeight,
antialias: true, // 抗鋸齒
zoom: 0.99999999, // 縮放
resolution: DPR || 1, // 分辨率
physics: { // 物理系統
default: 'arcade',
arcade: {
gravity: { y: 0 }, // y 重力
debug: false
}
},
scene: [InitScene], // 場景
}
const game = new Phaser.Game(config)

創建游戲中場景並綁定鍵盤控制飛機移動
// 創建場景, 場景2(游戲中)
class GameScene extends Phaser.Scene {
constructor() {
super({ key: 'GameScene' })
}
// 只要是給當前類設置的屬性並且值為 null,則會在下面 create 中進行初始化
// phaser 內置鍵盤管理器
cursors = null
// 游戲背景
bg = null
initData() {
this.isGameOver = false // 判斷游戲是否結束
// 我方飛機x,y(非實時,用於拖拽和初始化使用,獲取實時直接player.x/player.y)
this.x = viewWidth / 2
this.y = viewHeight - 200
// 游戲運行全局速度控制
this.speed = 0.4
}
preload() {
this.load.image('gameBG', 'assets/imgs/gameBG.png')
this.load.spritesheet('myPlane', 'assets/imgs/myPlane.png', { frameWidth: 66, frameHeight: 82 })
}
create() {
this.initData()
// 初始化 phaser 內置鍵盤管理器
this.cursors = this.input.keyboard.createCursorKeys()
// 使用 tileSprite 添加背景,在 update 函數中 y 值自減使背景無限滾動
this.bg = this.add.tileSprite(viewWidth / 2, viewHeight / 2, viewWidth, viewHeight, 'gameBG')
// 創建我飛機精靈並開啟交互
this.player = this.physics.add.sprite(this.x, this.y, 'myPlane').setInteractive()
// 設置世界邊界防止我方飛機飛出屏幕
this.player.setCollideWorldBounds(true)
// 重力設置與 config 中一致,飛機大戰游戲我方飛機不需要重力
this.player.body.setGravityY(0)
// 創建我方飛機正常游戲動畫
this.anims.create({
key: 'myPlaneRun',
frames: this.anims.generateFrameNumbers('myPlane', { start: 0, end: 1 }),
frameRate: 8,
repeat: -1
})
// 創建我方飛機爆炸動畫
this.anims.create({
key: 'myPlaneBoom',
frames: this.anims.generateFrameNumbers('myPlane', { start: 2, end: 5 }),
frameRate: 8,
})
}
update() {
if (this.isGameOver) {
// game over 播放我方飛機爆炸動畫
this.player.anims.play('myPlaneBoom', true)
} else {
// 背景無限滾動
this.bg.tilePositionY -= this.speed
// 播放我放飛機正常動畫
this.player.anims.play('myPlaneRun', true)
// 鍵盤控制我方飛機移動
if (this.cursors.left.isDown) {
this.player.setVelocityX(-260)
} else if (this.cursors.right.isDown) {
this.player.setVelocityX(260)
} else {
this.player.setVelocityX(0)
}
if (this.cursors.up.isDown) {
this.player.setVelocityY(-260)
} else if (this.cursors.down.isDown) {
this.player.setVelocityY(260)
} else {
this.player.setVelocityY(0)
}
}
}
}
config 中 scene 添加場景:
scene: [InitScene, GameScene],
現在點擊開始游戲后即可看到飛機跑起來了,並且可以鍵盤上下左右控制移動

綁定拖拽控制飛機移動
有了鍵盤控制飛機移動,再添加一個拖拽控制飛機移動,在 initData 中添加
// 判斷鼠標或手指是否在我方飛機上按下屏幕
this.draw = false
// 給場景綁定鼠標或手指移動事件,如果按下我放飛機並移動則使飛機跟隨指針移動
this.input.on('pointermove', pointer => {
if (this.draw) {
this.player.x = this.x + pointer.x - pointer.downX
this.player.y = this.y + pointer.y - pointer.downY
}
})
上述手指或鼠標移動事件添加在 initData 中是因為游戲結束后需要解綁事件,游戲結束后再次開始游戲時只需要調用 initData 即可初始化數據而不用銷毀場景重新創建
繼續完善事件綁定,在 create 中添加
// 將鼠標或手指按下事件綁定給我方飛機
this.player.on('pointerdown', () => {
this.draw = true
this.x = this.player.x
this.y = this.player.y
})
// 將鼠標或手指抬起事件綁定給場景
this.input.on('pointerup', () => {
this.draw = false
})
現在我們鼠標左鍵點擊飛機拖拽看看

增加我方飛機發射子彈
我方飛機有了,還需要發射子彈殺敵,在 preload 中引入子彈圖片
this.load.image('myBullet', 'assets/imgs/bomb.png')
在 initData 中添加
// 我方飛機子彈連發數量,后續有子彈連發道具,每吃到一個會使子彈 +1,也就是這個變量進行記錄
this.myBulletQuantity = 1
隨即在 create 中初始化一個我方飛機子彈對象池
// 初始化我方飛機子彈對象池
this.myBullets = this.physics.add.group()
// 自動發射子彈,this.time.addEvent 類似 js 定時器,不過它是跟隨場景的,場景暫停或停止,它也會跟隨暫停或停止
this.time.addEvent({
delay: 260, // 調用間隔
loop: true, // 是否循環調用
callback: () => { // 被執行的回調函數
// 創建子彈,createMyBullet 方法在下面創建
this.createMyBullet()
}
})
給當前 GameScene 類添加 createMyBullet 方法用於生成子彈
update() {
// ...
}
// 生成我方飛機子彈
createMyBullet() {
// 動態子彈連發x坐標處理
for (let i = 0; i < this.myBulletQuantity; i++) {
// 這里的 x 坐標判斷主要實現子彈創建時數量不論多少都能在我方飛機上面均勻排列發射
let x =
i < this.myBulletQuantity / 2
?
(
this.myBulletQuantity % 2 != 0 && i > this.myBulletQuantity / 2 - 1
?
this.player.x
:
this.player.x - ((this.myBulletQuantity - i - this.myBulletQuantity / 2 - (this.myBulletQuantity % 2 != 0 ? 0.5 : 0)) * 20)
)
:
this.player.x + (i - this.myBulletQuantity / 2 + (this.myBulletQuantity % 2 != 0 ? 0.5 : 1)) * 20
// 從對象池取子彈,如果對象池沒有則會創建一個
const tmpMyBullet = this.myBullets.get(x, this.player.y - this.player.height / 2 + 10, 'myBullet')
tmpMyBullet.name = 'myBullet' // 子彈的名字
tmpMyBullet.setVelocity(0, -500) // 設置速度,x 不變, y 值 -500 使子彈往上發射
tmpMyBullet.setScale(0.6, 1) // 這個子彈是圓的,我想使它 x 縮放一點看起來好看點...
tmpMyBullet.setActive(true)
tmpMyBullet.setVisible(true)
/* 創建子彈后設置 active 和 visible 是 true 是因為下面馬上會設置子彈邊界檢測,
超出屏幕或者碰撞到敵機時會使子彈消失,使用的是 killAndHide(killAndHide 不會銷毀對象,
而是將active 和 visible 改為 false,供對象池下次 get 使用),而不是 destroy,
這樣子彈每次創建時都會去對象池找沒有工作的對象,從而進行復用,
不斷銷毀和創建會很浪費性能,后續敵方飛機和道具也會使用這種方式
*/
}
}
先看看效果

對象池特別注意
感覺上面沒什么問題,但實際子彈對象在不斷創建,如果繼續下去早晚會內存泄漏,在上面代碼注釋中有說明
// 在自動發射子彈定時器中打印即可看到,添加 console.log(this.myBullets.getChildren())
callback: () => { // 被執行的回調函數
// 創建子彈,createMyBullet 方法在下面創建
this.createMyBullet()
console.log(this.myBullets.getChildren())
}

現在往 update 函數中添加子彈邊界檢測
update() {
// ...
// 我方飛機子彈對象池子彈邊界檢測,使用 killAndHide 進行復用提高性能
this.myBullets.getChildren().forEach(item => {
if (item.active && item.y < -item.height) {
this.myBullets.killAndHide(item)
}
})
}
現在再看一下控制台打印

創建敵方飛機
創建敵方飛機與我方飛機發射子彈一樣使用對象池即可,其余至於生成規則、位置、移動速度等都根據需要自行處理,有興趣也可以去看我的源碼,
添加敵方飛機相關邏輯后再看看游戲

碰撞檢測
現在我們只需要加上子彈與敵方飛機碰撞檢測,敵方飛機與我方飛機碰撞檢測即可初步完成游戲
// 我方子彈與敵機碰撞檢測,有三種敵方飛機,只需要將我方子彈與這三個敵方飛機對象池設置碰撞檢測即可,
// 其中 enemyAndMyBulletCollision 為碰撞回調函數 enemyPlanes1/2/3 為三種敵機對象池
this.physics.add.overlap(this.myBullets, this.enemyPlanes1, this.enemyAndMyBulletCollision, null, this)
this.physics.add.overlap(this.myBullets, this.enemyPlanes2, this.enemyAndMyBulletCollision, null, this)
this.physics.add.overlap(this.myBullets, this.enemyPlanes3, this.enemyAndMyBulletCollision, null, this)
給當前 GameScene 類添加 enemyAndMyBulletCollision 方法用於處理我方子彈與敵機碰撞
// 我方子彈與敵機碰撞檢測
enemyAndMyBulletCollision(myBullet, enemyPlane) {
// 該回調函數在碰撞時只要對象沒銷毀就會多次觸發,所以這里使用 active 判斷對象是否存在屏幕
if (myBullet.active && enemyPlane.active) {
// 判斷敵機名字處理挨打,爆炸動畫
let animNames = []
let enemyPlanes = null
switch (enemyPlane.name) {
case 'midPlane':
animNames = ['midPlaneAida', 'midPlaneBoom']
enemyPlanes = this.enemyPlanes2
break
case 'bigPlane':
animNames = ['bigPlaneAida', 'bigPlaneBoom']
enemyPlanes = this.enemyPlanes3
break
case 'smallPlane':
animNames = ['', 'smallPlaneBoom']
enemyPlanes = this.enemyPlanes1
break
default:
break
}
enemyPlane.hp -= 1 // 1發子彈減少1滴血,初始化時小飛機,中飛機,大飛機血量分別是1,3,5
// 顯示敵機挨打動畫
if (enemyPlane.hp > 0) {
enemyPlane.anims.play(animNames[0])
}
// 血量沒了顯示敵機爆炸動畫,0.18s后消失,也就是有0.18s的爆炸動畫
if (enemyPlane.hp == 0) {
enemyPlane.anims.play(animNames[1]) // 播放爆炸動畫
enemyPlane.setVelocity(0, 0) // 血量沒了顯示爆炸動畫期間不再繼續往下移動
setTimeout(() => {
enemyPlanes.killAndHide(enemyPlane)
}, 180)
}
// 防止敵機在爆炸動畫中也會使子彈消失
if (enemyPlane.hp >= 0) {
this.myBullets.killAndHide(myBullet)
}
}
}

敵機與我方飛機碰撞檢測同理,只需要使游戲物理系統暫停、播放我機爆炸動畫、處理相應游戲結束邏輯即可
最后記分與道具屬於游戲內景上添花,自己隨便根據個人需求處理,源碼中也有對應示例
廢話
在使用Phaser之前,也使用PIXIJS寫過一些demo,PIXIJS寫起來更像原生開發,而不得不說Phaser封裝的很完善,世界、場景、精靈、動畫、事件、對象池、物理系統等等都是現成的,並且官網有很多教程和案例大大降低了學習成本。
半年前我使用dom + 原生js面向對象也寫過飛機大戰(原生js面向對象實現飛機大戰小游戲(有BOSS,含源碼)),而這次Phaser重構版,全篇只有一個canvas元素,一切都由引擎渲染,性能不言而喻,在碰撞檢測、音效、事件綁定等等方面也都有現成的API,在前端來講做一些小案例比較合適,對於較為復雜的項目還是得用cocos或者unity甚至虛幻等更專業引擎了。
