標籤:

蕭井陌的直播筆記 - 打磚塊(1)

直播編程寫遊戲 - 打磚塊(1)

本文根據 @蕭井陌 在嗶哩嗶哩直播視頻整理而成,作為個人的學習筆記,蕭大如果覺得不合適,請告知刪除。直播第一集我看了三四遍了,講的深入淺出,很多東西都是我之前知道應該這麼做,但是沒有辦法自己寫出來,比如將函數存入 object 中這種操作。但是直播存在一個問題就是,雖然能夠看到蕭大在重構代碼,但是對於我這種短期記憶幾乎為0的初學者來說,可能很難還原出代碼的變動過程,文字的形式總是比暫停視頻自己思考一下來的方便,也能夠看到全部的代碼,所以整理成下文,大家可以先刷直播視頻,然後再看看這個筆記,多多交流~

代碼是我自己實現的,所以有的地方和視頻中有些出入,比如我覺得碰撞檢測放在 Ball 中更合適,所以沒有完全按照蕭大的代碼來。

基礎知識

載入圖片:

var img = new Image()img.src = paddle.pngimg.onload = function() { context.drawImage(img, x, y)}

清除畫布:

context.clearRect(0, 0, 400, 300)

綁定事件:

window.addEventListener(keydown, function(event) { // FUNCTION BODY})

重複執行函數:

setInterval(function(){ // FUNCTION BODY}, 1000 / 60)

粗糙實現

插入一個canvas

<canvas id="id-canvas" width="400" height="300"></canvas>

設置相關:

var canvas = document.querySelector(#id-canvas)var context = canvas.getContext(2d)

載入一個 paddle 圖片

var x = 150var y = 250var speed = 10var img = new Image()img.src = paddle.pngimg.onload = function() { context.drawImage(img, x, y)}

綁定事件讓 paddle 可以左右移動

window.addEventListener(keydown, function(event) { if(event.key == a){ context.clearRect(0, 0, 400, 300) x -= speed context.drawImage(img, x, y) } else if(event.key == d) { context.clearRect(0, 0, 400, 300) x += speed context.drawImage(img, x, y) }})

優化:讓 paddle 可以平滑移動

leftDownrightDown 兩個變數記錄 a 和 d 按下的狀態,並在按下的時候持續的改變橫坐標,使得擋板能夠持續移動。

var leftDown = falsevar rightDown = falsewindow.addEventListener(keydown, function(event) { if(event.key == a){ leftDown = true } else if(event.key == d) { rightDown = true }})window.addEventListener(keyup, function(event) { if(event.key == a){ leftDown = false } else if(event.key == d) { rightDown = false }})setInterval(function() { context.clearRect(0, 0, 400, 300) context.drawImage(img, x, y) if(leftDown) { x -= speed } if(rightDown) { x += speed }}, 1000 / 30)

號稱垃圾代碼,從頭寫到尾。我們需要一個統一的入口,並且不斷的抽象,因為當我們加上子彈,加上磚塊,加上道具的時候,我們的代碼就會膨脹,變得難以維護,全局變數滿天飛。

代碼重構

載入圖片需要一個單獨的函數:

var imageFromPath = function(path) { var img = new Image() img.src = path return img}

將 paddle 相關的代碼放到 paddle 自己裡面去。

var Paddle = function() { var image = imageFromPath(paddle.png) var o = { image: image, x: 150, y: 250, speed:10, } o.moveLeft = function() { o.x -= o.speed } o.moveRight = function() { o.x += o.speed } return o}var paddle = Paddle()

將遊戲相關的準備工作和狀態也放到一個 Game 裡面去:

var myGame = function() { var canvas = document.querySelector(#id-canvas) var context = canvas.getContext(2d) o = { canvas: canvas, context: context, } return o}

這個時候還剩下兩個 window.addEventListener 函數和一個 setInterval 函數以及兩個全局變數 leftDown 和 rightDown 需要重構。

此時我們只有兩個按鍵需要按,但是有可能以後會需要更多的按鍵(比如發射子彈用 f 鍵,特殊道具使用按 c 鍵等),如果每一個都這樣寫的話,就非常複雜。另外我們還需要注意到,我們還有兩個全局變數沒有包起來,也即是 leftDown 和 rightDown 兩個變數,如果只是單純的將其放到 myGame 中的話,以後加入其它按鍵的時候,需要手動給 game 新建相應的屬性值,並不是十分方便。

所以我們的實現思路是,在 myGame 中加入兩個 object,一個記錄相應的按鍵是否被按下,類似於全局變數 leftDown 和 rightDown 的作用,另一個 object 存入對應的函數,也就是當對應的按鍵被按下時,需要執行什麼函數。

var myGame = function() { var canvas = document.querySelector(#id-canvas) var context = canvas.getContext(2d) o = { canvas: canvas, context: context, actions: {}, keydowns:{}, } window.addEventListener(keydown, function(event) { o.keydowns[event.key] = true }) window.addEventListener(keyup, function(event) { o.keydowns[event.key] = false }) return o}

但是這時候出現了一個問題,在 setInterval 中,我們執行的函數有 paddle.moveLeft 和 paddle.moveRight,這兩個函數是 paddle 自己的,我們在定義 myGame 的時候沒有辦法訪問到。那麼我們只能在外層,動態的註冊這兩個函數,以後也許會註冊更多函數,所以最好做一個函數來幫助我們做到這件事情。

o.registerAction = function(key, callback) { actions[key] = callback}

在最外面,我們可以這樣給 game 加上對應的函數

game.registerAction(a, function() { paddle.moveLeft()})game.registerAction(d, function() { paddle.moveRight()})

重新梳理一下我們目前的工作,我們已經可以做到:

  1. 按下按鍵的時候,在 game 的一個對象 keydowns(或許叫字典更好理解一些)中,新建一個對應的 key 並將其的值設置為 true,如果抬起該按鍵,則將其設置為 false。
  2. 我們在最外層通過 registerAction 函數註冊了兩個函數,將其存入了 game 中的另一個對象 actions 中。

這時候我們唯一的問題是,當按下某個鍵的時候,keydowns 的相應值改變了,但是卻並沒有觸發相應的函數執行,那麼我們可以在 myGame 中運行 setInterval 來實現。

setInterval(function() { // 主要思路是,獲得 actions 中的所有 key // 然後查詢 keydowns 中對應的 key 是否為 true // 如果為 true 則執行 actions 中對應的函數 var actions = Object.keys(o.actions) for (var i = 0; i < actions.length; i++) { var key = actions[i] if(o.keydowns[key]) { o.actions[key]() } } // 我們順便把繪製 paddle 和清除畫布的功能一起放進來了。 game.context.clearRect(0, 0, 400, 300) game.context.drawImage(paddle.image, paddle.x, paddle.y)}, 1000 / 30)

注意到上面的代碼,我們在 myGame 函數內部引入的 paddle.image 和 paddle.x 以及 paddle.y,這樣並不好,我們最好能夠在 myGame 中定義一個 draw 函數,然後再在外面動態的將 draw 函數實現。

我們在最外層實現:

game.draw = function() { game.context.clearRect(0, 0, game.canvas.width, game.canvas.height) game.context.drawImage(paddle.image, paddle.x, paddle.y)}

這時候將 setInterval 中對應的代碼改為 o.draw() 即可。

最後一個小問題,我們覺得 game.context.drawImage 這樣來繪圖比較麻煩,我們希望只需要一個 paddle 傳入,我們自動獲取它的長和寬即可,那麼我們在 myGame 中定義一個函數:

o.drawImage = function(Image) { o.context.drawImage(Image.image, Image.x, Image.y)}

這樣我們就完全完成了代碼的重構,啊不,還剩下一個小問題,剩下的代碼放到 __main 函數中:

var __main = function() { var game = myGame() var paddle = Paddle() game.registerAction(a, function() { paddle.moveLeft() }) game.registerAction(d, function() { paddle.moveRight() }) game.draw = function() { game.context.clearRect(0, 0, game.canvas.width, game.canvas.height) game.drawImage(paddle) }}

至此全部搞定,我們基於這個重構過的代碼,可以方便的增加一個子彈(球),直接將原來的 Paddle 函數改為:

var Ball = function() { var image = imageFromPath(ball.png) var o = { image: image, x: 100, y: 230, speedX:10, speedY:10, fired: false, } o.collide = function(Image) { var i = Image if (o.x < i.x + i.image.width && o.x + o.image.width > i.x && o.y < i.y + i.image.height && o.image.height + o.y > i.y) { return true } return false } o.move = function() { if(o.fired == true){ if(o.x < 0 || o.x > 400) { o.speedX *= -1 } if(o.y < 0 || o.y > 300) { o.speedY *= -1 } o.x += o.speedX o.y += o.speedY } } return o}

和 paddle 相比,修改了幾個地方:

  1. 將 speed 改為 speedX 和 speedY,因為球能夠在任意方向運動。
  2. 增加了 move 函數,和與之關聯的 fired 屬性,當 fired 屬性為 true 時,移動小球(注意判斷邊界),我們將 move 函數放在 game.update 中,後台持續運行。而按下 f 鍵則改變 fired 屬性值即可使得小球開始運動。
  3. 將 collide 函數實現,判斷小球是否和 paddle 相撞,注意在原視頻中,collide 函數放在 paddle 中,但是我認為小球能夠和 paddle 以及後來的磚塊相撞,那麼相撞函數的實現放在 Ball 中比較合適。

更新的 update 函數如下:

game.update = function() { ball.move() if(ball.collide(paddle)) { ball.speedY *= -1 }}

至此整個實現完成。myGame 的 update 函數用於更新遊戲狀態,draw 函數用於繪製圖形。

思路總結

  1. 將一個對象相關的屬性和方法都放到一起,並返回,這樣能夠讓各個對象只負責自己的事情即可。
  2. 利用一個 game 對象來進行遊戲的底層初始化和一些臟活,跟蹤按鍵並響應,保存對應的響應函數。
  3. object(或者說字典)是一個非常有用的數據結構,裡面可以存放函數,這樣就能夠將一類函數存放在一個地方,並方便調用。

個人思考

  1. 清除畫布和繪製可以放在同一個函數中,負責底層的動畫實現
  2. 根據按鍵調用對應的函數,放在 setInterval 中不是很合適,可以用函數包起來,然後再 setInterval 中調用。
  3. 需要註冊的按鍵增加時,是否可以通過更加抽象的方法,方便的對按鍵進行註冊?比如傳入一個字典?
  4. update 函數中放入了相撞的邏輯,後續可能加入磚塊和球相撞等,可以考慮後續繼續抽象。

推薦閱讀:

Go 語法糖水 - 單元測試
現在學編程,晚么?
C語言基礎:不定參數
從零開始手敲次世代遊戲引擎(四十三)

TAG:編程 |