学习canvas之实现喵咪跳蛋糕

“这是我参与8月更文挑战的第6天,活动详情查看: [8月更文挑战]”web

这几天学习canvas看了不少基础API,也用了很多,这里再作一个总结练习,作一个稍微有趣的:喵咪跳蛋糕小游戏,以便来再巩固总结canvas的学习,而且这个游戏也是我十分喜好的,不过由于我的canvas水平是刚入门,因此只能作一个简版的了。canvas

基础感受差很少了,进阶的话还不知道怎么搞,后面看看开源库吧。
废话很少说,先来看看效果图:数组

nices.gif

nicet.gif

PC/移动的在线试玩地址(Github Page须要代理,否则进入可能会很慢或者进不了):websocket

试玩:在线试玩markdown

由于接触canvas时间较短,作的时间也不长,也缺少素材,因此效果很通常,不过也算勉强合格,至少能有基本的游戏逻辑。socket

开始

又要逼逼赖赖说一下用的API了,这里其实没用到什么新的API,就是多学习了一个绘制文本的API,这个API的效果仍是很是棒的,这里介绍fillText了吧,估摸着strokeText也差不了多少:oop

先说一下fillText的4个参数:学习

参数 做用
text 规定在画布上输出的文本。
x 开始绘制文本的 x 坐标位置(相对于画布)。
y 开始绘制文本的 y 坐标位置(相对于画布)。
maxWidth* 可选。容许的最大文本宽度,以像素计。
ctx.font = '30px "微软雅黑"'
ctx.fillText('Hello', 30, 50)
复制代码

image.png

其实,使用fillText这类API不只仅只靠这几个参数,像经过fillStylefont等咱们也能够修改绘制的w文本,好比我如今说的这个比较有用的textAlign属性:
颇有意思,textAlignleftright等属性是以坐标点来划分的。一张图看懂这个属性:优化

image.png

更多的属性具体还能够看看文本相关的API介绍:Canvas文本APIMDN文档ui

喵咪跳蛋糕

接下来,我就说一下我使用canvas来作喵咪跳蛋糕小游戏的思路。其实,我以为吧,canvas不少API其实单独使用起来是不难的,关键是组合起来完成某一个东西,难度就比较大了,有些还须要数学知识。
废话就很少说了,开始来看看喵咪是怎么跳蛋糕的。

要素

喵咪跳蛋糕小游戏是这样的:

  1. 猫咪能够点击跳起来而后自由落体;
  2. 蛋糕会从边界一边移动到另外一边;
  3. 小猫如果自由落体踩中则叠加起来,撞死则Game Over!

因此,思路基本清楚了:
要画的的是猫咪和蛋糕,而后就是实现碰撞检测计算新位置便可!

建立方格地图

为啥喜欢在作某一些Demo时,我比较喜欢首先画一个方格地图呢,由于方便观察和计算物体绘画的点,对于刚学习的我来讲,用处仍是很是大的。

initBoard(len) {
    const num = Math.floor(this.cvs.width / len)
    for (let i = num ; i >= 0 ; i --) {
      this.ctx.beginPath()
      this.ctx.strokeStyle = '#ccc'
      this.ctx.lineTo(i * len, 0)
      this.ctx.lineTo(i * len, this.cvs.width)
      this.ctx.stroke()
      this.ctx.beginPath()
      this.ctx.lineTo(0, i * len)
      this.ctx.lineTo(this.cvs.width, i * len)
      this.ctx.stroke()
    }
}
复制代码

image.png

方格能够弄小一点,经过观察方格,咱们也能够更方便的给物体设置大小、位置、移动速度(以几个方格为标准)等属性。

建立Role也就是小喵咪

小喵咪的建立很简单,由于小喵咪除了在Y轴的方向会由于叠加蛋糕变化外,其余是不会变化的,因此咱们只须要一开始把喵咪画出来,然你后动态增长y值便可:

this.role = { x: 0, y: 0, t: 0 }
this.initRole(this.cvs.width - 10)
复制代码
initRole(y) {
    this.role.x = this.cvs.width / 2
    this.role.y = y
    this.role.t = this.cvs.width - y
}
paintRole() {
    this.ctx.beginPath()
    this.ctx.arc(this.role.x - 10, this.role.y, 10, 0, 2 * Math.PI)
    this.ctx.fillStyle = 'red'
    this.ctx.fill()
}
复制代码

image.png

这里我用这个小球表示咱们的Role喵咪,能够看到喵咪已经在底部中间的位置了,如今就差点击跳动了,这里的话实现方式应该会有不少种,我这里就随便简单实现了下:

paintRole() {
    this.ctx.beginPath()
    if (this.isClick) {
      // 判断角色是否已到落地点(落地点可能在方块上)
      if (this.role.y >= this.cvs.width - this.role.t && this.type === 'down') {
        this.speed = 2.8
        this.role.y = this.cvs.width - this.role.t
        this.isClick = false
        this.type = 'up'
        return
      }
      this.role.y -= this.speed
      if (this.speed <= 0) {
        this.type = 'down'
      } else {
        this.type = 'up'
      }
      this.speed -= .1
    }
    this.ctx.arc(this.role.x - 10, this.role.y, 10, 0, 2 * Math.PI)
    this.ctx.fillStyle = 'red'
    this.ctx.fill()
}
复制代码

niceu.gif

固然,paintRole这个方法会一直经过定时器不停的重绘,都入门一段时间了,这些代码逻辑就不贴了。
这里的主要思路就是:

点击的时候是朝上减速的方向,而后又落体加速,因此我这里就直接经过speed--来实现了;

而后再落下的时候即typedown的时候,咱们要计算小球的当前位置和落地点位置(这里使用的是属性t来记录,由于落地点可能由于蛋糕的叠加而不一样,因此单独储存),计算到达落地点后重置状态便可。

建立蛋糕并移动

建立蛋糕的方式也很简单,就是从一边到另外一边,先从简单的状况开始:蛋糕只是变化X轴的位置,那么咱们每次绘图时增长x的值便可。

paintCake() {
    if (this.cake.x >= this.cvs.width) {
      this.cake.x = -this.cake.w
    }
    this.cake.x ++
    this.ctx.beginPath()
    this.ctx.fillStyle = 'skyblue'
    this.ctx.fillRect(this.cake.x, this.cake.y, this.cake.w, this.cake.h)
}
复制代码

nicev.gif

这样便可实现方块的移动了,如今咱们就要实现检测的机制了,这里的话检测稍微麻烦一点,由于小球不只撞着要死,落体运动踩在蛋糕时是会叠加的。

碰撞检测

碰撞的检测仍是挺麻烦的,这里说一下思路就行:

  1. 当小球落体时,出于蛋糕的范围X轴范围里面的话,当Y轴距离大于等于了蛋糕的Y轴点时即踩上蛋糕,其余碰撞则为失败。
  2. 当小球踩上蛋糕后,咱们得记录当前被踩的蛋糕的位置,引入record对象数组来记录;
  3. 由于叠加了蛋糕,因此要重置小球的和蛋糕出现的Y轴点。

看一下代码的简单实现:

check() {
    if (
      this.role.x + 5 >= this.cake.x &&
      this.role.x - 5 <= this.cake.x + this.cake.w && 
      this.role.y >= this.cake.y
    ) {
      if (this.type === 'down') {
        if (this.cakes.length) {
          // 增长断定范围,方便重叠蛋糕
          const last = this.cakes[this.cakes.length - 1]
          const range = 5
          if (
            (this.cake.x > last.x - range && this.cake.x < last.x + range) ||
            (this.cake.x > last.x + last.w - range && this.cake.x < last.x + last.w + range)
          ) {
            this.scoreRatio += 1
            this.cake.x = last.x
            this.paintRatioText()
          } else {
            this.scoreRatio = 1
          }
        }
        this.moveEnd = true
        this.score += this.scoreRatio
        this.cakes.push({ x: this.cake.x, y: this.cake.y })
        this.initCake(this.cake.y - 10)
        this.initRole(this.role.y - 10)
      } else {
        this.ctx.font = '30px "微软雅黑"'
        this.ctx.textAlign = 'center'
        this.ctx.fillStyle = 'red'
        this.ctx.fillText('Game Over!', this.cvs.width / 2, this.cvs.width / 2)
        clearTimeout(this.timer)
        this.timer = null
        this.end = true
      }
    }
}
复制代码

由于踩蛋糕,若是在同一位置的话,应该是分越加越高,可是按1px来计算的话,这样难以彻底重叠,因此我设置了一个范围区间;
这里我引入了一个range值,只要当前踩中的和上一个的位置偏差不超过这个range,则算两块蛋糕重叠,并适当作连击提示:

// 绘制连击
paintRatioText() {
    this.showRatioText = true
    this.ctx.font = '15px "微软雅黑"'
    this.ctx.textAlign = 'left'
    this.ctx.fillStyle = 'red'
    this.ctx.fillText('X' + this.scoreRatio, this.role.x + 30, this.role.y - 15)
    setTimeout(() => {
      this.showRatioText = false
    }, 1e3)
}
复制代码
// 重绘记录,也就是把踩中的蛋糕再绘上去
paintReocrd() {
    this.cakes.forEach(cake => {
      this.ctx.beginPath()
      this.ctx.fillStyle = 'orange'
      this.ctx.fillRect(cake.x, cake.y, this.cake.w, this.cake.h)
    })
}
复制代码

nicew.gif

下降高度

由于蛋糕是会一个一个重叠上去的,因此咱们得在必定高度后,下降蛋糕的高度,方法有两种:

  1. 直接删除数组得元素;
  2. 把数组里面的对象Y值给改了;

我这里用的第二个方法,由于想到就作了,没考虑太多...

// 移动蛋糕,以避免叠过高了超出范围
check() {
    ...
    if(this.cakes.length && this.cakes.length % 10 === 0 && this.moveEnd) {
        this.moving = true
        this.moveEnd = false
        setTimeout(() => {
        this.moving = false
        this.cake.y += this.movingY
        this.role.t -= this.movingY
        this.movingY = 0
        }, 550) 
    }
    this.initCake(this.cake.y - 10)
    this.initRole(this.role.y - 10)
    ...
}
复制代码
initRole(y) {
    this.role.x = this.cvs.width / 2
    this.role.y = y
    this.role.t = this.cvs.width - y
}
initCake(y) {
    this.cake.x = -this.cake.w
    this.cake.y = y
}
// 移动蛋糕
moveCake() {
    const y = this.cakes.length > 10 ? 2 : 1
    this.cakes = this.cakes.map(cake => {
      return {
        ...cake,
        y: cake.y + y
      }
    })
}

// 移动角色
moveRole() {
    const y = this.cakes.length > 10 ? 2 : 1
    this.movingY += y
    this.role.y += y
}
复制代码

这里我经过moving这个状态来保存:蛋糕是否在下移状态,当下移完成时,新蛋糕才从新绘制。
而后使用movingY来保存下移的距离,使得小球和当前蛋糕的Y轴距离同步。

nicex.gif

这样的话,游戏的基本逻辑就已经大体完成了,剩下的就是一些优化部分了。

优化

例如,咱们能够把小球和蛋糕替换成功图片,而且把方格背景去掉。

像喵咪的话,咱们还能够找一组素材,这里我找了一张跳的和一张坐的,这样的话,跳起来自由落体运动就不会看起来很单一,显得人物比较丰富。

图片的话,仍是须要预先加载的,不然没有图片就开始游戏逻辑很傻瓜的。

也能够加一些蛋糕依据速度的变化而变快,出来的方向不一样等玩法,让游戏更加的丰富。

若是能够的话在连击时能够加一些特效的效果,甚至加上websocket使得可对战,固然这些都是后话了。

总结

游戏是简单完成了,效果得话很通常,多是由于我绘画的一些逻辑没处理的太好吧,因此看起来只是功能比较正常,视觉就差了火候。

不过也好了,canvas完了几天入门应该仍是入门了的,API的使用仍是很是简单的,只是要经过这些API组合起来使用要完成某个功能就比较麻烦了。

因此,我觉着吧,canvas这玩儿意,我以为首先对代码设计的要好是一方面,另外一方面以为数学要好,否则计算坐标宽度等仍是挺打脑袋的。

后续的话也不知道咋个进阶,太难的很难作出来,精力也不够,还要学Vue恰饭,再去了解了解开源库吧。

再看看效果把:

nices.gif

nicet.gif

试玩:在线试玩

好了,canvas入门达成:得到称号:新手canvas玩家!

相关文章
相关标签/搜索