值此七夕佳节之际,正适合讨论创造生命的话题。虽然你未必有机会谈一笔几个亿的大生意,但有了 JavaScript,在网页里征服几百万个像素还是绰绰有余的——这就是我们这次的主题,康威生命游戏。
康威生命游戏,是英国数学家 John Horton Conway 发明的一种元胞自动机(记得这个概念吗?它可以用来证明 CSS 的图灵完备性)。这个游戏的规则(亦即代码实现)极为简单,但却能得到相当令人惊叹的「演化」式效果。得益于其规则的简单性,本文其实更可以当作一篇 WebGL 的入门教程——当然,还是基于我们自己的 WebGL 基础库 Beam 做了大幅简化的。
背景
所谓生命游戏,其实只是一套在二维网格上进行的计算规则而已。假设每个网格内有一个细胞,它在下一个时刻的生死,取决于相邻 8 个网格中的活细胞数量:
- 如果周围活细胞过多,这个细胞会因为资源匮乏而死去(内卷)。
- 如果周围活细胞过少,它也会因为太孤单而死去(人口过少)。
- 如何周围活细胞数量合适,死细胞所在网格也可以变成存活状态(繁殖)。
这套规则的具体实现可以自由发挥,其中康威的版本最为经典,但其实也很简单:
- 如果当前细胞存活,且周围活细胞数为 2 或 3,保持原样。
- 如果当前细胞存活,且周围活细胞数小于 2,该细胞死亡。
- 如果当前细胞存活,且周围活细胞超过 3 个,该细胞死亡。
- 如果当前细胞死亡,且周围活细胞为 3 个,该细胞转为存活。
理解这几条规则后,就很容易解读下面这种周期状态了:
相对更复杂一些的,是可持续的繁殖模式。像这样:
不过这显然还不是最壮观的景象。本文题图中的效果要比这些简单图案复杂得多,在后面我们会看到如何编码实现它。
那么,我们要从哪里开始呢?在进入 WebGL 之前,不妨先用朴素的实现练练手吧。
朴素实现
如果仅仅给定上面这几条生命游戏规则,不限制实现方式和性能,相信很多做题家同行们应该随手就能写出来——只要用装着 0
或 1
的 number[][]
二维数组来存储状态,然后逐帧更新即可:
function update (oldCells) {
const newCells = []
for (let i = 0; i < oldCells.length; i++) {
const row = []
for (let j = 0; j < oldCells[i].length; j++) {
const oldCell = get(oldCells, i, j)
const total = (
get(oldCells, i - 1, j - 1) +
get(oldCells, i - 1, j) +
get(oldCells, i - 1, j + 1) +
get(oldCells, i, j - 1) +
get(oldCells, i, j + 1) +
get(oldCells, i + 1, j - 1) +
get(oldCells, i + 1, j) +
get(oldCells, i + 1, j + 1)
)
let newCell = 0
if (oldCell === 0) {
if (total === 3) newCell = 1
}
else if (total === 2 || total === 3) newCell = 1
row.push(newCell)
}
newCells.push(row)
}
return newCells
}