Vue3集成Phaser-飞机大战游戏(设计与源码)

文章目录

更多相关内容可查看

引言

飞机大战(也被称为射击游戏或空战游戏)是一种非常受欢迎的休闲游戏类型。在这个博客中,我们将探讨如何使用 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 来开发更多有趣的游戏和应用程序!

相关推荐
腾讯TNTWeb前端团队2 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰5 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪5 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy6 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom7 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom7 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom7 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom7 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom7 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试