文章目录
引言
飞机大战(也被称为射击游戏或空战游戏)是一种非常受欢迎的休闲游戏类型。在这个博客中,我们将探讨如何使用 Vue.js 框架来构建一个简单的飞机大战游戏。我们将从基本的游戏逻辑开始,逐步增加游戏元素和交互性,代码详解可参考注释,最终展示画面在文章底部
项目初始化
git地址:https://gitee.com/its-a-little-bad/vue-project---aircraft-battle.git
node版本:20.8.1
游戏设计和结构
在 Vue.js 中,我们通常将游戏的各个部分分解为不同的场景。
主场景
游戏场景
游戏程序实现
Vue页面嵌入Phaser
在 Vue 应用中嵌入一个 Phaser 游戏
js
<template>
<!-- Phaser 游戏的容器 -->
<div id="container"></div>
</template>
<script setup lang="ts">
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 isMobile = /(iPhone|iPad|Android)/i.test(navigator.userAgent);
// 定义了一个 game 变量来存储 Phaser 游戏实例
let game: Game;
onMounted(() => {
game = new Game({
parent: "container",
type: AUTO,
width: 375,
//游戏的大小根据设备类型进行调整。如果设备是移动设备,则高度会根据设备的纵横比计算得出。
height: isMobile ? (window.innerHeight / window.innerWidth) * 375 : 667,
//游戏的缩放模式也根据设备类型进行设置。移动设备使用 Scale.FIT,这意味着游戏将尽可能地适应屏幕大小,
//而不会保持其原始纵横比。非移动设备则使用 Scale.NONE,这意味着游戏将保持其原始大小。
scale: {
mode: isMobile ? Scale.FIT : Scale.NONE,
},
physics: {
default: "arcade",
arcade: {
debug: false,
},
},
scene: [Preloader, Main, End],
});
});
onUnmounted(() => {
game.destroy(true);
});
</script>
<style>
body {
margin: 0;
}
#app {
height: 100%;
}
</style>
Preloader 场景加载
创建一个 Preloader 场景来加载游戏所需的资源和设置一些基本的游戏元素,示例如下
程序实现:
js
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 spritesImg from "../assets/images/sprites.png";
import spritesJson from "../assets/json/sprites.json?url";
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 {
// 构造函数,定义场景名称为 "Preloader"
constructor() {
super("Preloader");
}
// 预加载资源的方法
preload() {
// 加载背景图片
this.load.image("background", backgroundImg);
// 加载敌人图片
this.load.image("enemy", enemyImg);
// 加载玩家图片
this.load.image("player", playerImg);
// 加载子弹图片
this.load.image("bullet", bulletImg);
// 加载爆炸动画的精灵表(spritesheet)
this.load.spritesheet("boom", boomImg, {
frameWidth: 64,
frameHeight: 48,
});
// 加载精灵图集(atlas)
this.load.atlas("sprites", spritesImg, spritesJson);
// 加载背景音乐
this.load.audio("bgm", bgmAudio);
// 加载爆炸音效
this.load.audio("boom", boomAudio);
// 加载子弹音效
this.load.audio("bullet", bulletAudio);
}
// 创建场景的方法
create() {
const { width, height } = this.cameras.main;
// 显示背景(通常在Preloader场景中不展示实际游戏内容,这里仅为示例)
this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
// 播放背景音乐(在Preloader场景中播放通常是为了给玩家一个等待的反馈)
this.sound.play("bgm", { loop: true }); // 循环播放背景音乐
// 添加标题(通常也不在Preloader场景中,但可以作为加载提示)
this.add
.text(width / 2, height / 4, "飞机大战", {
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") // 假设"sprites"图集中有名为"button"的帧
.setScale(3, 2)
.setInteractive()
.on("pointerdown", () => {
// 当按钮被点击时,切换到主场景(这里主场景名为'Main')
this.scene.start('Main');
});
// 按钮文案
this.add
.text(button.x, button.y, "开始游戏", {
fontFamily: "Arial",
fontSize: 20,
color: "#e3f2ed",
})
.setOrigin(0.5);
}
// 创建动画,命名为 boom,后面使用
this.anims.create({
key: "boom",
frames: this.anims.generateFrameNumbers("boom", { start: 0, end: 18 }),
repeat: 0,
});
}
在Phaser 3框架中,从一个场景(如Preloader)切换到另一个场景(如Main)通常使用this.scene.start('Main')这样的代码来实现。这是Phaser场景管理系统的一部分,它允许你动态地加载、创建、运行和销毁游戏的不同部分。
游戏场景功能实现
程序实现
js
// 定义 Main 场景类,继承自 Phaser 的 Scene 类
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;
// 场景数据
let score: number;
export class Main extends Scene {
constructor() {
super("Main");
}
create() {
let { width, height } = this.cameras.main;
// 创建背景
background = this.add
.tileSprite(0, 0, width, height, "background")
.setOrigin(0, 0);
// 创建玩家,调用Player类
player = new Player(this);
// 创建敌军组
// 注解:enemys 是一个 Phaser 的物理组,用于存储和管理多个 Enemy 对象
// frameQuantity 表示从 enemy 纹理集中加载的帧数,key 是纹理集的名称
// enable, active, visible 分别是启用物理、激活和可见性标志
// classType 指示组中新创建对象的类型
enemys = this.physics.add.group({
frameQuantity: 30,
key: "enemy",
enable: false,// 在此初始状态下不启用物理
active: false,// 在此初始状态下不激活
visible: false,// 在此初始状态下不可见
classType: Enemy,// 当组中添加新对象时使用的类
});
// 创建子弹
// 注解:与敌军组类似,但用于存储和管理多个 Bullet 对象
bullets = this.physics.add.group({
frameQuantity: 15,
key: "bullet",
enable: false,
active: false,
visible: false,
classType: Bullet,
});
// 创建爆炸
// 注解:booms 组用于存储和管理多个 Boom 对象,可能是用于显示爆炸动画
booms = this.add.group({
frameQuantity: 30,
key: "boom",
active: false,
visible: false,
classType: Boom,
});
// 分数
// 注解:score 变量用于跟踪玩家的分数,scoreText 是显示分数的文本对象
score = 0;
scoreText = this.add.text(10, 10, "0", {
fontFamily: "Arial",
fontSize: 20,
});
// 注册事件
this.addEvent();
}
// 注册事件
addEvent() {
// 定时器
// 注解:此定时器每 400 毫秒触发一次回调,生成敌军和发射子弹
this.time.addEvent({
delay: 400,
callback: () => {
// 生成2个敌军
for (let i = 0; i < 2; i++) {
enemys.getFirstDead()?.born();
}
// 发射1颗子弹
bullets.getFirstDead()?.fire(player.x, player.y - 32);
},
callbackScope: this,
repeat: -1,
});
// 子弹和敌军碰撞,会调用 hit 方法
this.physics.add.overlap(bullets, enemys, this.hit, null, this);
// 玩家和敌军碰撞,会调用 gameOver 方法
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);
// 分数增加
scoreText.text = String(++score);
}
// 游戏结束
gameOver() {
// 暂停当前场景,并没有销毁
this.sys.pause();
// 保存分数
this.registry.set("score", score);
// 打开结束场景
this.game.scene.start("End");
}
update() {
// 设置背景瓦片不断移动
background.tilePositionY -= 1;
}
}
功能类定义
Boom爆炸类
js
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);
// 爆炸动画播放结束事件
this.on("animationcomplete-boom", this.hide, this);
}
/**
* 显示爆炸
* @param x 爆炸x坐标
* @param y 爆炸y坐标
*/
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);
}
}
Bullet子弹类
js
import { Physics, Scene } from "phaser";
export class Bullet extends Physics.Arcade.Sprite {
constructor(scene: Scene, x: number, y: number, texture: string) {
super(scene, x, y, texture);
// 设置属性
this.setScale(0.25);
}
/**
* 发射子弹
* @param x 子弹x坐标
* @param y 子弹y坐标
*/
fire(x: number, y: number) {
this.enableBody(true, x, y, true, true);
this.setVelocityY(-300);
this.scene.sound.play("bullet");
}
preUpdate(time: number, delta: number) {
super.preUpdate(time, delta);
// 子弹走到头,销毁
if (this.y <= -14) {
this.disableBody(true, true);
}
}
}
Enemy敌军类
js
import { Physics, Math, Scene } from "phaser";
export class Enemy extends Physics.Arcade.Sprite {
constructor(scene: Scene, x: number, y: number, texture: string) {
// 创建对象
super(scene, x, y, texture);
scene.add.existing(this);
scene.physics.add.existing(this);
// 设置属性
this.setScale(0.5);
this.body.setSize(100, 60);
}
/**
* 生成敌军
*/
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));
}
preUpdate(time: number, delta: number) {
super.preUpdate(time, delta);
let { height } = this.scene.cameras.main;
// 敌军走到头,销毁
if (this.y >= height + 20) {
this.disableBody(true, true)
}
}
}
Player玩家类
js
import { Physics, Scene } from "phaser";
export class Player extends Physics.Arcade.Sprite {
isDown: boolean = false;
downX: number;
downY: number;
constructor(scene: Scene) {
// 创建对象
let { width, height } = scene.cameras.main;
super(scene, width / 2, height - 80, "player");
scene.add.existing(this);
scene.physics.add.existing(this);
// 设置属性
this.setInteractive();
this.setScale(0.5);
this.setCollideWorldBounds(true);
this.body.setSize(120, 120);
// 注册事件
this.addEvent();
}
/**
* 注册事件
*/
addEvent() {
// 手指按下我方飞机
this.on("pointerdown", () => {
this.isDown = true;
this.downX = this.x;
this.downY = this.y;
});
// 手指抬起
this.scene.input.on("pointerup", () => {
this.isDown = false;
});
// 手指移动
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;
}
});
}
}
End游戏结束类
js
import { Scene } from "phaser";
export class End extends Scene {
constructor() {
super("End");
}
create() {
let { width, height } = this.cameras.main;
// 结束面板
this.add.image(width / 2, height / 2, "sprites", "result").setScale(2.5);
// 标题
this.add
.text(width / 2, height / 2 - 85, "游戏结束", {
fontFamily: "Arial",
fontSize: 24,
})
.setOrigin(0.5);
// 当前得分
let score = this.registry.get("score");
this.add
.text(width / 2, height / 2 - 10, `当前得分:${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", () => {
// 点击事件:关闭当前场景,打开Main场景
this.scene.start("Main");
});
// 按钮文案
this.add
.text(button.x, button.y, "重新开始", {
fontFamily: "Arial",
fontSize: 20,
})
.setOrigin(0.5);
}
}
总结
通过使用 Vue.js 框架,我们可以轻松地构建出一个简单而有趣的飞机大战游戏。从基本的游戏逻辑开始,逐步增加游戏元素和交互性,最终得到一个完整且吸引人的游戏作品。希望这个博客能对你有所启发,并鼓励你尝试使用 Vue.js 来开发更多有趣的游戏和应用程序!