4. 程序化生成
2026/4/14大约 7 分钟
程序化生成
程序化生成(Procedural Generation)是指用算法自动生成游戏内容,而不是手工设计每一个细节。这项技术可以让游戏拥有近乎无限的关卡变化,大幅提升重玩价值。
4.1 噪声算法基础
噪声算法是程序化生成的核心工具。它能生成看起来自然、随机但又有规律的数值。
Perlin 噪声 vs 单纯随机
| 特性 | 纯随机 | Perlin 噪声 |
|---|---|---|
| 相邻值关系 | 完全无关 | 平滑过渡 |
| 视觉效果 | 噪点感 | 自然感 |
| 适用场景 | 粒子散布 | 地形、云朵 |
Godot 4 内置了 FastNoiseLite,支持多种噪声类型:
C#
using Godot;
public partial class NoiseDemo : Node3D
{
// 创建地形高度图
public float[,] GenerateHeightMap(int width, int height, int seed = 0)
{
var noise = new FastNoiseLite();
noise.Seed = seed;
noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Perlin;
noise.Frequency = 0.05f; // 频率越低,地形越平缓
noise.FractalOctaves = 4; // 叠加层数,越多细节越丰富
noise.FractalLacunarity = 2.0f; // 每层频率倍增
noise.FractalGain = 0.5f; // 每层振幅衰减
var heightMap = new float[width, height];
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// GetNoise2D 返回 -1 到 1 的值
float value = noise.GetNoise2D(x, y);
// 映射到 0~1
heightMap[x, y] = (value + 1f) * 0.5f;
}
}
return heightMap;
}
// 根据高度决定地形类型
public string GetTerrainType(float height)
{
return height switch
{
< 0.2f => "deep_water",
< 0.35f => "shallow_water",
< 0.45f => "beach",
< 0.65f => "grass",
< 0.8f => "forest",
< 0.9f => "mountain",
_ => "snow_peak"
};
}
}GDScript
extends Node3D
func generate_height_map(width: int, height: int, seed: int = 0) -> Array:
var noise := FastNoiseLite.new()
noise.seed = seed
noise.noise_type = FastNoiseLite.TYPE_PERLIN
noise.frequency = 0.05
noise.fractal_octaves = 4
noise.fractal_lacunarity = 2.0
noise.fractal_gain = 0.5
var height_map := []
for x in width:
height_map.append([])
for y in height:
var value := noise.get_noise_2d(x, y)
height_map[x].append((value + 1.0) * 0.5)
return height_map
func get_terrain_type(height: float) -> String:
if height < 0.2: return "deep_water"
elif height < 0.35: return "shallow_water"
elif height < 0.45: return "beach"
elif height < 0.65: return "grass"
elif height < 0.8: return "forest"
elif height < 0.9: return "mountain"
else: return "snow_peak"4.2 随机地图生成:基于房间的地牢
经典的地牢生成算法:先随机放置房间,再用走廊连接。
C#
using Godot;
using System.Collections.Generic;
public partial class DungeonGenerator : Node3D
{
[Export] private int _mapWidth = 50;
[Export] private int _mapHeight = 50;
[Export] private int _roomCount = 10;
[Export] private int _minRoomSize = 4;
[Export] private int _maxRoomSize = 10;
// 地图格子类型
public enum TileType { Wall, Floor, Corridor, Door }
private TileType[,] _map;
private List<Rect2I> _rooms = new();
public TileType[,] Generate(int seed = 0)
{
GD.Seed((ulong)seed);
_map = new TileType[_mapWidth, _mapHeight];
_rooms.Clear();
// 初始化全部为墙
for (int x = 0; x < _mapWidth; x++)
for (int y = 0; y < _mapHeight; y++)
_map[x, y] = TileType.Wall;
// 随机放置房间
for (int i = 0; i < _roomCount; i++)
TryPlaceRoom();
// 连接所有房间
ConnectRooms();
return _map;
}
private void TryPlaceRoom()
{
int w = GD.RandRange(_minRoomSize, _maxRoomSize);
int h = GD.RandRange(_minRoomSize, _maxRoomSize);
int x = GD.RandRange(1, _mapWidth - w - 1);
int y = GD.RandRange(1, _mapHeight - h - 1);
var newRoom = new Rect2I(x, y, w, h);
// 检查是否与已有房间重叠
foreach (var room in _rooms)
{
if (newRoom.Intersects(room.Grow(1)))
return; // 重叠,放弃
}
// 挖出房间
for (int rx = x; rx < x + w; rx++)
for (int ry = y; ry < y + h; ry++)
_map[rx, ry] = TileType.Floor;
_rooms.Add(newRoom);
}
private void ConnectRooms()
{
// 按顺序连接相邻房间(最简单的连接策略)
for (int i = 0; i < _rooms.Count - 1; i++)
{
var centerA = _rooms[i].GetCenter();
var centerB = _rooms[i + 1].GetCenter();
CarveCorridorL(centerA, centerB);
}
}
// L 形走廊:先横后竖
private void CarveCorridorL(Vector2I from, Vector2I to)
{
// 横向走廊
int startX = Mathf.Min(from.X, to.X);
int endX = Mathf.Max(from.X, to.X);
for (int x = startX; x <= endX; x++)
_map[x, from.Y] = TileType.Corridor;
// 纵向走廊
int startY = Mathf.Min(from.Y, to.Y);
int endY = Mathf.Max(from.Y, to.Y);
for (int y = startY; y <= endY; y++)
_map[to.X, y] = TileType.Corridor;
}
public List<Rect2I> GetRooms() => _rooms;
}GDScript
extends Node3D
enum TileType { WALL, FLOOR, CORRIDOR, DOOR }
@export var map_width: int = 50
@export var map_height: int = 50
@export var room_count: int = 10
@export var min_room_size: int = 4
@export var max_room_size: int = 10
var _map: Array = []
var _rooms: Array[Rect2i] = []
func generate(seed: int = 0) -> Array:
seed(seed)
_map = []
_rooms.clear()
for x in map_width:
_map.append([])
for y in map_height:
_map[x].append(TileType.WALL)
for i in room_count:
_try_place_room()
_connect_rooms()
return _map
func _try_place_room() -> void:
var w := randi_range(min_room_size, max_room_size)
var h := randi_range(min_room_size, max_room_size)
var x := randi_range(1, map_width - w - 1)
var y := randi_range(1, map_height - h - 1)
var new_room := Rect2i(x, y, w, h)
for room in _rooms:
if new_room.intersects(room.grow(1)):
return
for rx in range(x, x + w):
for ry in range(y, y + h):
_map[rx][ry] = TileType.FLOOR
_rooms.append(new_room)
func _connect_rooms() -> void:
for i in _rooms.size() - 1:
var center_a := _rooms[i].get_center()
var center_b := _rooms[i + 1].get_center()
_carve_corridor_l(center_a, center_b)
func _carve_corridor_l(from: Vector2i, to: Vector2i) -> void:
for x in range(mini(from.x, to.x), maxi(from.x, to.x) + 1):
_map[x][from.y] = TileType.CORRIDOR
for y in range(mini(from.y, to.y), maxi(from.y, to.y) + 1):
_map[to.x][y] = TileType.CORRIDOR4.3 关卡随机化:波函数坍缩(WFC)简化版
波函数坍缩(Wave Function Collapse)是一种基于约束的地图生成算法,能生成符合规则的自然地图。
C#
using Godot;
using System.Collections.Generic;
using System.Linq;
// 简化版 WFC:基于规则的地图生成
public partial class SimpleWFC : Node
{
// 地块类型
public enum Tile { Grass, Water, Sand, Forest }
// 邻接规则:每种地块可以与哪些地块相邻
private static readonly Dictionary<Tile, HashSet<Tile>> AdjacencyRules = new()
{
[Tile.Grass] = new() { Tile.Grass, Tile.Sand, Tile.Forest },
[Tile.Water] = new() { Tile.Water, Tile.Sand },
[Tile.Sand] = new() { Tile.Sand, Tile.Grass, Tile.Water },
[Tile.Forest] = new() { Tile.Forest, Tile.Grass },
};
private int _width, _height;
private List<Tile>[,] _possibilities; // 每个格子的可能地块列表
public Tile[,] Generate(int width, int height, int seed = 0)
{
GD.Seed((ulong)seed);
_width = width;
_height = height;
// 初始化:每个格子都可能是任何地块
_possibilities = new List<Tile>[width, height];
var allTiles = System.Enum.GetValues<Tile>().ToList();
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
_possibilities[x, y] = new List<Tile>(allTiles);
// 迭代坍缩
while (HasUncollapsed())
{
var (cx, cy) = FindLowestEntropy();
Collapse(cx, cy);
Propagate(cx, cy);
}
// 提取结果
var result = new Tile[width, height];
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
result[x, y] = _possibilities[x, y][0];
return result;
}
private bool HasUncollapsed()
{
for (int x = 0; x < _width; x++)
for (int y = 0; y < _height; y++)
if (_possibilities[x, y].Count > 1) return true;
return false;
}
private (int, int) FindLowestEntropy()
{
int minEntropy = int.MaxValue;
int bestX = 0, bestY = 0;
for (int x = 0; x < _width; x++)
{
for (int y = 0; y < _height; y++)
{
int count = _possibilities[x, y].Count;
if (count > 1 && count < minEntropy)
{
minEntropy = count;
bestX = x; bestY = y;
}
}
}
return (bestX, bestY);
}
private void Collapse(int x, int y)
{
var options = _possibilities[x, y];
int idx = GD.RandRange(0, options.Count - 1);
_possibilities[x, y] = new List<Tile> { options[idx] };
}
private void Propagate(int startX, int startY)
{
var queue = new Queue<(int, int)>();
queue.Enqueue((startX, startY));
while (queue.Count > 0)
{
var (x, y) = queue.Dequeue();
var currentOptions = _possibilities[x, y];
// 检查四个方向的邻居
int[][] dirs = { new[]{0,1}, new[]{0,-1}, new[]{1,0}, new[]{-1,0} };
foreach (var dir in dirs)
{
int nx = x + dir[0], ny = y + dir[1];
if (nx < 0 || nx >= _width || ny < 0 || ny >= _height) continue;
var neighborOptions = _possibilities[nx, ny];
int before = neighborOptions.Count;
// 移除不符合邻接规则的选项
neighborOptions.RemoveAll(tile =>
!currentOptions.Any(ct => AdjacencyRules[ct].Contains(tile)));
if (neighborOptions.Count < before)
queue.Enqueue((nx, ny));
}
}
}
}GDScript
extends Node
enum Tile { GRASS, WATER, SAND, FOREST }
const ADJACENCY_RULES := {
Tile.GRASS: [Tile.GRASS, Tile.SAND, Tile.FOREST],
Tile.WATER: [Tile.WATER, Tile.SAND],
Tile.SAND: [Tile.SAND, Tile.GRASS, Tile.WATER],
Tile.FOREST: [Tile.FOREST, Tile.GRASS],
}
var _width: int
var _height: int
var _possibilities: Array = []
func generate(width: int, height: int, p_seed: int = 0) -> Array:
seed(p_seed)
_width = width
_height = height
_possibilities = []
var all_tiles := [Tile.GRASS, Tile.WATER, Tile.SAND, Tile.FOREST]
for x in width:
_possibilities.append([])
for y in height:
_possibilities[x].append(all_tiles.duplicate())
while _has_uncollapsed():
var pos := _find_lowest_entropy()
_collapse(pos.x, pos.y)
_propagate(pos.x, pos.y)
var result := []
for x in width:
result.append([])
for y in height:
result[x].append(_possibilities[x][y][0])
return result
func _has_uncollapsed() -> bool:
for x in _width:
for y in _height:
if _possibilities[x][y].size() > 1:
return true
return false
func _find_lowest_entropy() -> Vector2i:
var min_entropy := 9999
var best := Vector2i.ZERO
for x in _width:
for y in _height:
var count: int = _possibilities[x][y].size()
if count > 1 and count < min_entropy:
min_entropy = count
best = Vector2i(x, y)
return best
func _collapse(x: int, y: int) -> void:
var options: Array = _possibilities[x][y]
var idx := randi() % options.size()
_possibilities[x][y] = [options[idx]]
func _propagate(start_x: int, start_y: int) -> void:
var queue := [[start_x, start_y]]
while queue.size() > 0:
var pos: Array = queue.pop_front()
var x: int = pos[0]
var y: int = pos[1]
var current_options: Array = _possibilities[x][y]
for dir in [Vector2i(0,1), Vector2i(0,-1), Vector2i(1,0), Vector2i(-1,0)]:
var nx := x + dir.x
var ny := y + dir.y
if nx < 0 or nx >= _width or ny < 0 or ny >= _height:
continue
var neighbor: Array = _possibilities[nx][ny]
var before := neighbor.size()
neighbor = neighbor.filter(func(tile):
return current_options.any(func(ct): return tile in ADJACENCY_RULES[ct])
)
_possibilities[nx][ny] = neighbor
if neighbor.size() < before:
queue.append([nx, ny])4.4 道具随机摆放
基于权重的随机道具摆放系统:
C#
using Godot;
using System.Collections.Generic;
public partial class ItemSpawner : Node3D
{
// 道具配置:名称、场景路径、权重
private record ItemConfig(string Name, string ScenePath, float Weight);
private readonly List<ItemConfig> _items = new()
{
new("coin", "res://scenes/items/coin.tscn", 60f),
new("gem", "res://scenes/items/gem.tscn", 25f),
new("health_pot", "res://scenes/items/health_pot.tscn", 12f),
new("rare_item", "res://scenes/items/rare_item.tscn", 3f),
};
// 按权重随机选择道具
public string PickRandomItem()
{
float totalWeight = 0f;
foreach (var item in _items) totalWeight += item.Weight;
float roll = GD.Randf() * totalWeight;
float cumulative = 0f;
foreach (var item in _items)
{
cumulative += item.Weight;
if (roll <= cumulative)
return item.Name;
}
return _items[^1].Name;
}
// 在指定区域随机生成道具
public void SpawnItemsInRoom(Rect2 roomBounds, int count)
{
for (int i = 0; i < count; i++)
{
string itemName = PickRandomItem();
var config = _items.Find(x => x.Name == itemName);
if (config == null) continue;
float x = GD.Randf() * roomBounds.Size.X + roomBounds.Position.X;
float z = GD.Randf() * roomBounds.Size.Y + roomBounds.Position.Y;
var scene = GD.Load<PackedScene>(config.ScenePath);
var instance = scene.Instantiate<Node3D>();
instance.Position = new Vector3(x, 0, z);
AddChild(instance);
}
}
}GDScript
extends Node3D
const ITEMS := [
{ "name": "coin", "scene": "res://scenes/items/coin.tscn", "weight": 60.0 },
{ "name": "gem", "scene": "res://scenes/items/gem.tscn", "weight": 25.0 },
{ "name": "health_pot", "scene": "res://scenes/items/health_pot.tscn", "weight": 12.0 },
{ "name": "rare_item", "scene": "res://scenes/items/rare_item.tscn", "weight": 3.0 },
]
func pick_random_item() -> Dictionary:
var total_weight := 0.0
for item in ITEMS:
total_weight += item.weight
var roll := randf() * total_weight
var cumulative := 0.0
for item in ITEMS:
cumulative += item.weight
if roll <= cumulative:
return item
return ITEMS[-1]
func spawn_items_in_room(room_bounds: Rect2, count: int) -> void:
for i in count:
var item := pick_random_item()
var x := randf() * room_bounds.size.x + room_bounds.position.x
var z := randf() * room_bounds.size.y + room_bounds.position.y
var scene := load(item.scene) as PackedScene
var instance := scene.instantiate() as Node3D
instance.position = Vector3(x, 0, z)
add_child(instance)种子系统
为程序化生成提供种子(seed)参数,可以让玩家分享关卡代码,重现相同的随机关卡。这是 Roguelike 游戏的常见设计。
