canvas在小程序里写小游戏

1,079 阅读4分钟

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

具体的游戏步骤,是通过长按按钮蓄力,松开时卡通人物跳起,卡通人物跳起碰撞到上面的元宝等元素的得分,这里我们需要关注的主要在于以下几点:

  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 || 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
        }
      }
    }


参考: