用JavaScript编写推箱子拼图游戏 2021-11-10 默认分类 暂无评论 1760 次阅读 有一天,我用JavaScript做了一个推箱子益智游戏的实现。 游戏由一堵墙、一个可玩的角色、积木和地面上作为存储位置的点组成。游戏的目的是将所有的积木推到所有的存储位置。这是很有挑战性的,因为很容易出现积木不能再移动的情况,现在你必须重新开始游戏。 [代码演示地址](http://www.guobacai.com/index.php/go/2MSrMqi2/ "代码") [DEMO地址](http://www.guobacai.com/index.php/go/IAQH42wD/ "DEMO地址") 这是我做的一个。  原版游戏的画面稍好  在我的版本中,大蓝点是角色,粉红点是存储位置,橙色块是箱子。 我在几个小时内飞快地写完了它。制作小游戏与我平时的工作有很大不同,所以我发现这是一个有趣的、可实现的挑战。幸运的是,通过以前的一些项目(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。 从这里开始,很容易在现有推箱子关卡的基础上制作一张地图,其中每个坐标都是游戏中的一个实体--地形、玩家,等等。 Entities ``` const EMPTY = 'empty' const WALL = 'wall' const BLOCK = 'block' const SUCCESS_BLOCK = 'success_block' const VOID = 'void' const PLAYER = 'player' ``` Map ``` 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画布上把它渲染到屏幕上。因此,现在我有了一张看起来正确的地图,但它还没有做任何事情。 ## 游戏逻辑 没有太多的动作需要担心。玩家可以正向移动--上、下、左、右--有几件事需要考虑 - 玩家和积木不能穿过墙。 - 玩家和积木可以通过一个空的空间或一个无效的空间(存储位置)移动。 - 玩家可以推动一个积木 - 当一个BLOCK在一个VOID的上面时,它就变成了SUCCESS_BLOCK。 这就是字面上的意思了。我还编码了一件事,它不是原始游戏的一部分,但对我来说很有意义。 - 一个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) ``` ## 渲染 在一个二维数组中跟踪整个游戏,并在每次移动时将更新的游戏渲染到屏幕上是很容易的。游戏的勾选非常简单--任何时候发生上、下、左、右的按键事件(或者对于激烈的游戏者来说是w、a、s、d),就会调用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画布上的所有渲染都是为轮廓(stroke)做一个路径,为内部(fill)做一个路径。由于每个坐标的一个像素将是一个相当小的游戏,我把每个值乘以一个倍数,在这种情况下是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() } ``` 渲染功能也会检查胜利条件(现在所有的存储位置都是成功块),如果你赢了,就会显示 "胜利者是你!"。 ## 总结 这是个有趣的小游戏。我是这样组织文件的。 实体数据的常量,地图数据,将颜色映射到实体,以及关键数据。 用于检查在特定坐标上存在什么类型的实体的实用函数,并确定玩家的新坐标应该是什么。 推箱子类,用于维护游戏状态、逻辑和渲染。 用于初始化应用程序的实例和处理关键事件的脚本。 我发现编码比解决问题更容易。 标签: javascript, 积木, 游戏, 存储, 益智
评论已关闭