在 CoffeeScript 和 canvas 中创建游戏

使用 CoffeeScript 和 HTML5 canvas元素创建小游戏

Conway 的 Game of Life 是一个无玩家的游戏,它仅依赖于初始配置,无需进一步输入即可运行。本文将引导您实现该游戏的您自己的版本。学习使用 CoffeeScript 功能和 HTML5 canvas元素创建游戏。文中提供了代码样例。

David Strauß, 编程人员, NerdKitchen

David Strauß photoDavid Strauß 是一位富有创意的编程人员,他创建了有用的软件并运行着 NerdKitchen。他很小就通过父亲对木料雕刻的痴迷发现了创造事物的魔力。David 主要使用 CoffeeScript 和 Ruby on Rails 框架。他的一个早期项目 Marble Run 在 Mozilla 的 2010 年游戏大赛中获胜,击败了 Fantasy Interactive 和 Google Chrome Team。



2013 年 11 月 04 日

简介

小游戏是学习新技术的一种有趣方式。本文将引导您使用 CoffeeScript 和 HTML5 canvas元素亲自实现Conway 的 Game of Life 。尽管Conway 的 Game of Life 可能算不上一个游戏,但它是一个非常容易管理的出色的小任务。

常用缩写词

  • CSS:级联样式表
  • DOM:文档对象模型
  • HTML:超文本标记语言

在技术方面,您将需要:

  • 一个可处理 HTML5 canvas元素的最新的 Web 浏览器。
  • CoffeeScript,这是一种编译为 JavaScript 的小型编程语言。建议您花 10 分钟在网上了解一下 CoffeeScript 功能,或者阅读免费的在线图书 The Little Book on CoffeeScript(请参见 参考资料)。
  • CoffeeScript 编译器用于编写 CoffeeScript。建议安装 Node.js,使用 Node Package Manager (NPM) 安装它(请参见 参考资料)。本文中的示例使用了 1.3.3 版的 CoffeeScript 编译器。
  • 您最喜欢的文本编辑器。

您可 下载本文中使用的示例的源代码。


Conway 的 Game of Life 游戏

Conway 的 Game of Life 基本上是在一个包含方形细胞的二维正交网格上进行的一种模拟。每个细胞具有两种可能的状态:死或活。该游戏提供定期更新(也称为记号)。每次更新时,当前一代细胞会演化为下一代细胞。在一次更新期间,以下规则适用于每个细胞:

  • 如果一个活细胞的活邻居少于 2 个,那么它将由于人口不足而死去。
  • 具有两个或 3 个活邻居的活细胞会存活到下一代。
  • 具有超过 3 个活邻居的活细胞会由于人口过多而死去。
  • 刚好具有 3 个活邻居的死细胞会由于繁殖而变为活细胞。

第一代细胞是随机创建的。在这之后,该模拟会运行到所有细胞都死去或出现模式。请参见 参考资料,了解有关不同模式的更多信息和Conway 的 Game of Life 历史。

图 1显示了本文中的练习的最终结果。您也可以在线查看结果并自行体验(请参阅 参考资料中作者的Conway 的 Game of Life 版本)。

图 1. Conway 的 Game of Life 的实现示例
完成的Conway 的 Game of Life 实现

实现

在本文中,Conway 的 Game of Life 实现包含两部分:第一部分是 HTML5 标记和 CSS,它是第二部分需要的基础工作;第二部分是游戏的实际 CoffeeScript 实现


HTML5 标记和 CSS

第一步是创建一个名为 game-of-life 的目录,您的所有示例文件都将保存在这里。标记和 CSS 需要一个存放位置,所以要在新的 game-of-life 目录中创建一个新 index.html 文件。清单 1显示了Conway 的 Game of Life 实现需要的 HTML5 标记和 CSS。

清单 1. index.html 文件的标记和 CSS
 <html> 
 <head> 
  <title>Game of Life</title> 

  <script type="text/javascript" src="javascripts/game_of_life.js"> 
  </script> 

  <style type="text/css"> 
    body { 
      background-color: rgb(38, 38, 38); 
    } 

    canvas { 
      border: 1px solid rgba(242, 198, 65, 0.1); 
      margin: 50px auto 0 auto; 
      display: block; 
    } 
  </style> 
 </head> 
 <body> 
  <script type="text/javascript"> 
    new GameOfLife(); 
  </script> 
 </body> 
 </html>

清单 1提供了游戏的名称,包含一个来自 javascripts 目录的 JavaScript 文件 game_of_life.js。不要担心,我们将使用 CoffeeScript 进行编程。将此行放在 index.html 文件中,即使 game_of_life.js 文件还不存在。

添加一个简单的样式,使此生命游戏更加美观。为 body 元素提供一个暗背景,为 canvas 元素添加一个边框。借助 CSS 属性 margin 和 display,您可确保 canvas 放在了屏幕的中心。

为了启动游戏,该代码向标记的主体添加一个小脚本块并创建一个新 GameOfLife实例。


生命游戏的 CoffeeScript 实现

您可能想知道为什么 清单 1中包含的是 JavaScript 文件 game_of_life.js,而不是 CoffeeScript 文件。许多不同的浏览器还无法理解 CoffeeScript,所以您需要将 CoffeeScript 代码编译为 JavaScript,使浏览器可以解释它。这也是您的 game-of-life 目录中需要两个新目录的原因。第一个目录名为 coffeescripts,包含所有 CoffeeScript 代码。第二个目录名为 javascripts,包含编译后的 JavaScript 代码。

示例实现仅包含一个类,所以您只需创建一个对应的文件。game_of_life.coffee 文件(包含所有 CoffeeScript 代码)位于 coffeescripts 目录中。


编译 CoffeeScript

在开始实现游戏逻辑之前,您需要找到一种方式来将 CoffeeScript 代码编译为 JavaScript。幸运的是,CoffeeScript 编译器提供了一些选项来完成此任务。自动将 CoffeeScript 代码编译为 JavaScript 的命令是:

 coffee --output javascripts/ --watch --compile coffeescripts/

要运行此命令,您需要在您喜欢的命令行工具中导航到 game-of-life 目录。使用 coffee命令将 CoffeeScript 代码编译为 JavaScript。首先,该示例使用了 --output标志来指定输出文件夹。然后在 coffeescripts 目录上使用了 --watch--compile标志。

所有这些是何含义?只要 coffeescripts 目录中的一个文件被修改,coffee命令就会发现它,编译它,然后将编译后的 JavaScript 文件保存到 javascripts 目录中。现在您应该了解了我们从不创建 清单 1中包含的 game_of_life.js 文件的原因。当在 game_of_life.coffee 文件中编写 CoffeeScript 代码时,它将被编译,结果 JavaScript 代码会自动保存在 game_of_life.js 文件中。


初始化游戏

现在已经解决了编译问题,您可以开始编写生命游戏的示例版本了。在文本编辑器中打开文件 game_of_life.coffee。如 清单 2中所示,只有一个包含多个属性的 GameOfLife类。

清单 2. GameOfLife类和属性
 class GameOfLife 
  currentCellGeneration: null 
  cellSize: 7 
  numberOfRows: 50 
  numberOfColumns: 50 
  seedProbability: 0.5 
  tickLength: 100 
  canvas: null 
  drawingContext: null

不言自明的变量和方法名称使得源代码很容易阅读。在 清单 2中:

  • 因为Conway 的 Game of Life 包含一个二维细胞网格,所以示例也需要一个类似的网格。currentCellGeneration属性将持有一个二维数组中的所有细胞。
  • cellSize指定一个细胞的宽度和高度 —在本例中为 7 个像素。
  • 属性 numberOfRowsnumberOfColumns用于确定网格的大小。
  • Conway 的 Game of Life 需要一种初始细胞模式,这也被称为种子。当创建种子时,使用 seedProbability属性来确定一个细胞的死活。
  • tickLength属性指定了游戏更新的时间间隔。在示例中,游戏每 100 毫秒更新一次。
  • canvas属性将保存您将要创建的 canvas 元素。
  • 要在 canvas 上绘制图形,您需要绘图上下文,该上下文存储在 drawingContext属性中。

GameOfLife类的构造函数负责设置游戏。如 清单 3中所示,您需要创建一个 canvas,将它调整为正确的尺寸。然后,您可以使用新创建的 canvas 创建绘图上下文。在这之后,您就可以创建初始种子模式并启动第一次更新来开始游戏循环了。让我们首先创建 canvas。

清单 3. GameOfLife类的构造函数
  constructor: -> 
    @createCanvas() 
    @resizeCanvas() 
    @createDrawingContext() 

    @seed() 

    @tick()

因为现代浏览器提供了一个出色的 API 来操作文档对象模型 (DOM),所以我们的示例不需要任何花哨的东西;您可摆脱外部框架,比如 jQuery。使用 document.createElement方法创建一个新 canvas 元素,然后将它存储在一个同名的属性中。将新创建的元素附加到页面主体中。所有这些都在 createCanvas方法中完成,如 清单 4所示。

清单 4. 设置 canvas 元素
  createCanvas: -> 
    @canvas = document.createElement 'canvas'
    document.body.appendChild @canvas 

  resizeCanvas: -> 
    @canvas.height = @cellSize * @numberOfRows 
    @canvas.width = @cellSize * @numberOfColumns 

  createDrawingContext: -> 
    @drawingContext = @canvas.getContext '2d'

resizeCanvas方法使用 cellSizenumberOfRowsnumberOfColumns来计算 canvas 元素的高度和宽度。清单 4中的第三个方法 createDrawingContext从 canvas 获取二维上下文并存储它供未来使用。

除了这 3 个方法,清单 4中的构造函数还调用了另外两个方法:seedtick。它们占据了代码的很大一部分,这些将在后面的各节中探讨。


创建初始种子模式

Conway 的 Game of Life 需要一个初始种子模式。基于初始种子模式,网格上的细胞在每次更新时演化到下一代。要创建种子,必须使用 seed 方法随机决定网格上的细胞的死活,如 清单 5中所示。两个嵌套的 for 循环允许您访问网格上的每个细胞。

外循环对所有行进行循环,这在一个名为 范围的 CoffeeScript 功能中完成。for row in [0...@numberOfRows]中的第 3 个句点 (.) 表明范围是排他性的。如果 numberOfRows的值为 3,那么迭代器(在本例中为 row 变量)将具有 0 到 2 范围内的值。这允许您创建二维数组 currentCellGeneration

内循环对所有列进行循环,为每个细胞创建一个新的 seedCell。它使用当前行和列调用 createSeedCell方法。创建种子细胞后,将它存储在 currentCellGeneration中的正确位置。

清单 5. 初始种子模式
  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 个属性。 清单 5中的 createSeedCell方法表示将行和列参数传递到细胞对象。isAlive属性用于确定细胞是死的还是活的。借助 Math.random方法和 seedProbability属性,您可以随机创建死细胞或活细胞。您可能已经注意到无需使用 return 关键字,因为 CoffeeScript 方法会自动返回它们的最终值。


游戏循环

现在您已经创建了初始种子模式,是时候为生命游戏注入活力了。您需要向 canvas 绘制当前的一代细胞,并将这一代演化到下一代。所有这些都需要在一个定期间隔内完成。如 清单 6中所示,调用 tick方法来开始此间隔。清单 6中的 tick方法完成了三件事。它:

  • 调用 drawGrid方法来绘制当前一代细胞。
  • 将当前一代细胞演化到下一代。
  • 设置一个超时来保持游戏循环持续运行。

setTimeout方法使用两个参数。第一个参数是应调用的方法,在本例中为 tick方法本身。第二个参数定义在调用之前应等待的毫秒数。您可以使用 tickLength属性控制游戏循环的速度。

您可能已注意到 tick方法和其他所有方法之间的区别。tick 方法使用了 CoffeeScript 的粗箭头 (=>) 功能。粗箭头将方法绑定到当前上下文。该上下文将始终是正确的。没有此箭头,超时将会是无效的。

清单 6. 游戏循环的 tick 方法
  tick: => 
    @drawGrid() 
    @evolveCellGeneration() 

    setTimeout @tick, @tickLength

绘制网格很容易。清单 7中的 drawGrid方法使用两个嵌套循环来访问网格上的每个细胞,然后将该细胞传递给 drawCell方法。drawCell方法使用 cellSize以及细胞的 row 和 column 属性计算网格上的 x 和 y 位置。依赖于 isAlive属性,设置细胞的填充样式。在使用 canvas 方法 strokeRectfillRect绘制细胞之前,设置 canvas 的 strokeStylefillStyle属性。

清单 7. 绘制网格
  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方法如 清单 8中所示。类似于 seed方法,使用两个嵌套循环创建一个名为 newCellGeneration的二维数组,它将存储演化后的一代细胞。内循环将该细胞传递给 evolveCell方法,该方法将返回演化后的细胞。演化后的细胞然后存储在 newCellGeneration数组中的正确位置。演化当前一代的每个细胞后,您可以更新 currentCellGeneration属性。

清单 8. 演化当前一代细胞
 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

清单 9中的 evolveCell方法首先创建一个 evolvedCell变量,它具有与传递的细胞相同的属性。为了决定细胞是死的、复活了还是仍然是活的,您需要知道有多少个邻居细胞是活的。要获得此数字,可对该细胞调用 countAliveNeighbors方法。此方法计算并返回活邻居的数量。

有了活邻居的数量后,您可使用生命游戏的规则更新演化后的细胞的 isAlive属性。更新该属性后,只需返回 evolvedCell对象。

清单 9. 演化一个细胞
  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

清单 10中的 countAliveNeighbors方法接受一个细胞作为参数,返回活细胞数量。通常,网格上的一个细胞有 8 个邻居。但是,如果细胞位于网格边缘上,邻居数量会更少。计算活邻居是一项稍微复杂的任务。

要获得此问题的一个容易理解的不错解决方案,您需要计算您搜索活邻居的区域。对于网格中间的细胞,很容易计算搜索的界限。位于第 4 行和第 5 列的细胞在第 3、4、5 和列 4、5、6 中都有邻居。

位于第 0 行和第 0 列的细胞属于不同的情况。邻居细胞在第 0 行到第 1 行和第 0 列到第 1 列之间。行的下边界是细胞减一后的行号,但最小值为 0。您可使用 Math.max方法实现此用途,如 清单 10中所示。列的下边界可使用相同方式计算。

上边界使用 Math.min方法计算。确保细胞行加一不会大于最后一个行索引。拥有行和列的上边界和下边界后,就可在两个嵌套循环中对它们进行循环了。在本例中,示例使用 CoffeeScript 的隐式运算符来确保还使用了 upperRowBoundupperColumnBound值。

您不希望计算该细胞本身,所以需要在内循环中放入一个 continue 语句,它在循环的 row 和 column 变量与该细胞的属性匹配时执行。在这之后,如果当前访问的细胞是活的,将 numberOfAliveNeighbors计数器加一。最后,您仅需返回此计数器。

清单 10. 计算一个细胞的活邻居
  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中所示。如果某个地方出错了,您可对比您的版本与作者的完整源代码(请参见 参考资料)。


结束语

尽管Conway 的 Game of Life 是一款具有简单规则的小游戏,但您需要解决一些棘手的问题。它是一个用于学习新编程语言或提高您的技能的不错示例。

小方法和不言自明的变量名使您的代码更容易理解。它们还有助于良好地构造您的源代码。CoffeeScript 非常适合此用途,可为您省略大量无用的 JavaScript 语法。CoffeeScript 还提供了可提升您的生产力的便捷功能。


下载

描述名字大小
本文的样例源代码game-of-life.zip4KB

参考资料

学习

获得产品和技术

讨论

  • developerWorks 社区:探索由开发人员推动的博客、论坛、群组和维基,并与其他 developerWorks 用户进行交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=951581
ArticleTitle=在 CoffeeScript 和 canvas 中创建游戏
publish-date=11042013