每日算法 — 使用java实现泰拉瑞亚:2D地形生成与洞穴细胞自动机

《泰拉瑞亚》是一款经典的 2D 沙盒游戏,其世界由程序生成的丰富地形和地下洞穴网络构成。本文用 Java 实现泰拉瑞亚风格的地形生成系统,重点讲解Perlin 噪声高度图生成地表起伏、细胞自动机(Cellular Automaton)平滑洞穴结构,以及矿石簇的随机散落与生物群落划分。读者将掌握程序生成内容(PCG)的核心技术,并理解噪声与自动机在地图生成中的协同作用。

一、地形生成的分层架构

泰拉瑞亚的世界生成可以分为三个层次:

  1. 地表高度图:使用 Perlin 噪声生成连续的山丘与峡谷,决定每个地表列的泥土层深度。
  2. 洞穴网络:使用细胞自动机在地下区域生成连通的空洞,模拟天然溶洞与矿道。
  3. 内容填充:在生成好的地形中放置矿石、地下水和生物群落边界。

二、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;
                }
            }
        }
    }

四、细胞自动机:洞穴生成

细胞自动机通过简单的局部规则迭代,能够生成类似自然洞穴的有机结构。算法步骤:

  1. 初始化:在地下区域随机填充”墙”(石头)和”空”(空气)。
  2. 迭代:对每个格子,统计周围 8 邻域的墙数量。若墙数量超过阈值则变为墙,否则变为空。
  3. 结果:经过数次迭代后,空区域形成连通的洞穴网络。
    /**
     * 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 噪声创造自然连续的地表起伏,通过细胞自动机迭代生成有机的洞穴网络,再通过噪声阈值控制矿石的空间分布。核心思想是分层生成:先宏观结构(高度图),再微观细节(洞穴与矿物)。读者可以进一步扩展水体流动模拟、生物群落温度湿度图、以及更复杂的结构生成(如地下城、神庙)。