hellow大家好,中秋佳节到了,欢乐度节的同时,技术也要跟上呀,这次我们通过canvas实现一个中秋接月饼的小游戏,三连不迷路哦~
展示一下游戏成品:
准备游戏背景
首先我们将游戏背景界面绘制出来。 游戏背景界面由一个游戏窗口和两个装饰元素组成。
准备游戏背景
首先我们将游戏背景界面绘制出来。 游戏背景界面由一个游戏窗口和两个装饰元素组成。
html
<template>
<div class="canvasBar">
<canvas
id="myCanvas"
width="900"
height="600"
style="background-color: #dc181b; border-radius: 20px"
></canvas>
<img class="icon1" src="../assets/test/icon2.gif" alt="" />
<img class="icon2" src="../assets/test/icon1.png" alt="" />
</div>
</template>
scss
.canvasBar {
background-color: #f9c245;
position: relative;
width: 1100px;
margin-top: 70px;
padding: 15px 0;
.icon1 {
position: absolute;
top: 0px;
left: 90px;
width: 300px;
}
.icon2 {
position: absolute;
bottom: -24px;
right: 80px;
width: 200px;
}
}
游戏背景就完成了,是不是很有节日气氛呢? 接下来我们绘制从天而降的月饼(因为我们是接月饼游戏嘛~)
绘制一个月饼
我们先绘制一个静态的月饼到界面上试试水。 首先我们使用 document.querySelector("#myCanvas")获取到刚刚的 canvas 元素,然后使用 canvas.getContext("2d")获取 2d 画笔,使用最后使用 drawImage 函数将月饼图片绘制到界面上。
注意,drawImage 四个参数分别为:Image 对象(需要使用 new Image()获取并为 src 赋值)、left 值、top 值、宽度、高度。
js
let ctx;
function init() {
let canvas = document.querySelector("#myCanvas");
ctx = canvas.getContext("2d");
draw();
}
function draw() {
let img = new Image();
img.src = new URL("../assets/test/moonCake1.png", import.meta.url).href;
ctx.drawImage(img, 300, 40, 90, 90);
}
一个月饼完成啦。
一个下落的月饼
我们计划除了月饼还可能降落一些其他玩意儿,比如炸弹、锦囊之类的道具。
所以这里写两个类,一个是降落物品类(里面包含降落函数和降落速度),一个是月饼类(里面包含月饼图片)和月饼位置。 这里月饼类继承了降落物品类。因为它是个会降落的月饼。
js
...
class FallReward {
basePace = 500;
fallDown() {
this.top = this.top + 10;
setTimeOut(() => {
this.fallDown();
}, this.basePace);
}
}
class MoonCake extends FallReward {
constructor({ left, top = 0, src }) {
super();
this.top = top;
this.left = left;
this.img = new Image();
this.img.src = src;
this.fallDown();
}
}
...
上面我们写好了 2 个类,FallReward 类中,有下落 10 的的间隔时间(默认 500,后续越来快,该时间也越来越小),和下落方法(一旦触发该方法物品将一直下落。) MoonCake 类继承了 FallReward 类,同时描述了物品的具体位置和图片,也就是在这个类中,准备好了要绘制的一切参数,并且触发了下落。
然后我们使用 MoonCake 类构建一个下落的月饼实例,并且绘制到屏幕上。
因为绘制的部分会越来越复杂,我们将绘制的部分放置在 draw 方法中。
js
let ctx;
function init() {
let canvas = document.querySelector("#myCanvas");
ctx = canvas.getContext("2d");
draw();
}
let moonObj = new MoonCake({
left: 300,
src: new URL("../assets/test/moonCake1.png", import.meta.url).href,
});
function draw() {
ctx.clearRect(0, 0, 900, 600); // 清空画布 如不调用
ctx.drawImage(moonObj.img, moonObj.left, moonObj.top, 80, 80);
requestAnimationFrame(draw); // 触发重绘
}
init();
这里有 3 个要点:
- 在 moonObj 中的 src 参数使用了 new URL 的句式,不懂的朋友可以在下面留言或者点点 ♥♥。这里是因为博主使用了 vite 框架,在引入图片的时候这样写,如果是 webpack 框架,就可以采用 require(...)来引入。
- clearRect 函数,不清楚这个作用的同学也可以 ♥♥ 点起来。这里是将之前绘制的清空,然后重新绘制,下落的月饼实际是由于无数个不同坐标的月饼组成,如果不在每次绘制前清空,下落轨迹会留下一串月饼。
- requestAnimationFrame 函数,在屏幕的每个刷新时刻调用,可以看作 setIntervel 升级版。
会降落的月饼就画好啦。
一群下落的月饼
游戏是接月饼,当然不能接完一个月饼就结束啦,要有很多月饼随机的往下掉。 这里的随机指的是横向位置随机和产生数量随机,纵向位置是固定的:都是从最上方开始掉落。
接下来我们绘制一群下落的月饼。
在 draw 函数中,我们要让所有的月饼一起下落,可以将所有的月饼实例维护在一个列表中,然后再 draw 中绘制列表中的所有月饼当前的位置。
刚刚月饼实例在创建的时候就已经触发了不断下落的周期性变更内部属性的函数,所以在 draw 函数中只要遍历列表进行绘制即可。
js
...
function draw() {
ctx.clearRect(0, 0, 900, 600); // 清空画布
getMoonList.forEach((moonObj) => {
ctx.drawImage(moonObj.img, moonObj.left, moonObj.top, 80, 80);
})
requestAnimationFrame(draw); // 触发重绘
}
...
接下来创建月饼实例列表 getMoonList,该列表周期性的填入一批数量随机的月饼。
js
function init() {
let canvas = document.querySelector("#myCanvas");
ctx = canvas.getContext("2d");
setMoonList()
setInterval(() => {
setMoonList()
}, 5000)
draw();
}
...
let getMoonList = [];
let setMoonList = () => {
for (let i = 0; i < Math.round(Math.random() * 4); i++) {
getMoonList.push(
new MoonCake({
left: Math.round(Math.random() * 700),
src: new URL("../assets/test/moonCake1.png", import.meta.url).href,
})
);
}
};
...
从上面的代码中可以看到,getMoonList 周期性的填入数量 0-5 个月饼,位置为 left 0-700。
一批下落的月饼就写好啦。
接月饼的篮子
然后我们放置一个篮子来接月饼,篮子如果接到了月饼,分数就+1。 篮子只能在窗口底部活动,所以 bottom 为 0。
篮子使用键盘左右按键来控制。
绘制篮子,还是创建一个类,然后再 flaw 中绘制,篮子类和月饼类的区别就是位置不同,也没有下落功能。
js
...
class Basket {
constructor({ left, top = 490, src }) {
this.top = top;
this.left = left;
this.img = new Image();
this.img.src = src;
}
}
let basket = new Basket({
left: 350,
src: new URL("../assets/test/basket.png", import.meta.url).href,
});
...
function draw() {
ctx.clearRect(0, 0, 900, 600); // 清空画布
getMoonList.forEach((moonObj) => {
ctx.drawImage(moonObj.img, moonObj.left, moonObj.top, 80, 80);
});
ctx.drawImage(basket.img, basket.left, basket.top, 150, 120); // 这行是新加的
requestAnimationFrame(draw); // 触发重绘
}
...
这时篮子就出现在游戏界面了,然后为篮子添加键盘监听,在监听到键盘左右键时,改变篮子的位置。
js
...
class Basket {
constructor({ left, top = 490, src }) {
this.top = top;
this.left = left;
this.img = new Image();
this.img.src = src;
document.addEventListener("keydown", (event) => {
if(event.code === 'ArrowLeft') {
this.left = this.left - 20
} else if(event.code === 'ArrowRight') {
this.left += 20
}
});
}
}
...
这样,一个可以用键盘控制移动的篮子就做好啦。
分数记录
接到月饼,就要记录分数啦。我们在界面上添加一个分数牌用来展示分数。
分数可以在月饼坐标改变的时候,计算其与篮子是否重合,具体算法是当月饼 top 值小于篮子 top 值时,月饼的 left 值是否在篮子 left 值-篮子 left 加篮子宽度减月饼宽度范围内,如果符合条件,则加一分。
分值展示在界面左下角。
首先在月饼类和下落类中添加分值 scoreSpace,下落类分值默认为 0,月饼类中分值修改为 1. 在下落类的下落函数中添加判断。
且月饼掉入篮子后就隐藏了,一个月饼只记一分。这里添加 display 标志位。
js
let scoreNum = ref(0) // vue3语法,可以更方便的将scoreNum绑定到界面 也可以使用document获取元素的方式为界面赋值
class FallReward {
...
scoreSpace = 0
display = false
fallDown() {
...
if(this.top > basket.top && this.top < basket.top + 80 && this.left > basket.left && this.left < basket.left + 70 && !this.display) {
scoreNum.value = scoreNum.value + this.scoreSpace
this.display = true
}
setTimeout(() => {
this.fallDown();
}, this.basePace);
}
}
class MoonCake extends FallReward {
constructor({ left, top = 0, src }) {
...
this.scoreSpace = 1
}
}
function draw() {
ctx.clearRect(0, 0, 900, 600);
getMoonList.forEach((moonObj) => {
if(!moonObj.display) { // 已经接到的话就不展示啦
ctx.drawImage(moonObj.img, moonObj.left, moonObj.top, 80, 80);
}
});
ctx.drawImage(basket.img, basket.left, basket.top, 150, 120);
requestAnimationFrame(draw);
}
scoreNum.value(vue3 访问方式)的值就是分值啦。
然后我们将分值展示在界面左下角。
html
...
<div class="scoreCard">
<img class="scoreIcon" src="../assets/test/moonCake1.png" alt="" />× {{
scoreNum }}
</div>
...
scss
...
.scoreCard {
text-align: left;
margin-left: 110px;
display: flex;
align-items: center;
margin-top: -50px;
.scoreIcon {
width: 40px;
}
}
...
游戏结束
到这一步,游戏主体就完成啦!但是游戏还没有终点,我们可以通过在界面中掉落炸弹的方式来结束游戏。
炸弹如何添加呢?在哪里判断是否接到了炸弹呢? 这作为大家的一个思考题,欢迎大家在评论中秀出你的炸弹代码。
思考题结果会在晚一点之后补充到评论区,在此之前请大家踊跃思考吧!
此外,大家对增加游戏的趣味性还有什么建议呢?也欢迎大家补充到评论区哦,有趣的游戏创意可能被翻牌到下一章节哦。
游戏效果: