头号玩家-保卫大司马

2,554 阅读3分钟

我正在参加掘金社区游戏创意投稿大赛团队赛,详情请看:游戏创意投稿大赛

项目地址: bilibili.codeape.site/#/protectTh…

基本介绍

开发技术:canvas,vue2,js

前几天看到逛掘金看到了这个活动,正好我也在弄这个项目,就打算参加一下了,这次也是我在掘金发表的第一篇文章。

这是一个网页版的塔防小游戏,名称叫做保卫大司马(类似于保卫萝卜),大司马作为终点,有生命值,选用植物大战僵尸的素材来充当敌人和塔防,部分塔防使用一些主播火柴人图片(自己画的,有点丑)。

在本项目的开发过程中也涉及到不少的知识点,同时也挺锻炼思维的,由于最近忙着毕业答辩和一些工作,时间比较赶,很多功能还未完善。

开发流程

1. canvas开发小游戏基本流程

首先需要把canvas的2d上下文保存下来(接下来就对其进行绘画就好了),然后是初始化数据,接下来是加载图片

this.canvas = this.$refs.canvasRef;
this.ctx = this.canvas.getContext("2d");
this.initAllGrid()
this.initMovePath()
this.onKeyDown()
await this.allGifToStaticImg()
// 加载图片
this.imgOnloadObj = await this.loadImage(this.imgObj);
this.towerOnloadImg = await this.loadImage(this.towerList, 'img');
this.towerBulletOnloadImg = await this.loadImage(this.towerList, 'bulletImg');
this.startAnimation()

2. 初始化数据

将所需要的数据进行初始化,这里需要初始化页面中所有的格子数据(暂时定为每格大小50px*50px),初始化行动轨迹(就是页面中地下可供移动的格子)

/** 初始化所有格子 */
initAllGrid() {
  const { x_num, y_num } = this.gridInfo
  const arr = []
  for(let i = 0; i < x_num; i++) {
    arr.push([])
    for(let j = 0; j < y_num; j++) {
      arr[i][j] = 0
    }
  }
  this.gridInfo.arr = arr
},
/** 初始化行动轨迹 */
initMovePath() {
  const size = this.gridInfo.size
  const movePathItem = {x: 0, y: 50, x_y: 3}
  const movePath = []
  // 控制x y轴的方向 1:左 2:下 3:右 4:上
  let x_y = 3
  for(let i = 0; i < this.floorTile.num; i++) {
    switch (i) {
      // case: 等于多少就 向一个方向移动(控制x_Y等于几)
    }
    // 将该份数据保存下来
    movePath.push(JSON.parse(JSON.stringify(movePathItem)))
    // 给格子数组中赋值,标记该位置情况为:有地板了
    this.gridInfo.arr[movePathItem.y/size][movePathItem.x/size] = 1
  }
  this.movePath = movePath
},

3. gif图转png

由于canvas是一帧一帧将内容画出来的,所以gif图对于canvas不管用,需要转成多张静态图片。这里就需要用到Promise.all了等待所有图片都转成静态图片再开始下面的操作。而其中单张gif转静态图片需要用到libgif.js,其中的SuperGif方法可以用来操作gif图片。

/** 等待所有的gif图生成静态图片 */
async allGifToStaticImg() {
  return Promise.all(this.enemySource.map((item, index) => 
      this.gifToStaticImg(index))).then(res => {
        this.loadingDone = true
      }
  )
},
/** 单张gif转静态图片 */
gifToStaticImg(index) {
    // 主要流程如下
    const gifImg = document.createElement('img');
    gifImg.src = imgSource
    // gifImg.style.transform = 'rotate(90deg)';
    // 创建gif实例
    const rub = new SuperGif({ gif: gifImg } );
    rub.load(() => {
      const imgList = [];
      for (let i = 1; i <= rub.get_length(); i++) {
        // 遍历gif实例的每一帧
        rub.move_to(i);
        const imgUrl = rub.get_canvas()
        imgList.push(imgUrl)
      }
      this.enemySource[index].imgList = imgList
      resolve()
    });
}

4. 加载图片

canvas绘画的图像需要创建一个Image对象,等待图片加载完成,再将数据保存下来。

/** 加载图片 imgUrl: 图片数组, objKey: 在数组中的key值  */
loadImage(imgUrl, objKey) {
  return new Promise((resolve, reject) => {
    const imgObj = {}; // 保存图片资源
    let tempImg, imgLength = 0, loaded = 0;
    for (let key in imgUrl) {
      imgLength++; // 初始化要加载图片的总数
      tempImg = new Image();
      tempImg.src = !objKey ? imgUrl[key] : imgUrl[key][objKey];
      imgObj[key] = tempImg;
      tempImg.onload = function () {
        loaded++; // 统计已经加载完毕的图像
        // 所有的图片都加载完毕
        if (loaded >= imgLength) {
          resolve(imgObj)
        }
      };
    }
  })
},

5. 开始动画绘画

这里使用requestAnimationFrame()来进行绘画,它的作用类似于定时器,但是它能做到比定时器更加流程的动画效果,能达到每秒60帧的刷新率,对于需要高刷新的小游戏很适合。

/** 开启动画绘画 */
startAnimation() {
  const that = this;
  (function go() {
    that.startDraw();
    if (!that.isPause) {
      // 时间间隔为 1000/60 每秒 60 帧
      requestAnimationFrame(go);
    }
  })();
},

6. 开始绘画和操作数据

当上面准备工作做好后,就开始游戏开发了,主要开发模式为:操作数据,然后绘画图像。由于页面的绘画一直是持续的。对于场上敌人的出现,只需处理数据就好了,对于建造塔防,发射子弹等就需要额外绘画了。

7. 判断敌人是否进入塔防攻击范围

主要是通过以下函数来进行判断,判断敌人图片的四个角是否进入了攻击范围(这里就不复杂化:数学中一个圆和一个矩形相遇),四个角都计算到圆心的距离是否小于半径即可。

/** 判断值是否在圆内 */
checkValInCircle(enemy, tower) {
  const {x, y, w, h} = enemy
  const angleList = [
    this.calculateDistance(tower, x, y),
    this.calculateDistance(tower, x + w, y),
    this.calculateDistance(tower, x + w, y + h),
    this.calculateDistance(tower, x , y + h),
  ]
  if(angleList.some(item => item <= tower.r)) {
    return true
  }
  return false
},
/** 计算点到圆心的距离之间的距离 */
calculateDistance(tower, x, y) {
  const {x: _x, y: _y} = tower
  const size_2 = this.gridInfo.size / 2
  return this.powAndSqrt(_x + size_2 - x, _y + size_2 - y)
},
/** 两值平方相加并开方 求斜边 */
powAndSqrt(val1, val2) {
  return Math.sqrt(Math.pow(val1, 2) + Math.pow(val2, 2))
},

以上就是本塔防游戏开发的主要思路以及主要功能代码了。