在 CoffeeScript 和 canvas 中创建游戏(3)
- UID
- 1066743
|
在 CoffeeScript 和 canvas 中创建游戏(3)
创建初始种子模式Conway 的 Game of Life 需要一个初始种子模式。基于初始种子模式,网格上的细胞在每次更新时演化到下一代。要创建种子,必须使用 seed 方法随机决定网格上的细胞的死活,如 中所示。两个嵌套的 for 循环允许您访问网格上的每个细胞。
外循环对所有行进行循环,这在一个名为 范围的 CoffeeScript 功能中完成。for row in [0...@numberOfRows]中的第 3 个句点 (.) 表明范围是排他性的。如果 numberOfRows的值为 3,那么迭代器(在本例中为 row 变量)将具有 0 到 2 范围内的值。这允许您创建二维数组 currentCellGeneration。
内循环对所有列进行循环,为每个细胞创建一个新的 seedCell。它使用当前行和列调用 createSeedCell方法。创建种子细胞后,将它存储在 currentCellGeneration中的正确位置。
清单 5. 初始种子模式1
2
3
4
5
6
7
8
9
10
11
12
13
14
| seed: ->
@currentCellGeneration = []
for row in [0...@numberOfRows]
@currentCellGeneration[row] = []
for column in [0...@numberOfColumns]
seedCell = @createSeedCell row, column
@currentCellGeneration[row][column] = seedCell
createSeedCell: (row, column) ->
isAlive: Math.random() < @seedProbability
row: row
column: column
|
创建一个新种子细胞很简单。细胞是一个简单的对象,包含 3 个属性。 中的 createSeedCell方法表示将行和列参数传递到细胞对象。isAlive属性用于确定细胞是死的还是活的。借助 Math.random方法和 seedProbability属性,您可以随机创建死细胞或活细胞。您可能已经注意到无需使用 return 关键字,因为 CoffeeScript 方法会自动返回它们的最终值。
游戏循环现在您已经创建了初始种子模式,是时候为生命游戏注入活力了。您需要向 canvas 绘制当前的一代细胞,并将这一代演化到下一代。所有这些都需要在一个定期间隔内完成。如 中所示,调用 tick方法来开始此间隔。 中的 tick方法完成了三件事。它:
- 调用 drawGrid方法来绘制当前一代细胞。
- 将当前一代细胞演化到下一代。
- 设置一个超时来保持游戏循环持续运行。
为 setTimeout方法使用两个参数。第一个参数是应调用的方法,在本例中为 tick方法本身。第二个参数定义在调用之前应等待的毫秒数。您可以使用 tickLength属性控制游戏循环的速度。
您可能已注意到 tick方法和其他所有方法之间的区别。tick 方法使用了 CoffeeScript 的粗箭头 (=>) 功能。粗箭头将方法绑定到当前上下文。该上下文将始终是正确的。没有此箭头,超时将会是无效的。
清单 6. 游戏循环的 tick 方法1
2
3
4
5
| tick: =>
@drawGrid()
@evolveCellGeneration()
setTimeout @tick, @tickLength
|
绘制网格很容易。 中的 drawGrid方法使用两个嵌套循环来访问网格上的每个细胞,然后将该细胞传递给 drawCell方法。drawCell方法使用 cellSize以及细胞的 row 和 column 属性计算网格上的 x 和 y 位置。依赖于 isAlive属性,设置细胞的填充样式。在使用 canvas 方法 strokeRect和 fillRect绘制细胞之前,设置 canvas 的 strokeStyle和 fillStyle属性。
清单 7. 绘制网格1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| drawGrid: ->
for row in [0...@numberOfRows]
for column in [0...@numberOfColumns]
@drawCell @currentCellGeneration[row][column]
drawCell: (cell) ->
x = cell.column * @cellSize
y = cell.row * @cellSize
if cell.isAlive
fillStyle = 'rgb(242, 198, 65)'
else
fillStyle = 'rgb(38, 38, 38)'
@drawingContext.strokeStyle = 'rgba(242, 198, 65, 0.1)'
@drawingContext.strokeRect x, y, @cellSize, @cellSize
@drawingContext.fillStyle = fillStyle
@drawingContext.fillRect x, y, @cellSize, @cellSize
|
当前一代细胞的演化包含三个方法。evolveCellGeneration方法如 中所示。类似于 seed方法,使用两个嵌套循环创建一个名为 newCellGeneration的二维数组,它将存储演化后的一代细胞。内循环将该细胞传递给 evolveCell方法,该方法将返回演化后的细胞。演化后的细胞然后存储在 newCellGeneration数组中的正确位置。演化当前一代的每个细胞后,您可以更新 currentCellGeneration属性。
清单 8. 演化当前一代细胞1
2
3
4
5
6
7
8
9
10
11
| evolveCellGeneration: ->
newCellGeneration = []
for row in [0...@numberOfRows]
newCellGeneration[row] = []
for column in [0...@numberOfColumns]
evolvedCell = @evolveCell @currentCellGeneration[row][column]
newCellGeneration[row][column] = evolvedCell
@currentCellGeneration = newCellGeneration
|
中的 evolveCell方法首先创建一个 evolvedCell变量,它具有与传递的细胞相同的属性。为了决定细胞是死的、复活了还是仍然是活的,您需要知道有多少个邻居细胞是活的。要获得此数字,可对该细胞调用 countAliveNeighbors方法。此方法计算并返回活邻居的数量。
有了活邻居的数量后,您可使用生命游戏的规则更新演化后的细胞的 isAlive属性。更新该属性后,只需返回 evolvedCell对象。
清单 9. 演化一个细胞1
2
3
4
5
6
7
8
9
10
11
12
| evolveCell: (cell) ->
evolvedCell =
row: cell.row
column: cell.column
isAlive: cell.isAlive
numberOfAliveNeighbors = @countAliveNeighbors cell
if cell.isAlive or numberOfAliveNeighbors is 3
evolvedCell.isAlive = 1 < numberOfAliveNeighbors < 4
evolvedCell
|
中的 countAliveNeighbors方法接受一个细胞作为参数,返回活细胞数量。通常,网格上的一个细胞有 8 个邻居。但是,如果细胞位于网格边缘上,邻居数量会更少。计算活邻居是一项稍微复杂的任务。
要获得此问题的一个容易理解的不错解决方案,您需要计算您搜索活邻居的区域。对于网格中间的细胞,很容易计算搜索的界限。位于第 4 行和第 5 列的细胞在第 3、4、5 和列 4、5、6 中都有邻居。
位于第 0 行和第 0 列的细胞属于不同的情况。邻居细胞在第 0 行到第 1 行和第 0 列到第 1 列之间。行的下边界是细胞减一后的行号,但最小值为 0。您可使用 Math.max方法实现此用途,如 中所示。列的下边界可使用相同方式计算。
上边界使用 Math.min方法计算。确保细胞行加一不会大于最后一个行索引。拥有行和列的上边界和下边界后,就可在两个嵌套循环中对它们进行循环了。在本例中,示例使用 CoffeeScript 的隐式运算符来确保还使用了 upperRowBound和 upperColumnBound值。
您不希望计算该细胞本身,所以需要在内循环中放入一个 continue 语句,它在循环的 row 和 column 变量与该细胞的属性匹配时执行。在这之后,如果当前访问的细胞是活的,将 numberOfAliveNeighbors计数器加一。最后,您仅需返回此计数器。
清单 10. 计算一个细胞的活邻居1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| countAliveNeighbors: (cell) ->
lowerRowBound = Math.max cell.row - 1, 0
upperRowBound = Math.min cell.row + 1, @numberOfRows - 1
lowerColumnBound = Math.max cell.column - 1, 0
upperColumnBound = Math.min cell.column + 1, @numberOfColumns - 1
numberOfAliveNeighbors = 0
for row in [lowerRowBound..upperRowBound]
for column in [lowerColumnBound..upperColumnBound]
continue if row is cell.row and column is cell.column
if @currentCellGeneration[row][column].isAlive
numberOfAliveNeighbors++
numberOfAliveNeighbors
|
因为 CoffeeScript 将每个文件包装在自己的闭包中,所以您需要导出 GameOfLife类,以便可在其文件外部使用它。将一个 GameOfLife属性添加到 window 对象中,如下所示: window.GameOfLife = GameOfLife。
大功告成!您已完成了Conway 的 Game of Life 的示例实现。如果在浏览器中打开 index.html 文件,您应能看到您自己的生命游戏版本,如 图 1中所示。如果某个地方出错了,您可对比您的版本与作者的完整源代码(请参见 )。 |
|
|
|
|
|