使用 Vue3 + Vite + Canvas + Worker 实现简单版的 "雷霆战机"

今日份分享:飞机小游戏

使用 Vue3 + Vite + Canvas + Worker 实现简单版的 "雷霆战机"

先看看效果

在敲代码时遇到一些问题:

  1. 在vue3+vite的项目中直接用 const worker = new Worker('./worker.js');的写法会报错,也不用去配置什么,vite已经给我们配置好了

    正确用法:

    @/workers/worker.js

    js 复制代码
    addEventListener('message', e=>{
        const {data} = e;
        console.log(data);
        setTimeout(()=>{
            return postMessage('线程完成')
        })
    })
    export default {}

    在 .vue文件里使用

    js 复制代码
    import Worker from "@/workers/worker.js?worker";
    let worker = new Worker();
    worker.postMessage({msg: 'start'});
    worker.onmessage = (e)=>{
        console.log(e, 'onmessage--------');
    }
  1. 在切换浏览器窗口后,setInterval 定时器会发生异常

    解决方案:

    A.使用window.onblur和window.onfocus来解决

    代码如下:

    js 复制代码
    window.addEventListener('blur', ()=>{
      ...
    })
    window.addEventListener('focus', ()=>{
      ...
    })

    B.使用浏览器页面可见性 API visibilitychange事件解决 (推荐)

    当浏览器最小化窗口或切换到另一个选项卡时就会触发visibilitychange事件,我们可以在事件中用Document.hidden或者Document.visibilityState判断当前窗口的状态,来决定除定时器后者重新开始定时器

    代码如下

    js 复制代码
      document.addEventListener('visibilitychange', () => {
        if (document.hidden === true) {
          ...
        } else {
          ...
        }
      })

代码:

bullet.js

js 复制代码
​
// 子弹
function Bullet({ canvas, image, width=20, height= 20 }) {
    canvas.style.cssText = `
      background-color: none;
    `
    this.ctx = canvas.getContext('2d');
    // 子弹的大小
    this.width = width;
    this.height = height;
    // 画布的大小
    this.CanvasWidth = canvas.width;
    this.CanvasHeight = canvas.height;
    // 子弹运行速度
    this.speed = 5;
    this.move = 0;
    // 是否存在
    this.show = true;
    this.image = image;
    // 子弹坐标
    this.x = 0;
    this.y = 0;
    this.stop = false;
    // 坐标记录 用于暂停游戏
    this.points = null;
}
Bullet.prototype = {
    draw(startPoint) {
        this.points = startPoint;
        if(this.stop){
            return
        }
        let { ctx, width, height, image } = this;
        // 清除子弹上一个位置
        ctx.clearRect(
            startPoint.x - this.width/2, 
            startPoint.y - this.height/2 - this.move, 
            width, 
            height
        );
        if(!this.show){
            return
        }
        if (startPoint.y - 16 - this.move > -this.height) {
            this.move += this.speed;
            this.x = startPoint.x - this.width/2;
            this.y = startPoint.y - this.height/2 - this.move;
        } else {
            this.show = false;
            return
        }
        ctx.drawImage(image, this.x, this.y, width, height);
​
        requestAnimationFrame(() => {
            setTimeout(()=>{
                this.isHit();
                this.draw(startPoint);
            },10)
        })
    },
    isHit(){
        if(!this.show){
            return
        }
        if(!this.game || !this.game.airships){
            console.log(this);
            return
        }
        
        this.game.airships.forEach((item,index)=>{
            if(!item.show){
                return
            }
            let minX = this.x - item.width - 5;
            let maxX = this.x + 5;
            if(
                item.x <= maxX && 
                item.x >= minX && 
                item.y >= this.y - item.height /2
            ){
                this.game.airships[index].show = false;
                this.show = false;
                this.game.airships.splice[index, 1];
                this.game.score += item.scoreValue;
            }
        })
        
    },
    // 销毁
    destroy(){
        this.show = false;
    },
    getScore(){
        return this.game.score;
    }
​
}
export default Bullet;

plane.js

js 复制代码
let Bullet = null; 
/**
 * @canvas  目标canvas画布 
 * @images  飞机和子弹的图片资源
 * @width   飞机的宽
 * @height  飞机的高
 * */ 
// 飞机元素
function Plane({ canvas, images, width=50, height= 30 }) {
    this.width = width;
    this.height = height;
    this.ctx = canvas.getContext('2d');
    this.CanvasWidth = canvas.width;
    this.CanvasHeight = canvas.height;
    this.images = images;
    this.planeImage = images.plane;
    this.bulletImage = images.bullet;
    // this.planeImage = images.plane;
    this.bullets = [];
    this.points = [];
    this.bulletCanvas = null;
    // 飞机坐标
    this.x = 0;
    this.y = 0;
    this.shootSpeed = 5
    this.init();
}
​
Plane.prototype = {
    async init() {
        if(!Bullet){
            Bullet = (await import(/* webpackChunkName: 'Bullet' */'./bullet.js')).default;
        }
        for (let i = 0; i < 10; i++) {
            try {
                this.createBullets();
            } catch (error) {
                console.log(error);
            }
        }
    },
    createBullets() {
        if (this.bullets.length < 10) {
            let bullet = new Bullet({ canvas: this.bulletCanvas, image: this.bulletImage, width:15, height:15 });
            bullet.speed = this.shootSpeed;
            this.bullets.push(bullet);
        }
    },
    draw(points) {
        let { CanvasWidth, CanvasHeight, ctx, planeImage } = this;
        this.x = points.x - this.width/2;
        this.y = points.y - this.height/2;
        ctx.clearRect(0, 0, CanvasWidth, CanvasHeight);
        ctx.drawImage(planeImage, this.x, this.y);
        this.points = points;
    },
    shoot() {
        if (this.bullets.length == 0) {
            this.init();
            return
        }
        let bullet = this.bullets.shift();
        requestAnimationFrame(() => {
            bullet.draw(this.points);
        })
        return bullet;   
​
    },
}
​
export default Plane;

airship.js

js 复制代码
import createRandom from '@/ulits/random.js';
// 飞船
function Airship({ canvas, images, width=50, height=50 }) {
    this.ctx = canvas.getContext('2d');
    this.width = width;
    this.height = height;
    this.CanvasWidth = canvas.width;
    this.CanvasHeight = canvas.height;
    let scoreValue = createRandom(3);
    this.image = images[ scoreValue ];
    this.scoreValue = (scoreValue + 1) * 3;
    this.move = 0;
    // 飞船运动速度
    this.speed = 2;
    // 是否存在
    this.show = true;
    this.x = 0;
    this.y = 0;
    this.stop = false;
    this.points = null;
}
Airship.prototype = {
    draw(points){
        this.points = points;
        if(this.stop){
            return
        }
        let {ctx, CanvasWidth, CanvasHeight, width, height} = this;
​
        ctx.clearRect(
            points.x - this.width/2, 
            points.y + this.move - this.height/2, 
            width, 
            height
        );
        
        if(!this.show){
            return
        }
        if(points.y + this.move < CanvasHeight){
            this.move += this.speed;
            // this.x = points.x;
            // this.y = points.y + this.move;
            this.x = points.x - this.width/2;
            this.y = points.y + this.move - this.height/2;
        }else{
            this.show = false;
            return
        }
        ctx.drawImage(this.image, this.x, this.y, width, height );
        requestAnimationFrame(()=>{
            this.draw(points);
        })
    },
    // 销毁
    destroy(){
        this.show = false;
    },
}
export default Airship;

createAirship.js

js 复制代码
let Airship = null;
// 创建飞船
function CreateAirship({ canvas, total, images }) {
    this.total = total;
    this.airships = [];
    this.num = 5;
    this.canvas = canvas;
    this.images = images;
}
CreateAirship.prototype = {
    async create() {
        if(!Airship){
            Airship = (await import(/* webpackChunkName: 'Airship' */'./airship.js')).default
        }
        // 没有飞船了
        if(this.total<=0){
            return
        }
        // 每次生产的飞船数量
        let cnt = this.num >= this.total ? this.total : this.num;
        for (let i = 0; i < cnt; i++) {
            let airship = new Airship({
                canvas: this.canvas,
                images: this.images
            });
            this.airships.push(airship)
        }
        this.total -= this.num;
    },
    // 出击
    hitOut() {
        let airship = this.airships.shift();
        if (this.airships.length == 0) {
            this.create();
        }
        return airship;
    },
}
export default CreateAirship;

createGame.js

js 复制代码
import Plane from '@/game/plane.js';
import CreateAirship from '@/game/createAirship.js';
// 随机数
import createRandom from '@/ulits/random.js';
class CreateGame {
    constructor(){
        
    }
    myPlane = null;// 飞机对象
    gameImages = {};// 游戏图片
    airshipImages = [];// 飞船图片集合
    // 敌军舰队定时器
    airshipInit = null;
    airFleet = null; //敌军飞船集合
    // 子弹自动攻击定时器
    autoShootInit = null;
    // 飞机的坐标
    planePoints = {
        // x: canvasWidth / 2, y: canvasHeight - 20
    }
    // 暂停时 保存飞机坐标
    lastPlanePoint = null
    canvas = null
    bulletCanvas = null
    game = null
    over = false
    shootSpeed = 5
    airshipTotal = 30
    // 监听游戏结束
    onGameOver() {
​
    }
    // 暂停游戏
    stop() {
        this.stopShoot();
        this.airshipStopMove();
    }
    // 开始游戏
    begin() {
        this.autoShootInit = true;
        this.airshipInit = true;
        this.autoShoot();
        this.airshipAutoMove();
    }
    getGameStatus(){
        return this.game
    }
    init() {
        this.createPlane();
        this.drawPlane();
        this.createAirFleet();
    }
    // 创建飞机
    createPlane() {
        this.myPlane = new Plane({
            canvas: this.canvas,
            images: this.gameImages,
        })
        console.log(this.bulletCanvas, 'this.bulletCanvas------');
        // 子弹的画布
        this.myPlane.bulletCanvas = this.bulletCanvas;
    }
​
    // 画飞机
    drawPlane() {
        this.myPlane.draw(this.planePoints);
    }
    // 停止射击 清除定时器
    stopShoot() {
        this.autoShootInit = false;
    }
    // 自动射击
    autoShoot() {
        this.autoShootInterval();
    }
    // 
    autoShootInterval(delay=400) {
        if(!this.autoShootInit){
            return
        }
        // console.log(this.canvas, 'this.canvas----');
        let { game } = this;
        const bullet = this.myPlane.shoot();
        
        if (bullet) {
            game.bullets.push(bullet);
            game.airships = game.airships.filter(item => item.show);
            bullet.game = game;
        }
        setTimeout( this.autoShootInterval.bind(this),delay)
    }
​
​
    // 创建舰队
    createAirFleet() {
        this.airFleet = new CreateAirship({
            canvas: this.airshipCanvas,
            total: this.airshipTotal,// 舰队总数
            images: this.airshipImages, // 飞船图片集合
        });
    }
    // 飞船停止移动 清除定时器
    airshipStopMove() {
        this.airshipInit = false;
    }
    // 飞船自动移动
    airshipAutoMove() {        
        this.airshipAutoMoveInterval();
    }
​
    airshipAutoMoveInterval(delay=1500) {
        if(this.over){
            return
        }
        if(!this.airshipInit){
            return
        }
        let { airFleet, game } = this
        let airship = airFleet.hitOut();
        if (airFleet.total <= 0 && airFleet.airships.length == 0) {
            let existAirship = game.airships.filter(item => item.show);
            // 最后一架飞船消失后 游戏结束
            if (!existAirship.length) {
                this.airshipInit = null
                // 游戏结束
                // gameOver.value = true;
                // gameStop.value = true;
                // maskLayerShow.value = true;
                this.stop();
                console.log('onGameOver', this.over);
                this.onGameOver();
                return
            }
        }
        if (airship) {
            game.airships.push(airship);
            airship.draw({
                x: createRandom(450, 50),
                y: 0
            })
        }
        setTimeout( this.airshipAutoMoveInterval.bind(this),delay)
    }
​
}
​
export default CreateGame;

game.vue

js 复制代码
​
<template>
  <div class="container-box">
    <div class="operation-box">
      <div class="score-displayer">{{ game.score }}</div>
      <div class="begin-btn" v-if="!gameOver" @click="handleStopGame">{{ gameStop ? "开始" : "暂停" }}</div>
      <div class="begin-btn" v-if="gameOver" @click="handleNextGame">冲击下一关</div>
    </div>
​
    <div class="canvas-box">
      <div class="mask-layer" v-show="maskLayerShow">
        {{ gameOver ? "游戏结束" : "暂停" }}
      </div>
      <canvas id="game-canvas"></canvas>
      <canvas id="bullet-canvas"></canvas>
      <canvas id="airship-canvas"></canvas>
    </div>
  </div>
</template>
​
<script setup>
import { ref, reactive, onMounted } from 'vue';
import Worker from "@/workers/worker.js?worker";
import CreateGame from "./js/createGame.js"
// 画布
let canvasBox = null;
let canvas = null;
let bulletCanvas = null;
let airshipCanvas = null;
// 画布尺寸
let canvasWidth = 500;
let canvasHeight = 500;
​
​
​
let gameImages = {};// 游戏图片
let airshipImages = [];// 飞船图片集合
let airshipTotal = 30
// 飞机的坐标
let planePoints = {
  x: canvasWidth / 2, y: canvasHeight - 20
}
// 暂停时 保存飞机坐标
let lastPlanePoint = null
// 遮罩层标志位
let maskLayerShow = ref(true);
// 游戏暂停标志位
let gameStop = ref(true);
// 游戏结束标志位
let gameOver = ref(false);
// 游戏得分对象
let game = reactive({
  bullets: [],
  airships: [],
  score: 0
})
​
let worker = new Worker();
worker.postMessage({ type: 'init', planePoints });
let CGame = new CreateGame();
const onGameOver = ()=>{
  console.log('over');
  CGame.over = true;
  CGame.stop();
​
  gameOver.value = true;
  gameStop.value = true;
  maskLayerShow.value = true;
}
CGame.onGameOver = onGameOver
/**
 * @canvas        包裹画布的容器
 * @airshipCanvas 敌军飞船画布
 * @bulletCanvas  子弹画布
 * @gameImages    游戏图片资源集合
 * @planePoints   飞机坐标
 * @game          游戏管理对象
 * @airshipImages 飞船图片资源集合
 * @shootSpeed    控制子弹速度
 * @airshipTotal  控制敌军飞船总数
 * */ 
function initGame({
  canvas,
  airshipCanvas,
  bulletCanvas, 
  gameImages, 
  planePoints, 
  game, 
  airshipImages,
  shootSpeed,
  airshipTotal
}){
  CGame.canvas = canvas;
  CGame.airshipCanvas = airshipCanvas;
  CGame.bulletCanvas = bulletCanvas;
​
  CGame.gameImages = gameImages;
  CGame.planePoints = planePoints;
  CGame.game = game;
  CGame.airshipImages = airshipImages;
  CGame.shootSpeed = shootSpeed || 400
  CGame.airshipTotal = airshipTotal || 30;
  
  CGame.init();
  // CGame.begin();
}
onMounted(() => {
  canvasBox = document.querySelector('.canvas-box')
  canvas = document.getElementById('game-canvas');
  bulletCanvas = document.getElementById('bullet-canvas');
  airshipCanvas = document.getElementById('airship-canvas');
  // 所有画布统一宽高配置
  [canvas, bulletCanvas, airshipCanvas].forEach(item => {
    item.width = canvasWidth;
    item.height = canvasHeight;
  })
​
  worker.onmessage = (e) => {
    // return
    let { data } = e;
    (data && data.images) && data.images.forEach((item) => {
      gameImages[item.name] = item.image
    })
    // 飞船数据收集
    let { airship1, airship2, airship3 } = gameImages
    airshipImages = [airship1, airship2, airship3];
    // 游戏界面是否隐藏
    initGame({
      canvas,
      airshipCanvas,
      bulletCanvas, 
      gameImages, 
      planePoints, 
      game, 
      airshipImages,
      shootSpeed: 12,
      airshipTotal: airshipTotal
    })
    windowIsHidden();
  }
  canvasBox.addEventListener('mousedown', mousedown)
  canvasBox.addEventListener('mouseup', mouseup)
})
​
// 窗口是否隐藏
function windowIsHidden() {
  document.addEventListener('visibilitychange', () => {
    if (document.hidden === true || gameStop.value) {
      // 遮罩层存在 就不继续执行了
      if(maskLayerShow.value){
        return
      }
      CGame.stop();
      maskLayerShow.value = true;
      gameStop.value = true;
      let existAirship = CGame.game.airships.filter(item=>item.show);
      let existBullet = CGame.game.bullets.filter(item=>item.show);
      existAirship.forEach(item=>{
        item.stop = true;
      })
      existBullet.forEach(item=>{
        item.stop = true;
      })
    } else {
​
    }
  })
}
​
​
​
function mousedown(e) {
  // 游戏暂停出现遮罩层
  if (maskLayerShow.value) {
    return
  }
  canvasBox && canvasBox.addEventListener('mousemove', mousemove);
  CGame.drawPlane();
}
function mousemove(e) {
  // 
  if (maskLayerShow.value) {
    canvasBox && canvasBox.removeEventListener('mousemove', mousemove)
    return
  }
  let { offsetX, offsetY } = e;
  planePoints = {
    x: offsetX,
    y: offsetY
  }
  CGame.planePoints = planePoints
  CGame.drawPlane();
}
function mouseup(e) {
  canvasBox && canvasBox.removeEventListener('mousemove', mousemove)
}
​
​
const handleStopGame = () => {
  gameStop.value = !gameStop.value;
  if (gameStop.value) {
    lastPlanePoint = { ...planePoints };
    CGame.stop();
    maskLayerShow.value = true;
  } else {
    if(gameOver.value){
      return
    }
    CGame.begin();
    maskLayerShow.value = false;
  }
  CGame.game.airships.filter(item=>item.show).forEach(item => {
    // stop 标志位 控制所有 元素运行或停止
    item.stop = gameStop.value;
    if (!gameStop.value) {
      item.draw(item.points)
    }
  })
  CGame.game.bullets.filter(item=>item.show).forEach(item => {
    if(!item){
      return
    }
    item.stop = gameStop.value;
    if (!gameStop.value) {
      item.points && item.draw(item.points)
    }
  })
}
const handleNextGame = () => {
  
  canvasBox = document.querySelector('.canvas-box')
  canvas = document.getElementById('game-canvas');
  bulletCanvas = document.getElementById('bullet-canvas');
  airshipCanvas = document.getElementById('airship-canvas');
  console.log(canvas, 'canvas-------');
  gameOver.value = false;
  gameStop.value = false;
  maskLayerShow.value = false;
  CGame = new CreateGame()
  CGame.onGameOver = onGameOver
  CGame.over = false;
  setTimeout(()=>{
    initGame({
      canvas,
      airshipCanvas,
      bulletCanvas, 
      gameImages, 
      planePoints, 
      game, 
      airshipImages,
      shootSpeed: 12,
      airshipTotal: airshipTotal
    })
    CGame.begin();
  })
    
}
</script>
​
​
<style scoped>
@font-face {
  font-family: Number;
  src: url(@/assets/font/calculatorFontBold.TTF);
}
​
.container-box {
  width: 500px;
  height: 100vh;
  margin: 0 auto;
​
}
​
.operation-box {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
}
​
.begin-btn {
  padding: 5px 0;
  font-size: 1.5rem;
  cursor: pointer;
  margin-bottom: 10px;
  margin-right: 10px;
  padding: 5px 15px;
  background-color: #1AA034;
  color: #fff;
  border-radius: 5px;
}
​
.score-displayer {
  color: blue;
  user-select: none;
  display: inline-block;
  padding: 5px 0;
  width: 150px;
  font-size: 2rem;
  font-family: Number;
  background-color: #fff;
  margin-bottom: 10px;
  text-align: center;
}
​
#airship-canvas,
#bullet-canvas,
#game-canvas {
  position: absolute;
  top: 0;
  left: 0;
}
​
#game-canvas {
  background-color: #fff;
  z-index: 100;
}
​
#airship-canvas {
  z-index: 200;
}
​
#bullet-canvas {
  z-index: 200;
}
​
.canvas-box {
  position: relative;
  width: 500px;
  height: 500px;
  margin: 0 auto;
}
​
​
.mask-layer {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1000;
  /* width: 550px;
  height: 550px; */
  background-color: rgba(0, 0, 0, 0.5);
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 4rem;
  font-weight: bold;
  color: #fff;
}
​
</style>
​
相关推荐
bysking17 分钟前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓33 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_41136 分钟前
无网络安装ionic和运行
前端·npm
理想不理想v38 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云1 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205871 小时前
web端手机录音
前端
齐 飞1 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
神仙别闹1 小时前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0012 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html