canvas在小程序里写小游戏
最近接了个小需求需要写个小游戏,由简单的帧动画加上碰撞相关的处理,组成。具体页面信息如下图

具体的游戏步骤,是通过长按按钮蓄力,松开时卡通人物跳起,卡通人物跳起碰撞到上面的元宝等元素的得分,这里我们需要关注的主要在于以下几点:
- 图片的缓存加载问题
- 金币,元宝的移动问题,从左向右,或者从右向左
- 卡通人物的跳起加速度问题,蓄力人物压缩问题(模拟起跳)
- 人物和元素的碰撞问题
初始化canvas对象和ctx对象
initCanvas() {
(uni.createSelectorQuery().select('#canvas') as any).fields({ node: true, context:true, size: true }).exec((res:any)=>{
console.log(uni.getSystemInfoSync());
const dpr = (uni.getSystemInfoSync() as any).devicePixelRatio || 1
const canvas = res[0].node
// 获取canvas对象
this.canvas = canvas
this.setCanvasSize(canvas, dpr)
this.loadImgs(this.imgArr);
// 获取上下文ctx对象
this.ctx = canvas.getContext('2d')
this.ctx.scale(dpr,dpr)
})
}
// canvas默认初始化尺寸为300*150,如果通过css设置的话将会把canvas拉伸,导致绘制的时候出现图形扭曲,通过dom对象设置width和height可以达到真是的尺寸
setCanvasSize(canvas:any,dpr:any) {
// 乘上dpr 会使画出来的图片没有那么糊,更清晰
canvas.width = this.screenWidth * dpr
canvas.height = this.screenHeight * dpr
}
canvas的图片加载和缓存问题
// 缓存几种金币,元宝图片,避免canvas绘制时还需要异步读取图片
loadImgs(arr:any) {
return new Promise<void>((resolve) => {
let count = 0;
// 循环图片数组,每张图片都生成一个新的图片对象
const len = arr.length;
for (let i = 0; i < len; i++) {
if(typeof arr[i].img === 'object' ){
count++
if (count == len) {
console.log(arr)
arr.forEach((ele:any) => {
this.loadImgObj[ele.key] = ele
});
// 加载好,清空定时器,设置加载进度为100%
(this.$refs.GameLoadref as any).loadnum = 100;
// 隔200ms 去除加载页面
const timerOut = setTimeout(() => {
(this.$refs.GameLoadref as any).show = false;
this.showcount = true
this.initPageelement()
clearTimeout(timerOut)
resolve();
}, 600);
}
}else {
const image = this.canvas.createImage()
// 成功的异步回调
image.onload = () => {
count++;
arr.splice(i, 1, {
// 加载完的图片对象都缓存在这里了,canvas可以直接绘制
img: image,
width: arr[i].width,
height: arr[i].height,
x: arr[i].x,
y: arr[i].y,
key:arr[i].key,
speed:arr[i].speed,
prepareSpeed:arr[i].prepareSpeed || 0
// 这里可以直接生成并缓存离屏canvas,用于优化性能,但本次不用,只是举个例子
// offScreenCanvas: this.createOffScreenCanvas(image)
});
// 这里说明 整个图片数组arr里面的图片全都加载好了
if (count == len) {
// this.preloaded = true;
arr.forEach((ele:any) => {
this.loadImgObj[ele.key] = ele
});
// 加载好,清空定时器,设置加载进度为100%
(this.$refs.GameLoadref as any).loadnum = 100;
// 隔200ms 去除加载页面
const timerOut = setTimeout(() => {
(this.$refs.GameLoadref as any).show = false;
this.showcount = true
this.initPageelement()
clearTimeout(timerOut)
resolve();
}, 600);
}
}
image.src = arr[i].img;
}
}
});
}
金币,元宝的移动问题
核心思想就是改变x轴的坐标,类似我们以前写的红包雨动画 差不多意思
// 绘制金币元宝对象
drawCoins() {
// 遍历这个金币对象数组
this.coinArr.forEach((coin:any, index:any) => {
if(!coin) return
const result = this.checkCollision(coin,index,this.loadImgObj['uni'])
if(result) return
const newCoin = {
// 运动的关键 每次只有x不一样
x: coin.x + coin.speed,
y: coin.y ,
width:coin.width,
height:coin.height,
key:coin.key,
img: coin.img,
speed: coin.speed
};
// 绘制某个金币对象时,也同时生成一个新的金币对象,替换掉原来的它,唯一的区别就是它的x变了,下一帧绘制这个金币时,就运动了一点点距离
this.coinArr.splice(index, 1, newCoin);
this.ctx.drawImage(
coin.img,
this.calculatePos(coin.x),
this.calculateHeight(coin.y) ,
this.calculatePos(coin.width),
// coin.height/coin.width * this.calculatePos(coin.width)
this.calculateHeight(coin.height)
);
});
}
生成金币元宝
pushCoins() {
if(!this.addCoinsTimer) return
// 每次随机生成3~5个金币或者元宝等
const random = this.randomRound(3,5);
let arr:any = [];
for (let i = 0; i < random; i++) {
const randomNum = this.randomRound(0, 4)
// 创建新的金币对象
let newCoin = {
x:0 - this.calculatePos(Math.random() * 250),
y:this.randomRound(
this.calculateHeight(100),
this.calculateHeight(450)
),
width:this.imgArr[randomNum].width,
key:this.imgArr[randomNum].key,
height:this.imgArr[randomNum].height,
img: this.imgArr[randomNum].img, // 随机取一个金币图片对象,这几个图片对象在页面初始化时就已经缓存好了
speed: this.calculatePos(Math.random() * 7 + 5) // 移动速度 随机
};
// 控制页面中的爆竹的数量,我们还有减分项,就是🧨,所以需要控制多少,不能完全随机出,不然会出现满屏的爆竹
const hasBomb = this.coinArr.find((ele:any,index:any)=>{
return ele && ele.key === 'bomb'
})
if(hasBomb && newCoin.key === 'bomb') {
// 取反太烦 直接这么写,你们别学我
}else {
arr.push(newCoin as never);
}
}
// 每次都插入一批新金币对象arr到运动的金币数组this.coinArr
this.coinArr = [...this.coinArr, ...arr];
// 定时删除数组中跑到屏幕外面的数据
for (let i = 2; i >=0 ; i--) {
if(!this.coinArr[i] || (this.coinArr[i] && this.calculatePos(this.coinArr[i].x) > this.screenWidth)){
this.coinArr.splice(i,1)
}
}
// 间隔多久生成一批金币
this.addCoinsTimer = setTimeout(() => {
this.pushCoins();
}, 1000);
}
移动金币元宝,需要一个api requestAnimationFrame
,通过这个来绘制帧动画
,在h5中是直接挂载在window
上的,小程序中,是挂载在canvas
对象上的,所以这就是为什么我们初始化的时候,要获取一个canvas
对象
moveCoins() {
// 清空canvas
this.ctx.clearRect(0, 0, this.screenWidth, this.screenHeight);
// 绘制背景
this.drewBg()
// 绘制新的一帧动画
this.drawCoins();
this.drawblessBagDelay && this.drawblessBag()
// 不断执行绘制,形成动画
this.moveCoinAnimation = this.canvas.requestAnimationFrame(this.moveCoins);
// 绘制指示器的动画
this.drawIndicator()
// 画碰撞的分数
// 把opacity为0的全部清除
this.bubbleArr.forEach((ele:any, index:any) => {
if (ele.opacity < 0) {
this.bubbleArr.splice(index, 1);
}
});
// 碰撞的分数动画
this.drawPoint();
// 画uni
this.drawUni()
// 画按钮
if(this.drawBtnFlag) {
this.drawBtnUni([this.loadImgObj['btnLongpress']])
}else {
this.drawBtnUni([this.loadImgObj['btnReleasejump']])
}
}
卡通人物的跳起加速度问题,蓄力人物压缩问题(模拟起跳)
蓄力压缩,类似于我们起跳前的蹲下蓄力的动作
// 蓄力压缩
if(!this.drawUniFlag && this.longpressflag ) {
// const prepareSpeed = this.loadImgObj['uni'].prepareSpeed
this.loadImgObj['uni'].y = this.loadImgObj['uni'].y + prepareSpeed
this.loadImgObj['uni'].height = this.loadImgObj['uni'].height - prepareSpeed
this.loadImgObj['uni'].x = this.loadImgObj['uni'].x - prepareSpeed/4
this.loadImgObj['uni'].width = this.loadImgObj['uni'].width + prepareSpeed/2
// this.longpressflag
if(this.loadImgObj['uni'].y > 864 ){
this.loadImgObj['uni'].y = 864
this.loadImgObj['uni'].height = 227
this.loadImgObj['uni'].x = 244
this.loadImgObj['uni'].width = 247
}
return
}
放开的时候 先是回到正常大小,然后再起跳
// 反弹回正常大小
if(this.loadImgObj['uni'].y > 824 && !this.drawUniFlag ){
this.loadImgObj['uni'].y = this.loadImgObj['uni'].y - 8* prepareSpeed
this.loadImgObj['uni'].height = this.loadImgObj['uni'].height + 8 * prepareSpeed
this.loadImgObj['uni'].x = this.loadImgObj['uni'].x + 2* prepareSpeed
this.loadImgObj['uni'].width = this.loadImgObj['uni'].width + 4 * prepareSpeed
if(this.loadImgObj['uni'].y <= 824 || this.loadImgObj['uni'].x >= 254){
this.loadImgObj['uni'].y = 824
this.loadImgObj['uni'].height = 267
this.loadImgObj['uni'].x = 254
this.loadImgObj['uni'].width = 227
this.drawUniFlag = true
}
return
}
关于加速度的问题,起跳时速度最快,到达最大高度时,速度最小,然后做类似于自由落体的反向加速度下落
// uni加速度
const easing = 0.05
const vy = (this.loadImgObj['uni'].y - this.uniJumpY) * easing
this.loadImgObj['uni'].y = this.loadImgObj['uni'].y + this.loadImgObj['uni'].speed* vy+ this.loadImgObj['uni'].speed *3
if(this.loadImgObj['uni'].y > 824){
// 停止uni动画
this.stopMoveUniFlag = true
}else if(this.loadImgObj['uni'].y <= this.uniJumpY && this.loadImgObj['uni'].speed < 0){
// 到顶了,反向
this.loadImgObj['uni'].speed = - this.loadImgObj['uni'].speed
}
人物和元素的碰撞问题
碰撞其实是比较简单的,就是检查 人物和元素直接的坐标有没有重叠的部分
// 检查是否碰撞
checkCollision(coinItem:any,index:any,uniItem:any){
if(coinItem.key !== 'blessingbag' && uniItem.y > 450){
return false
}
if(coinItem.x > uniItem.x && coinItem.x < (uniItem.x + uniItem.width) ||((coinItem.x + coinItem.width ) > uniItem.x && (coinItem.x +coinItem.width ) < (uniItem.x + uniItem.width))){
if(uniItem.y > coinItem.y && uniItem.y < (coinItem.y+ coinItem.height) || ((uniItem.y+uniItem.height )>coinItem.y && (uniItem.y+uniItem.height ) < (coinItem.y +coinItem.height) )){
// 碰撞
const partx =coinItem.x
const party =coinItem.y
// 删掉当前 金币
this.coinArr.splice(index, 1, undefined);
// 加分的动画冒泡,加入数组
const bubble = {
x: partx + coinItem.width/2,
y: party,
key:coinItem.key,
opacity:1
}
this.bubbleArr.push(bubble)
// 积分
this.totalPoints = this.totalPoints + (this.enumkey[coinItem.key] as any).score
return true
}
}
}
参考:
转载自:https://juejin.cn/post/7173955145263218718