《泰拉瑞亚》是一款经典的 2D 沙盒游戏,其世界由程序生成的丰富地形和地下洞穴网络构成。本文用 Java 实现泰拉瑞亚风格的地形生成系统,重点讲解Perlin 噪声高度图生成地表起伏、细胞自动机(Cellular Automaton)平滑洞穴结构,以及矿石簇的随机散落与生物群落划分。读者将掌握程序生成内容(PCG)的核心技术,并理解噪声与自动机在地图生成中的协同作用。
一、地形生成的分层架构
泰拉瑞亚的世界生成可以分为三个层次:
- 地表高度图:使用 Perlin 噪声生成连续的山丘与峡谷,决定每个地表列的泥土层深度。
- 洞穴网络:使用细胞自动机在地下区域生成连通的空洞,模拟天然溶洞与矿道。
- 内容填充:在生成好的地形中放置矿石、地下水和生物群落边界。
二、Perlin 噪声高度图
Perlin 噪声是一种梯度噪声,能够产生自然连续的随机起伏。我们实现一个简化版 1D Perlin 噪声用于地表高度:
import java.util.*;
/**
* 简化版 1D/2D Perlin 噪声
*/
class PerlinNoise {
private final int[] permutation;
private final int[] p;
public PerlinNoise(long seed) {
Random rand = new Random(seed);
permutation = new int[256];
for (int i = 0; i < 256; i++) permutation[i] = i;
// Fisher-Yates 洗牌
for (int i = 255; i > 0; i--) {
int j = rand.nextInt(i + 1);
int tmp = permutation[i];
permutation[i] = permutation[j];
permutation[j] = tmp;
}
p = new int[512];
for (int i = 0; i < 512; i++) p[i] = permutation[i % 256];
}
private double fade(double t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
private double lerp(double t, double a, double b) {
return a + t * (b - a);
}
private double grad(int hash, double x) {
return (hash & 1) == 0 ? x : -x;
}
private double grad(int hash, double x, double y) {
int h = hash & 3;
double u = h < 2 ? x : y;
double v = h < 2 ? y : x;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
/**
* 1D Perlin 噪声
*/
public double noise1D(double x) {
int X = (int) Math.floor(x) & 255;
x -= Math.floor(x);
double u = fade(x);
return lerp(u, grad(p[X], x), grad(p[X + 1], x - 1));
}
/**
* 2D Perlin 噪声
*/
public double noise2D(double x, double y) {
int X = (int) Math.floor(x) & 255;
int Y = (int) Math.floor(y) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
double u = fade(x);
double v = fade(y);
int A = p[X] + Y, B = p[X + 1] + Y;
return lerp(v, lerp(u, grad(p[A], x, y), grad(p[B], x - 1, y)),
lerp(u, grad(p[A + 1], x, y - 1), grad(p[B + 1], x - 1, y - 1)));
}
/**
* 叠加多倍频噪声(FBM - Fractal Brownian Motion)
*/
public double fbm2D(double x, double y, int octaves, double persistence) {
double total = 0;
double frequency = 1;
double amplitude = 1;
double maxValue = 0;
for (int i = 0; i < octaves; i++) {
total += noise2D(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= 2;
}
return total / maxValue;
}
}
三、世界网格与地形生成
我们将世界表示为二维瓦片网格,每个格子有类型(空气、泥土、石头、矿石等):
enum TileType {
AIR, DIRT, STONE, ORE_IRON, ORE_GOLD, ORE_DIAMOND, GRASS, WATER
}
class World {
int width;
int height;
TileType[][] tiles;
int[] surfaceHeight; // 每列的地表高度
public World(int width, int height) {
this.width = width;
this.height = height;
this.tiles = new TileType[width][height];
this.surfaceHeight = new int[width];
for (int x = 0; x < width; x++) {
Arrays.fill(tiles[x], TileType.AIR);
}
}
}
class TerrainGenerator {
private final PerlinNoise noise;
private final Random rand;
public TerrainGenerator(long seed) {
this.noise = new PerlinNoise(seed);
this.rand = new Random(seed);
}
/**
* 生成完整世界
*/
public World generate(int width, int height) {
World world = new World(width, height);
// 1. 生成地表高度图
generateSurface(world);
// 2. 填充地下泥土与石头
fillUnderground(world);
// 3. 使用细胞自动机生成洞穴
generateCaves(world);
// 4. 放置矿石
placeOres(world);
// 5. 表面种草
growGrass(world);
return world;
}
/**
* 1. 使用 Perlin 噪声生成地表起伏
*/
private void generateSurface(World world) {
int baseHeight = world.height / 3; // 基准高度约在画面上方 1/3 处
for (int x = 0; x < world.width; x++) {
double nx = x * 0.015; // 噪声采样频率
double n = noise.noise1D(nx); // [-1, 1]
int h = baseHeight + (int) (n * 20); // 高度偏移 ±20 格
world.surfaceHeight[x] = h;
for (int y = h; y < world.height; y++) {
world.tiles[x][y] = TileType.DIRT;
}
}
}
/**
* 2. 分层填充:上方为泥土,深层为石头
*/
private void fillUnderground(World world) {
for (int x = 0; x < world.width; x++) {
int surface = world.surfaceHeight[x];
for (int y = surface; y < world.height; y++) {
int depth = y - surface;
if (depth > 15 + rand.nextInt(5)) {
world.tiles[x][y] = TileType.STONE;
}
}
}
}
四、细胞自动机:洞穴生成
细胞自动机通过简单的局部规则迭代,能够生成类似自然洞穴的有机结构。算法步骤:
- 初始化:在地下区域随机填充”墙”(石头)和”空”(空气)。
- 迭代:对每个格子,统计周围 8 邻域的墙数量。若墙数量超过阈值则变为墙,否则变为空。
- 结果:经过数次迭代后,空区域形成连通的洞穴网络。
/**
* 3. 细胞自动机生成洞穴
*/
private void generateCaves(World world) {
int w = world.width;
int h = world.height;
boolean[][] caveMap = new boolean[w][h]; // true = 墙/石头, false = 空/空气
// 初始化:仅在地下深层区域生成洞穴,地表附近保留
for (int x = 0; x < w; x++) {
int surface = world.surfaceHeight[x];
for (int y = 0; y < h; y++) {
if (y < surface + 5) {
caveMap[x][y] = true; // 地表附近不生成洞穴
} else {
caveMap[x][y] = rand.nextDouble() < 0.48; // 48% 概率为墙
}
}
}
// 细胞自动机迭代 5 次
for (int iter = 0; iter < 5; iter++) {
boolean[][] next = new boolean[w][h];
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
int wallCount = countWallNeighbors(caveMap, x, y, w, h);
if (wallCount > 4) {
next[x][y] = true;
} else if (wallCount < 4) {
next[x][y] = false;
} else {
next[x][y] = caveMap[x][y];
}
}
}
caveMap = next;
}
// 将洞穴映射回世界网格:false(空)替换为 AIR
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
if (!caveMap[x][y] && world.tiles[x][y] != TileType.AIR) {
world.tiles[x][y] = TileType.AIR;
}
}
}
}
private int countWallNeighbors(boolean[][] map, int cx, int cy, int w, int h) {
int count = 0;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue;
int nx = cx + dx;
int ny = cy + dy;
if (nx < 0 || nx >= w || ny < 0 || ny >= h) {
count++; // 边界视为墙
} else if (map[nx][ny]) {
count++;
}
}
}
return count;
}
五、矿石放置与生物群落
/**
* 4. 使用 Perlin 噪声控制矿石分布密度
*/
private void placeOres(World world) {
for (int x = 0; x < world.width; x++) {
for (int y = 0; y < world.height; y++) {
if (world.tiles[x][y] != TileType.STONE) continue;
double oreNoise = noise.noise2D(x * 0.08, y * 0.08);
int depth = y - world.surfaceHeight[x];
if (oreNoise > 0.6 && depth > 20) {
world.tiles[x][y] = TileType.ORE_DIAMOND;
} else if (oreNoise > 0.35 && depth > 10) {
world.tiles[x][y] = TileType.ORE_GOLD;
} else if (oreNoise > 0.15 && depth > 5) {
world.tiles[x][y] = TileType.ORE_IRON;
}
}
}
}
/**
* 5. 表面种草:泥土上方暴露于空气的格子变为草地
*/
private void growGrass(World world) {
for (int x = 0; x < world.width; x++) {
for (int y = 0; y < world.height - 1; y++) {
if (world.tiles[x][y] == TileType.DIRT && world.tiles[x][y - 1] == TileType.AIR) {
world.tiles[x][y] = TileType.GRASS;
}
}
}
}
六、世界可视化与连通性检测
/**
* 打印 ASCII 地图预览
*/
public void printWorld(World world) {
for (int y = 0; y < world.height; y++) {
StringBuilder line = new StringBuilder();
for (int x = 0; x < world.width; x++) {
char c = switch (world.tiles[x][y]) {
case AIR -> ' ';
case DIRT -> '.';
case STONE -> '#';
case GRASS -> 'w';
case ORE_IRON -> 'i';
case ORE_GOLD -> 'g';
case ORE_DIAMOND -> 'D';
case WATER -> '~';
};
line.append(c);
}
System.out.println(line);
}
}
/**
* BFS 检测地下最大的连通空洞面积(验证洞穴连通性)
*/
public int largestCaveArea(World world) {
boolean[][] visited = new boolean[world.width][world.height];
int maxArea = 0;
for (int x = 0; x < world.width; x++) {
for (int y = 0; y < world.height; y++) {
if (world.tiles[x][y] == TileType.AIR && !visited[x][y]) {
int area = bfsCave(world, x, y, visited);
maxArea = Math.max(maxArea, area);
}
}
}
return maxArea;
}
private int bfsCave(World world, int sx, int sy, boolean[][] visited) {
int area = 0;
Queue<int[]> q = new LinkedList<>();
q.add(new int[]{sx, sy});
visited[sx][sy] = true;
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
while (!q.isEmpty()) {
int[] cur = q.poll();
area++;
for (int i = 0; i < 4; i++) {
int nx = cur[0] + dx[i];
int ny = cur[1] + dy[i];
if (nx >= 0 && nx < world.width && ny >= 0 && ny < world.height
&& !visited[nx][ny] && world.tiles[nx][ny] == TileType.AIR) {
visited[nx][ny] = true;
q.add(new int[]{nx, ny});
}
}
}
return area;
}
public static void main(String[] args) {
TerrainGenerator gen = new TerrainGenerator(12345L);
World world = gen.generate(80, 30);
gen.printWorld(world);
System.out.println("Largest connected cave area: " + gen.largestCaveArea(world));
}
}
七、复杂度分析
- Perlin 噪声生成:$O(W \times H)$,每个像素独立计算。
- 细胞自动机迭代:$O(K \times W \times H)$,$K$ 为迭代次数(通常 4~6 次)。
- 连通性检测(BFS):$O(W \times H)$,遍历整个网格一次。
- 总生成时间:对于 1000×500 的世界,在 Java 中可在毫秒级完成。
八、总结
本文实现了泰拉瑞亚风格的地形生成系统,通过Perlin 噪声创造自然连续的地表起伏,通过细胞自动机迭代生成有机的洞穴网络,再通过噪声阈值控制矿石的空间分布。核心思想是分层生成:先宏观结构(高度图),再微观细节(洞穴与矿物)。读者可以进一步扩展水体流动模拟、生物群落温度湿度图、以及更复杂的结构生成(如地下城、神庙)。