用 JavaScript 編寫推箱子益智遊戲
所以前幾天,我用 JavaScript 實現了一個推箱子益智遊戲。
這是源代碼,這是演示。
遊戲由一堵牆、一個可玩角色、積木和地面上的存儲位置組成。遊戲的目的是將所有塊推入所有存儲位置。這可能具有挑戰性,因為很容易最終處於無法再移動塊的狀態,現在您必須重新開始遊戲。
這是我做的:
原版遊戲畫面稍好:
在我的版本中,大藍點是角色,粉紅色點是存儲位置,橙色塊是板條箱。
我在幾個小時的時間裡匆匆寫下了它。製作小遊戲與我通常在工作中所做的有很大不同,所以我發現這是一個有趣且可以實現的挑戰。幸運的是,在之前的一些項目(Snek 和 Chip8)中,我對繪製坐標的概念有了一些經驗。
地圖和實體
我做的第一件事是構建地圖,它是一個二維數組,其中每一行對應一個 y 坐標,每列對應一個 x 坐標。
const map = [
['y0 x0', 'y0 x1', 'y0 x2', 'y0 x3'],
['y1 x0', 'y1 x1', 'y1 x2', 'y1 x3'],
// ...etc
]
所以訪問 map[0][0]
將是 y0 x0
和 map[1][3]
將是 y1 x3
.
從那裡,很容易根據現有推箱子關卡製作地圖,其中每個坐標都是遊戲中的一個實體 - 地形、玩家等。
實體const EMPTY = 'empty'
const WALL = 'wall'
const BLOCK = 'block'
const SUCCESS_BLOCK = 'success_block'
const VOID = 'void'
const PLAYER = 'player'
地圖const map = [
[EMPTY, EMPTY, WALL, WALL, WALL, WALL, WALL, EMPTY],
[WALL, WALL, WALL, EMPTY, EMPTY, EMPTY, WALL, EMPTY],
[WALL, VOID, PLAYER, BLOCK, EMPTY, EMPTY, WALL, EMPTY],
// ...etc
有了這些數據,我可以將每個實體映射到一種顏色,並將其渲染到 HTML5 畫布上的屏幕上。所以現在我有一張看起來不錯的地圖,但它還沒有做任何事情。
遊戲邏輯
沒有太多需要擔心的動作。玩家可以正交移動 - 上、下、左和右 - 需要考慮以下幾點:
PLAYER
和BLOCK
無法通過WALL
PLAYER
和BLOCK
可以通過EMPTY
空格或VOID
空間(存儲位置)- 玩家可以推送一個
BLOCK
- 一個
BLOCK
變成SUCCESS_BLOCK
當它位於VOID
之上時 .
這就是字面意思。我還在原版遊戲中編寫了另外一件事,但它對我來說很有意義:
- 一個
BLOCK
可以推送所有其他BLOCK
件
當玩家推動一個與其他方塊相鄰的方塊時,所有方塊都會移動,直到撞到牆壁為止。
為了做到這一點,我只需要知道與玩家相鄰的實體,以及如果玩家正在推動一個塊,則與塊相鄰的實體。如果玩家在推多個塊,我將不得不遞歸計算有多少。
移動
因此,每當發生變化時,我們需要做的第一件事就是找到玩家的當前坐標,以及它們的上方、下方、左側和右側的實體類型。
function findPlayerCoords() {
const y = map.findIndex(row => row.includes(PLAYER))
const x = map[y].indexOf(PLAYER)
return {
x,
y,
above: map[y - 1][x],
below: map[y + 1][x],
sideLeft: map[y][x - 1],
sideRight: map[y][x + 1],
}
}
現在你有了玩家和相鄰的坐標,每個動作都將是一個移動動作。如果玩家試圖穿過一個可穿越的單元格(空的或無效的),只需移動玩家。如果玩家試圖推塊,移動玩家並阻止。如果相鄰的單位是牆,什麼也不做。
function move(playerCoords, direction) {
if (isTraversible(adjacentCell[direction])) {
movePlayer(playerCoords, direction)
}
if (isBlock(adjacentCell[direction])) {
movePlayerAndBlocks(playerCoords, direction)
}
}
使用初始遊戲狀態,您可以確定應該存在什麼。只要我將方向傳遞給函數,我就可以設置新坐標 - 添加或刪除 y
將上下,添加或刪除一個x
將向左或向右。
function movePlayer(playerCoords, direction) {
// Replace previous spot with initial board state (void or empty)
map[playerCoords.y][playerCoords.x] = isVoid(levelOneMap[playerCoords.y][playerCoords.x])
? VOID
: EMPTY
// Move player
map[getY(playerCoords.y, direction, 1)][getX(playerCoords.x, direction, 1)] = PLAYER
}
如果玩家正在移動一個方塊,我寫了一個小遞歸函數來檢查一行中有多少個方塊,一旦有了這個計數,它就會檢查相鄰的實體是什麼,如果可能的話移動方塊,然後移動玩家如果方塊移動了。
function countBlocks(blockCount, y, x, direction, board) {
if (isBlock(board[y][x])) {
blockCount++
return countBlocks(blockCount, getY(y, direction), getX(x, direction), direction, board)
} else {
return blockCount
}
}
const blocksInARow = countBlocks(1, newBlockY, newBlockX, direction, map)
然後,如果該塊可以移動,它會移動它或將其移動並轉換為成功塊,如果它超過存儲位置,然後移動播放器。
map[newBoxY][newBoxX] = isVoid(levelOneMap[newBoxY][newBoxX]) ? SUCCESS_BLOCK : BLOCK
movePlayer(playerCoords, direction)
渲染
很容易在 2D 數組中跟踪整個遊戲,並在每次移動時將更新遊戲渲染到屏幕上。遊戲的滴答聲非常簡單 - 任何時候上、下、左、右(或 w、a、s、d 對於激烈的遊戲玩家)發生 keydown 事件時,move()
函數將被調用,該函數使用玩家索引和相鄰單元格類型來確定遊戲的新更新狀態應該是什麼。更改後,render()
函數被調用,它只是用更新的狀態繪製整個板。
const sokoban = new Sokoban()
sokoban.render()
// re-render
document.addEventListener('keydown', event => {
const playerCoords = sokoban.findPlayerCoords()
switch (event.key) {
case keys.up:
case keys.w:
sokoban.move(playerCoords, directions.up)
break
case keys.down:
case keys.s:
sokoban.move(playerCoords, directions.down)
break
case keys.left:
case keys.a:
sokoban.move(playerCoords, directions.left)
break
case keys.right:
case keys.d:
sokoban.move(playerCoords, directions.right)
break
default:
}
sokoban.render()
})
渲染函數只是映射每個坐標並創建一個具有正確顏色的矩形或圓形。
function render() {
map.forEach((row, y) => {
row.forEach((cell, x) => {
paintCell(context, cell, x, y)
})
})
}
基本上,HTML 畫布中的所有渲染都為輪廓(筆觸)和內部(填充)創建了一條路徑。由於每個坐標一個像素是一個非常小的遊戲,我將每個值乘以 multipler
,即 75
在這種情況下是像素。
function paintCell(context, cell, x, y) {
// Create the fill
context.beginPath()
context.rect(x * multiplier + 5, y * multiplier + 5, multiplier - 10, multiplier - 10)
context.fillStyle = colors[cell].fill
context.fill()
// Create the outline
context.beginPath()
context.rect(x * multiplier + 5, y * multiplier + 5, multiplier - 10, multiplier - 10)
context.lineWidth = 10
context.strokeStyle = colors[cell].stroke
context.stroke()
}
渲染函數還檢查獲勝條件(所有存儲位置現在都是成功塊)並顯示“獲勝者是你!”如果你贏了。
結論
這是一個有趣的小遊戲。我是這樣組織文件的:
- 實體數據、地圖數據、將顏色映射到實體以及關鍵數據的常量。
- 用於檢查特定坐標處存在什麼類型的實體並確定玩家的新坐標應該是什麼的實用函數。
- Sokoban 類,用於維護遊戲狀態、邏輯和渲染。
- 用於初始化應用實例和處理關鍵事件的腳本。
我發現編碼比解決更容易。 😆
希望您喜歡閱讀這篇文章,並在製作自己的小遊戲和項目時受到啟發。