使用canvas實(shí)現(xiàn)康威的生命游戲

前不久看到約翰·康威逝世的消息勾给,才了解了關(guān)于他的一些事情袋毙,其中他在1970年發(fā)明的生命游戲(Conway's Game of Life沪摄,是一種簡(jiǎn)單的元胞自動(dòng)機(jī))引起了我的興趣诱篷,于是想嘗試實(shí)現(xiàn)一下。
JS實(shí)現(xiàn)這種簡(jiǎn)單的平面游戲的話有兩個(gè)比較直接的選項(xiàng)崭孤,一是通過(guò)DOM繪制类嗤,二是通過(guò)canvas繪制糊肠。考慮到DOM重繪的性能消耗較大遗锣,于是直接采用canvas來(lái)進(jìn)行實(shí)現(xiàn)货裹。


wiki中關(guān)于生命游戲的規(guī)則定義如下:

每個(gè)細(xì)胞有兩種狀態(tài) - 存活或死亡,每個(gè)細(xì)胞與以自身為中心的周?chē)烁窦?xì)胞產(chǎn)生互動(dòng)(如圖精偿,黑色為存活弧圆,白色為死亡)

當(dāng)前細(xì)胞為存活狀態(tài)時(shí),當(dāng)周?chē)拇婊罴?xì)胞低于2個(gè)時(shí)(不包含2個(gè))笔咽,該細(xì)胞變成死亡狀態(tài)搔预。(模擬生命數(shù)量稀少)

當(dāng)前細(xì)胞為存活狀態(tài)時(shí),當(dāng)周?chē)?個(gè)或3個(gè)存活細(xì)胞時(shí)叶组,該細(xì)胞保持原樣拯田。

當(dāng)前細(xì)胞為存活狀態(tài)時(shí),當(dāng)周?chē)谐^(guò)3個(gè)存活細(xì)胞時(shí)甩十,該細(xì)胞變成死亡狀態(tài)勿锅。(模擬生命數(shù)量過(guò)多)

當(dāng)前細(xì)胞為死亡狀態(tài)時(shí),當(dāng)周?chē)?個(gè)存活細(xì)胞時(shí)枣氧,該細(xì)胞變成存活狀態(tài)。(模擬繁殖)

可以把最初的細(xì)胞結(jié)構(gòu)定義為種子垮刹,當(dāng)所有在種子中的細(xì)胞同時(shí)被以上規(guī)則處理后达吞,可以得到第一代細(xì)胞圖。按規(guī)則繼續(xù)處理當(dāng)前的細(xì)胞圖荒典,可以得到下一代的細(xì)胞圖酪劫,周而復(fù)始。

經(jīng)過(guò)分析之后寺董,總結(jié)一下核心邏輯:
構(gòu)建一個(gè)m * n的二維數(shù)組覆糟,數(shù)組有0和1兩個(gè)值,分別代表生存和死亡遮咖,每輪周期根據(jù)周?chē)?格的狀態(tài)分別判斷每個(gè)數(shù)組項(xiàng)的狀態(tài)并更新滩字。

下面按步驟介紹游戲的實(shí)現(xiàn)過(guò)程:

  1. 準(zhǔn)備工作,
// 定義常量御吞,棋盤(pán)寬度50麦箍,高度50,每個(gè)方塊的寬高為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對(duì)象
      matrix: [],        // 保存二維數(shù)組
      isOnGoing: false,        // 保存生命游戲的開(kāi)始或停止?fàn)顟B(tài)
    }
  }
  componentDidMount() {
    // 獲取canvas的context對(duì)象
    const canvas = document.getElementById("game-board")
    const ctx = canvas.getContext('2d')
    // 在state中保存canvas的context對(duì)象陶珠,并在回調(diào)中執(zhí)行初始化邏輯
    this.setState({ ctx }, () => {
      this.initBoard()
    })
  }
  // 初始化棋盤(pán)
  initBoard() {
    // 創(chuàng)建初始二維數(shù)組
    const matrix = Array(HEIGHT).fill().map( () => Array(WIDTH).fill(0))
    this.setState({ matrix: matrix }, () => {
      //  繪制空棋盤(pán)
      this.drawMatrix()
    })
  }
  // 繪制棋盤(pán)
  drawMatrix() {
    const { ctx, matrix } = this.state
    // 因?yàn)閏anvas是覆寫(xiě)式的邏輯挟裂,所以繪制之前必須先清空區(qū)域
    ctx.clearRect(0,0, WIDTH * ITEM_WIDTH, HEIGHT * ITEM_WIDTH); 
    // 雙重循環(huán)繪制每個(gè)方格
    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>
  }
}

實(shí)現(xiàn)這一步之后打開(kāi)當(dāng)前頁(yè)面,你已經(jīng)能看到一個(gè)50 * 50的棋盤(pán)界面


  1. 增加輸入邏輯揍诽,在componentDidMount中給canvas元素綁定clickonmousedown方法捕捉點(diǎn)擊和點(diǎn)擊拖動(dòng)事件诀蓉,然后將觸發(fā)的區(qū)域切換狀態(tài)并重繪栗竖;
componentDidMount() {
    ...
    // 綁定點(diǎn)擊事件
    canvas.addEventListener('click', this.handleClick.bind(this))
    // 綁定點(diǎn)擊拖動(dòng)事件
    canvas.onmousedown= (e) => {
      //按下后可移動(dòng)
      canvas.onmousemove = (e) => {
          const x = Math.floor(e.clientX / ITEM_WIDTH)
          const y = Math.floor(e.clientY / ITEM_WIDTH)
          this.switchSingle(x, y)
      };
      //鼠標(biāo)抬起清除綁定事件
      canvas.onmouseup = function(){
          canvas.onmousemove = null;
          canvas.onmouseup = null;
      };
    }
    ...
}
  // 切換并繪制單個(gè)方塊的方法
  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 })
    // 清空當(dāng)前位置
    ctx.clearRect(x * ITEM_WIDTH, y * ITEM_WIDTH, ITEM_WIDTH, ITEM_WIDTH); 
    // 根據(jù)當(dāng)前狀態(tài)繪制方塊
    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);
    }
  }
  // 單擊方塊的回調(diào)方法
  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)
  }

完成這一步后,你可以通過(guò)鼠標(biāo)在棋盤(pán)中點(diǎn)擊添加初始狀態(tài):


  1. 實(shí)現(xiàn)主循環(huán)邏輯
    定義startGame,pauseGame,endGame三個(gè)方法渠啤,它們分別控制主循壞的開(kāi)始狐肢,暫停和結(jié)束,并將它們分別綁定到“開(kāi)始”埃篓,“暫痛ζ海”, “停止”三個(gè)按鈕上
  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)}>開(kāi)始</button>
        <button onClick={this.pauseGame.bind(this)}>暫停</button>
        <button onClick={this.endGame.bind(this)}>停止</button>
        ...
    </div>
  }

traverse方法是主循壞中執(zhí)行的方法架专,它會(huì)遍歷二維數(shù)組同窘,并對(duì)每個(gè)數(shù)組項(xiàng)調(diào)用check方法推算其下一步的狀態(tài)

  // 遍歷所有方格
  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()
    })
  }

  // 檢查當(dāng)前方格
  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) {
      // 周?chē)?xì)胞數(shù)為3時(shí),一定為1
      return 1
    } else if(count == 2) {
      // 周?chē)?xì)胞數(shù)為2時(shí)部脚,1保持1想邦,0保持0
      return matrix[y][x]
    } else {
      // 其他情況,一定為0
      return 0
    }
  }
  // 該方法返回對(duì)應(yīng)坐標(biāo)的0 || 1狀態(tài)
  getItemValue(x, y) {
    const { matrix } = this.state
    return (matrix[y] || [])[x] || 0
  }

到這一步委刘,游戲的基本邏輯就實(shí)現(xiàn)完成丧没,可以通過(guò)開(kāi)始,暫停锡移,停止按鈕來(lái)運(yùn)行和重置生命游戲呕童,我們可以通過(guò)不同的輸入觀察到不同的執(zhí)行結(jié)果:


該項(xiàng)目的demo可以點(diǎn)擊連接后,在"生命游戲"路徑下查看
項(xiàng)目的源代碼點(diǎn)擊這里查看

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末淆珊,一起剝皮案震驚了整個(gè)濱河市夺饲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌施符,老刑警劉巖往声,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異戳吝,居然都是意外死亡浩销,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)听哭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)慢洋,“玉大人,你說(shuō)我怎么就攤上這事欢唾∏揖” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵礁遣,是天一觀的道長(zhǎng)斑芜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)祟霍,這世上最難降的妖魔是什么杏头? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任盈包,我火速辦了婚禮,結(jié)果婚禮上醇王,老公的妹妹穿的比我還像新娘呢燥。我一直安慰自己,他們只是感情好寓娩,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布叛氨。 她就那樣靜靜地躺著,像睡著了一般棘伴。 火紅的嫁衣襯著肌膚如雪寞埠。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天焊夸,我揣著相機(jī)與錄音仁连,去河邊找鬼。 笑死阱穗,一個(gè)胖子當(dāng)著我的面吹牛饭冬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播揪阶,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼昌抠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了鲁僚?” 一聲冷哼從身側(cè)響起扰魂,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蕴茴,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體姐直,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡倦淀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了声畏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撞叽。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖插龄,靈堂內(nèi)的尸體忽然破棺而出愿棋,到底是詐尸還是另有隱情,我是刑警寧澤均牢,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布糠雨,位于F島的核電站,受9級(jí)特大地震影響徘跪,放射性物質(zhì)發(fā)生泄漏甘邀。R本人自食惡果不足惜琅攘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望松邪。 院中可真熱鬧坞琴,春花似錦、人聲如沸逗抑。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)邮府。三九已至荧关,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挟纱,已是汗流浹背羞酗。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留紊服,地道東北人檀轨。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像欺嗤,于是被迫代替她去往敵國(guó)和親参萄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容