9. 程序化生成
2026/4/14大约 13 分钟
程序化生成
你玩《我的世界》的时候,每次创建新世界,地图都不一样——有高山、有海洋、有沙漠、有森林。这些地图不是设计师一个一个手动摆出来的,而是电脑自动生成的。
这就是程序化生成(Procedural Generation,简称 PCG)。简单来说:用代码和算法来自动创建游戏内容,而不是人工一个个放置。
打个比方:程序化生成就像"自动乐高机"——你丢进去一些参数(比如"我要一个 100x100 的地图,有 30% 的山、10% 的水"),机器就自动帮你拼出一个独一无二的乐高世界。
什么是程序化生成
为什么需要程序化生成
| 原因 | 说明 |
|---|---|
| 节省人力 | 手动设计一张大地图可能需要几个月,程序化生成几秒钟搞定 |
| 增加可玩性 | 每次玩都不一样,玩家愿意反复尝试 |
| 减小包体 | 不需要把巨大的地图数据打包进游戏,只需要存储生成算法 |
| 无限扩展 | 理论上可以生成无限大的世界(只要算力够) |
程序化生成能做什么
| 生成内容 | 举例游戏 |
|---|---|
| 地形 | 《我的世界》的地形、《泰拉瑞亚》的地下洞穴 |
| 关卡 | 《哈迪斯》的房间、《死亡细胞》的地图 |
| 物品 | 《暗黑破坏神》的随机装备属性 |
| 角色 | 《星露谷物语》的村民外貌组合 |
| 音乐 | 部分游戏用算法生成背景音乐 |
| 剧情 | 少数游戏尝试用 AI 生成对话和事件 |
FastNoiseLite —— Godot 的噪声生成器
噪声(Noise)是程序化生成中最常用的工具。你可以把噪声理解成"有规律的无规律"——它看起来是随机的,但实际上有平滑的过渡。
打个比方:真正的随机数就像把一把豆子撒在地上——到处都是,毫无规律。噪声就像起伏的山丘——高高低低,但相邻的地方高度差不多,过渡很自然。
Godot 4 内置了 FastNoiseLite 节点,专门用来生成各种类型的噪声。
FastNoiseLite 的基本使用
C
using Godot;
public partial class NoiseGenerator : Node
{
private FastNoiseLite _noise;
public override void _Ready()
{
_noise = new FastNoiseLite();
// 设置噪声参数
_noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex; // 噪声类型
_noise.Frequency = 0.05f; // 频率:越小,地形越大越平缓
_noise.Seed = 42; // 种子:同样的种子生成同样的地形
// 获取某个坐标的噪声值(返回 -1 到 1 之间的浮点数)
float value = _noise.GetNoise2D(100, 200);
GD.Print($"坐标 (100, 200) 的噪声值:{value}");
}
}GDScript
extends Node
var _noise: FastNoiseLite
func _ready() -> void:
_noise = FastNoiseLite.new()
# 设置噪声参数
_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX # 噪声类型
_noise.frequency = 0.05 # 频率:越小,地形越大越平缓
_noise.seed = 42 # 种子:同样的种子生成同样的地形
# 获取某个坐标的噪声值(返回 -1 到 1 之间的浮点数)
var value := _noise.get_noise_2d(100, 200)
print("坐标 (100, 200) 的噪声值:%f" % value)噪声参数详解
| 参数 | 作用 | 常用值 |
|---|---|---|
NoiseType | 噪声类型(Simplex 最常用) | Simplex、Perlin、Cellular |
Frequency | 频率,值越小地形越平缓 | 0.01(大山脉)到 0.1(小丘陵) |
Seed | 种子,决定生成的内容 | 任意整数,相同种子 = 相同结果 |
FractalOctaves | 叠加层数,越多细节越丰富 | 1-6 |
FractalLacunarity | 每层的频率倍数 | 通常 2.0 |
FractalGain | 每层的振幅衰减 | 通常 0.5 |
用噪声生成地形
下面我们用噪声生成一个简单的 2D 地形图。不同的噪声值对应不同的地形类型:
- 值 < -0.3:水(蓝色)
- 值 -0.3 到 0.0:沙滩(黄色)
- 值 0.0 到 0.4:草地(绿色)
- 值 0.4 到 0.7:森林(深绿色)
- 值 > 0.7:山地(灰色)
C
using Godot;
public partial class TerrainGenerator : Node2D
{
[Export] public int MapWidth { get; set; } = 100;
[Export] public int MapHeight { get; set; } = 100;
[Export] public int TileSize { get; set; } = 16;
[Export] public int Seed { get; set; } = 42;
[Export] public float Frequency { get; set; } = 0.05f;
private FastNoiseLite _noise;
private TileMapLayer _tileMap;
// 地形类型枚举
private enum TerrainType { Water, Sand, Grass, Forest, Mountain }
// 地形对应的颜色(用于调试可视化)
private static readonly Color[] TerrainColors = new[]
{
new Color(0.2f, 0.4f, 0.8f), // 水 - 蓝色
new Color(0.9f, 0.8f, 0.5f), // 沙滩 - 黄色
new Color(0.3f, 0.7f, 0.3f), // 草地 - 绿色
new Color(0.1f, 0.5f, 0.2f), // 森林 - 深绿色
new Color(0.5f, 0.5f, 0.5f), // 山地 - 灰色
};
public override void _Ready()
{
_noise = new FastNoiseLite();
_noise.Seed = Seed;
_noise.Frequency = Frequency;
_noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
_noise.FractalOctaves = 4; // 4 层叠加,让地形更自然
GenerateTerrain();
}
public void GenerateTerrain()
{
GD.Print("开始生成地形...");
// 方法一:用 Image 可视化(方便调试)
var image = Image.CreateEmpty(MapWidth, MapHeight, false, Image.Format.Rgba8);
for (int x = 0; x < MapWidth; x++)
{
for (int y = 0; y < MapHeight; y++)
{
float noiseValue = _noise.GetNoise2D(x, y);
TerrainType terrain = GetTerrainType(noiseValue);
image.SetPixel(x, y, TerrainColors[(int)terrain]);
}
}
// 保存为图片文件,方便查看
var texture = ImageTexture.CreateFromImage(image);
var sprite = GetNodeOrNull<Sprite2D>("Preview");
if (sprite != null)
{
sprite.Texture = texture;
}
texture.GetImage().SavePng("user://terrain_preview.png");
GD.Print("地形生成完成!预览图已保存");
}
private TerrainType GetTerrainType(float noiseValue)
{
// 噪声值范围通常是 -1 到 1
if (noiseValue < -0.3f) return TerrainType.Water;
if (noiseValue < 0.0f) return TerrainType.Sand;
if (noiseValue < 0.4f) return TerrainType.Grass;
if (noiseValue < 0.7f) return TerrainType.Forest;
return TerrainType.Mountain;
}
}GDScript
extends Node2D
@export var map_width: int = 100
@export var map_height: int = 100
@export var tile_size: int = 16
@export var seed_val: int = 42
@export var frequency: float = 0.05
var _noise: FastNoiseLite
# 地形类型
enum TerrainType { WATER, SAND, GRASS, FOREST, MOUNTAIN }
# 地形对应的颜色(调试用)
var _terrain_colors := [
Color(0.2, 0.4, 0.8), # 水 - 蓝色
Color(0.9, 0.8, 0.5), # 沙滩 - 黄色
Color(0.3, 0.7, 0.3), # 草地 - 绿色
Color(0.1, 0.5, 0.2), # 森林 - 深绿色
Color(0.5, 0.5, 0.5), # 山地 - 灰色
]
func _ready() -> void:
_noise = FastNoiseLite.new()
_noise.seed = seed_val
_noise.frequency = frequency
_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
_noise.fractal_octaves = 4 # 4 层叠加
generate_terrain()
func generate_terrain() -> void:
print("开始生成地形...")
# 用 Image 可视化
var image := Image.create_empty(map_width, map_height, false, Image.FORMAT_RGBA8)
for x in range(map_width):
for y in range(map_height):
var noise_value := _noise.get_noise_2d(x, y)
var terrain := _get_terrain_type(noise_value)
image.set_pixel(x, y, _terrain_colors[terrain])
# 保存预览图
var texture := ImageTexture.create_from_image(image)
var preview := get_node_or_null("Preview") as Sprite2D
if preview:
preview.texture = texture
texture.get_image().save_png("user://terrain_preview.png")
print("地形生成完成!预览图已保存")
func _get_terrain_type(noise_value: float) -> int:
if noise_value < -0.3:
return TerrainType.WATER
elif noise_value < 0.0:
return TerrainType.SAND
elif noise_value < 0.4:
return TerrainType.GRASS
elif noise_value < 0.7:
return TerrainType.FOREST
else:
return TerrainType.MOUNTAIN随机地牢生成——BSP 算法简化版
地牢生成是程序化生成中的经典问题。最常见的算法是 BSP(Binary Space Partitioning,二叉空间分割)。
BSP 算法的原理
打个比方:你有一块长方形的地皮,你想在上面建几间房子。
- 第一步:把地皮从中间切一刀(横切或竖切),分成两块
- 第二步:对每一块再切一刀,继续分
- 第三步:切到每一块都够小了,就在每一块里放一个房间
- 第四步:用走廊把相邻的房间连起来
这就是 BSP 算法的核心思路。
C
using Godot;
using System.Collections.Generic;
public partial class DungeonGenerator : Node2D
{
[Export] public int MapWidth { get; set; } = 64;
[Export] public int MapHeight { get; set; } = 64;
[Export] public int MinRoomSize { get; set; } = 6;
[Export] public int MaxRoomSize { get; set; } = 14;
[Export] public int Seed { get; set; } = 42;
[Export] public int MaxDepth { get; set; } = 4;
private RandomNumberGenerator _rng;
private int[,] _map; // 0=墙壁, 1=地板, 2=走廊
// 地牢房间数据
public class Room
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int CenterX => X + Width / 2;
public int CenterY => Y + Height / 2;
public Rect2I GetRect() => new(X, Y, Width, Height);
}
public List<Room> GeneratedRooms { get; private set; } = new();
public override void _Ready()
{
_rng = new RandomNumberGenerator();
_rng.Seed = (ulong)Seed;
GenerateDungeon();
}
public void GenerateDungeon()
{
_map = new int[MapWidth, MapHeight];
GeneratedRooms.Clear();
// 用 BSP 递归分割空间
var rootArea = new Rect2I(1, 1, MapWidth - 2, MapHeight - 2);
SplitSpace(rootArea, 0);
// 在地图上绘制房间
foreach (var room in GeneratedRooms)
{
DrawRoom(room);
}
// 连接相邻房间
for (int i = 0; i < GeneratedRooms.Count - 1; i++)
{
ConnectRooms(GeneratedRooms[i], GeneratedRooms[i + 1]);
}
GD.Print($"地牢生成完成!共 {GeneratedRooms.Count} 个房间");
}
// 递归分割空间
private void SplitSpace(Rect2I area, int depth)
{
// 深度够了或者面积太小,就放一个房间
if (depth >= MaxDepth ||
area.Size.X < MinRoomSize * 2 ||
area.Size.Y < MinRoomSize * 2)
{
CreateRoom(area);
return;
}
// 决定横切还是竖切
bool splitHorizontally;
if (area.Size.X > area.Size.Y * 1.25f)
splitHorizontally = false; // 宽了就竖切
else if (area.Size.Y > area.Size.X * 1.25f)
splitHorizontally = true; // 高了就横切
else
splitHorizontally = _rng.Randf() > 0.5f; // 差不多就随机
// 找到分割点
int split;
if (splitHorizontally)
{
int minSplit = area.Position.Y + MinRoomSize;
int maxSplit = area.End.Y - MinRoomSize;
split = _rng.RandiRange(minSplit, maxSplit);
// 上半部分
var top = new Rect2I(
area.Position.X, area.Position.Y,
area.Size.X, split - area.Position.Y
);
// 下半部分
var bottom = new Rect2I(
area.Position.X, split,
area.Size.X, area.End.Y - split
);
SplitSpace(top, depth + 1);
SplitSpace(bottom, depth + 1);
}
else
{
int minSplit = area.Position.X + MinRoomSize;
int maxSplit = area.End.X - MinRoomSize;
split = _rng.RandiRange(minSplit, maxSplit);
// 左半部分
var left = new Rect2I(
area.Position.X, area.Position.Y,
split - area.Position.X, area.Size.Y
);
// 右半部分
var right = new Rect2I(
split, area.Position.Y,
area.End.X - split, area.Size.Y
);
SplitSpace(left, depth + 1);
SplitSpace(right, depth + 1);
}
}
// 在一个区域内创建房间
private void CreateRoom(Rect2I area)
{
int roomWidth = _rng.RandiRange(
Mathf.Max(MinRoomSize, area.Size.X - 4),
Mathf.Min(MaxRoomSize, area.Size.X - 2)
);
int roomHeight = _rng.RandiRange(
Mathf.Max(MinRoomSize, area.Size.Y - 4),
Mathf.Min(MaxRoomSize, area.Size.Y - 2)
);
int roomX = _rng.RandiRange(area.Position.X + 1, area.End.X - roomWidth - 1);
int roomY = _rng.RandiRange(area.Position.Y + 1, area.End.Y - roomHeight - 1);
GeneratedRooms.Add(new Room
{
X = roomX,
Y = roomY,
Width = roomWidth,
Height = roomHeight
});
}
// 在地图上画房间(把房间区域标记为地板)
private void DrawRoom(Room room)
{
for (int x = room.X; x < room.X + room.Width; x++)
{
for (int y = room.Y; y < room.Y + room.Height; y++)
{
if (x > 0 && x < MapWidth - 1 && y > 0 && y < MapHeight - 1)
{
_map[x, y] = 1;
}
}
}
}
// 用 L 形走廊连接两个房间
private void ConnectRooms(Room roomA, Room roomB)
{
int startX = roomA.CenterX;
int startY = roomA.CenterY;
int endX = roomB.CenterX;
int endY = roomB.CenterY;
// 随机决定先走横还是先走竖
if (_rng.Randf() > 0.5f)
{
DrawHorizontalCorridor(startX, endX, startY);
DrawVerticalCorridor(startY, endY, endX);
}
else
{
DrawVerticalCorridor(startY, endY, startX);
DrawHorizontalCorridor(startX, endX, endY);
}
}
private void DrawHorizontalCorridor(int x1, int x2, int y)
{
int minX = Mathf.Min(x1, x2);
int maxX = Mathf.Max(x1, x2);
for (int x = minX; x <= maxX; x++)
{
if (x > 0 && x < MapWidth - 1 && y > 0 && y < MapHeight - 1)
{
_map[x, y] = 2;
}
}
}
private void DrawVerticalCorridor(int y1, int y2, int x)
{
int minY = Mathf.Min(y1, y2);
int maxY = Mathf.Max(y1, y2);
for (int y = minY; y <= maxY; y++)
{
if (x > 0 && x < MapWidth - 1 && y > 0 && y < MapHeight - 1)
{
_map[x, y] = 2;
}
}
}
// 获取地图数据
public int GetTile(int x, int y)
{
if (x < 0 || x >= MapWidth || y < 0 || y >= MapHeight)
return 0;
return _map[x, y];
}
}GDScript
extends Node2D
@export var map_width: int = 64
@export var map_height: int = 64
@export var min_room_size: int = 6
@export var max_room_size: int = 14
@export var seed_val: int = 42
@export var max_depth: int = 4
var _rng := RandomNumberGenerator.new()
var _map: Array = [] # 0=墙壁, 1=地板, 2=走廊
var generated_rooms: Array[Dictionary] = []
func _ready() -> void:
_rng.seed = seed_val
generate_dungeon()
func generate_dungeon() -> void:
# 初始化地图
_map.clear()
for x in range(map_width):
var col: Array = []
for y in range(map_height):
col.append(0)
_map.append(col)
generated_rooms.clear()
# BSP 递归分割
var root_area := Rect2i(1, 1, map_width - 2, map_height - 2)
_split_space(root_area, 0)
# 绘制房间
for room in generated_rooms:
_draw_room(room)
# 连接相邻房间
for i in range(generated_rooms.size() - 1):
_connect_rooms(generated_rooms[i], generated_rooms[i + 1])
print("地牢生成完成!共 %d 个房间" % generated_rooms.size())
# 递归分割空间
func _split_space(area: Rect2i, depth: int) -> void:
if depth >= max_depth or area.size.x < min_room_size * 2 or area.size.y < min_room_size * 2:
_create_room(area)
return
# 决定切的方向
var split_horizontally: bool
if area.size.x > area.size.y * 1.25:
split_horizontally = false
elif area.size.y > area.size.x * 1.25:
split_horizontally = true
else:
split_horizontally = _rng.randf() > 0.5
if split_horizontally:
var min_split := area.position.y + min_room_size
var max_split := area.end.y - min_room_size
var split := _rng.randi_range(min_split, max_split)
var top := Rect2i(area.position.x, area.position.y, area.size.x, split - area.position.y)
var bottom := Rect2i(area.position.x, split, area.size.x, area.end.y - split)
_split_space(top, depth + 1)
_split_space(bottom, depth + 1)
else:
var min_split := area.position.x + min_room_size
var max_split := area.end.x - min_room_size
var split := _rng.randi_range(min_split, max_split)
var left := Rect2i(area.position.x, area.position.y, split - area.position.x, area.size.y)
var right := Rect2i(split, area.position.y, area.end.x - split, area.size.y)
_split_space(left, depth + 1)
_split_space(right, depth + 1)
# 在区域内创建房间
func _create_room(area: Rect2i) -> void:
var room_w := _rng.randi_range(maxi(min_room_size, area.size.x - 4), mini(max_room_size, area.size.x - 2))
var room_h := _rng.randi_range(maxi(min_room_size, area.size.y - 4), mini(max_room_size, area.size.y - 2))
var room_x := _rng.randi_range(area.position.x + 1, area.end.x - room_w - 1)
var room_y := _rng.randi_range(area.position.y + 1, area.end.y - room_h - 1)
generated_rooms.append({
"x": room_x, "y": room_y,
"width": room_w, "height": room_h,
"center_x": room_x + room_w / 2,
"center_y": room_y + room_h / 2
})
# 绘制房间
func _draw_room(room: Dictionary) -> void:
for x in range(room["x"], room["x"] + room["width"]):
for y in range(room["y"], room["y"] + room["height"]):
if x > 0 and x < map_width - 1 and y > 0 and y < map_height - 1:
_map[x][y] = 1
# 用 L 形走廊连接两个房间
func _connect_rooms(room_a: Dictionary, room_b: Dictionary) -> void:
var start_x: int = room_a["center_x"]
var start_y: int = room_a["center_y"]
var end_x: int = room_b["center_x"]
var end_y: int = room_b["center_y"]
if _rng.randf() > 0.5:
_draw_h_corridor(start_x, end_x, start_y)
_draw_v_corridor(start_y, end_y, end_x)
else:
_draw_v_corridor(start_y, end_y, start_x)
_draw_h_corridor(start_x, end_x, end_y)
func _draw_h_corridor(x1: int, x2: int, y: int) -> void:
var min_x := mini(x1, x2)
var max_x := maxi(x1, x2)
for x in range(min_x, max_x + 1):
if x > 0 and x < map_width - 1 and y > 0 and y < map_height - 1:
_map[x][y] = 2
func _draw_v_corridor(y1: int, y2: int, x: int) -> void:
var min_y := mini(y1, y2)
var max_y := maxi(y1, y2)
for y in range(min_y, max_y + 1):
if x > 0 and x < map_width - 1 and y > 0 and y < map_height - 1:
_map[x][y] = 2
# 获取地图数据
func get_tile(x: int, y: int) -> int:
if x < 0 or x >= map_width or y < 0 or y >= map_height:
return 0
return _map[x][y]随机物品和敌人放置
地牢生成好了,里面空空的可不行。需要在房间里随机放置宝箱、怪物、陷阱等。
C
using Godot;
using System.Collections.Generic;
public partial class EntitySpawner : Node
{
[Export] public PackedScene ChestScene { get; set; }
[Export] public PackedScene EnemyScene { get; set; }
[Export] public PackedScene TrapScene { get; set; }
private RandomNumberGenerator _rng = new();
// 在生成的地牢中放置实体
public void PopulateDungeon(DungeonGenerator dungeon, Node2D parent)
{
_rng.Randomize();
foreach (var room in dungeon.GeneratedRooms)
{
// 每个房间至少一个宝箱(30% 概率)
if (_rng.Randf() < 0.3f)
{
SpawnEntity(ChestScene, room, parent, "Chest");
}
// 每个房间有敌人(50% 概率)
if (_rng.Randf() < 0.5f)
{
int enemyCount = _rng.RandiRange(1, 3);
for (int i = 0; i < enemyCount; i++)
{
SpawnEntity(EnemyScene, room, parent, "Enemy");
}
}
// 陷阱(20% 概率)
if (_rng.Randf() < 0.2f)
{
SpawnEntity(TrapScene, room, parent, "Trap");
}
}
}
private void SpawnEntity(PackedScene scene, DungeonGenerator.Room room, Node2D parent, string type)
{
if (scene == null) return;
var entity = scene.Instantiate<Node2D>();
// 在房间内随机一个位置(避开边缘)
float randomX = room.X + 2 + _rng.Randf() * (room.Width - 4);
float randomY = room.Y + 2 + _rng.Randf() * (room.Height - 4);
entity.Position = new Vector2(randomX * 16, randomY * 16);
parent.AddChild(entity);
GD.Print($"放置 {type} 在 ({randomX:F0}, {randomY:F0})");
}
}GDScript
extends Node
@export var chest_scene: PackedScene
@export var enemy_scene: PackedScene
@export var trap_scene: PackedScene
var _rng := RandomNumberGenerator.new()
# 在生成的地牢中放置实体
func populate_dungeon(dungeon: Node, parent: Node2D) -> void:
_rng.randomize()
for room in dungeon.generated_rooms:
# 宝箱(30% 概率)
if _rng.randf() < 0.3:
_spawn_entity(chest_scene, room, parent, "宝箱")
# 敌人(50% 概率,1-3 个)
if _rng.randf() < 0.5:
var enemy_count := _rng.randi_range(1, 3)
for i in range(enemy_count):
_spawn_entity(enemy_scene, room, parent, "敌人")
# 陷阱(20% 概率)
if _rng.randf() < 0.2:
_spawn_entity(trap_scene, room, parent, "陷阱")
func _spawn_entity(scene: PackedScene, room: Dictionary, parent: Node2D, type: String) -> void:
if not scene:
return
var entity := scene.instantiate() as Node2D
# 在房间内随机位置
var random_x: float = room["x"] + 2 + _rng.randf() * (room["width"] - 4)
var random_y: float = room["y"] + 2 + _rng.randf() * (room["height"] - 4)
entity.position = Vector2(random_x * 16, random_y * 16)
parent.add_child(entity)
print("放置 %s 在 (%.0f, %.0f)" % [type, random_x, random_y])种子系统——让世界可复现
"种子"(Seed)是程序化生成中非常重要的概念。你可以把种子想象成一串密码——同样的种子 + 同样的算法 = 完全相同的结果。
这意味着:
- 玩家 A 和玩家 B 输入相同的种子,会生成一模一样的地图
- 你可以用种子来"分享"一个特别好看的地图
- 存档时只需要保存种子,不需要保存整个地图数据
C
using Godot;
public partial class WorldSeedManager : Node
{
private int _currentSeed;
// 根据种子生成世界
public void GenerateWorld(int seed)
{
_currentSeed = seed;
var rng = new RandomNumberGenerator();
rng.Seed = (ulong)seed;
// 用这个 rng 来生成所有随机内容
// 地形、物品、敌人位置……全部用同一个 rng
// 这样同样的种子就能得到完全相同的世界
GD.Print($"使用种子 {seed} 生成世界");
}
// 用字符串生成种子(方便玩家分享)
// 比如 "mountain_dream" 转换成一个数字种子
public int StringToSeed(string seedString)
{
int hash = 0;
foreach (char c in seedString)
{
hash = hash * 31 + c;
}
return Mathf.Abs(hash);
}
// 随机生成一个种子
public int GenerateRandomSeed()
{
var rng = new RandomNumberGenerator();
rng.Randomize();
return (int)rng.Randi();
}
}GDScript
extends Node
var _current_seed: int = 0
# 根据种子生成世界
func generate_world(seed: int) -> void:
_current_seed = seed
var rng := RandomNumberGenerator.new()
rng.seed = seed
# 用这个 rng 来生成所有随机内容
print("使用种子 %d 生成世界" % seed)
# 用字符串生成种子(方便玩家分享)
func string_to_seed(seed_string: String) -> int:
var hash: int = 0
for c in seed_string:
hash = hash * 31 + c.unicode_at(0)
return absi(hash)
# 随机生成一个种子
func generate_random_seed() -> int:
var rng := RandomNumberGenerator.new()
rng.randomize()
return rng.randi()最终建议
- 从噪声地形开始,这是程序化生成最基础也最实用的部分
- 种子系统一定要做,它让程序化生成的内容可复现、可分享
- 生成的内容要有规则约束,比如"房间里不能放太多敌人"、"走廊不能太窄"
- 做好缓存,生成一次就够了,不要每帧都重新生成
- 程序化生成 + 手动调整结合效果最好——先用算法生成骨架,再人工打磨细节
