1024专题:基于PixiJs实现掘金掘了个绝~

1,160 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情

题记

之前写了篇掘金商城版的羊了个羊薅到了不少流量,因为最近在学习PixiJs,所以就有了拿PixiJs复刻的打算,毕竟是曾经的流量密码,这既能练习了新技术,又优化了实现,一箭双雕的事情何乐而不为呢?

异同

因为学习PixiJs还不足一周,所有见解都是我一家之言,如果有什么理解偏颇的地方,还请见谅,按我的理解来看,通过原生Js实现游戏和通过PixiJs实现游戏,主要有这几点异同

  • 原生Js实现主要靠操作dom,PixiJs实现靠的是操作精灵
  • 原生Js特效靠的是animationtransion等,PixiJs实现靠的是AnimatedSprite
  • 相同点是都是通过事件来驱动游戏执行的

建议补充的基础知识

# 通过飞机大战游戏学习PixiJs基操【上】

# 通过飞机大战游戏学习PixiJs基操【下】

核心实现

  • 资源加载 略 上文已包含相关内容,故 略
  • 背景及舞台,依然是容器的思路,因上文基础知识内包含大量的讲解,这里不做赘述,只需要注意,我们注册的几个纹理即可,bg是整个场景的背景纹理、boom是消除特效纹理、box是下方承载元素的容器纹理、resources内包含了所有的图片
 const bg = 'https://pic.qy566.com/pixijs/images/yangbg.png'
  const boom = 'https://pic.qy566.com/pixijs/images/boom.json'
  const box = 'https://pic.qy566.com/pixijs/images/box.png'
  const resources = [
    'https://img01.yzcdn.cn/upload_files/2022/07/20/Fk1rj4gSaia1i8yD-FSO8CWLWlSu.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/03/16/Fo3SKNvQG5cIj3IspG_SOs_T216M.png!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/07/19/FsT754_weL6KZgUC2TuRWxr5zLnG.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/05/30/FguIz4XVMqAS3ft_7thNqlJrT0_F.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/05/09/FvG5_KInsbyhVq9W6xbygpo9jb9i.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/05/30/Fpht4CzextqfHA048FGU8m_t4NP5.jpeg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/03/15/FlTBO9tItc9TXslq6wYd87udb-6t.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/03/15/Fi77MVQB0xREZRQdzaKHyHNuG-yg.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/03/16/FtBQGLwuPQJBuIHs0LjxbaSJVloV.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/03/16/FrZxLRso63OS_oKTD6xXk6Ao4Z2C.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/05/09/FvpbuQ35dFtL_oOcjJCfgPuPseIY.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/05/16/FlvZNZzU96K_d0DzxgFv3mh5IDA3.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/03/15/Fi-VLtnXgalfGpC0w3Try3Tks5qv.jpg!280x280.jpg',
    'https://img01.yzcdn.cn/upload_files/2022/03/15/Fn1QBOetrZhCcKMoVFU-9lJN1gnP.jpg!280x280.jpg',
  ]
  • 随机乱序算法 目的是为了打乱形成的纹理数组的顺序
function randomArr(array) {
  const arr = [].concat(array)
  for (let i = arr.length - 1; i >= 0; i--) {
    const inx = Math.floor(Math.random() * i + 1)
    const next = arr[inx]
    arr[inx] = arr[i]
    arr[i] = next
  }
  return arr
}
  • 创建纹理数组并乱序
for (let i = 0; i < 12; i++) {
    resources.map((v, m) => {
      source.push({ texture: textures[m].texture, inx: m })
    })
}
source = randomArr(source)
    • 创建中间待消除的区域 我们的区域形状分为5层,从下往上,元素越来越少,这里我们通过for循环进行实现
    • for循环由3层构成,为了形成金字塔型,我们需要做的就是最外层循环的key作为最内层循环的结束条件
    • 位置由基础坐标+根据key值增加的变量构成,k是偏移量 i是行数j是列数
    • 维护了递增的zIndex 帮助我们进行碰撞判断
    • 以圆角矩形的mask的形式实现图片的圆角剪切
    • 将创建好的精灵维护在itemArr这个数组内,每创建一个,就从资源source内移除一个
let zIndex = 1
// 外层的key就是内层的结束条件,以形成金字塔型
for (let k = 0; k < 5; k++) {
for (let i = 0; i < 5; i++) {
  for (let j = k; j > 0; j--) {
    let sourceItem = source.splice(0, 1)[0]
    let item = new Sprite(sourceItem.texture);
    item.name = sourceItem.inx
    item.width = 100;
    item.height = item.width;
    let x = Math.ceil((screenWidth / 2 - 250) + j * item.width) + 10 * k;
    let y = Math.ceil((i + 1) * item.width) + 100;
    item.x = x
    item.y = y
    item.zIndex = zIndex
    // 圆角矩形
    let radius = new PIXI.Graphics()
    radius.beginFill(0xFF0000) // 填充
    radius.drawRoundedRect(x, y, item.width, item.width, 16) //x,y,w,h,圆角度数
    radius.endFill()
    radius.interactive = true
    item.interactive = true
    item.cursor = 'pointer';
    radius.cursor = 'pointer';
    radius.isCover = false
    radius.inx = sourceItem.inx
    item.on('mousedown', itemClick)
    item.mask = radius
    scene.addChild(item);
    itemArr.push(item)
    updateBlur()
  }
}
zIndex++
}
  • 将剩下的资源均分,放置在屏幕上方左右分开,这里x位置的算法是当小于len,直接进行乘法即可,每个元素间隔为5,如果大于len 则宽度等于 小于len元素所占宽度加上屏幕宽度减去总宽度的余量 然后再加上 两个需要展示的元素的宽度,再加上 每个元素的间隔
let len = Math.ceil(source.length / 2)
  sourceArr = []
  source.forEach((v, i) => {
    let sourceItem = v
    let item = new Sprite(sourceItem.texture);
    item.name = sourceItem.inx
    item.width = 100;
    item.height = item.width;
    if (i > len) {
      item.x = len * 5 + (screenWidth - len * 10) + 190 + (len - i) * 5
    } else {
      item.x = 10 + i * 5
    }
    item.y = 0
    item.zIndex = i + 1
    // 圆角矩形
    let radius = new PIXI.Graphics()
    radius.beginFill(0xFF0000) // 填充
    radius.drawRoundedRect(item.x, item.y, item.width, item.width, 16) //x,y,w,h,圆角度数
    radius.endFill()
    radius.interactive = true
    item.interactive = true
    item.cursor = 'pointer';
    radius.cursor = 'pointer';
    item.isCover = true
    updateCover()
    radius.inx = sourceItem.inx
    item.mask = radius
    scene.addChild(item);
    itemArr.push(item)
    sourceArr.push(item)
    item.on('mousedown', sourceClick)
  })
  • 处理覆盖的函数 updateBlur 这里的实现逻辑是,通过双层遍历,找到是否存在这样一个元素,元素的层级高于当前元素,并且与当前元素发生了碰撞,发生碰撞就意味着覆盖,isCover标识是否被覆盖,这在我们处理事件的部分 非常有用,我们通过PixiJs提供的内置滤镜Blur来实现被覆盖后的效果
 function updateBlur() {
  for (let i = 0; i < itemArr.length; i++) {
    if (itemArr.find(v => v.zIndex > itemArr[i].zIndex && isHit(v, itemArr[i]))) {
      itemArr[i].filters = [blurFilter];
      itemArr[i].isCover = true
    } else {
      itemArr[i].filters = []
      itemArr[i].isCover = false
    }
  }
}
  • 元素点击事件的处理 这里我们采用了将当前点击坐标与整个元素数组遍历进行碰撞的方式来实现,首先我们需要判断的是,进行碰撞的元素首先应该是可点击的,然后,如果发生了碰撞,要将元素移动到下方的容器内【从位置上来说】,这里需要注意,因为我们采用的是mask的形式,所以需要针对mask做处理,保证我们的元素可见性。这里我们借助了补间动画函数tween,这里要注意的是,在动画完成后,我们需要将mask补充回来,并且进行 我们游戏逻辑的判断,这样做的原因是因为,动画是异步的,要保证动画效果的完成,就需要在动画完成函数内进行判断
 function itemClick(event) {
      itemArr.map((v, inx) => {
        if (!v.isCover) {
          if (isHit(event.data.global, v)) {
            v.isCover = true
            v.zIndex += 1
            if (temp[v.name]) {
              temp[v.name].push(inx)
            } else {
              temp[v.name] = []
              temp[v.name].push(inx)
            }
            // 位移设定
            let mask = v.mask
            v.mask = null
            mask.destroy()
            tempArr.push(v)
            let tween = new TWEEN.Tween(v)
              .to(
                {
                  x: (tempArr.length - 1) * 105 + boxContainer.x + 10,
                  y: boxContainer.y + 10,
                },
                500 // tween持续时间
              )
              .easing(TWEEN.Easing.Linear.None)
              .onComplete(function () {
                mask = new PIXI.Graphics()
                mask.beginFill(0xFF0000) // 填充
                mask.drawRoundedRect(v.x, v.y, v.width, v.width, 16) //x,y,w,h,圆角度数
                mask.endFill()
                v.mask = mask
                judge()
              });
            tween.start();
          }
        }
      })
    }
  • 游戏实现的逻辑判断函数 judge

这里,首先我们判断缓存在下面的元素是否大于7,如果大于7,那么就触发游戏结束的逻辑,接下来,通过遍历的方式,判断是否存在三个相同的元素,如果存在,就需要创建动画精灵,制作消除效果了。

 function judge() {
      let num = 0
      for (let i in temp) {
        num += temp[i].length
      }
      if (num > 7) {
        itemArr.map(v => {
          v.off('mousedown')
        })
        $('.result').fadeIn()
        $('.btn').show()
        return false
      }
      for (let i in temp) {
        if (temp[i].length === 3) {
          temp[i].map(m => {
            let k = itemArr[m]
            // 爆炸效果
            let fireArea = [
            ];
            for (let i = 0; i <= 23; i++) {
              fireArea.push(textures.boom.textures['boom' + i + '.png']);
            }
            let boom = new PIXI.AnimatedSprite(fireArea);
            boom.width = k.width * 5;
            boom.height = k.height * 5;
            boom.x = k.x - boom.width * 0.5;
            boom.y = k.y - boom.height * 0.5;
            boom.name = 'boom';
            boom.loop = false;
            scene.addChild(boom)
            boom.play();
          })
          temp[i].map(m => {
            itemArr[m].visible = false
          })
          temp[i].map(m => {
            itemArr.map(mm => {
              if (mm == itemArr[m]) {
                mm.isDelete = true
              }
            })
            tempArr.map(mm => {
              if (mm == itemArr[m]) {
                mm.isDelete = true
              }
            })
          })
          tempArr = tempArr.filter(v => !v.isDelete)
          itemArr = itemArr.filter(v => !v.isDelete)
          console.log(tempArr)
          tempArr.map((v,inx) => {
            v.position.x = inx * 105 + boxContainer.x + 10
            v.position.y = boxContainer.y + 10
            // 位移设定
            let mask = v.mask
            v.mask = null
            mask.destroy()
            mask = new PIXI.Graphics()
            mask.beginFill(0xFF0000) // 填充
            mask.drawRoundedRect(v.x, v.y, v.width, v.width, 16) //x,y,w,h,圆角度数
            mask.endFill()
            v.mask = mask
          })
          temp[i] = []
        }
      }
      updateBlur()
      
    }
  • 让动画有效果

补间动画依赖帧更新方法app.ticker.add,我们需要注册方法,来实现动画效果的更新

// app 是我们创建好的应用
app.ticker.add(function () {
    return gameLoop();
});
 function gameLoop() {
  // 更新位置
  TWEEN.update();
}

码上掘金

相比较原来的实现,有赖于PixiJs比较丰富的周边生态,我们可以很方便的进行碰撞检测,覆盖的逻辑实现起来更为容易,而且因为是canvas的实现,性能相比较创建dom会好一些, 而且还可以实现很多css没办法实现 的粒子特效,我们只需要关注,元素从哪来,到哪去即可,关注点更为集中。