魔改Minecraft Classic网页重制版的地形生成器

为了庆祝Minecraft十周年,一份用JavaScript重制的Classic 0.0.23a_01被上传到了Minecraft的官网上。虽然此版本的内容不多,但作为试验地形生成算法的原材料却十分合适(尤其是便于调试),于是我把资源文件下载到了本地,以便进行魔改。
重制版原来的JavaScript脚本把游戏本体和地形生成器分开了,正好方便修改。因为我想把地形生成得独特一点,所以选择使用Worley Noise来营造一种异世界的风格。
Worley Noise有多个变种,最简单的是直接取每个点到最近的随机生成的特征点的距离作为该点的高度,效果如下(为了直观地看出地形的高度差异,我把地表不同的高度用不同颜色的布料来填充):

通常将像素到特征点的距离称为F values, 如F1是到最近的特征点的距离,F2是到第二近的特征点的距离,以此类推。如果把F2的值作为输出,效果如下:

那如果用F2-F1呢?结果是这样的:

可以看到,生成的地形呈现出了规则的多边形形状,其实就是特征点所形成的泰森多边形。

最后还是把修改后的代码(RandomLevelWorker.js)放上来,供大家参考:

// World generation as a worker.
/**
 * Creates a pseudo-random value generator. The seed must be an integer.
 *
 * Uses an optimized version of the Park-Miller PRNG.
 * http://www.firstpr.com.au/dsp/rand31/
 */
function Random(seed) {
	this._seed = seed % 2147483647;
	if (this._seed <= 0) this._seed += 2147483646;
}

/**
 * Returns a pseudo-random value between 1 and 2^32 - 2.
 */
Random.prototype.next = function () {
	return this._seed = this._seed * 16807 % 2147483647;
};

Random.prototype.nextInt = function (max) {
	return Math.floor(this.nextFloat() * max);
};

/**
 * Returns a pseudo-random floating point number in range [0, 1).
 */
Random.prototype.nextFloat = function (opt_minOrMax, opt_max) {
	// We know that result of next() will be 1 to 2147483646 (inclusive).
	return (this.next() - 1) / 2147483646;
};

var RandomLevel = function () {

	var progress = {
		string: "",
		percent: 0,
		tiles: null
	}

	this.createLevel = function (seed, xSize, zSize, ySize) {
		//this.progressRenderer.progressStart("Generating level");
		//return;
		var random = new Random(seed);


		this.xSize = xSize;
		this.zSize = zSize;
		this.ySize = 64;
		this.random = random.nextFloat();
		this.tiles = [];//new Array(xSize*zSize*ySize);//[];
		this.fillQueue = [];

		// Bobcorn's Edition
		class WorleyNoise {
			constructor(config) {
				console.log(config.dim);
				config = config || {};
				if (config.dim !== 2 && config.dim !== 3 && config.dim !== undefined)
					throw '"dim" can be 2 or 3';

				this._dim = config.dim || 2;
				//this._rng = new Alea(config.seed || Math.random());
				this._points = [];

				for (let i = 0; i < config.numPoints; i++) {
					console.log(i);
					this._points.push({
						x: this.random(),
						y: this.random(),
						z: this.random()
					});
				}
			}

			addPoint(coord) {
				this._points.push(coord);
			}

			printPoints() {
				console.log(this._points)
			}

			getEuclidean(coord, k) {
				return Math.sqrt(this._calculateValue(coord, k, euclidean));
			}

			getManhattan(coord, k) {
				return this._calculateValue(coord, k, manhattan);
			}

			_calculateValue(coord, k, distFn) {
				let minDist;
				this._points.forEach(p => { p.selected = false; });

				for (let j = 0; j < k; ++j) {
					let minIdx;
					minDist = Number.POSITIVE_INFINITY;

					for (let i = 0; i < this._points.length; ++i) {
						const p = this._points[i];
						const dz = this._dim === 2 ? 0 : coord.z - p.z;
						const dist = distFn(coord.x - p.x, coord.y - p.y, dz);

						if (dist < minDist && !p.selected) {
							minDist = dist;
							minIdx = i;
						}
					}

					this._points[minIdx].selected = true;
				}

				return minDist;
			}
		}

		const euclidean = (dx, dy, dz) => dx * dx + dy * dy + dz * dz;
		const manhattan = (dx, dy, dz) => Math.abs(dx) + Math.abs(dy) + Math.abs(dz);

		this.genWorley = function (aint) {
			var i = this.xSize;
			var j = this.zSize;
			var k = this.ySize;

			wor = new WorleyNoise({});

			for (var ia = 0; ia < 50; ++ia) {
				wor.addPoint({ x: Math.random(), y: Math.random(), z: 0 });
			}

			wor.printPoints();

			for (var l = 0; l < i; ++l) { // x direction
				progress.percent = l * 100 / (this.xSize - 1);
				self.postMessage(progress);

				for (var i1 = 0; i1 < j; ++i1) { // z direction
					var j1;
					//var loc = parseInt( ((j1 = parseInt(aint[l + i1 * i]) + [Y OFFSET]) * this.zSize + i1 + [Z OFFSET]) * this.xSize + l + [X OFFSET]); Get the height at (l ,i1)
					//var block = (parseInt(this.tiles[((j1 + 1) * this.zSize + i1) * this.xSize + l]) & 255)

					var ht = wor.getEuclidean({ x: l / this.xSize, y: i1 / this.zSize, z: 0 }, 1) * 160 + 20; //F1
					//var ht = wor.getEuclidean({x: l/this.xSize,y: i1/this.zSize,z: 0}, 2) * 160 + 20; //F2
					//var ht = (wor.getEuclidean({x: l/this.xSize,y: i1/this.zSize,z: 0}, 2) - wor.getEuclidean({x: l/this.xSize,y: i1/this.zSize,z: 0}, 1)) * 220 + 20; //F2-F1
					ht = parseInt(Math.min(Math.max(ht, 1), 63));

					//Make ground
					var loc = parseInt(((j1 = parseInt(ht)) * this.zSize + i1) * this.xSize + l);
					var loc1 = parseInt(((j1 = parseInt(ht) - 1) * this.zSize + i1) * this.xSize + l);
					this.tiles[loc] = ht % 13 + 24;
					this.tiles[loc1] = (ht - 1) % 13 + 24;
				}
			}

		}

		progress.string = "Filling Air..";
		var j2 = this.xSize;
		var k2 = this.zSize;

		j1 = this.ySize;

		var l2;
		var i3;

		for (l = 0; l < j2; ++l) {
			progress.percent = l * 100 / (xSize - 1);
			self.postMessage(progress);

			for (i1 = 0; i1 < k2; ++i1) {
				for (i3 = 0; i3 < j1; ++i3) {
					var j3 = (i3 * zSize + i1) * xSize + l;
					this.tiles[j3] = 0;
				}
			}
		}

		progress.string = "Generating Worley..";
		this.genWorley();

		progress.tiles = this.tiles;
		progress.string = "";
		self.postMessage(progress);
	}
}

function startGeneration(obj) { //{worldSize: worldSize, seed: props.seed, seedrandom: seedrandom}
	var level = new RandomLevel();
	//console.log(level)
	var width = obj.worldSize;
	var depth = obj.worldSize;
	var height = 64;
	level.createLevel(obj.seed, width, depth, height);
}

self.addEventListener('message', function (e) {
	//console.log("worker get "+e.data);
	startGeneration(e.data)
}, false);

Special thanks to:

图形学—Cell/Worley Noise - 腾讯游戏学院
Zsoltc - Github
Classic remake data value - Minecraft Wiki

posted @ 2020-10-17 22:35  DevBobcorn  阅读(806)  评论(0编辑  收藏  举报