65.9K
CodeProject 正在变化。 阅读更多。
Home

HTML5多生命游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (10投票s)

2011 年 10 月 20 日

CPOL

6分钟阅读

viewsIcon

32210

downloadIcon

780

Conway生命游戏的HTML5版本,但有多个生命形式!

引言

本文介绍使用新的 Canvas 元素实现的 Conway 生命游戏的 HTML5 版本。我之前写过一篇介绍 WPF/XBAP 版本生命游戏文章,可以在 这里 查看。我并非显示缺乏想象力,而是为了对比两种不同的编码风格。我的结论是,对于小型应用程序,HTML5/JavaScript 编码大约是 WPF 的两倍容易,并且具有真正跨平台的优势。在我尝试的五种主流浏览器上的性能也非常出色。

背景

有关更多细节,请参阅我之前的 WPF 文章,或者直接谷歌搜索“Conway 生命游戏”。本质上,生命游戏模拟的是在网格上生存的简单自动机,它们根据周围邻居的数量来决定生死。你需要恰好有三个邻居才能诞生,有两个或三个邻居才能继续存活。为了避免边缘效应,网格通常有两种模型:要么是无限网格,要么是环面,其中每个边缘都与它的对面边缘相连。

在这个版本的生命游戏中,我试图使其尽可能地与 WPF 版本相似,但我做了一个重要的改变,即实现了多生命形式版本。在这个游戏中,有四种可能的生命形式,每种都显示为不同的颜色。每种生命形式都遵循正常的规则,并且它们之间不相互作用,除了如果一个单元格被一种类型占据,就不能被另一种类型填充。另外,当一种类型诞生时,我使用了简单的规则,即在固定的顺序中,第一个拥有三个邻居的类型会诞生。我曾想过尝试开发某种捕食者/猎物版本的生命游戏规则,但认为最好还是坚持基本规则,只做上述一种变体。

Using the Code

代码只包含一个文件,文件大小仅为 8KB!核心代码是环面或网格,我将其实现为一个名为 LifeTorus 的 JavaScript 类,如下所示:

function LifeTorus(size) {
    this.size = size;
    var count = size * size;
    this.torus = new Array(count);

    this.clear = function () {
        for (var i = 0; i < count; i++)
            this.torus[i] = 0;// 0 means empty for convenience and speed
    };

    // returns count of the number of neighbours of each kind
    this.getNeighbours = function (x, y) {
        var count = [0, 0, 0, 0, 0];
        // prev row
        count[this.get(x - 1, y - 1)]++;
        count[this.get(x, y - 1)]++;
        count[this.get(x + 1, y - 1)]++;

        // this row
        count[this.get(x - 1, y)]++;
        count[this.get(x + 1, y)]++;

        // next row
        count[this.get(x - 1, y + 1)]++;
        count[this.get(x, y + 1)]++;
        count[this.get(x + 1, y + 1)]++;

        return count;
    };

    this.get = function (x, y) {
        return this.torus[this.getIndex(x, y)];
    };

    this.set = function (x, y, value) {
        this.torus[this.getIndex(x, y)] = value;
    };

    // Treats the two dimensional array as a torus, i.e.
    // the top and bottom edges of the array are adjacent and the left and right edges
    // are adjacent.
    this.getIndex = function (x, y) {
        if (x < -1 || y < -1 || x > size || y > size)
            throw "Index out of bounds";
        if (x == -1)
            x = size - 1;
        else if (x == size)
            x = 0;
        if (y == -1)
            y = size - 1;
        else if (y == size)
            y = 0;
        return x + y * this.size;
    };

    this.clear();
}       

该类实现了一个 NxN 数组,但内部存储为一个一维数组,因此要访问元素 (i, j),你需要访问索引 = i * N + j。这基本上就是 getIndex() 函数所做的,但有一个小技巧来实现环面。所以,行 -1 被映射到行 N-1,行 N 被映射到行 0,列也是如此。getIndex 函数接着被一个简单的设置值和获取值函数使用,这些函数又被名为 getNeighbours() 的主函数使用,该函数返回一个长度为 5 的数组,第一个元素不使用,其余四个元素是每种生命形式的数量。第一个元素不使用的原因是简化代码,因为生命形式以整数形式存储在网格中,例如,单元格值为 0 表示为空,值为 1 表示生命形式类型 1。唯一另一个函数是 clear() ,它将所有值设置为 0 (空)。

LifeTorus 类实现了我的模型,编写起来相对容易。将其渲染到 Canvas 也并不难,但我确实遇到了一个问题,即将任何鼠标函数(如 mousedown mousemove)得到的全局鼠标坐标转换为相对于我的 Canvas 元素的坐标。经过一番搜索,我在 www.stackoverflow.com 上找到了 Ryan Artecona 提供的一个优雅的解决方案,如下所示:

function relMouseCoords(event) {
    var totalOffsetX = 0;
    var totalOffsetY = 0;
    var canvasX = 0;
    var canvasY = 0;
    var currentElement = this;

    do {
        totalOffsetX += currentElement.offsetLeft;
        totalOffsetY += currentElement.offsetTop;
    }
    while (currentElement = currentElement.offsetParent)

    canvasX = event.pageX - totalOffsetX;
    canvasY = event.pageY - totalOffsetY;

    return { x: canvasX, y: canvasY }
}
HTMLCanvasElement.prototype.relMouseCoords = relMouseCoords; 

上面的代码在我测试的浏览器上都能正常工作,并且考虑了页面滚动。简单来说,它将所有父级的偏移量相加,然后从提供的坐标中减去。

顺便提一下:为了使事情更有趣和更高效,我使用了两个 Canvas 元素,一个叠在另一个上面,而不是单个 Canvas。Z 顺序(显示顺序)靠下的 Canvas 是第一个,我用它来绘制网格和背景颜色,第二个 Canvas 仅用于渲染生命形式。用于实现此目的的 HTML 如下所示:

<div style="position:relative">
<canvas id='canvas2' width='641' height='641' on></canvas> 
<!-- Lowest in Z-order - provides background -->
<canvas id='canvas1' width='641' height='641' on>
Canvas is not supported by this browser.</canvas>
</div>

我使用 CSS 定位了两个 Canvas 元素。关键是它们需要放置在一个具有 position: relative 的 <div> 中,并且嵌入的 Canvas 样式表设置为 position: absolute,top 和 bottom 都设置为 0。

其余代码的大部分如下所示:

function advance() {
	// torus1 contains the current model, process into torus2 then swap the
	// references so torus1 refers to the next generation
	var _population = 0;
	for (var x = 0; x < _size; x++)
		for (var y = 0; y < _size; y++) {
			var neighbours = _torus1.getNeighbours(x, y);// dim 5 array
			var alive = 0;
			var kind = _torus1.get(x, y);
			if (kind > 0) {
				// it's alive - it will stay alive if it has 
				// 2 or 3 neighbours
				var count = neighbours[kind];
				alive = (count == 2 || count == 3) ? kind : 0;
			}
			else {
				// It's dead but will be born 
				// if any "kind" has exactly 3 neighbours
				// This isn't "fair" but we use the 
				// first kind that has three neighbours
				for (kind = 1; kind <= 4 && alive == 0; kind++) {
					if (neighbours[kind] == 3)
						alive = kind;
				}
			}
			_torus2.set(x, y, alive);
			if (alive)
				_population++;
		}

	var temp = _torus1; // arrays are only references!
	_torus1 = _torus2;
	_torus2 = temp;
	render();
	generation.textContent = "Generation: " + String(++_generation);
	population.textContent = "Population: " + String(_population);
	if (_animate)
		setTimeout("advance()", 50);
}

function renderCanvas(canvas, size, torus) {
	// read from LifeTorus and write to canvas
	var context = canvas.getContext('2d');
	context.fillStyle = '#ff7f50';
	context.clearRect(0, 0, size * _cellSize, size * _cellSize);
	for (var x = 0; x < size; x++)
		for (var y = 0; y < size; y++) {
			var kind = _torus1.get(x, y) - 1;
			if (kind >= 0) {
				context.fillStyle = color_menu0.options[kind].value;
				context.fillRect(x * _cellSize, y * _cellSize, 
					_cellSize, _cellSize);
			}
		}
}

function render() {
	renderCanvas(canvas1, _size, _torus1);
}

function drawGrid() {
	// Only ever called once!
	var context = canvas2.getContext('2d'); // canvas2 is the background canvas
	context.strokeStyle = '#808080';
	context.beginPath();
	for (var i = 0; i <= _size; i++) {
		// Draw vertical lines
		context.moveTo(i * _cellSize + 0.5, 0.5);
		context.lineTo(i * _cellSize + 0.5, _size * _cellSize);
		// Draw horizontal lines
		context.moveTo(0.5, i * _cellSize + 0.5);
		context.lineTo(_size * _cellSize, i * _cellSize + 0.5);
	}
	context.stroke();
}

上面代码中主要关注的函数是 advance()render()drawGrid()advance() 函数简单地遍历每个单元格,获取当前生命形式的类型(如果为空则为 0)以及每种类型的邻居数量。然后,它遵循我前面提到的规则来设置该单元格为空或以适当的类型存活。函数结束时,它会检查 animate 标志是否为 true - 如果是,它调用 setTimeout 来调用自身(即 advance),延迟 50ms - 这与我在 WPF 版本中使用的方法类似。

render() 函数遍历单元格,如果存活,则用该类型的相应颜色填充单元格的矩形。我们在这里采取了一个捷径,使用类型减一作为颜色选择器 color_menu0 附加选项数组的索引。

最后,drawGrid() 函数绘制一系列垂直和水平线来分隔单元格边界。唯一值得一提的是,它只绘制一次,并且绘制到 canvas1 - 请记住 canvas2 canvas1 之上,而 canvas2 用于绘制生命形式。另一个小细节是,为了获得清晰的线条,你需要将偏移量绘制为 0.5,否则,如果偏移量为 0.0,则厚度为 1 的线条会出现在每条线两侧的单元格中(这与 WPF 中的情况相同)。

关注点

我没有尝试让我的按钮看起来与 WPF 按钮相同。我了解到可以使用 CSS3 制作一些漂亮的按钮,但如果你只想要一个带有图像和文本的简单按钮,那么同时垂直居中文本和图像是困难的。但这只是一个小问题,因为整体开发相对简单,事实上,由于所有现代浏览器似乎都内置了开发者工具,你甚至不需要像 VS2010 这样的 IDE。顺便说一句,Firefox 有一个名为 Firebug 的漂亮的开发者附加组件。

历史

除了考虑不同的游戏规则外,我还考虑过包含一些知名模式列表,并允许用户将它们拖放到画布上。这当然是可能的,例如使用第三个 canvas 叠加在另外两个和列表之上。最终,我决定不让intended to be a comparison of HTML5 and WPF 的内容变得混乱。顺便说一句,我无法将 Silverlight 纳入比较,因为它没有 Canvas 元素的等价物。

© . All rights reserved.