美文网首页JS学习笔记
使用canvas实现康威的生命游戏

使用canvas实现康威的生命游戏

作者: _敏讷 | 来源:发表于2020-04-21 15:15 被阅读0次

    前不久看到约翰·康威逝世的消息,才了解了关于他的一些事情,其中他在1970年发明的生命游戏(Conway's Game of Life,是一种简单的元胞自动机)引起了我的兴趣,于是想尝试实现一下。
    JS实现这种简单的平面游戏的话有两个比较直接的选项,一是通过DOM绘制,二是通过canvas绘制。考虑到DOM重绘的性能消耗较大,于是直接采用canvas来进行实现。


    wiki中关于生命游戏的规则定义如下:

    每个细胞有两种状态 - 存活或死亡,每个细胞与以自身为中心的周围八格细胞产生互动(如图,黑色为存活,白色为死亡)

    当前细胞为存活状态时,当周围的存活细胞低于2个时(不包含2个),该细胞变成死亡状态。(模拟生命数量稀少)

    当前细胞为存活状态时,当周围有2个或3个存活细胞时,该细胞保持原样。

    当前细胞为存活状态时,当周围有超过3个存活细胞时,该细胞变成死亡状态。(模拟生命数量过多)

    当前细胞为死亡状态时,当周围有3个存活细胞时,该细胞变成存活状态。(模拟繁殖)

    可以把最初的细胞结构定义为种子,当所有在种子中的细胞同时被以上规则处理后,可以得到第一代细胞图。按规则继续处理当前的细胞图,可以得到下一代的细胞图,周而复始。

    经过分析之后,总结一下核心逻辑:
    构建一个m * n的二维数组,数组有0和1两个值,分别代表生存和死亡,每轮周期根据周围8格的状态分别判断每个数组项的状态并更新。

    下面按步骤介绍游戏的实现过程:

    1. 准备工作,
    // 定义常量,棋盘宽度50,高度50,每个方块的宽高为10
    const WIDTH = 50
    const HEIGHT = 50
    const ITEM_WIDTH = 10
    
    class LifeGame extends Component {
      constructor(props) {
        super(props)
        this.state = {
          ctx: null,        // 保存canva的context对象
          matrix: [],        // 保存二维数组
          isOnGoing: false,        // 保存生命游戏的开始或停止状态
        }
      }
      componentDidMount() {
        // 获取canvas的context对象
        const canvas = document.getElementById("game-board")
        const ctx = canvas.getContext('2d')
        // 在state中保存canvas的context对象,并在回调中执行初始化逻辑
        this.setState({ ctx }, () => {
          this.initBoard()
        })
      }
      // 初始化棋盘
      initBoard() {
        // 创建初始二维数组
        const matrix = Array(HEIGHT).fill().map( () => Array(WIDTH).fill(0))
        this.setState({ matrix: matrix }, () => {
          //  绘制空棋盘
          this.drawMatrix()
        })
      }
      // 绘制棋盘
      drawMatrix() {
        const { ctx, matrix } = this.state
        // 因为canvas是覆写式的逻辑,所以绘制之前必须先清空区域
        ctx.clearRect(0,0, WIDTH * ITEM_WIDTH, HEIGHT * ITEM_WIDTH); 
        // 双重循环绘制每个方格
        matrix.forEach( (arr, y) => {
          arr.forEach( (item, x) => {
            if(item == 1) {
              ctx.fillRect(x * ITEM_WIDTH, y * ITEM_WIDTH, ITEM_WIDTH, ITEM_WIDTH);
            } else {
              ctx.strokeRect(x * ITEM_WIDTH, y * ITEM_WIDTH, ITEM_WIDTH, ITEM_WIDTH);
            }
          })
        })
      }
      render() {
        return <div>
          <canvas width={WIDTH * ITEM_WIDTH} height={HEIGHT * ITEM_WIDTH} id='game-board' className='canvas' />
        </div>
      }
    }
    

    实现这一步之后打开当前页面,你已经能看到一个50 * 50的棋盘界面


    1. 增加输入逻辑,在componentDidMount中给canvas元素绑定clickonmousedown方法捕捉点击和点击拖动事件,然后将触发的区域切换状态并重绘;
    componentDidMount() {
        ...
        // 绑定点击事件
        canvas.addEventListener('click', this.handleClick.bind(this))
        // 绑定点击拖动事件
        canvas.onmousedown= (e) => {
          //按下后可移动
          canvas.onmousemove = (e) => {
              const x = Math.floor(e.clientX / ITEM_WIDTH)
              const y = Math.floor(e.clientY / ITEM_WIDTH)
              this.switchSingle(x, y)
          };
          //鼠标抬起清除绑定事件
          canvas.onmouseup = function(){
              canvas.onmousemove = null;
              canvas.onmouseup = null;
          };
        }
        ...
    }
      // 切换并绘制单个方块的方法
      switchSingle(x, y) {
        const { ctx, matrix } = this.state
        const nextMatrix = cloneDeep(matrix)
        nextMatrix[y][x] = nextMatrix[y][x] == 1 ? 0 : 1
        // 更新state
        this.setState({ matrix: nextMatrix })
        // 清空当前位置
        ctx.clearRect(x * ITEM_WIDTH, y * ITEM_WIDTH, ITEM_WIDTH, ITEM_WIDTH); 
        // 根据当前状态绘制方块
        if(nextMatrix[y][x] == 1) {
          ctx.fillRect(x * ITEM_WIDTH, y * ITEM_WIDTH, ITEM_WIDTH, ITEM_WIDTH);
        } else {
          ctx.strokeRect(x * ITEM_WIDTH, y * ITEM_WIDTH, ITEM_WIDTH, ITEM_WIDTH);
        }
      }
      // 单击方块的回调方法
      handleClick(e) {
        if(!this.state.isOnGoing) {
          const x = Math.floor(e.pageX / ITEM_WIDTH)
          const y = Math.floor(e.pageY / ITEM_WIDTH)
          this.switchSingle(x, y)
        }
        console.log(e.pageX, e.pageY)
      }
    

    完成这一步后,你可以通过鼠标在棋盘中点击添加初始状态:


    1. 实现主循环逻辑
      定义startGame,pauseGame,endGame三个方法,它们分别控制主循坏的开始,暂停和结束,并将它们分别绑定到“开始”,“暂停”, “停止”三个按钮上
      startGame() {
        this.gameLoop = setInterval(() => {
          this.traverse()
        }, 500);
      }
    
      pauseGame() {
        clearInterval(this.gameLoop)
      }
    
      endGame() {
        clearInterval(this.gameLoop)
        this.initBoard()
      }
    
      render() {
        return <div>
            ...
             <button onClick={this.startGame.bind(this)}>开始</button>
            <button onClick={this.pauseGame.bind(this)}>暂停</button>
            <button onClick={this.endGame.bind(this)}>停止</button>
            ...
        </div>
      }
    

    traverse方法是主循坏中执行的方法,它会遍历二维数组,并对每个数组项调用check方法推算其下一步的状态

      // 遍历所有方格
      traverse() {
        const { matrix } = this.state
        const nextMatrix = cloneDeep(matrix)
        matrix.forEach( (arr, y) => {
          arr.forEach( (item, x) => {
            nextMatrix[y][x] = this.check(x, y)
          })
        })
        this.setState({ matrix: nextMatrix}, () => {
          this.drawMatrix()
        })
      }
    
      // 检查当前方格
      check(x, y) {
        const { matrix } = this.state
        const count =  this.getItemValue(x-1, y - 1) 
          + this.getItemValue(x, y - 1)
          + this.getItemValue(x + 1, y - 1)
          + this.getItemValue(x - 1, y)
          + this.getItemValue(x + 1, y)
          + this.getItemValue(x - 1, y + 1)
          + this.getItemValue(x, y + 1)
          + this.getItemValue(x + 1, y + 1)
        if(count == 3) {
          // 周围细胞数为3时,一定为1
          return 1
        } else if(count == 2) {
          // 周围细胞数为2时,1保持1,0保持0
          return matrix[y][x]
        } else {
          // 其他情况,一定为0
          return 0
        }
      }
      // 该方法返回对应坐标的0 || 1状态
      getItemValue(x, y) {
        const { matrix } = this.state
        return (matrix[y] || [])[x] || 0
      }
    

    到这一步,游戏的基本逻辑就实现完成,可以通过开始,暂停,停止按钮来运行和重置生命游戏,我们可以通过不同的输入观察到不同的执行结果:


    该项目的demo可以点击连接后,在"生命游戏"路径下查看
    项目的源代码点击这里查看

    相关文章

      网友评论

        本文标题:使用canvas实现康威的生命游戏

        本文链接:https://www.haomeiwen.com/subject/tlluihtx.html