使用 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>
​
相关推荐
y先森3 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy3 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189113 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿4 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡5 小时前
commitlint校验git提交信息
前端
虾球xz5 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇6 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒6 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员6 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐6 小时前
前端图像处理(一)
前端