使用canvas编写飞机大战小游戏
本文章讲述如何使用canvas实现一个飞机大战的小游戏,游戏我取名为fight-AO(点击查看源码)实现可以发射子弹、击败怪兽、自动生成怪兽…怪兽名字为AO,击败AO的叫做Superman。点击试玩
文章目录
前言
在了解如何编写游戏过程时,你需要知道vue框架,element ui帮助你快速快速开发。
一、组件版本
├─┬ @vue/cli-plugin-babel@4.5.13
│ └─┬ @vue/babel-preset-app@4.5.13
│ └── vue@2.6.14 deduped
├─┬ element-ui@2.15.3 -> ./node_modules/_element-ui@2.15.3@element-ui
│ └── vue@2.6.14 deduped
└── vue@2.6.14
二、项目结构
.
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public #开放的资源
│ ├── favicon.ico
│ ├── fight-AO
│ │ ├── AO.png
│ │ └── superman.png
│ └── index.html
└── src #源文件
├── App.vue
├── assets #项目资源文件
│ └── logo.png
├── components #vue组件
│ └── fight-AO.vue #本文章讲解的文件
├── main.js #项目主配置
└── router #vue组件路由
└── router.js
三、代码&思路讲解
以下讲解对编写游戏过程的讲解,不会深入剖析底层代码。
1. 使用canvas
1.1 代码
<canvas id="canvas"></canvas>
//样式
<style scoped>
#canvas {
margin: 0;
background-image: url("../../public/fight-AO/sky.jpeg");
}
</style>
//获取canvas
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
//宽和高指定为页面布局大小
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight - 50;
1.2 知识点
- canvas全部教程
- 使用canvas来绘制图形
- 使用图像 Using images
- 2d布局里的xy轴来指定位置如图:
2. 移动超人
2.1 代码
//超人外形
this.supermanImg.src = '/fight-AO/superman.png';
//超人初始位置
this.supermanX = (this.canvas.width - this.supermanW) / 2;
this.supermanY = (this.canvas.height - this.supermanH) / 1.2;
//外形加载完成后设置superman初始位置
this.supermanImg.onload = () => {
this.supermanMove();
}
//监听走位
window.onkeydown = this.keyDown;
window.ontouchmove = this.touchmove;
/**
* 移动超人
*/
supermanMove() {
this.moveArr.push({x: this.supermanX, y: this.supermanY, w: this.supermanW, h: this.supermanH})
//如果有移动就删除指定位置
if (this.moveArr.length > 1) {
let m = this.moveArr[this.moveArr.length - 2];
this.ctx.clearRect(m.x, m.y, m.w, m.h);
}
let m = this.moveArr[this.moveArr.length - 1];
this.ctx.drawImage(this.supermanImg, m.x, m.y, m.w, m.h);
//清除以前步数
for (let i = 0; i < this.moveArr.length - 3; i++) {
delete this.moveArr[i];
}
}
/**
* 键盘事件
*/
keyDown(event) {
if (!this.checkActionStartStatus()) return;
switch (event.keyCode) { // 获取当前按下键盘键的编码
case 32 : // 按空格暂停
break;
case 37 : // 按下左箭头键,向左移动
this.supermanX -= this.speed;
break;
case 39 : // 按下右箭头键,向右移动
this.supermanX += this.speed;
break;
case 38 : // 按下上箭头键,向上移动
this.supermanY -= this.speed;
break;
case 40 : // 按下下箭头键,向下移动
this.supermanY += this.speed;
break;
// default:
// return;
}
//是否超出边界
if (this.supermanX >= this.canvas.width - (this.supermanW / 2)) {
this.supermanX -= this.speed;
} else if (this.supermanX + (this.supermanW / 2) <= 0) {
this.supermanX += this.speed;
} else if (this.supermanY >= this.canvas.height - this.supermanW) {
this.supermanY -= this.speed;
} else if (this.supermanY <= 0) {
this.supermanY += this.speed;
}
this.supermanMove();
},
/**
* 手指摁下
* @param event
*/
touchmove(event) {
if (!this.checkActionStartStatus()) return;
let moveX = event.changedTouches[0].pageX;
let moveY = event.changedTouches[0].pageY;
this.supermanX = moveX;
this.supermanY = moveY;
this.supermanMove();
}
2.2 知识点
- 通过速度
this.speed
改变超人移动的速度,只有在键盘事件有用,玩家操作上下左右分别对应y-speed, y+speed, x-speed, x+speed。改变this.supermanX,this.supermanY
的值超人就可以轻易的移动。 supermanMove()
函数封装了记录this.moveArr
超人位置,this.ctx.clearRect
清除上一次显示位置,this.ctx.drawImage
画出当前移动位置,清除历史移动delete this.moveArr[i];
3. 发射炮弹cannonball
3.1 代码
//启动炮弹
setInterval(() => {
if (this.checkActionStartStatus()) {
this.cannonball();
this.upup = 10;
}
}, 300)
/**
* 炮弹
*/
cannonball() {
let cannonballArr = []
//先把y画出来 当前y-upup直到小于0 y越小就越往上 直到小于-AO的高度
while (this.supermanY - this.upup > -this.aoHeight) {
this.upup += this.cannonballSpace;//加上每个炮弹的间距
//记录当前这组炮弹发射的轨迹
cannonballArr.push({x: this.supermanX + (this.supermanW / 2), y: this.supermanY - this.upup});
}
let index = 0;//炮弹位置
let clearCannonball = 0;//清除多余炮弹
let cannonballNum = this.cannonballNum;//炮弹数量
//通过定时器实现炮弹动起来的效果
let interval = setInterval(() => {
//炮弹发完了 只有发射一次炮弹才会走这个判断 避免浪费资源
if (cannonballArr.length === 0) {
clearInterval(interval);
return;
}
//到顶了炮弹看不见了
if (cannonballNum >= cannonballArr.length) {
cannonballNum = cannonballArr.length - 1;
}
//清除历史炮弹
// console.log(index + '===' + cannonballNum + '====' + clearCannonball + '====' + cannonballArr.length)
if (index >= cannonballNum && clearCannonball < cannonballArr.length) {
let arrElement = cannonballArr[clearCannonball];
this.ctx.clearRect(arrElement.x, arrElement.y + this.cannonballHeight,
this.cannonballWidth + this.cannonballWidth / 2,
this.cannonballSpace)
clearCannonball++;
}
//炮弹发完了
if (index >= cannonballArr.length - 1) {
if (clearCannonball > cannonballArr.length - 1) clearInterval(interval);
} else {
//发射炮弹
let arrElement = cannonballArr[index];
if (!arrElement) return;
//炮弹的样式
this.ctx.fillStyle = "rgb(200,100,200)"
this.ctx.fillRect(arrElement.x, arrElement.y, this.cannonballWidth, this.cannonballHeight)
//清除啊哦 这段判断主要是消灭AO
if (this.aoArr.length > 0) {
// let arrElement = cannonballArr[cannonballArr.length - 1 - index];
//查找啊哦的位置
let aoObj = this.aoArr.find(o =>
//把区间放大
(o.x + this.aoWidth) >= arrElement.x && (o.x) <= arrElement.x
&& o.y + this.aoWidth >= arrElement.y && o.y - this.aoHeight <= arrElement.y
);
if (aoObj) {
this.aoDieNum++;
// console.log(index + '===' + arrElement.x + '===' + arrElement.y)
this.ctx.clearRect(aoObj.x, aoObj.y, this.aoWidth, this.aoHeight)
aoObj.x = 1000;
aoObj.y = 1000;
this.resetAO();
}
}
index++;
}
}, 30);
}
3.2 知识点
this.upup
一组炮弹移动间隙,通过它来改变y值的变化让AO动起来this.cannonballSpace
每个炮弹之间的空间,让炮弹看起来更加有节奏this.cannonballNum
每次发射的炮弹数量
4. AO小怪兽
4.1 代码
this.aoImg1.src = "/fight-AO/AO01.png"
this.aoImg2.src = "/fight-AO/AO02.png"
this.aoImg3.src = "/fight-AO/AO03.png"
this.aoImg4.src = "/fight-AO/AO04.png"
this.aoImg5.src = "/fight-AO/AO05.png"
//初始化啊哦
this.AO();
//开启AO移动速度 50ms移动一次
setInterval(() => {
//检查游戏是否开始 如果以开始就判断超人是否已触碰到AO游戏结束
if (this.checkActionStartStatus()) {
//查找啊哦的位置
let aoObj = this.aoArr.find(o =>
//把区间放大
o.x + (this.aoWidth / 2) >= this.supermanX && o.x - (this.aoWidth * 1.1) <= this.supermanX
&& o.y + this.aoHeight >= this.supermanY && o.y - this.aoHeight <= this.supermanY
);
//是否已触碰到超人
if (aoObj) {
//游戏暂停
this.actionPause();
//提示游戏结束
this.$alert('游戏结束💀重新开始', '提示', {
confirmButtonText: '确定',
showClose: false,
customClass: 'hintAlert',
callback: () => {
location.reload();
}
});
}
//重置AO 让它开起来是在移动的
this.resetAO();
}
}, 50);
// 被消灭者【啊哦】
AO() {
let AOAO = setInterval(() => {
this.setDataStatus = false;
if (!this.checkActionStartStatus()) return;
//AO是否已经全部出现
if (this.aoNum < 1) {
clearInterval(AOAO);
//ao已被初始化还完成可以设置
this.setDataStatus = true;
return;
}
//随机出现位置
let numX = Math.round(Math.random() * (this.canvas.width - 5)) + 5;
let numY = -this.aoHeight;//Math.round(Math.random() * 50)
// console.log(numX + '==' + numY)
let random = Math.round(Math.random() * 5);
this.drawAO(random, numX, numY);
this.aoArr.push({x: numX, y: numY, random: random})
this.aoNum--;
}, 500)
}
/**
* 画啊哦 在任意一个case画一个AO 每个case的AO样式是不同的
*/
drawAO(random, numX, numY) {
switch (random) {
case 1:
this.ctx.drawImage(this.aoImg1, numX, numY, this.aoWidth, this.aoHeight)
break;
case 2:
this.ctx.drawImage(this.aoImg2, numX, numY, this.aoWidth, this.aoHeight)
break;
case 3:
this.ctx.drawImage(this.aoImg3, numX, numY, this.aoWidth, this.aoHeight)
break;
case 4:
this.ctx.drawImage(this.aoImg4, numX, numY, this.aoWidth, this.aoHeight)
break;
case 5:
this.ctx.drawImage(this.aoImg5, numX, numY, this.aoWidth, this.aoHeight)
break;
}
},
/**
*让AO往左移动还是右
* @param i ao位置
* @param type 1加 右 , 2减 左
*/
leftOrRight(i, type) {
switch (type) {
case 1:
this.aoArr[i].x += Math.round(Math.random() * 2)
break;
case 2:
this.aoArr[i].x -= Math.round(Math.random() * 2)
break;
}
},
/**
* 1、补全未被消灭的啊哦 2、AO互相之间触碰他是被误伤的 3、让啊哦持续动起来
*/
resetAO() {
//先清当前位置的AO
for (let i = 0; i < this.aoArr.length; i++) {
this.ctx.clearRect(this.aoArr[i].x, this.aoArr[i].y, this.aoWidth, this.aoHeight)
}
//重新渲染新的AO
for (let i = 0; i < this.aoArr.length; i++) {
//改变x轴移动位置 计算x轴的位置占总宽的多少
let xPosition = (this.aoArr[i].x + this.aoWidth) / this.canvas.width;
//随机以下往左还是右
let type = Math.round(Math.random() * 2);
//定义了几种简单的移动规则
if (xPosition > 0 && 0.1 > xPosition) {
this.leftOrRight(i, type);
} else if (xPosition > 0.1 && 0.2 > xPosition) {
this.leftOrRight(i, type);
} else if (xPosition > 0.2 && 0.3 > xPosition) {
this.leftOrRight(i, type);
} else if (xPosition > 0.3 && 0.5 > xPosition) {
this.leftOrRight(i, type);
} else if (xPosition > 0.5 && 0.7 > xPosition) {
this.leftOrRight(i, 2);
} else if (xPosition > 0.7 && 1 > xPosition) {
this.leftOrRight(i, 2);
} else {
this.leftOrRight(i, 2);
}
//让AO在页面上显示出来
this.drawAO(this.aoArr[i].random, this.aoArr[i].x, this.aoArr[i].y++)
}
}
4.2 知识点
this.aoNum
AO生成数量resetAO()
函数可以更友好地显示重叠在一起的AO不被互相清除,通过先清空当前显示的AO,在计算下一个位置的AO。AO()
函数只有在第一次初始化AO的时候才是有用的。虽然两个函数都是可以生成AO的逻辑,但是他们虽执行的业务逻辑是不一样的。AO()
初始化,resetAO()
改变数据。- AO的行动轨迹只是一个和简单的区域划分,这个地方可以提前生成每一个AO的行动轨迹不应该每次都要重新判断AO下一步的位置应该是哪。
TODO
四、总结
以上就是使用canvas来实现这个游戏的主要代码逻辑,使用到的知识都是通过查看canvas官网上的demo来进行学习,主要用的函数有ctx.fillRect
绘制矩形,ctx.drawImage
绘制图像,ctx.clearRect
清除指定矩形区域(让清除部分完全透明),监听键盘事件,触屏摁下事件。这个游戏设计想法有参考微信小游戏一些的实现,AO的命名想法来源是AI Object,这个怪兽可以是任意一个对象通过AI让他赋予生命。一种简单的2d游戏还是可以通过js来实现的,在编写的过程中很好玩。