在Cocos中开垦属于你的“Minecraft”


事情是这样的,有个建模师朋友跟我聊天:

朋友:雕地形把颈椎雕坏了,能不能帮我写一个代码自动生成地形?
我:可以…

于是有了这篇文章。

今天,就带大家揭秘 “无限地形生成” 的底层技术。

这项技术不仅是《Minecraft》、《Terraria》、《No Man’s Sky》这些大作背后的核心技术之一!

更能让你用几行代码,在 Cocos 中亲手开垦出属于自己的“无限大陆”!

关键技术 —— 噪声

这里说的“噪声”,不是电流里的杂音,而是指一种连续的伪随机函数

以下是常见的噪声生成函数:

  • Perlin Noise(柏林噪声):经典,很多老游戏用。
  • Simplex Noise:改进版,性能更好。
  • OpenSimplex Noise:社区维护的版本,专门解决专利和维度问题。

它们的特点是:

  1. 看起来像随机,但其实是连续的。
  2. 相邻的输入点,输出不会差太多,像自然界里的山脉、河流一样。
  3. 输入坐标可以无限扩展,所以理论上能生成无限地图。

以下是使用柏林噪声和白噪声(纯随机)的对比图,可以看到柏林噪声生成的数值是连续的。

由于本文更关注于噪声的应用,因此关于噪声的原理就不再赘述,大家可以自行了解。

游戏里怎么用?

很多你耳熟能详的游戏,都在背后默默用着这项技术:

游戏名称
类型
噪声应用场景
特点与效果
Minecraft
沙盒建造
地形(平原、山脉、洞穴)、矿脉分布
经典案例,无限方块世界
Terraria
2D 沙盒冒险
地下洞穴系统、地形起伏
横版随机地图,每局不同
No Man’s Sky
太空探索
星球地貌、动植物生态、气候
分形+噪声生成,几乎无限的宇宙

在 Cocos Creator 里实践:用噪声生成地形

光说原理可能有点抽象,咱们直接来点实战。

在 Cocos Creator 里,配合OpenSimplexNoiseTerrain组件,我们可以很快就搞出一个“无限地图的雏形”。

我们这里将会使用开源的open-simplex-noise库,可以直接使用npm安装,并在 Cocos 工程中使用。

1. 最简单的噪声生成

我们先写一个最简单的函数:直接用噪声生成高度,不加任何花里胡哨的参数。

import { _decorator, Component, Terrain, CCInteger, HeightField } from'cc';
import OpenSimplexNoise from"open-simplex-noise";
import { type Noise2D } from"open-simplex-noise/lib/2d";

const { makeNoise2D } = OpenSimplexNoise;
const { ccclass, property, executeInEditMode } = _decorator;

@ccclass('TerrainDemo')
@executeInEditMode(true// 关键!让脚本在编辑器模式下运行
exportclass TerrainDemo extends Component {

@property(Terrain)
public terrain: Terrain = null!;

@property({ type: CCInteger, range: [09999991] })
public seed: number = 12345;

// 编辑器按钮:生成地形
@property({
    displayName: "🎲 生成地形",
    tooltip: "点击生成基础噪声地形"
  })
get generateButton() { returnfalse; }
set generateButton(value: boolean) {
    if (value) this.generateTerrain();
  }

private noise2D: Noise2D;

  onLoad() {
    this.noise2D = makeNoise2D(this.seed);
  }

// 最简单的噪声高度生成
private generateHeight(x: number, z: number): number {
    // 直接返回噪声值,范围 [-1, 1]
    returnthis.noise2D(x, z);
  }

// 生成地形的核心方法
public generateTerrain() {
    console.log('开始生成基础噪声地形...');

    const info = this.terrain.info;
    const tileCount = info.tileCount;
    const tileSize = info.tileSize;

    // 创建高度场
    const heightField = new HeightField(tileCount[0], tileCount[1]);

    // 计算地形的世界尺寸
    const terrainWorldWidth = tileCount[0] * tileSize;
    const terrainWorldHeight = tileCount[1] * tileSize;
    const startWorldX = -terrainWorldWidth / 2;
    const startWorldZ = -terrainWorldHeight / 2;

    // 为每个瓦片生成高度
    for (let z = 0; z 1]; z++) {
      for (let x = 0; x 0]; x++) {
        const worldX = startWorldX + x * tileSize;
        const worldZ = startWorldZ + z * tileSize;

        // 使用最简单的噪声生成
        const noiseValue = this.generateHeight(worldX, worldZ);

        // 转换为 Cocos Creator 地形高度格式
        const finalHeight = 32768 + noiseValue * 1000// 简单缩放

        heightField.set(x, z, finalHeight);
      }
    }

    // 应用到地形
    this.terrain.importHeightField(heightField, 1);
    this.terrain.rebuild(this.terrain.info);

    console.log('基础地形生成完成!');
  }
}

在编辑器中新建地形节点并绑定好脚本。

然后点击编辑器中的生成地形后,就可以直接在编辑器中看到效果了。

这样得到的地形是连续的,而非跳变的,尽管已经可以直接使用,但我们还需要进一步操作修改。

2. 添加高度缩放控制

原始噪声值的范围通常在 [-1, 1] 之间,这个范围对于地形高度来说太小了。

我们需要一个参数heightScale,来控制地形的整体高度范围。

在组件中添加属性:改进 generateHeight 方法

@ccclass('TerrainDemo')
@executeInEditMode(true)
exportclass TerrainDemo extends Component {
/** ... */

@property
public heightScale: number = 20// 高度缩放

private generateHeight(x: number, z: number): number {
    const n = this.noise2D(x, z); // 原始噪声值 [-1,1]
    return n * this.heightScale;  // 缩放到给定的高度范围
  }

/** ... */
}

heightScale的作用很直观,就是单纯的放大原始的噪声值:

  • heightScale = 10→ 地形高度范围 [-10, 10],比较平缓
  • heightScale = 50→ 地形高度范围 [-50, 50],起伏较大
  • heightScale = 100→ 地形高度范围 [-100, 100],高山深谷

实际应用中的经验值

  • 平原地形heightScale = 5-15
  • 丘陵地带heightScale = 20-40
  • 山地地形heightScale = 50-100
  • 极地地形heightScale = 100+

这个参数是最容易理解和调节的,也是在调试地形时首先要确定的。

3. 加入 noiseScale —— 控制采样粒度

有时候噪声“起伏得太快”,导致地图像一堆抖动的沙子。

这时候就需要设置参数noiseScale,它能拉伸噪声的坐标空间,在更小尺度的空间里采样,让地形更平滑或更紧凑。

@ccclass('TerrainDemo')
@executeInEditMode(true)
exportclass TerrainDemo extends Component {
/** ... */

@property
public noiseScale: number = 0.05// 噪声缩放

private generateHeight(x: number, z: number): number {
    const n = this.noise2D(x * this.noiseScale, z * this.noiseScale);
    return n * this.heightScale;
  }

/** ... */
}
  • noiseScale 小 → 波动慢,适合大片平原/缓坡。
  • noiseScale 大 → 波动快,适合碎石滩/陡峭小山。

下面两张图中,第二张是0.1倍的缩放,可以看到绿色的框就是第一张图0.05倍的结构。

因为噪声的尺度被放大了两倍,所以这1/4个区域采样的是之前的那部分噪声。


4. 加入 octaves、persistence 和 lacunarity 增加细节层次

自然界的地形往往不是单一频率的,而是包含:

  • 大山的起伏
  • 小丘陵的细节
  • 石块的凹凸

因此,我们常用分形噪声(Fractal Noise)的方法来叠加多层噪声。

@ccclass('TerrainDemo')
@executeInEditMode(true)
exportclass TerrainDemo extends Component {
/** ... */
@property
public octaves: number = 4// 噪声层数

@property
public persistence: number = 0.5// 持续性

@property
public lacunarity: number = 2.0// 间隙度

// 使用多层噪声生成高度
private generateHeight(x: number, z: number): number {
    let height = 0;
    let amplitude = this.heightScale;     // 初始强度
    let frequency = this.noiseScale;      // 初始频率

    for (let i = 0; i this.octaves; i++) {
      height += this.noise2D(x * frequency, z * frequency) * amplitude;
      amplitude *= this.persistence;    // 每层衰减
      frequency *= this.lacunarity;     // 每层频率增加
    }
    return height;
  }

/** ... */
}

这几个参数的含义如下:

  • heightScale:地形起伏的整体高度。
  • noiseScale:噪声的采样间距,越小越平滑,越大越碎裂。
  • octaves:叠加噪声层数,越多越细节丰富。
  • persistence:每层噪声的强度衰减,越小,细节越柔和。
  • lacunarity:每层噪声的频率增加倍数,越大,细节越密集。

可以这么理解:

  • 想要大平原noiseScale小一点,octaves 少一点。
  • 想要高山密布heightScale拉大,octaves 增加。
  • 想要碎石滩noiseScale大一点,地形就会更破碎。

这就是噪声的魅力,一切尽在参数掌控之中。

5. 利用曲线调控地形

光靠噪声函数算出来的高度,虽然已经比纯随机好看很多,但有时候地形还是“太数学”,不够自然。

比如可能会出现高原过于稀少、山峰太尖锐、或者平原面积不够大的情况。

这时候就可以用曲线来调节。

利用曲线,可以把“原始噪声值”映射成“更符合需求的高度值”。

为了方便调试,我们这里将随机种子也放到参数中:

import { RealCurve } from'cc';

@ccclass('TerrainDemo')
@executeInEditMode(true)
exportclass TerrainDemo extends Component {
/** ... */

@property(RealCurve)
  curve: RealCurve = new RealCurve();

private _seed: number = 0;
@property({ type: CCInteger, range: [-9999999999991] })
get seed() {
    returnthis._seed;
  };

set seed(seed: number) {
    this._seed = seed;
    // 修改种子后重新创建噪声生成器
    this.noise2D = makeNoise2D(this.seed);
  }

// 在地形生成中使用曲线调整
private applyHeightCurve(noiseHeight: number): number {
    // 将噪声值标准化到 [0, 1] 范围
    const normalizedNoise = (noiseHeight + this.heightScale) / (2 * this.heightScale);

    // 应用曲线来调整高度分布
    const curveValue = this.curve.evaluate(normalizedNoise);

    // 转换为最终高度
    return curveValue * this.heightScale;
  }
/** ... */
}

这样我们就能通过拖动曲线点,直观地控制地形比例:

  • 平原想大一点?就在曲线前段拉平。

  • 山脉想更陡峭?就在后段加陡。

  • 想要大面积海洋?把 0 附近压低。

由于小尺度的地形很难看出效果,所以这里我将地形瓦片调整到了5*5。

然后我调了一个曲线,可以生成很好看的山地和平原地形。



无限地图是怎么做到的?

到目前为止,原理其实非常简单:

噪声函数的输入是坐标 (x, z),输出是一个确定的值。

所以当玩家走到地图边界时,只要继续往外算新的坐标点,地形就会自然延展,理论上是无限的。

配合分块加载技术,玩家周围可以生成若干块地形。

玩家移动时,卸载远处的块,加载新的块,这样就能模拟出无限世界,是不是很酷!

总结与进阶

噪声函数是一个看似简单却威力巨大的工具,它能生成自然、连续、可控的数据。

配合参数和曲线,就能灵活定制出符合游戏设计需求的地形。

再加上分块加载,就能实现无限可玩的地图。

所以,下次你在 Minecraft 里挖矿时,别忘了背后支撑这一切的,可能就是几行噪声函数的代码。

从地形到生态

到这里我们只是生成了高度地图,但游戏世界远不止“凹凸的地面”。

噪声的妙用在于,它可以被重复利用,生成更多“有机”的内容:

  • 环境划分
    用一组噪声来决定气候区,比如高纬度是雪原,低纬度是沙漠,中间是森林。

  • 生物群落
    可以用噪声来分布怪物或动物,例如:某些区域狼群密集,某些地方只有羊驼。

  • 植被分布
    再加一层噪声,就能控制森林的浓密度:某些块长满树,某些块只有零星几棵。

  • 矿物/资源
    类似的思路还能用来生成地下矿脉分布,像《Minecraft》就是这么干的。

换句话说:

地形噪声只是“基底”,你可以继续在它上面叠加生态噪声资源噪声气候噪声……

这样一步步,就能拼出一个真正无限而且生机勃勃的世界

作者介绍:

感谢还有醋v的投稿,更多干货内容,可以关注他的公众号。

有需要 demo 的朋友可以在以下链接获取:

demo 地址:https://store.cocos.com/app/detail/8191

感谢阅读,希望这篇文章对你有所帮助!

欢迎在评论区交流心得,一起进步!

关注 Cocos 官方公众号

获取更多干货内容