蕭井陌的直播筆記 - 打磚塊(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 可以平滑移動
用 leftDown
和 rightDown
兩個變數記錄 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()})
重新梳理一下我們目前的工作,我們已經可以做到:
- 按下按鍵的時候,在 game 的一個對象 keydowns(或許叫字典更好理解一些)中,新建一個對應的 key 並將其的值設置為 true,如果抬起該按鍵,則將其設置為 false。
- 我們在最外層通過 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 相比,修改了幾個地方:
- 將 speed 改為 speedX 和 speedY,因為球能夠在任意方向運動。
- 增加了 move 函數,和與之關聯的 fired 屬性,當 fired 屬性為 true 時,移動小球(注意判斷邊界),我們將 move 函數放在 game.update 中,後台持續運行。而按下 f 鍵則改變 fired 屬性值即可使得小球開始運動。
- 將 collide 函數實現,判斷小球是否和 paddle 相撞,注意在原視頻中,collide 函數放在 paddle 中,但是我認為小球能夠和 paddle 以及後來的磚塊相撞,那麼相撞函數的實現放在 Ball 中比較合適。
更新的 update 函數如下:
game.update = function() { ball.move() if(ball.collide(paddle)) { ball.speedY *= -1 }}
至此整個實現完成。myGame 的 update 函數用於更新遊戲狀態,draw 函數用於繪製圖形。
思路總結
- 將一個對象相關的屬性和方法都放到一起,並返回,這樣能夠讓各個對象只負責自己的事情即可。
- 利用一個 game 對象來進行遊戲的底層初始化和一些臟活,跟蹤按鍵並響應,保存對應的響應函數。
- object(或者說字典)是一個非常有用的數據結構,裡面可以存放函數,這樣就能夠將一類函數存放在一個地方,並方便調用。
個人思考
- 清除畫布和繪製可以放在同一個函數中,負責底層的動畫實現
- 根據按鍵調用對應的函數,放在 setInterval 中不是很合適,可以用函數包起來,然後再 setInterval 中調用。
- 需要註冊的按鍵增加時,是否可以通過更加抽象的方法,方便的對按鍵進行註冊?比如傳入一個字典?
- update 函數中放入了相撞的邏輯,後續可能加入磚塊和球相撞等,可以考慮後續繼續抽象。
推薦閱讀:
※Go 語法糖水 - 單元測試
※現在學編程,晚么?
※C語言基礎:不定參數
※從零開始手敲次世代遊戲引擎(四十三)
TAG:編程 |