godot-procedural-generation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Procedural Generation

程序化生成

Seeded algorithms, noise functions, and constraint propagation define replayable content generation.
基于种子的算法、噪声函数和约束传播技术,可实现可复现的内容生成。

Available Scripts

可用脚本

wfc_level_generator.gd

wfc_level_generator.gd

Expert Wave Function Collapse implementation with tile adjacency rules.
基于瓦片邻接规则的专业Wave Function Collapse实现。

NEVER Do in Procedural Generation

程序化生成中的禁忌操作

  • NEVER forget to seed RNG
    randi()
    without seed = same dungeon every time. Use
    seed(hash(Time.get_ticks_msec()))
    OR expose seed for speedrunning.
  • NEVER use
    randf()
    in
    _ready()
    for multiplayer
    — Each client calls
    _ready()
    at different times = desynced RNG = different dungeons. Use shared seed from server.
  • NEVER skip validation — Drunkard's walk dungeon with no exit? Playability fail. ALWAYS validate (e.g., A* from start to end) OR regenerate.
  • NEVER use noise.get_noise_2d() every frame — Calling noise 10,000x/frame = lag. Pre-generate heightmap in
    _ready()
    , cache in Array.
  • NEVER use BSP without minimum room size — Infinite splits = 1x1 rooms = crash. Set
    min_size
    (e.g., 6x6) to prevent over-subdivision.
  • NEVER ignore WFC contradictions — Wave Function Collapse fails when no valid tiles remain. MUST detect contradiction, backtrack OR restart generation.
  • NEVER block main thread for large generations — Generating 1000x1000 terrain in
    _ready()
    = freeze. Use worker thread OR split across frames with
    await
    .

gdscript
func generate_dungeon(width: int, height: int, fill_percent: float = 0.4) -> Array:
    var grid := []
    for y in height:
        var row := []
        for x in width:
            row.append(1)  # 1 = wall
        grid.append(row)
    
    # Start in center
    var x := width / 2
    var y := height / 2
    var floor_tiles := 0
    var target_floor := int(width * height * fill_percent)
    
    while floor_tiles < target_floor:
        if grid[y][x] == 1:
            grid[y][x] = 0  # Create floor
            floor_tiles += 1
        
        # Random walk
        var dir := randi() % 4
        match dir:
            0: x = clampi(x + 1, 0, width - 1)
            1: x = clampi(x - 1, 0, width - 1)
            2: y = clampi(y + 1, 0, height - 1)
            3: y = clampi(y - 1, 0, height - 1)
    
    return grid
  • 绝对不要忘记为RNG设置种子 — 未设置种子的
    randi()
    会导致每次生成的地牢完全相同。使用
    seed(hash(Time.get_ticks_msec()))
    ,或者为速通玩法暴露种子参数。
  • 绝对不要在多人游戏的
    _ready()
    中使用
    randf()
    — 每个客户端调用
    _ready()
    的时间不同,会导致RNG不同步,进而生成不同的地牢。应使用服务器提供的共享种子。
  • 绝对不要跳过验证步骤 — 生成的醉汉游走地牢没有出口?这会导致游戏性失效。务必进行验证(例如用A*算法检查起点到终点的连通性),或者重新生成内容。
  • 绝对不要每帧调用
    noise.get_noise_2d()
    — 每帧调用10000次噪声函数会导致卡顿。应在
    _ready()
    中预先生成高度图,并缓存到数组中。
  • 绝对不要在使用BSP树时不设置最小房间尺寸 — 无限分割会产生1x1的房间,甚至导致崩溃。设置
    min_size
    (例如6x6)以防止过度分割。
  • 绝对不要忽略Wave Function Collapse的矛盾情况 — 当没有有效瓦片可选时,Wave Function Collapse会失败。必须检测矛盾情况,进行回溯或重新开始生成。
  • 绝对不要在主线程中执行大规模生成操作 — 在
    _ready()
    中生成1000x1000的地形会导致游戏冻结。应使用工作线程,或者通过
    await
    将生成任务拆分到多帧执行。

gdscript
func generate_dungeon(width: int, height: int, fill_percent: float = 0.4) -> Array:
    var grid := []
    for y in height:
        var row := []
        for x in width:
            row.append(1)  # 1 = wall
        grid.append(row)
    
    # Start in center
    var x := width / 2
    var y := height / 2
    var floor_tiles := 0
    var target_floor := int(width * height * fill_percent)
    
    while floor_tiles < target_floor:
        if grid[y][x] == 1:
            grid[y][x] = 0  # Create floor
            floor_tiles += 1
        
        # Random walk
        var dir := randi() % 4
        match dir:
            0: x = clampi(x + 1, 0, width - 1)
            1: x = clampi(x - 1, 0, width - 1)
            2: y = clampi(y + 1, 0, height - 1)
            3: y = clampi(y - 1, 0, height - 1)
    
    return grid

Perlin Noise Terrain

Perlin噪声地形

gdscript
var noise := FastNoiseLite.new()

func generate_terrain(width: int, height: int) -> Array:
    noise.seed = randi()
    noise.frequency = 0.05
    
    var terrain := []
    for y in height:
        var row := []
        for x in width:
            var value := noise.get_noise_2d(x, y)
            
            # Map noise to tile types
            var tile: int
            if value < -0.2:
                tile = 0  # Water
            elif value < 0.2:
                tile = 1  # Grass
            else:
                tile = 2  # Mountain
            
            row.append(tile)
        terrain.append(row)
    
    return terrain
gdscript
var noise := FastNoiseLite.new()

func generate_terrain(width: int, height: int) -> Array:
    noise.seed = randi()
    noise.frequency = 0.05
    
    var terrain := []
    for y in height:
        var row := []
        for x in width:
            var value := noise.get_noise_2d(x, y)
            
            # Map noise to tile types
            var tile: int
            if value < -0.2:
                tile = 0  # Water
            elif value < 0.2:
                tile = 1  # Grass
            else:
                tile = 2  # Mountain
            
            row.append(tile)
        terrain.append(row)
    
    return terrain

BSP Rooms

BSP房间

gdscript
class_name BSPRoom

var x: int
var y: int
var width: int
var height: int
var left: BSPRoom = null
var right: BSPRoom = null

func split(min_size: int = 6) -> bool:
    if left or right:
        return false  # Already split
    
    # Choose split direction
    var split_horizontal := randf() > 0.5
    
    if width > height and float(width) / float(height) >= 1.25:
        split_horizontal = false
    elif height > width and float(height) / float(width) >= 1.25:
        split_horizontal = true
    
    var max := (height if split_horizontal else width) - min_size
    if max <= min_size:
        return false  # Too small
    
    var split_pos := randi_range(min_size, max)
    
    if split_horizontal:
        left = BSPRoom.new()
        left.x = x
        left.y = y
        left.width = width
        left.height = split_pos
        
        right = BSPRoom.new()
        right.x = x
        right.y = y + split_pos
        right.width = width
        right.height = height - split_pos
    else:
        left = BSPRoom.new()
        left.x = x
        left.y = y
        left.width = split_pos
        left.height = height
        
        right = BSPRoom.new()
        right.x = x + split_pos
        right.y = y
        right.width = width - split_pos
        right.height = height
    
    return true

func generate_bsp_dungeon(width: int, height: int, iterations: int = 4) -> Array[BSPRoom]:
    var root := BSPRoom.new()
    root.x = 0
    root.y = 0
    root.width = width
    root.height = height
    
    var rooms: Array[BSPRoom] = [root]
    
    for i in iterations:
        var new_rooms: Array[BSPRoom] = []
        for room in rooms:
            if room.split():
                new_rooms.append(room.left)
                new_rooms.append(room.right)
            else:
                new_rooms.append(room)
        rooms = new_rooms
    
    return rooms
gdscript
class_name BSPRoom

var x: int
var y: int
var width: int
var height: int
var left: BSPRoom = null
var right: BSPRoom = null

func split(min_size: int = 6) -> bool:
    if left or right:
        return false  # Already split
    
    # Choose split direction
    var split_horizontal := randf() > 0.5
    
    if width > height and float(width) / float(height) >= 1.25:
        split_horizontal = false
    elif height > width and float(height) / float(width) >= 1.25:
        split_horizontal = true
    
    var max := (height if split_horizontal else width) - min_size
    if max <= min_size:
        return false  # Too small
    
    var split_pos := randi_range(min_size, max)
    
    if split_horizontal:
        left = BSPRoom.new()
        left.x = x
        left.y = y
        left.width = width
        left.height = split_pos
        
        right = BSPRoom.new()
        right.x = x
        right.y = y + split_pos
        right.width = width
        right.height = height - split_pos
    else:
        left = BSPRoom.new()
        left.x = x
        left.y = y
        left.width = split_pos
        left.height = height
        
        right = BSPRoom.new()
        right.x = x + split_pos
        right.y = y
        right.width = width - split_pos
        right.height = height
    
    return true

func generate_bsp_dungeon(width: int, height: int, iterations: int = 4) -> Array[BSPRoom]:
    var root := BSPRoom.new()
    root.x = 0
    root.y = 0
    root.width = width
    root.height = height
    
    var rooms: Array[BSPRoom] = [root]
    
    for i in iterations:
        var new_rooms: Array[BSPRoom] = []
        for room in rooms:
            if room.split():
                new_rooms.append(room.left)
                new_rooms.append(room.right)
            else:
                new_rooms.append(room)
        rooms = new_rooms
    
    return rooms

Random Loot

随机战利品

gdscript
func generate_loot(loot_level: int) -> Array[Item]:
    var items: Array[Item] = []
    var roll_count := randi_range(1, 3)
    
    for i in roll_count:
        var rarity := roll_rarity()
        var item := get_random_item(rarity, loot_level)
        items.append(item)
    
    return items

func roll_rarity() -> String:
    var roll := randf()
    if roll < 0.6:
        return "common"
    elif roll < 0.85:
        return "uncommon"
    elif roll < 0.95:
        return "rare"
    else:
        return "legendary"
gdscript
func generate_loot(loot_level: int) -> Array[Item]:
    var items: Array[Item] = []
    var roll_count := randi_range(1, 3)
    
    for i in roll_count:
        var rarity := roll_rarity()
        var item := get_random_item(rarity, loot_level)
        items.append(item)
    
    return items

func roll_rarity() -> String:
    var roll := randf()
    if roll < 0.6:
        return "common"
    elif roll < 0.85:
        return "uncommon"
    elif roll < 0.95:
        return "rare"
    else:
        return "legendary"

Wave Function Collapse

Wave Function Collapse

gdscript
undefined
gdscript
undefined

Simplified WFC for tile patterns

Simplified WFC for tile patterns

Load compatible tile adjacency rules

Load compatible tile adjacency rules

var tile_rules := { "grass": ["grass", "path", "water_edge"], "water": ["water", "water_edge"], "path": ["grass", "path"] }
func wfc_generate(width: int, height: int) -> Array: var grid := [] for y in height: var row := [] for x in width: row.append(null) # Uncollapsed grid.append(row)
# Collapse cells until complete
while has_uncollapsed(grid):
    var pos := find_lowest_entropy(grid)
    collapse_cell(grid, pos)
    propagate_constraints(grid, pos)

return grid
undefined
var tile_rules := { "grass": ["grass", "path", "water_edge"], "water": ["water", "water_edge"], "path": ["grass", "path"] }
func wfc_generate(width: int, height: int) -> Array: var grid := [] for y in height: var row := [] for x in width: row.append(null) # Uncollapsed grid.append(row)
# Collapse cells until complete
while has_uncollapsed(grid):
    var pos := find_lowest_entropy(grid)
    collapse_cell(grid, pos)
    propagate_constraints(grid, pos)

return grid
undefined

Best Practices

最佳实践

  1. Seeding - Use seeds for reproducibility
  2. Validation - Ensure playable levels
  3. Performance - Generate async if needed
  1. 种子化 - 使用种子确保可复现性
  2. 验证 - 确保生成的关卡可游玩
  3. 性能 - 必要时使用异步生成

Reference

参考

  • Related:
    godot-tilemap-mastery
    ,
    godot-resource-data-patterns
  • 相关内容:
    godot-tilemap-mastery
    ,
    godot-resource-data-patterns

Related

相关

  • Master Skill: godot-master
  • 核心技能:godot-master