今日份分享:飞机小游戏
使用 Vue3 + Vite + Canvas + Worker 实现简单版的 "雷霆战机"
先看看效果
在敲代码时遇到一些问题:
-
在vue3+vite的项目中直接用 const worker = new Worker('./worker.js');的写法会报错,也不用去配置什么,vite已经给我们配置好了
正确用法:
@/workers/worker.js
jsaddEventListener('message', e=>{ const {data} = e; console.log(data); setTimeout(()=>{ return postMessage('线程完成') }) }) export default {}
在 .vue文件里使用
jsimport Worker from "@/workers/worker.js?worker"; let worker = new Worker(); worker.postMessage({msg: 'start'}); worker.onmessage = (e)=>{ console.log(e, 'onmessage--------'); }
-
在切换浏览器窗口后,setInterval 定时器会发生异常
解决方案:
A.使用window.onblur和window.onfocus来解决
代码如下:
jswindow.addEventListener('blur', ()=>{ ... }) window.addEventListener('focus', ()=>{ ... })
B.使用浏览器页面可见性 API visibilitychange事件解决 (推荐)
当浏览器最小化窗口或切换到另一个选项卡时就会触发visibilitychange事件,我们可以在事件中用Document.hidden或者Document.visibilityState判断当前窗口的状态,来决定除定时器后者重新开始定时器
代码如下
jsdocument.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>