概述
这是一组 GDScript 脚本,用于在 Godot 编辑器中和运行时过程化地生成地牢。
功能特性
-
您可以配置地牢的整体大小、房间数量,以及房间和走廊的最小/最大宽度和高度。
-
基于种子(seed)生成布局。
-
系统首先创建外部和内部的布局数据,然后使用地牢的基本平铺图案来放置装饰和物体。
-
对于每种图案,您可以可视化配置最小/最大出现次数、生成权重以及相邻方向。
-
对于每个图层和图案,您可以配置单元格是否被视为占用、图案覆盖率,以及是否允许覆盖现有图块。
通过设置多个图层,您可以构建多层装饰。
使用方法
配置选项很多,但下载一次示例项目将帮助您顺利理解工作流程。
1. 下载示例项目
-
从仓库下载所需的文件。
https://github.com/ggg-shirokishi/procedural-layout-tools -
将
addons和scripts目录直接放置在项目根目录下。
2. 将布局生成器节点添加到场景中
-
向任意场景添加一个
Node2D,并从scripts目录附加布局脚本room_rayout_generator。 -
在检查器中配置
room_rayout_generator的参数。- 地图大小:
width、height - 房间数量:
room_count - 房间尺寸范围:
room_w_min / room_w_max、room_h_min / room_h_max - 走廊宽度及相关参数:
corridor_widthcorridor_width_min / maxcorridor_width_randomize_each_corridor- 当启用
corridor_width_randomize_each_corridor时,每个走廊的宽度将在corridor_width_min / max之间随机化。
- 当启用
- 对角连通性设置:
connectivity_allow_diagonal- 当启用
connectivity_allow_diagonal时,房间可以沿对角线连接。
- 当启用
- 种子:
seed
- 地图大小:
-
如果启用
editor_auto_generate/editor_live_update,任何参数更改将立即反映在编辑器中。
3. 设置 TileMapLayer 和 TerrainAtlasPatternPlacer
-
向场景添加一个
TileMapLayer和一个Node2D(用于运行放置脚本),并将scripts目录中的terrain_atlas_pattern_placer附加到Node2D上。- 目标 TileMap 图层与放置
Node2D之间是 1:1 的关系,您可以添加任意数量的配对。 - 图层数量没有下限或上限,但在大多数情况下,您至少需要两个
TileMapLayer + terrain_atlas_pattern_placer配对:一个用于外部(地形图块放置),一个用于内部。
(在示例项目中,这些分别是TerrainTileLayer / TerrainTilePlacer和InteriorBaseLayer / InteriorBasePlacer。)
- 目标 TileMap 图层与放置
-
为
TileMapLayer分配一个地形TileSet,并注册地形(Terrains)和图案(Patterns)。- 该系统从分配给每个 TileMapLayer 的 TileSet 中读取地形和图案。
请在 TileMapLayer 端设置 TileSet。 - 关于地形的说明,请参阅:
https://docs.godotengine.org/zh-cn/4.x/tutorials/2d/using_tilesets.html#creating-terrain-sets-autotiling
- 该系统从分配给每个 TileMapLayer 的 TileSet 中读取地形和图案。
-
在附加了
TerrainAtlasPatternPlacer脚本的节点上,配置以下项目。
TerrainAtlasPatternPlacer 的主要设置
layout_node: 上一节中添加的room_rayout_generator节点的路径。target_layer_path: 此放置器操作的TileMapLayer的路径。target_kind: 此图层处理的单元格类型(WALLS/FLOORS)
WALLS: 外部 / 不可行走的地形(通常是地形图块)
FLOORS: 内部placement_type: 使用地形绘制还是盖印图案(TERRAIN/PATTERN)。clear_before_place: 每次放置前是否清除图层。register_used_cells_to_layout: 是否将使用的单元格注册到布局的used_cells_mask中。
启用此选项后,如果您在具有较大execution_order的图层/图案上使用only_place_on_unoccupied,则可以防止它们在已使用的单元格上放置。only_place_on_unoccupied: 限制仅放置在未占用的单元格上。auto_update_on_layout_signal: 是否在收到布局更新信号时自动重新放置。pattern_coverage_ratio: 图案放置的整体密度。execution_order: 存在多个TerrainAtlasPatternPlacer节点时的处理顺序。
值较小的节点先运行。与only_place_on_unoccupied和register_used_cells_to_layout结合使用以控制重叠。
4. 详细的图案设置(编辑器扩展检查器)
当将 TileMapLayer 链接到 TerrainAtlasPatternPlacer 时,检查器中将添加一个列出 TileSet 中所有图案的附加面板。
您可以为每种图案配置以下项目。
-
pattern_required_cells: 必需的接触单元格
(图案内部必须接触边缘的单元格)
与pattern_adjacent_dirs结合使用。
您可以通过勾选缩略图下的单元格来配置此项。 -
pattern_weights: 生成权重。 -
pattern_min_counts,pattern_max_counts: 最小/最大放置次数(默认无限制)。 -
pattern_adjacent_dirs: 相邻方向
(必需单元格必须相邻的方向,在上/下/左/右中选择。)示例:
如果您将顶部两个单元格设置为pattern_required_cells,并将pattern_adjacent_dirs设置为“上”,则该图案仅放置在顶部两个单元格从上方接触的地形上。 -
pattern_register_used_cells_override: 注册单元格为占用的覆盖设置
允许您控制此图案是否将其单元格注册为占用。- 继承全局设置(与
TerrainAtlasPatternPlacer相同) - 强制开启
- 强制关闭
- 继承全局设置(与
-
pattern_only_place_on_unoccupied_override: “仅放置在未占用单元格上”的覆盖设置- 继承全局设置(与
TerrainAtlasPatternPlacer相同) - 强制开启
- 强制关闭
- 继承全局设置(与
配置完这些大致内容后,按 run_on_editor_button 生成布局。
运行时再生设置(可选)
- 如果您希望在游戏中使用键盘输入来重新生成地牢,请将等效于
RuntimeLayoutKeyboardController的脚本附加到Node上。
主要设置
layout_node_path: 布局生成器节点。input_action_name: 用于再生的 InputMap 动作名称(例如regen_dungeon)。randomize_layout_size/randomize_room_count/randomize_room_size/randomize_corridor_width: 是否在每次再生时随机化每个参数。move_target_node_path: 再生后需要移动的节点,例如玩家。move_target_tilemap_layer_path: 用于地板检测的TileMapLayer。move_after_generation: 再生后是否将目标移动到空闲单元格。zoom_camera_path: 其limit应调整为布局的相机。
游戏开始后,按下指定的动作键将执行以下操作:
- 更改种子。
- 重新生成布局。
- 可选地移动玩家并调整相机。
使用多层进行多层装饰
通过让多个 TerrainAtlasPatternPlacer 节点共享同一个布局节点,并使用 execution_order 控制其执行顺序,您可以创建如下多层装饰:
- 第一层:外部(基础墙/地板图块)
- 第二层:内部(柱子、窗户、横梁等图案)
- 第三层:物体(家具、碎石、装饰物)
对于每个图层,您可以切换:
register_used_cells_to_layoutonly_place_on_unoccupied
并使用每种图案的覆盖设置来:
- “叠加在顶部而不破坏基础”
- “仅覆盖部分基础”
提示和注意事项
-
TileMap 图层大小和 TileSet 大小必须统一。
-
如果您想随机放置游戏物体:
您可以向 TileSet 添加场景集合,在 TileMap 上放置物体,然后将它们烘焙回图案以进行放置。
(您可以在示例项目的GameObjectLayer和InteriorBaseLayer中看到这些设置。)
每个脚本的详细信息
room_rayout_generator.gd
使用概述
这是一个附加到 Node2D 的仅布局生成器。
它从不写入 TileMap。
生成结果存储在以下属性中:
grid:int的二维数组
CELL_WALL = 0,CELL_FLOOR = 1,CELL_DOOR = 2rooms:Array[Rect2i](房间矩形)centers:Array[Vector2i](房间中心)room_id_grid: 每个单元格的房间 ID(标识房间)。corridor_id_grid: 每个单元格的走廊 ID。used_cells_mask: 外部(来自放置器)注册的“使用单元格”的掩码。
布局生成的公共 API
generate_now()
同步生成布局,并在完成时发出generation_finished(success: bool)信号。generate_async() -> Signal
异步生成布局。它检查_is_generating以查看是否忙碌,使用await执行处理,最后通过相同的generation_finished信号返回结果。
信号
layout_updated(grid, rooms, centers)
当网格和房间信息更新时发出。generation_finished(success)
通知整个生成是否成功。
TerrainAtlasPatternPlacer 等节点引用生成的 grid 并使用
register_used_cells() / get_free_cells()
在布局中共享“空闲单元格/使用单元格”。
在编辑器中,editor_auto_generate / editor_live_update / editor_generate_button 控制:
- 加载场景时自动生成。
- 在检查器中更改参数时自动重新生成。
主要变量和属性
输出和状态
signal layout_updated(grid: Array, rooms: Array, centers: Array)
通知当前布局信息的信号。signal generation_finished(success: bool)
通知生成是否成功完成的信号。const CELL_WALL: int = 0
网格中表示“墙”的值。const CELL_FLOOR: int = 1
网格中表示“地板”的值。const CELL_DOOR: int = 2
网格中表示“计划中的门单元格”的值。var rng: RandomNumberGenerator
用于随机数的 RNG 实例,从seed和_seed_internal播种。var _is_generating: bool
指示异步生成是否正在进行的标志。在generate_async期间变为true,完成时变为false。var grid: Array
二维数组grid[y][x]。值为上述CELL_*常量之一。var rooms: Array[Rect2i]
成功生成的房间矩形数组。var centers: Array[Vector2i]
每个房间的中心单元格坐标数组。var room_id_grid: Array
room_id_grid[y][x]存储该单元格属于哪个房间 ID(如果不是房间,则为-1)。var corridor_id_grid: Array
corridor_id_grid[y][x]存储走廊 ID(如果不是走廊,则为-1)。var _next_corridor_id: int
用于分配走廊 ID 的内部计数器。var used_cells_mask: Array
布尔值的二维数组used_cells_mask[y][x],指示“该单元格是否已从 TilePlacer 端注册为使用”。
register_used_cells(cells: Array[Vector2i])将其设置为true。
get_free_cells(kind: int)返回掩码为false的单元格作为空闲单元格。
地图大小和房间参数
@export var width: int = 80
布局宽度(以单元格为单位)。_set_width()防止值小于8,并在更改时在编辑器中重新生成。@export var height: int = 60
布局高度(以单元格为单位)。_set_height()保持其至少为8。@export var cell_padding: int = 1
从外边框的边距。房间不会放置在外框内的这些单元格中。@export var room_count: int = 18
尝试生成的目标房间数量。由于碰撞,实际数量可能较少。@export var room_w_min: int = 5/room_w_max: int = 14
房间宽度的最小/最大值(以单元格为单位)。Setter 保持room_w_min <= room_w_max。@export var room_h_min: int = 4/room_h_max: int = 12
房间高度的最小/最大值(以单元格为单位)。Setter 也保持其一致。
走廊参数
@export var corridor_width: int = 1
基础走廊宽度。当禁用随机宽度时,用作固定宽度。@export var corridor_width_min: int = 0
随机化时的最小走廊宽度。@export var corridor_width_max: int = 1
随机化时的最大走廊宽度。@export var corridor_width_randomize_each_corridor: bool = true
当为true时,每个走廊(段)在corridor_width_min到corridor_width_max范围内随机选择其宽度。@export var corridor_use_diagonal_path: bool = false
当为true时,房间中心之间的走廊挖掘允许对角线路径(路径呈对角线锯齿状)。@export var connectivity_allow_diagonal: bool = false
当为true时,连通性检查(是否所有房间都连接)将对角线(8 方向)相邻视为已连接。
随机性、重试和异步
var _seed_internal: int = 123456
实际传递给 RNG 的内部种子。@export var seed: int = 123456
布局生成的公共种子。Setter 更新_seed_internal,如果编辑器中editor_live_update为true,则立即重新生成。@export var max_retry: int = 25
房间或走廊生成失败时的最大重试次数,每次更改种子。@export var async_yield_rows: int = 6
在异步生成中,这控制在产生之前(例如通过await process_frame)处理多少行。
较大的值每批挖掘更多,但阻塞主线程的时间更长。
日志和编辑器特定设置
@export var log_enabled: bool = true
当为true时,_log()输出日志。@export var log_verbosity: int = 1
日志详细程度级别。值越大,输出的日志越详细。@export var editor_auto_generate: bool = true
加载场景时自动生成布局的标志。@export var editor_live_update: bool = true
在编辑器中更改参数时是否自动重新生成布局。@export var editor_generate_button: bool = false
检查器端的一次性“立即生成”触发标志。
设置为true时,调用_generate_editor_safe()一次,然后标志立即设置回false。
terrain_atlas_pattern_placer.gd (TerrainAtlasPatternPlacer)
使用概述
一个具有 class_name TerrainAtlasPatternPlacer 的 Node2D 脚本。
其作用是:
“引用 RoomLayoutGenerator 的 grid 并在指定的 TileMapLayer 上绘制地形或盖印图案。”
主要假设:
layout_node引用拥有grid并发出layout_updated/generation_finished的节点,如room_rayout_generator.gd。target_layer_path指定放置图块的目标TileMapLayer。
目标单元格类型:
- 当
target_kind = WALLS时,目标CELL_WALL单元格。 - 当
target_kind = FLOORS时,目标CELL_FLOOR和CELL_DOOR单元格。
放置模式:
- 当
placement_type = TERRAIN时,
使用set_cells_terrain_connect()使用地形绘制。 - 当
placement_type = PATTERN时,
使用TileSet的TileMapPattern对象盖印图案。
其他行为:
- 如果
auto_update_on_layout_signal为true,则在从layout_node收到layout_updated/generation_finished时自动重新放置图块。 register_used_cells_to_layout控制是否将使用的单元格注册到layout_node.register_used_cells(cells)并反映在used_cells_mask中。only_place_on_unoccupied限制放置到在布局和TileMapLayer上均未占用的单元格。
此外,pattern_*_override允许按图案覆盖此行为。- 当多个放置器共享同一个布局时,
execution_order控制它们的运行顺序(值较小的先运行)。
信号
signal placement_finished(success: bool)
当放置过程完成时发出。
主要变量和属性
基础和枚举
signal placement_finished(success: bool)
通知放置是否成功完成的信号。const CELL_WALL: int = 0/CELL_FLOOR: int = 1/CELL_DOOR: int = 2
布局网格值定义,对应RoomLayoutGenerator。enum TargetKind { WALLS, FLOORS }
要目标化的单元格类型。enum PlacementType { TERRAIN, PATTERN }
地形绘制或图案盖印的模式。
布局引用和目标图层
@export var layout_node: NodePath
布局生成节点(RoomLayoutGenerator等)的路径。
Setter_set_layout_node将引用缓存到_layout_ref中。var _layout_ref: Node
实际的布局节点引用。var _grid: Array
当前布局grid的内部缓存。@export var target_layer_path: NodePath
目标TileMapLayer的路径。var _target_layer: TileMapLayer
实际 TileMap 图层的引用。
执行控制和链接
@export var clear_before_place: bool = true
是否在放置前调用TileMapLayer.clear()。@export var register_used_cells_to_layout: bool = true
是否将用于放置的单元格列表传递给layout_node.register_used_cells(cells)。@export var only_place_on_unoccupied: bool = false
当为true时,仅针对在布局和TileMapLayer上均为空闲的单元格。
这结合了layout.get_free_cells(kind)和 TileMap 单元格检查。@export var run_on_editor_button: bool = false
编辑器端按钮触发标志。当设置为true时,调用_set_run_on_editor_button(),该函数:- 执行
layout.generate_now()。 - 然后运行等效于
place_now()的放置。
完成后,标志重置为false。
- 执行
@export var auto_update_on_layout_signal: bool = true
当为true时,自动响应来自layout_node的layout_updated/generation_finished信号。@export var auto_update_in_editor_only: bool = true
当为true时,自动更新仅在编辑器中工作,而在游戏运行时不工作。@export var auto_update_debounce_frames: int = 1
自动更新时批量更新前的去抖帧数。
即使设置为0,也至少等待 1 帧。@export var log_enabled: bool = true
是否输出日志。@export var log_verbosity: int = 1
日志详细程度级别。@export var execution_order: int = 0
共享同一个layout_node的多个放置器之间的执行顺序。
值较小的先运行;如果值相等,则顺序回退到升序instance_id。var _pending_auto_update_local: boolvar _last_used_cells: Array[Vector2i]var _last_pattern_force_register: bool
保存待定状态和最新放置信息的内部变量。
放置模式和地形设置
@export var target_kind: TargetKind = TargetKind.WALLS
指定目标是墙还是地板。@export var placement_type: PlacementType = PlacementType.TERRAIN
在地形模式和图案模式之间选择。@export var terrain_set_index: int = 0
要使用的地形集索引。@export var terrain_index: int = 0
集合内地形的索引。
图案相关设置
@export var pattern_indices: PackedInt32Array = PackedInt32Array()
要使用的TileMapPattern索引列表。如果为空,则目标化为所有可用图案。@export var pattern_avoid_overlap: bool = true
当为true时,图案放置时互不重叠。@export var pattern_coverage_ratio: float = 0.1
目标比率(0.0–1.0),表示目标单元格应被图案覆盖的比例。@export var pattern_weights: Dictionary = {}
每种图案的生成权重。- 键:
pattern_index - 值:
float权重
- 键:
@export var pattern_adjacent_dirs: Dictionary = {}
每种图案的方向位掩码。- 键:
pattern_index - 值:
int位掩码- 位:
1 = U(上),2 = R,4 = D,8 = L
- 位:
- 键:
@export var pattern_required_cells: Dictionary = {}
每种图案的必需接触单元格定义。- 键:
pattern_index - 值:内部图案单元格坐标的
Array[Vector2i]
- 键:
@export var pattern_min_counts: Dictionary = {}
每种图案的最小放置次数。@export var pattern_max_counts: Dictionary = {}
每种图案的最大放置次数。-1或缺失表示“无上限”。@export var pattern_register_used_cells_override: Dictionary = {}
register_used_cells_to_layout的每种图案覆盖。- 值:
0 = Inherit(继承全局设置) - 值:
1 = Force On(始终注册为使用) - 值:
2 = Force Off(从不注册为使用)
- 值:
@export var pattern_only_place_on_unoccupied_override: Dictionary = {}
only_place_on_unoccupied的每种图案覆盖。- 值:
0 = Inherit(继承全局设置) - 值:
1 = Force On(仅在此图案上放置在未占用单元格上) - 值:
2 = Force Off(允许此图案在已占用单元格上放置)
- 值:
terrain_pattern_placer_inspector.gd + plugin.gd / plugin.cfg
使用概述
plugin.cfg 和 plugin.gd 将此注册为 Godot 编辑器中的
“Terrain Pattern Tools” 插件。
plugin.cfg 的内容:
name="Terrain Pattern Tools"description="TerrainAtlasPatternPlacer 的自定义检查器(图案权重/相邻性)。"script="plugin.gd"
plugin.gd 扩展 EditorPlugin,并在 _enter_tree() 中:
- 通过
Script.new()创建terrain_pattern_placer_inspector.gd。 - 通过
add_inspector_plugin(_insp)注册检查器扩展。
在 _exit_tree() 中,它使用 remove_inspector_plugin(_insp) 移除检查器扩展。
当您在项目设置中启用此插件并选择附加了 TerrainAtlasPatternPlacer 的节点时,
- 标准检查器将获得一个用于每种图案设置的附加 UI。
通过附加 UI 可编辑的内容
对于每个 TileMapPattern:
- 显示图案图块外观的缩略图。
- 显示图案内部单元格布局的网格缩略图。
- 生成权重(
SpinBox)。 - 相邻方向(
U/R/D/L复选框)。 - 必需的接触单元格(内部图案单元格的按钮,通过点击切换)。
- 最小计数(
Min)。 - 最大计数(
Max,-1表示无限制)。 register_used_cells_to_layout的每种图案覆盖(Reg)。only_place_on_unoccupied的每种图案覆盖(Unocc)。
这些直接修改 TerrainAtlasPatternPlacer 上的以下属性:
pattern_weightspattern_adjacent_dirspattern_required_cellspattern_min_countspattern_max_countspattern_register_used_cells_overridepattern_only_place_on_unoccupied_override
class PatternPreviewControl extends Control
专用于图案预览渲染的 Control 子类。
var tileset: TileSet
用于预览的TileSet。var pattern: TileMapPattern
要预览的图案。var preview_size: Vector2
滚动视图内的显示大小。
在内部,它创建一个 TileMapLayer,使用 set_cell() 盖印 pattern,并在 _draw() 中按原样显示外观。
字典访问辅助函数
- 诸如
_get_dict_safe(placer, "pattern_weights")之类的函数检索每个Dictionary,根据 UI 操作更新它,并通过
placer.set("pattern_weights", d)写回。
runtime_rayout_controller.gd (RuntimeLayoutKeyboardController)
使用概述
附加到 Node 的运行时控制器。
其作用:
- 当按下指定的输入动作时:
- 随机更新布局节点的
seed。 - 可选地随机化布局参数(宽度、高度、房间数量、房间大小、走廊宽度)。
- 触发
layout_node.generate_now()以重新生成布局。
- 随机更新布局节点的
- 重新生成后,它可以:
- 将
move_target_node_path指定的节点移动到其中一个空闲单元格。 - 调整
zoom_camera_path指定的相机的limit_*属性以匹配布局的外边界。
- 将
假设
layout_node_path应指向附加了room_rayout_generator.gd的Node2D。
输入监控
- 在
_process()中,它检查Input.is_action_just_pressed(input_action_name),并在按下动作时调用_regenerate_with_random_seed()。
移动和相机
- 如果
move_after_generation为true,它在布局更新后调用_move_target_to_free_cell()。 - 如果设置了
zoom_camera_path,_update_camera_limits()重新计算
limit_left/right/top/bottom。
主要变量和属性
基本设置
const CELL_WALL: int = 0/const CELL_FLOOR: int = 1
网格值定义(与RoomLayoutGenerator相同)。enum MoveTargetKind { MOVE_ON_FLOORS, MOVE_NEAR_WALLS }
如何选择目标单元格。MOVE_ON_FLOORS: 移动到地板单元格。MOVE_NEAR_WALLS: 优先选择靠近墙壁的候选者,如地板单元格。
@export var layout_node_path: NodePath
布局节点(RoomLayoutGenerator)的路径。@export var input_action_name: String = "dungeon_regen"
用作布局再生触发器的InputMap动作名称。@export var log_enabled: bool = true
当为true时,通过print()/push_warning()等输出内部日志。var layout_node: Node = null
实际的布局节点引用。在_ready()中从layout_node_path检索。var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
用于每次再生随机化的 RNG。
随机化布局参数
@export var randomize_layout_size: bool = false
当为true时,在每次再生时随机化布局宽度和高度。@export var layout_width_range: Vector2i = Vector2i(80, 80)
width的随机范围(x = min,y = max)。@export var layout_height_range: Vector2i = Vector2i(60, 60)
height的随机范围。@export var randomize_room_count: bool = false
当为true时,在每次再生时随机化room_count。@export var room_count_range: Vector2i = Vector2i(18, 18)
room_count的随机范围。@export var randomize_room_size: bool = false
当为true时,在每次再生时随机化房间尺寸范围。@export var room_width_range: Vector2i = Vector2i(5, 14)
房间宽度的基础随机范围。在内部,绘制两个值来决定min和max。@export var room_height_range: Vector2i = Vector2i(4, 12)
房间高度的基础随机范围。@export var randomize_corridor_width: bool = false
当为true时,在每次再生时随机化走廊宽度。@export var corridor_width_range: Vector2i = Vector2i(0, 3)
走廊宽度的基础随机范围。绘制两个值并用于决定min/max或固定宽度。
移动目标和相机
@export var move_target_node_path: NodePath
布局更新后要移动的节点的路径(例如玩家角色)。@export var move_target_tilemap_layer_path: NodePath
对应于布局的TileMapLayer的路径。
用于确定哪些单元格是地板或墙壁,并获得候选目的地。@export var move_target_kind: MoveTargetKind = MoveTargetKind.MOVE_ON_FLOORS
如上所述,指定如何选择目标单元格。@export var move_after_generation: bool = true
当为true时,在每次布局重新生成后调用_move_target_to_free_cell()。@export var cell_size: Vector2 = Vector2(16.0, 16.0)
布局网格中一个单元格的像素大小。
用于将网格坐标转换为世界坐标。@export var zoom_camera_path: NodePath
一个Camera2D/ZoomCamera2D的路径,其limit_left/right/top/bottom在重新生成后进行调整。




