Flame游戏开发——噪声与地形实现(1)

85 阅读3分钟

— 基于真实源码的 FBM 取样、确定性随机与地形分类

image.png

摘要

本文对噪声子系统进行工程化剖析:以 fbm 为核心的噪声合成、以 tile 粒度 的地形分类、以及以 种子 + 坐标哈希 保证重现性的随机流。我们从源码抽取关键实现,给出公式与参数图表,并讨论数值稳定、锯齿与走样的控制方法。


1. 模块职责与数据流

  • noise_utils.dart:提供基础噪声、fbm 合成、坐标/缩放变换与哈希随机工具;
  • noise_tile_map_generator.dart:在 tile 级别采样噪声,执行阈值分类,将连续标量场映射为离散地形标签(如水、沙、草、林、岩等)。

典型调用:世界坐标 → 归一化/缩放 → fbm(x,y) → 阈值表 → 地形 ID


2. FBM 数学模型与实现

2.1 模型

fBm(Fractal Brownian Motion)按下式合成: image.png 其中 n(·) 为基础噪声(如 Perlin),Noctaveslacunarity 控制频谱增长,gain 控制幅值衰减。

2.2 源码摘录(fbm/采样)

double fbm(double x, double y, int octaves, double frequency, double persistence, [int? repeat]) { double total = 0.0; double amplitude = 1.0; double maxAmplitude = 0.0;

for (int i = 0; i < octaves; i++) {
  double f = frequency * pow(2.0, i);
  total += perlin(x * f, y * f, repeat) * amplitude;
  maxAmplitude += amplitude;
  amplitude *= persistence;
}
return total / maxAmplitude;

} }

摘自源码 57-70 行

参数探测:octaves=5,lacunarity=2.0,gain≈0.5,base_freq≈0.01

2.3 八度权重图

下图展示归一化的八度权重(由源码参数推断/默认值补齐):

fig_fbm_weights.png


3. 种子、哈希与确定性

为保证“同一 seed + 坐标 得到一致地形/随机事件”,实现中采用哈希扰动

double grad(int hash, double x, double y) { final g = gradients[hash % gradients.length]; return g[0] * x + g[1] * y; }

/// 🌱 Perlin噪声 (支持repeat) double perlin(double x, double y, [int? repeat]) { if (x.isNaN || y.isNaN || x.isInfinite || y.isInfinite) { throw Exception('💥 Invalid input to perlin: x=x,y=x, y=y'); }

if (repeat != null && repeat > 0) {
  x = x % repeat;
  y = y % repeat;
}

摘自源码 20-35 行

  • 典型做法:seed ⊕ (x * A) ⊕ (y * B) 形成 tile 级种子;
  • 避免相邻 tile 高相关:引入素数/大常数,或对坐标做旋转/扰动。

4. 地形分类(连续→离散)

4.1 阈值链与分类函数

String getTerrainTypeAtPosition(Vector2 worldPos) { final idx = _getTerrainIndex(worldPos.x, worldPos.y); return _terrainDefs[idx].$1; // ⚠️ 这里有意不把 brightness 带入逻辑判定,只用于渲染色调。 }

double _getBrightnessOffsetForY(double ny) { const segmentHeight = 200; const groupSize = 100; const offsetRange = 0.1;

final blockIndex = ny ~/ segmentHeight;
final localIndex = blockIndex % groupSize;

final mirroredIndex = localIndex <= groupSize ~/ 2
    ? localIndex
    : groupSize - localIndex;

final maxIndex = groupSize ~/ 2;
final step = offsetRange / maxIndex;

final offset = mirroredIndex * step;
return offset;

}

static int _packKey(int ix, int iy) => (ix.toUnsigned(32) << 32) | (iy.toUnsigned(32)); static int _packRowKey(int iy) => iy.toUnsigned(32); }

class _PendingChunk { final int cx; final int cy; final String key; _PendingChunk(this.cx, this.cy, this.key); }

class _TerrainSample { final int index; const _TerrainSample({required this.index}); }

摘自源码 345-384 行

4.2 阈值→类别曲线

下图基于源码/近似阈值重建“噪声值→地形类”的阶梯映射: fig_noise_thresholds.png

阈值的顺序与间距决定了海岸带宽度、林线高度以及整图的可通行比例。


5. 数值稳定与抗走样

  • 坐标缩放:以 base_freq 缩放世界坐标,避免高频抖动;
  • 多点稳固采样:在 tile 决策时做 3×3 近邻多数表决,可抑制“锯齿海岸”;
  • 带宽控制:通过 gain 与阈值间距调节“地形带”宽度,平衡细节与可玩性;
  • 确定性重现:所有随机分支由 seed + tile 产生,便于存档与网络同步。

6. 复杂度与性能

  • 单点 fbm 采样:O(N_octaves);
  • 单 tile 分类:常数时间(1 次采样或少量邻域采样);
  • 分块采样:对视野窗口做网格化,每帧只处理变化的 tile,时间与“新增 tile 数”线性相关。

7. 调参与可视化建议

  • 世界尺度:调低 base_freq → 更大地貌结构;
  • 细节纹理:调高 lacunarity 或增加 octaves;
  • 起伏强度:提高 gain;
  • 可通行性:调整阈值使草/林比例达 60%–80%,水/岩控制在 10%–25%。

8. 结论

该噪声系统以简洁的 FBM + 阈值分类实现“可重现、可控”的大地图语义;配合种子哈希与邻域稳固采样,可在移动端以常数级成本获得平滑、层次丰富的地形。


附录 · 关键源码片段(行号)

fbm/sample:见上文摘录 57–70、None–None 行。
哈希随机:见 20–35 行。
分类函数:见 345–384 行。
github.com/tao-999/xiu…