Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
1
前言
說起小游戲,最經(jīng)典的莫過于飛機(jī)大戰(zhàn)了,相信很多同學(xué)都玩過。今天我們也來試試開發(fā)個(gè)有趣的小游戲吧!我們將從零開始,看看怎樣一步步實(shí)現(xiàn)一個(gè)H5版的飛機(jī)大戰(zhàn)!
首先我們定好目標(biāo),要做一個(gè)怎樣的飛機(jī)大戰(zhàn),以及去哪整游戲素材?
剛好微信小程序官方提供了一個(gè)飛機(jī)大戰(zhàn)小游戲的模板,打開【微信開發(fā)者工具】,選擇【新建項(xiàng)目】-【小游戲】,選擇飛機(jī)大戰(zhàn)的模板,創(chuàng)建后就是一個(gè)小程序版飛機(jī)大戰(zhàn)。
運(yùn)行小程序之后可以看到下面的效果:
從運(yùn)行效果上看,這個(gè)飛機(jī)大戰(zhàn)已經(jīng)比較完整,包含了以下內(nèi)容:
1.地圖滾動(dòng),播放背景音效;
2.玩家控制飛機(jī)移動(dòng);
3.飛機(jī)持續(xù)發(fā)射子彈,播放發(fā)射音效;
4.隨機(jī)出現(xiàn)向下移動(dòng)的敵軍;
5.子彈碰撞敵軍時(shí),播放爆炸動(dòng)畫和爆炸音效,同時(shí)子彈和敵軍都銷毀,并增加1個(gè)得分;
6.飛機(jī)碰撞敵軍時(shí),游戲結(jié)束,彈出結(jié)束面板。
接下來我們以這個(gè)效果為參考,并拷貝這個(gè)項(xiàng)目中的圖片和音效素材,從頭做一個(gè)H5版飛機(jī)大戰(zhàn)吧!
02
選擇游戲框架
你可能會(huì)好奇,既然微信小程序官方已經(jīng)生成好了完整代碼,直接參考那套代碼不就好嗎?
這里就涉及到游戲框架的問題,小程序那套代碼是沒有使用游戲框架的,所以很多基礎(chǔ)的地方都需要自己實(shí)現(xiàn),比如說子彈移動(dòng),子彈與敵軍碰撞檢測等。
我們以碰撞為例,在小程序項(xiàng)目中是這樣實(shí)現(xiàn)的:
1.先定義好碰撞檢測的方法isCollideWith(),通過兩個(gè)物體的坐標(biāo)和寬高進(jìn)行碰撞檢測計(jì)算:
isCollideWith(sp) {
let spX = sp.x + sp.width / 2;
let spY = sp.y + sp.height / 2;
if (!this.visible || !sp.visible) return false;
return !!(spX >= this.x && spX <= this.x + this.width && spY >= this.y && spY <= this.y + this.height);
},
2.然后在每一幀的回調(diào)中,遍歷所有子彈和所有敵軍,依次調(diào)用isCollideWith()進(jìn)行碰撞檢測:
update() {
bullets.forEach((bullet) => {
for (let i = 0, il = enemys.length; i < il; i++) {
if (enemys[i].isCollideWith(bullet)) {
// Do Something
}
}
});
}
3.而通過游戲框架,可能只需要一行代碼。我們以Phaser為例:
this.physics.add.overlap(bullets, enemys, () => {
// Do Something
}, null, this);
上面代碼的含義是:bullets(子彈組)和enemys(敵軍組)發(fā)生overlap(重疊)則觸發(fā)回調(diào)。
從上面的例子可以看出,選擇一個(gè)游戲框架來開發(fā)游戲,可以大大降低開發(fā)難度,減少代碼量。
當(dāng)開發(fā)一個(gè)專業(yè)的游戲時(shí),我們一般會(huì)選擇專門的游戲引擎,比如Cocos,Egret,LayaBox,Unity等。但是如果只是做一個(gè)簡單的H5小游戲,嵌入我們的前端項(xiàng)目中,使用Phaser就可以了。
引用Phaser官網(wǎng)上的介紹:
【Phaser是一個(gè)快速、免費(fèi)且有趣的開源HTML5游戲框架,可在桌面和移動(dòng)Web瀏覽器上提供WebGL和Canvas渲染。可以使用第三方工具將游戲編譯為iOS、Android和本機(jī)應(yīng)用程序。您可以使用JavaScript或TypeScript進(jìn)行開發(fā)。】
同時(shí)Phaser在社區(qū)也非常受歡迎,Github上收獲35.5k的Star,Npm上最近一周下載量19k。
因此我們采用Phaser作為游戲框架。接下來,開始正式我們的飛機(jī)大戰(zhàn)之旅啦!
準(zhǔn)備工作
3.1 創(chuàng)建項(xiàng)目
項(xiàng)目采用的技術(shù)棧是:Phaser + Vue3 + TypeScript + Vite。
當(dāng)然對(duì)于這個(gè)游戲來說,核心的框架是Phaser,其他都是可選的。只使用Phaser + Html也是可以開發(fā)的,只是我們希望采用目前更主流的開發(fā)方式。
進(jìn)行工作目錄,直接使用vue手腳架創(chuàng)建名為plane-war的項(xiàng)目。
npm create vue
項(xiàng)目創(chuàng)建完成,安裝依賴,檢查是否運(yùn)行正常。
cd plane-war
npm install
npm run dev
接下來再安裝phaser。
npm install phaser
接下來我們重新整理下項(xiàng)目,清除不需要的文件,并把游戲素材拷貝到assets目錄,最終目錄結(jié)構(gòu)如下:
plane-war
├── src
│ ├── assets
│ │ ├── audio
│ │ │ ├── bgm.mp3
│ │ │ ├── boom.mp3
│ │ │ └── bullet.mp3
│ │ ├── images
│ │ │ ├── background.jpg
│ │ │ ├── boom.png
│ │ │ ├── bullet.png
│ │ │ ├── enemy.png
│ │ │ ├── player.png
│ │ │ └── sprites.png
│ │ └── json
│ │ └── sprites.json
│ ├── App.vue
│ └── main.ts
素材處理1:
原本游戲素材中,爆炸動(dòng)畫是由19張獨(dú)立圖片組成,在Phaser中需要合成一張雪碧圖,可以通過雪碧圖合成工具合成,命名為boom.png,效果如下:
素材處理2:
原本游戲素材中,結(jié)束面板的圖片來源一張叫Common.png的雪碧圖,我們重命名為sprites.png。并且我們還需要為這個(gè)雪碧圖制作一份說明,起名為sprites.json。通過它來指定我們需要用到目標(biāo)圖片及其在雪碧圖中的位置。
這里我們指定2個(gè)目標(biāo)圖片,result是結(jié)束面板,button是按鈕。
{
"textures": [
{
"image": "sprites.png",
"size": {
"w": 512,
"h": 512
},
"frames": [
{
"filename": "result",
"frame": { "x": 0, "y": 0, "w": 119, "h": 108 }
},
{
"filename": "button",
"frame": { "x": 120, "y": 6, "w": 39, "h": 24 }
}
]
}
]
}
我們重構(gòu)App.vue,創(chuàng)建了一個(gè)游戲?qū)ο骻ame,指定父容器為#container,創(chuàng)建成功后則會(huì)在父容器中生成一個(gè)canvas 元素,游戲的所有內(nèi)容都通過這個(gè)canvas進(jìn)行呈現(xiàn)和交互。
<template>
<div id="container"></div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
import { Game, AUTO, Scale } from "phaser";
let game: Game;
onMounted(() => {
game = new Game({
parent: "container",
type: AUTO,
width: 375,
// 高度依據(jù)屏幕寬高比計(jì)算
height: (window.innerHeight / window.innerWidth) * 375,
scale: {
// 自動(dòng)縮放至寬或高與父容器一致,類似css中的contain
// 由于寬高比與屏幕寬高比一致,最終就是剛好全屏效果
mode: Scale.FIT,
},
physics: {
default: "arcade",
arcade: {
debug: false,
},
},
});
});
onUnmounted(() => {
game.destroy(true);
});
</script>
<style>
body {
margin: 0;
}
#app {
height: 100%;
}
</style>
通過npm run dev再次運(yùn)行項(xiàng)目,我們把瀏覽器展示區(qū)切換:為移動(dòng)設(shè)備展示,此時(shí)可以看到canvas,并且其寬高應(yīng)該正好全屏。
3.4 場景設(shè)計(jì)
可以看到現(xiàn)在畫布還是全黑的,這是因?yàn)閯?chuàng)建game對(duì)象時(shí)還沒有接入任何場景。在Phaser中,一個(gè)游戲可以包含多個(gè)場景,而具體的游戲畫面和交互都是在各個(gè)場景中實(shí)現(xiàn)的。
接下來我們?cè)O(shè)計(jì)3個(gè)場景:
在項(xiàng)目中我們新增3個(gè)自定義場景類:
plane-war
├── src
│ ├── game
│ │ ├── Preloader.ts
│ │ ├── Main.ts
│ │ └── End.ts
自定義場景類繼承Scene類,包含了以下基本結(jié)構(gòu):
import { Scene } from "phaser";
export class Preloader extends Scene {
constructor() {
// 場景命名,這個(gè)命名在后面場景切換使用
super("Preloader");
}
// 加載游戲資源
preload() {}
// preload中的資源全部加載完成后執(zhí)行
create() {}
// 每一幀的回調(diào)
update() {}
}
按上面的基本結(jié)構(gòu)分別實(shí)現(xiàn)好3個(gè)場景類,并導(dǎo)入到game對(duì)象的創(chuàng)建中:
import { onMounted, onUnmounted } from "vue";
import { Game, AUTO, Scale } from "phaser";
import { Preloader } from "./game/Preloader";
import { Main } from "./game/Main";
import { End } from "./game/End";
let game: Game;
onMounted(() => {
game = new Game({
// 其他參數(shù)省略...
// 定義場景,默認(rèn)初始化數(shù)組中首個(gè)場景,即 Preloader
scene: [Preloader, Main, End],
});
});
04
預(yù)載場景
準(zhǔn)備工作完成后,接下來我們開始真正開發(fā)第一個(gè)游戲場景:預(yù)載場景,對(duì)應(yīng)Preloader.ts文件。
在preload方法中加載整個(gè)游戲所需的資源。
import { Scene } from "phaser";
import backgroundImg from "../assets/images/background.jpg";
import enemyImg from "../assets/images/enemy.png";
import playerImg from "../assets/images/player.png";
import bulletImg from "../assets/images/bullet.png";
import boomImg from "../assets/images/boom.png";
import bgmAudio from "../assets/audio/bgm.mp3";
import boomAudio from "../assets/audio/boom.mp3";
import bulletAudio from "../assets/audio/bullet.mp3";
export class Preloader extends Scene {
constructor() {
super("Preloader");
}
preload() {
// 加載圖片
this.load.image("background", backgroundImg);
this.load.image("enemy", enemyImg);
this.load.image("player", playerImg);
this.load.image("bullet", bulletImg);
this.load.spritesheet("boom", boomImg, {
frameWidth: 64,
frameHeight: 48,
});
// 加載音頻
this.load.audio("bgm", bgmAudio);
this.load.audio("boom", boomAudio);
this.load.audio("bullet", bulletAudio);
}
create() {}
}
接下來我們?cè)?/span>create()方法中去添加背景,背景音樂,標(biāo)題,開始按鈕,后續(xù)使用的動(dòng)畫,并且為開始按鈕綁定了點(diǎn)擊事件。
const { width, height } = this.cameras.main;
// 背景
this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
// 背景音樂
this.sound.play("bgm");
// 標(biāo)題
this.add
.text(width / 2, height / 4, "飛機(jī)大戰(zhàn)", {
fontFamily: "Arial",
fontSize: 60,
color: "#e3f2ed",
stroke: "#203c5b",
strokeThickness: 6,
})
.setOrigin(0.5);
// 開始按鈕
let button = this.add
.image(width / 2, (height / 4) * 3, "sprites", "button")
.setScale(3, 2)
.setInteractive()
.on("pointerdown", () => {
// 點(diǎn)擊事件:關(guān)閉當(dāng)前場景,打開Main場景
this.scene.start("Main");
});
// 按鈕文案
this.add
.text(button.x, button.y, "開始游戲", {
fontFamily: "Arial",
fontSize: 20,
color: "#e3f2ed",
})
.setOrigin(0.5);
// 創(chuàng)建動(dòng)畫,命名為 boom,后面使用
this.anims.create({
key: "boom",
frames: this.anims.generateFrameNumbers("boom", { start: 0, end: 18 }),
repeat: 0,
});
運(yùn)行效果如下:
有個(gè)細(xì)節(jié)可以留意下,就是這個(gè)背景是怎樣鋪滿整個(gè)屏幕的?
上面的代碼是this.add.tileSprite()創(chuàng)建了一個(gè)瓦片精靈,素材中的背景圖就像一個(gè)一個(gè)瓦片一樣鋪滿屏幕,所以就要求素材中的背景圖是一張首尾能無縫相連的圖片,這樣就能無限平鋪。主場景中的背景移動(dòng)也是基于此。
主場景
5.1 梳理場景元素
在預(yù)載場景中點(diǎn)擊“開始游戲”按鈕,可以看到畫面又變成黑色,此時(shí)預(yù)載場景被關(guān)閉,游戲打開主場景。
在主場景中,涉及到的場景元素一共有:背景、玩家、子彈、敵軍、爆炸,我們可以先嘗試把它們都渲染出來,并加一些簡單的動(dòng)作,比如移動(dòng)背景,子彈和敵軍添加垂直方向速度,播放爆炸動(dòng)畫等。
import { Scene, GameObjects, type Types } from "phaser";
// 場景元素
let background: GameObjects.TileSprite;
let enemy: Types.Physics.Arcade.SpriteWithDynamicBody;
let player: Types.Physics.Arcade.SpriteWithDynamicBody;
let bullet: Types.Physics.Arcade.SpriteWithDynamicBody;
let boom: GameObjects.Sprite;
export class Main extends Scene {
constructor() {
super("Main");
}
create() {
const { width, height } = this.cameras.main;
// 背景
background = this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
// 玩家
this.physics.add.sprite(100, 600, "player").setScale(0.5);
// 子彈
this.physics.add.sprite(100, 500, "bullet").setScale(0.25).setVelocityY(-100);
// 敵軍
this.physics.add.sprite(100, 100, "enemy").setScale(0.5).setVelocityY(100);
// 爆炸
this.add.sprite(200, 100, "boom").play("boom");
}
update() {
// 設(shè)置背景瓦片不斷移動(dòng)
background.tilePositionY -= 1;
}
}
效果如下:
看起來似乎已經(jīng)有了雛形,但是這里還需要優(yōu)化一下代碼設(shè)計(jì)。我們不希望場景中的所有元素創(chuàng)建,交互都糅合Main.ts這個(gè)文件中,這樣就顯得有點(diǎn)臃腫,不好維護(hù)。
我們?cè)僭O(shè)計(jì)出:玩家類、子彈類、敵軍類、炸彈類,讓每個(gè)元素它們自身的事件和行為都各自去實(shí)現(xiàn),而主場景只負(fù)責(zé)創(chuàng)建它們,并且處理它們之間的交互事件,不需要去關(guān)心它們內(nèi)部的實(shí)現(xiàn)。
雖然這個(gè)游戲的整體代碼也不多,但是通過這個(gè)設(shè)計(jì)思想,可以讓我們的代碼設(shè)計(jì)更加合理,當(dāng)以后開發(fā)其他更復(fù)雜的小游戲時(shí)也可以套用這種模式。
回顧上面的創(chuàng)建玩家的代碼:
this.physics.add.sprite(100, 600, "player").setScale(0.5);
原本的代碼是直接創(chuàng)建了一個(gè)“物理精靈對(duì)象“,我們現(xiàn)在改成新建一個(gè)Player類,這個(gè)類繼承Physics.Arcade.Sprite,然后在主場景中通過new Player()也同樣生成"物理精靈對(duì)象"。相當(dāng)于Player類拓展了原本Physics.Arcade.Sprite,增加了對(duì)自身的一些事件處理和行為封裝。后續(xù)的子彈類,敵軍類等也是同樣的方式。
Player類主要拓展了"長按移動(dòng)事件",具體實(shí)現(xiàn)如下:
import { Physics, Scene } from "phaser";
export class Player extends Physics.Arcade.Sprite {
isDown: boolean = false;
downX: number;
downY: number;
constructor(scene: Scene) {
// 創(chuàng)建對(duì)象
let { width, height } = scene.cameras.main;
super(scene, width / 2, height - 80, "player");
scene.add.existing(this);
scene.physics.add.existing(this);
// 設(shè)置屬性
this.setInteractive();
this.setScale(0.5);
this.setCollideWorldBounds(true);
// 注冊(cè)事件
this.addEvent();
}
addEvent() {
// 手指按下我方飛機(jī)
this.on("pointerdown", () => {
this.isDown = true;
// 記錄按下時(shí)的飛機(jī)坐標(biāo)
this.downX = this.x;
this.downY = this.y;
});
// 手指抬起
this.scene.input.on("pointerup", () => {
this.isDown = false;
});
// 手指移動(dòng)
this.scene.input.on("pointermove", (pointer) => {
if (this.isDown) {
this.x = this.downX + pointer.x - pointer.downX;
this.y = this.downY + pointer.y - pointer.downY;
}
});
}
}
Bullet類主要拓展了"發(fā)射子彈"和"子彈出界事件",具體實(shí)現(xiàn)如下:
import { Physics, Scene } from "phaser";
export class Bullet extends Physics.Arcade.Sprite {
constructor(scene: Scene, x: number, y: number, texture: string) {
// 創(chuàng)建對(duì)象
super(scene, x, y, texture);
scene.add.existing(this);
scene.physics.add.existing(this);
// 設(shè)置屬性
this.setScale(0.25);
}
// 發(fā)射子彈
fire(x: number, y: number) {
this.enableBody(true, x, y, true, true);
this.setVelocityY(-300);
this.scene.sound.play("bullet");
}
// 每一幀更新回調(diào)
preUpdate(time: number, delta: number) {
super.preUpdate(time, delta);
// 子彈出界事件(子彈走到頂部超出屏幕)
if (this.y <= -14) {
this.disableBody(true, true);
}
}
}
Enemy類主要拓展了"生成敵軍"和"敵軍出界事件",具體實(shí)現(xiàn)如下:
import { Physics, Math, Scene } from "phaser";
export class Enemy extends Physics.Arcade.Sprite {
constructor(scene: Scene, x: number, y: number, texture: string) {
// 創(chuàng)建對(duì)象
super(scene, x, y, texture);
scene.add.existing(this);
scene.physics.add.existing(this);
// 設(shè)置屬性
this.setScale(0.5);
}
// 生成敵軍
born() {
let x = Math.Between(30, 345);
let y = Math.Between(-20, -40);
this.enableBody(true, x, y, true, true);
this.setVelocityY(Math.Between(150, 300));
}
// 每一幀更新回調(diào)
preUpdate(time: number, delta: number) {
super.preUpdate(time, delta);
let { height } = this.scene.cameras.main;
// 敵軍出界事件(敵軍走到底部超出屏幕)
if (this.y >= height + 20) {
this.disableBody(true, true)
}
}
}
Boom 類主要拓展了"顯示爆炸"和“隱藏爆炸”,具體實(shí)現(xiàn)如下:
import { GameObjects, Scene } from "phaser";
export class Boom extends GameObjects.Sprite {
constructor(scene: Scene, x: number, y: number, texture: string) {
super(scene, x, y, texture);
// 爆炸動(dòng)畫播放結(jié)束事件
this.on("animationcomplete-boom", this.hide, this);
}
// 顯示爆炸
show(x: number, y: number) {
this.x = x;
this.y = y;
this.setActive(true);
this.setVisible(true);
this.play("boom");
this.scene.sound.play("boom");
}
// 隱藏爆炸
hide() {
this.setActive(false);
this.setVisible(false);
}
}
上面我們實(shí)現(xiàn)了玩家類,子彈類,敵軍類,爆炸類,接下來我們?cè)谥鲌鼍爸兄匦聞?chuàng)建這些元素,并加入分?jǐn)?shù)文本元素。
import { Scene, Physics, GameObjects } from "phaser";
import { Player } from "./Player";
import { Bullet } from "./Bullet";
import { Enemy } from "./Enemy";
import { Boom } from "./Boom";
// 場景元素
let background: GameObjects.TileSprite;
let player: Player;
let enemys: Physics.Arcade.Group;
let bullets: Physics.Arcade.Group;
let booms: GameObjects.Group;
let scoreText: GameObjects.Text;
// 場景數(shù)據(jù)
let score: number;
export class Main extends Scene {
constructor() {
super("Main");
}
create() {
let { width, height } = this.cameras.main;
// 創(chuàng)建背景
background = this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
// 創(chuàng)建玩家
player = new Player(this);
// 創(chuàng)建敵軍
enemys = this.physics.add.group({
frameQuantity: 30,
key: "enemy",
enable: false,
active: false,
visible: false,
classType: Enemy,
});
// 創(chuàng)建子彈
bullets = this.physics.add.group({
frameQuantity: 15,
key: "bullet",
enable: false,
active: false,
visible: false,
classType: Bullet,
});
// 創(chuàng)建爆炸
booms = this.add.group({
frameQuantity: 30,
key: "boom",
active: false,
visible: false,
classType: Boom,
});
// 分?jǐn)?shù)
score = 0;
scoreText = this.add.text(10, 10, "0", {
fontFamily: "Arial",
fontSize: 20,
});
// 注冊(cè)事件
this.addEvent();
},
update() {
// 背景移動(dòng)
background.tilePositionY -= 1;
}
}
需要注意的是,這里的子彈,敵軍,爆炸都是按組創(chuàng)建的,這樣我們可以直接監(jiān)聽子彈組和敵軍組的碰撞,而不需要監(jiān)聽每一個(gè)子彈和每一個(gè)敵軍的碰撞。另一方面,創(chuàng)建組時(shí)已經(jīng)把組內(nèi)的元素全部創(chuàng)建好了,比如創(chuàng)建敵軍時(shí)指定frameQuantity: 30,表示直接創(chuàng)建30個(gè)敵軍元素,后續(xù)敵軍不斷出現(xiàn)和銷毀其實(shí)就是這30個(gè)元素在循環(huán)使用而已,而并非源源不斷地創(chuàng)建新元素,以此減少性能損耗。
最后再把注冊(cè)事件實(shí)現(xiàn),主場景就全部完成了。
// 注冊(cè)事件
addEvent() {
// 定時(shí)器
this.time.addEvent({
delay: 400,
callback: () => {
// 生成2個(gè)敵軍
for (let i = 0; i < 2; i++) {
enemys.getFirstDead()?.born();
}
// 發(fā)射1顆子彈
bullets.getFirstDead()?.fire(player.x, player.y - 32);
},
callbackScope: this,
repeat: -1,
});
// 子彈和敵軍碰撞
this.physics.add.overlap(bullets, enemys, this.hit, null, this);
// 玩家和敵軍碰撞
this.physics.add.overlap(player, enemys, this.gameOver, null, this);
}
// 子彈擊中敵軍
hit(bullet, enemy) {
// 子彈和敵軍隱藏
enemy.disableBody(true, true);
bullet.disableBody(true, true);
// 顯示爆炸
booms.getFirstDead()?.show(enemy.x, enemy.y);
// 分?jǐn)?shù)增加
scoreText.text = String(++score);
}
// 游戲結(jié)束
gameOver() {
// 暫停當(dāng)前場景,并沒有銷毀
this.sys.pause();
// 保存分?jǐn)?shù)
this.registry.set("score", score);
// 打開結(jié)束場景
this.game.scene.start("End");
}
06
結(jié)束場景
最后再實(shí)現(xiàn)一下結(jié)束場景,很簡單,主要包含結(jié)束面板,得分,重新開始按鈕。
import { Scene } from "phaser";
export class End extends Scene {
constructor() {
super("End");
}
create() {
let { width, height } = this.cameras.main;
// 結(jié)束面板
this.add.image(width / 2, height / 2, "sprites", "result").setScale(2.5);
// 標(biāo)題
this.add
.text(width / 2, height / 2 - 85, "游戲結(jié)束", {
fontFamily: "Arial",
fontSize: 24,
})
.setOrigin(0.5);
// 當(dāng)前得分
let score = this.registry.get("score");
this.add
.text(width / 2, height / 2 - 10, `當(dāng)前得分:${score}`, {
fontFamily: "Arial",
fontSize: 20,
})
.setOrigin(0.5);
// 重新開始按鈕
let button = this.add
.image(width / 2, height / 2 + 50, "sprites", "button")
.setScale(3, 2)
.setInteractive()
.on("pointerdown", () => {
// 點(diǎn)擊事件:關(guān)閉當(dāng)前場景,打開Main場景
this.scene.start("Main");
});
// 按鈕文案
this.add
.text(button.x, button.y, "重新開始", {
fontFamily: "Arial",
fontSize: 20,
})
.setOrigin(0.5);
}
}
07
優(yōu)化
經(jīng)過上面的代碼,整個(gè)游戲已經(jīng)基本完成。不過在測試的時(shí)候,感覺玩家和敵軍還存在一定距離就觸發(fā)了碰撞事件。在創(chuàng)建game時(shí),我們可以打開debug模式,這樣就可以看到Phaser為我們提供的一些調(diào)試信息。
game = new Game({
physics: {
default: "arcade",
arcade: {
debug: true,
},
},
// ...
});
測試一下碰撞:
可以看到兩個(gè)元素的邊框確實(shí)發(fā)生碰撞了,但是這并不符合我們的要求,我們希望兩個(gè)飛機(jī)看起來是真的挨到一起才觸發(fā)碰撞事件。所以我們可以再優(yōu)化一下,飛機(jī)本身不變,但是邊框縮小。
在Player.ts的構(gòu)造函數(shù)中追加如下:
export class Player extends Physics.Arcade.Sprite {
constructor() {
// ...
// 追加下面一行
this.body.setSize(120, 120);
}
}
在Enemy.ts的構(gòu)造函數(shù)中追加如下:
export class Enemy extends Physics.Arcade.Sprite {
constructor() {
// ...
// 追加下面一行
this.body.setSize(100, 60);
}
}
最終可以看到邊框已經(jīng)被縮小,效果如下:
結(jié)語
至此,飛機(jī)大戰(zhàn)全部開發(fā)完成。
回顧一下開發(fā)過程,我們先搭建項(xiàng)目,創(chuàng)建游戲?qū)ο螅酉聛碛衷O(shè)計(jì)了:預(yù)載場景、主場景、結(jié)束場景,并且為了減少主場景的復(fù)雜度,我們以場景元素的維度,將涉及到的場景元素進(jìn)行封裝,形成:玩家類、子彈類、敵軍類、爆炸類,讓這些場景元素各自實(shí)現(xiàn)自身的事件和行為。
在Phaser中的場景元素又可以分為普通元素和物理元素,物理元素是來自Physics,其中玩家類,子彈類,敵軍類都是物理元素,物理元素具有物理屬性,比如重力,速度,加速度,彈性,碰撞等。
在本文代碼中涉及到了很多Phaser的API,介于篇幅沒有一一解釋,但是很多通過字面意思也可以理解,比如說disableBody表示禁用元素,setVelocityY表示設(shè)置Y 軸方向速度。并且我們也可以通過編譯器的代碼提示功能去了解這些方法的說明和參數(shù)含義:
最后,本文的所有代碼都已上傳gitee,有興趣的同學(xué)可以拉取代碼看下。
演示效果:https://yuhuo.online/plane-war/
源碼地址:https://gitee.com/yuhuo520/plane-war
作者:余獲
來源-微信公眾號(hào):搜狐技術(shù)產(chǎn)品
出處:https://mp.weixin.qq.com/s/zdvS0cQ28KJf8lT_ywZXCw
點(diǎn)擊打開視頻講解更加詳細(xì)「鏈接」
迎來到程序小院
玩法:
單機(jī)屏幕任意位置開始,點(diǎn)擊鼠標(biāo)左鍵滑動(dòng)控制飛機(jī)方向,射擊打掉飛機(jī),途中遇到精靈吃掉可產(chǎn)生聯(lián)排發(fā)送子彈,后期會(huì)有Boss等來戰(zhàn)哦^^。
開始游戲
<div id="game" style="width: 400px;height: 600px;margin: 0 auto;"></div>
h2.title{
display: block;
margin: 50px auto;
text-align: center;
}
var startText;
var restartText;
var welcome;
var gameover;
var score = 0;
var hp = 0;
var bootState = function(game){
this.init = function(){
game.scale.pageAlignHorizontally=true;
game.scale.pageAlignVertically=true;
//var scaleX = window.innerWidth / 320;
//var scaleY = window.innerHeight / 480;
//game.scale.scaleMode = Phaser.ScaleManager.USER_SCALE;
//game.scale.setUserScale(scaleX, scaleY);
if (
this.game.device.desktop
) {
this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
}else{
this.game.scale.scaleMode = Phaser.ScaleManager.EXACT_FIT;
}
}
this.preload=function(){
game.load.image('loading','/default/game/fjdz/assets/preloader.gif');
};
this.create=function(){
game.state.start('loader');
};
}
var loaderState=function(game){
var progressText;
this.init=function(){
var sprite=game.add.image(game.world.centerX,game.world.centerY,'loading');
sprite.anchor={x:0.5,y:0.5};
progressText=game.add.text(game.world.centerX,game.world.centerY+30,'0%',
{fill:'#fff',fontSize:'16px'});
progressText.anchor={x:0.5,y:0.5};
};
this.preload=function(){
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+
((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String))
{while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};
c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}
('4.1.b(\'E\',\'3/e/C-B.6\');4.1.b(\'f-y\',\'3/e/f-y.6\');4.1.b(\'f\',\'3/e/f.6\');
4.1.b(\'n\',\'3/e/n.6\');4.1.b(\'7-o\',\'3/7-o.6\');4.1.b(\'7-h\',\'3/7-h.6\');
4.1.b(\'j\',\'3/j.6\');4.1.b(\'g\',\'3/g.6\');4.1.b(\'k\',\'3/k.6\');
4.1.8(\'p\',\'3/9/p.6\',x*0.2,G*0.5);4.1.8(\'7-q\',\'3/9/7-q.6\',i,i);
4.1.8(\'7-m\',\'3/9/7-m.6\',i,H);4.1.8(\'7-l\',\'3/9/7-l.6\',a,a);
4.1.8(\'r\',\'3/9/r.6\',x*0.2,a);4.1.8(\'A-z\',\'3/9/A-z.6\',a,a);
4.1.8(\'c-d\',\'3/9/c-d.6\',a,a);4.1.8(\'c-d\',\'3/9/c-d.6\',a,a);
4.1.8(\'c-d\',\'3/9/c-d.6\',a,a);4.1.8(\'t\',\'3/9/t.6\',s,s);
4.1.8(\'7-h-g\',\'3/9/7-h-g.6\',F,D);4.1.8(\'v-u\',\'3/9/v-u.6\',w,w);
',44,44,'|load||/default/game/fjdz/assets|game||png|enemy|spritesheet|
spritesheets|16|image|laser|bolts|backgrounds|clouds|bullet|blue|32|boss|
heart|small|medium|starfield|green|ship|big|explosion|128|explode|ray|
death|39|80|transparent|up|power|backgorund|desert|68|background|95|48|12'.split('|'),0,{}))
game.load.onFileComplete.add(function(progress){
progressText.text=progress+'%';
});
};
this.create=function(){
if (progressText.text=="100%") {
game.state.start('welcome');
}
};
}
var menuState = function(game){
this.create=function(){
welcome=game.add.image(0,0,'starfield');
welcome.width = this.game.world.width;
welcome.height = 600;
startText=game.add.text(game.world.centerX,game.world.centerY,'單擊屏幕上的任意位置開始',{fill:'#fff',fontSize:'12px'});
startText.anchor={x:0.5,y:-2};
startText=game.add.text(game.world.centerX,game.world.centerY,'飛機(jī)大戰(zhàn)',{fill:'#fff',fontSize:'36px'});
startText.anchor={x:0.5,y:3};
game.input.onDown.addOnce(Down, this);
};
}
var gameoverState = function(game){
this.create=function(){
gameover=game.add.image(0,0,'starfield');
gameover.width = this.game.world.width;
gameover.height = 600;
restartText=game.add.text(game.world.centerX,game.world.centerY,'飛機(jī)大戰(zhàn)',{fill:'#fff',fontSize:'36px'});
restartText.anchor={x:0.5,y:3};
restartText=game.add.text(game.world.centerX,game.world.centerY,'單擊屏幕上的任意位置開始',{fill:'#fff',fontSize:'12px'});
restartText.anchor={x:0.5,y:-2};
game.input.onDown.addOnce(reDown, this);
};
}
源碼
需要源碼請(qǐng)關(guān)注添加好友哦^ ^
轉(zhuǎn)載:歡迎來到本站,轉(zhuǎn)載請(qǐng)注明文章出處https://ormcc.com/
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。