基于 Godot 4.x 的程序化地形生成项目,主要研究和实现多层地形生成算法和区块管理系统。项目参考了 Cataclysm: Dark Days Ahead 的地图生成思路。
采用了单例配置管理器来统一管理参数:
# AutoLoad 单例,便于全局访问配置
Config.terrain.TYPE_RIVER # 地形类型配置
Config.render.CHUNK_SIZE # 渲染参数配置主要模块分工:
OvermapRenderer: 地形生成和渲染NoiseManager: 噪声生成管理ConfigManager: 配置参数管理OvermapPlayer: 玩家控制OvermapUI: 调试信息显示
使用信号进行模块间通信:
signal chunk_generated(chunk_coord: Vector2i)
signal config_changed(category: String, key: String, value)
signal debug_mode_changed(enabled: bool)overmapchunkdemo/
├── 核心系统
│ ├── config_manager.gd # 配置管理 (约600行)
│ ├── overmap_renderer.gd # 地形生成引擎 (约2400行)
│ └── noise_manager.gd # 噪声生成管理
├── 用户交互
│ ├── overmap_player.gd # 玩家控制
│ └── overmap_ui.gd # 调试界面
├── 场景资源
│ ├── overmap_system.tscn # 主场景
│ └── assets/ # 瓦片集和纹理
└── 项目配置
├── project.godot # Godot 项目设置
├── LICENSE # MIT 许可证
└── README.md # 项目文档
使用随机游走算法,同时保证跨区块连续性:
# 河流路径生成的核心逻辑
func _draw_single_river_path(chunk_coord: Vector2i, start: Vector2i, end: Vector2i):
var p2 = start
while p2 != end:
# 随机游走
p2.x += randi_range(-1, 1)
p2.y += randi_range(-1, 1)
# 向目标点偏移
if end.x > p2.x and probability_check():
p2.x += 1
# ... 其他方向的偏移逻辑
# 应用河流笔刷,设置地形
apply_river_brush(p2, river_scale)实现要点:
- 检测相邻区块的河流边界,确保连接
- 用
river_scale参数控制河流宽度 - 避免在湖泊位置生成河流
基于噪声生成湖泊形状,然后用洪水填充算法确定连通区域:
# 四连通洪水填充算法
func _point_flood_fill_4_connected(start: Vector2i, visited: Dictionary, predicate: Callable) -> Array[Vector2i]:
var filled_points: Array[Vector2i] = []
var to_check: Array[Vector2i] = [start]
while not to_check.is_empty():
var current = to_check.pop_front()
if visited.has(current) or not predicate.call(current):
continue
visited[current] = true
filled_points.append(current)
# 添加四个方向的邻居点
for direction in [Vector2i(0,1), Vector2i(0,-1), Vector2i(1,0), Vector2i(-1,0)]:
to_check.append(current + direction)
return filled_points实现细节:
- 用 Simplex 噪声确定湖泊的基础形状
- 过滤掉面积小于
SIZE_MIN的水体 - 大型湖泊会自动连接到附近的河流
- 区分湖泊表面和湖岸两种地形
采用双层噪声系统,支持方向性密度调整:
# 森林噪声计算
func forest_noise_at(world_pos: Vector2i) -> float:
# 第一层:基础分布噪声
var base = (noise_1.get_noise_2d(pos.x, pos.y) + 1.0) * 0.5
base = pow(base, NOISE_1_POWER)
# 第二层:密度减少噪声
var density_reduction = (noise_2.get_noise_2d(pos.x, pos.y) + 1.0) * 0.5
density_reduction = pow(density_reduction, NOISE_2_POWER)
# 合成最终值
return max(0.0, base - density_reduction * 0.5)方向性密度调整:
# 根据世界坐标调整森林密度
func calculate_forestosity(chunk_coord: Vector2i):
var forest_size_adjust = 0.0
# 不同方向的增长率影响
if chunk_coord.x < 0: # 西方
forest_size_adjust -= chunk_coord.x * INCREASE_WEST
if chunk_coord.x > 0: # 东方
forest_size_adjust += chunk_coord.x * INCREASE_EAST
# ... 北方和南方的处理类似
# 限制森林覆盖率上限
forest_size_adjust = min(forest_size_adjust, LIMIT - THRESHOLD)模拟河流洪泛平原,在合适的位置生成沼泽:
# 河流洪泛平原计算
func _add_flood_buffer_fast(center: Vector2i, radius: int, floodplain: Dictionary):
var radius_sq = radius * radius
for x in range(center.x - radius, center.x + radius + 1):
for y in range(center.y - radius, center.y + radius + 1):
var distance_sq = (x - center.x) * (x - center.x) + (y - center.y) * (y - center.y)
if distance_sq <= radius_sq:
var point = Vector2i(x, y)
floodplain[point] = floodplain.get(point, 0) + 1 # 记录被覆盖次数生成逻辑:
- 每个河流点周围生成随机半径的洪泛缓冲区
- 根据洪泛次数和噪声值判断是否生成沼泽
- 只在森林地形上生成沼泽,符合生态逻辑
参考L-System思路,用递归算法生成街道网络:
# 递归街道生成
func _build_city_street(start: Vector2i, length: int, direction: int, city: City):
# 1. 规划街道路径
var path = _lay_out_street(start, direction, length)
# 2. 建造主干道
_build_connection(path)
# 3. 生成分支街道
for i in range(1, path.size()):
if should_branch(i, length):
var left_length = length - randi_range(1, 3)
var right_length = length - randi_range(1, 3)
# 递归生成左右分支
_build_city_street(path[i], left_length, turn_left(direction), city)
_build_city_street(path[i], right_length, turn_right(direction), city)建筑放置策略:
# 根据距离城市中心的远近选择建筑类型
func _pick_random_building_to_place(town_dist: int, town_size: int) -> Dictionary:
var shop_normal = max(shop_radius, normal_distribution(shop_radius, shop_sigma))
var park_normal = max(park_radius, normal_distribution(park_radius, park_sigma))
if shop_normal > town_dist:
return select_from_category("shops")
elif park_normal > town_dist:
return select_from_category("parks")
else:
return select_from_category("houses")使用统一的管理器来处理不同类型的噪声:
# 噪声管理器结构
class NoiseManager:
var _noise_instances: Dictionary = {
NoiseType.LAKE: FastNoiseLite, # 湖泊形状
NoiseType.FOREST_BASE: FastNoiseLite, # 森林基础分布
NoiseType.FOREST_DENSITY: FastNoiseLite, # 森林密度变化
NoiseType.FLOODPLAIN: FastNoiseLite # 洪泛平原
}不同噪声类型的参数设置:
| 类型 | 频率 | 倍频数 | 持续性 | 用途 |
|---|---|---|---|---|
| Lake | 0.002 | 8 | 0.5 | 湖泊边界 |
| Forest Base | 0.03 | 4 | 0.5 | 森林分布 |
| Forest Density | 0.07 | 6 | 0.5 | 森林密度 |
| Floodplain | 0.05 | 4 | 0.5 | 沼泽生成 |
只渲染可见区域,减少不必要的计算:
# 增量渲染实现
func update_canvas_rendering():
var new_render_area = calculate_visible_area()
if rendered_area != new_render_area:
clear_tiles_outside_area(new_render_area) # 清理视口外区域
render_terrain_in_area(new_render_area) # 只渲染新可见区域
rendered_area = new_render_area用冷却机制避免频繁生成:
# 冷却控制
var chunk_creation_cooldown: float = 0.0
func check_and_generate_chunks():
if chunk_creation_cooldown > 0:
return # 冷却期内不生成
# 执行生成逻辑...
chunk_creation_cooldown = COOLDOWN_TIME- 地形数据:用 Dictionary 缓存已生成的地形
- TileMap:只在可见区域创建瓦片
- 噪声值:按需计算,不预存储
为了便于添加新地形类型,设计了基础接口:
# 地形生成器基类
class_name BaseTerrainGenerator
func generate(chunk_coord: Vector2i, terrain_data: Dictionary) -> void:
# 子类实现具体生成逻辑
pass
func get_priority() -> int:
# 返回生成优先级
return 0支持在运行时调整参数:
# 动态配置修改
func set_runtime_config(key: String, value):
runtime_config[key] = value
config_changed.emit("runtime", key, value)
# 处理特定配置的变更
if key == "forest_size_adjust":
recalculate_forest_regions()- Godot 4.4 或更高版本
- 导入
project.godot文件 - 运行
overmap_system.tscn场景
用全局坐标系统和边界检测,确保地形特征(如河流)在区块边界处能够连接。
按以下顺序生成地形:
- 基础地形(陆地)
- 水系(河流、湖泊)
- 植被(森林)
- 湿地(沼泽)
- 人工建筑(城市、道路)
根据距离城市中心的远近来决定建筑类型,模拟现实城市的分区。
森林和城市的密度会根据世界坐标进行方向性调整,形成自然的地理分布模式。
经测试,在以下配置下运行良好:
- 最低:Godot 4.4, 2GB RAM
- 推荐:Godot 4.4+, 4GB RAM, 独立显卡
- 分辨率:1600x800 或更高
- 帧率:60fps 下可管理 9-25 个活跃区块
本项目使用 MIT 许可证,详情见 LICENSE 文件。
- 灵感来源:Cataclysm: Dark Days Ahead 的 Overmap 系统
- 技术支持:Godot Engine