學習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玩家!

相關文章
相關標籤/搜索