最近接了个小需求需要写个小游戏,由简单的帧动画加上碰撞相关的处理,组成。具体页面信息如下图
具体的游戏步骤,是通过长按按钮蓄力,松开时卡通人物跳起,卡通人物跳起碰撞到上面的元宝等元素的得分,这里我们需要关注的主要在于以下几点:
1.图片的缓存加载问题
2.金币,元宝的移动问题,从左向右,或者从右向左
3.卡通人物的跳起加速度问题,蓄力人物压缩问题(模拟起跳)
4.人物和元素的碰撞问题
初始化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 || 1const canvas = res[0].node// 获取canvas对象 this.canvas = canvasthis.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 * dprcanvas.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 = truethis.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 = truethis.initPageelement() clearTimeout(timerOut)resolve();}, 600);}}image.src = arr[i].img;}}});}
金币,元宝的移动问题
核心思想就是改变x轴的坐标,类似我们以前写的红包雨动画 差不多意思
//绘制金币元宝对象drawCoins() {// 遍历这个金币对象数组this.coinArr.forEach((coin:any, index:any) => {if(!coin) returnconst result =this.checkCollision(coin,index,this.loadImgObj['uni']) if(result) returnconst 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)=>{returnele &&ele.key === 'bomb'})if(hasBomb && newCoin.key === 'bomb') {// 取反太烦 直接这么写,你们别学我}else {arr.push(newCoin as never);}}// 每次都插入一批新金币对象arr到运动的金币数组this.coinArrthis.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() {// 清空canvasthis.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'].prepareSpeedthis.loadImgObj['uni'].y = this.loadImgObj['uni'].y + prepareSpeedthis.loadImgObj['uni'].height = this.loadImgObj['uni'].height - prepareSpeedthis.loadImgObj['uni'].x = this.loadImgObj['uni'].x - prepareSpeed/4this.loadImgObj['uni'].width = this.loadImgObj['uni'].width + prepareSpeed/2// this.longpressflagif(this.loadImgObj['uni'].y > 864){this.loadImgObj['uni'].y = 864this.loadImgObj['uni'].height= 227this.loadImgObj['uni'].x = 244this.loadImgObj['uni'].width= 247}return}
放开的时候 先是回到正常大小,然后再起跳
// 反弹回正常大小if(this.loadImgObj['uni'].y > 824 && !this.drawUniFlag){ this.loadImgObj['uni'].y = this.loadImgObj['uni'].y - 8* prepareSpeedthis.loadImgObj['uni'].height = this.loadImgObj['uni'].height + 8 * prepareSpeedthis.loadImgObj['uni'].x = this.loadImgObj['uni'].x + 2* prepareSpeedthis.loadImgObj['uni'].width = this.loadImgObj['uni'].width + 4 * prepareSpeedif(this.loadImgObj['uni'].y <= 824 ||this.loadImgObj['uni'].x >= 254){this.loadImgObj['uni'].y = 824this.loadImgObj['uni'].height= 267this.loadImgObj['uni'].x = 254this.loadImgObj['uni'].width= 227this.drawUniFlag = true}return}
关于加速度的问题,起跳时速度最快,到达最大高度时,速度最小,然后做类似于自由落体的反向加速度下落
// uni加速度const easing = 0.05const 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 *3if(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){returnfalse}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.xconst 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).scorereturn true}}}
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取