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

在这里插入图片描述


更多相关内容可查看

引言

飞机大战(也被称为射击游戏或空战游戏)是一种非常受欢迎的休闲游戏类型。在这个博客中,我们将探讨如何使用 Vue.js 框架来构建一个简单的飞机大战游戏。我们将从基本的游戏逻辑开始,逐步增加游戏元素和交互性,代码详解可参考注释,最终展示画面在文章底部

项目初始化

git地址:https://gitee.com/its-a-little-bad/vue-project—aircraft-battle.git
node版本:20.8.1

游戏设计和结构

在 Vue.js 中,我们通常将游戏的各个部分分解为不同的场景。

主场景

Game.vue:游戏的主界面,包含背景、飞机、敌机、子弹等。
Plane.vue:玩家的飞机,可以移动和发射子弹。
Enemy.vue:敌机,从屏幕上方随机出现并向下移动。
Bullet.vue:子弹,从玩家飞机发射并向上移动。

游戏场景
在这里插入图片描述

游戏程序实现

Vue页面嵌入Phaser

在 Vue 应用中嵌入一个 Phaser 游戏

<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 场景来加载游戏所需的资源和设置一些基本的游戏元素,示例如下
在这里插入图片描述
程序实现:

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场景管理系统的一部分,它允许你动态地加载、创建、运行和销毁游戏的不同部分。

游戏场景功能实现

在这里插入图片描述

程序实现

// 定义 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爆炸类

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子弹类

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敌军类

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玩家类

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游戏结束类

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 来开发更多有趣的游戏和应用程序!

  • 40
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来一杯龙舌兰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值