首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

在 CoffeeScript 和 canvas 中创建游戏(3)

在 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中所示。如果某个地方出错了,您可对比您的版本与作者的完整源代码(请参见  )。
返回列表