[Godot] C#基于噪声的简单TileMap地图生成
地图生成的方式有很多种,这里给大家讲一下我最近实现的基于噪声的TileMap地图生成
噪声生成
首先,我们需要生成一个噪声,为了可复现,我们设置了种子,以及噪声的频率,我为了方便调整用了一个变量,大家根据需要去写吧,具体函数如下
private float[,] GenerateNoise() //噪声图生成{var noise = new FastNoiseLite(){NoiseType = FastNoiseLite.NoiseTypeEnum.Perlin,Frequency = noiseValue,Seed = Seed,FractalType = FastNoiseLite.FractalTypeEnum.Ridged};float[,] noiseMap = new float[mapX, mapY];for (int x = 0; x < mapX; x++)for (int y = 0; y < mapY; y++){float n = (noise.GetNoise2D(x, y) + 1f) / 2f;noiseMap[x, y] = n;}return noiseMap;}使用该函数,我们将获得一个float类型的二维数组,方便我们接下来可以去生成不同的地块,我们声明一个变量用于存储他
private float[,] noise;地块生成
接下来,我们使用一个TileMapLayer去渲染地块,我这里因为需要,还写了一个bool类型的二维数组作为占用网格,来存储墙
private bool[,] tileOccupy;为了生成不同的地块,我添加了一个权重表,至于如何加载,大家就根据需要去写吧,我这里就不专门讲了
private List<(int index, float weight)> weightTable = new();最重要的,不要忘记声明地图的长和宽的变量
[Export] private int mapX; //长
[Export] private int mapY; //宽生成代码
private async Task GenerateTile(){//这里我的TileMap也是程序化生成的,大家根据需要去修改if (tileMaps.GetChildCount() == 0){tileMapLayer = new TileMapLayer();tileMapLayer.TileSet = tilesSet;tileMaps.AddChild(tileMapLayer);}tileOccupy = new bool[mapX, mapY]; //墙占用数组for (int i = 0; i < mapX; i++){for (int j = 0; j < mapY; j++){//这里我是根据自定义的地块权重表来得到不同的地块int tileIndex = GetWeightIndex(noise[i, j], weightTable);tileMapLayer.SetCell(new Vector2I(i, j), tileIndex, Vector2I.Zero);//这里是我自定义的地块数据,来设置是否墙tileOccupy[i, j] = packDatas[tileIndex].isWall;}//一列一列更新,看起来很顺滑await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);}}通过权重获取索引
private int GetWeightIndex(float n, List<(int index, float weight)> weights){foreach (var w in weights)if (n <= w.weight)return w.index;return weights[^1].index;}初始化生成
我们写了一个Init函数用来初始化,当然你可以根据需要写在Ready函数中,因为我需要传入一些自定义的数据,所以专门写了一个初始化函数
Init函数
public async Task Init(){Seed = seed;noise = GenerateNoise();rng = new Random(Seed);//这里是关于地块数据加载的,里面还有权重表加载,大家根据需要去写吧LoadTilePacksData();await GenerateMap();}变量的声明我就不一一列举了,接下来我们可以看看效果
生成效果
ps:我只有两种颜色的地块,看起来有很多颜色是因为我开启了碰撞体显示,用来看墙的生成状况,这个图使用的噪声频率是0.008

打通所有路
接下来,我们需要将所有的路都打通,这里我使用的是DFS来获取所有岛,得到主岛(地块最多的岛)和其他岛,刚开始我是直接获取所有岛的集合,但是经过思考我发现我只需要获得岛的外圈即可连接两个最近点,函数如下
DFS获取主岛和外岛
private (List<Vector2I> mainLand, List<List<Vector2I>> otherLands) DFS_Land(){int maxSize = 0;var mainLand = new List<Vector2I>();var otherLands = new List<List<Vector2I>>();var visited = new bool[mapX, mapY];var dirs = new (int x, int y)[] { (0, 1), (0, -1), (1, 0), (-1, 0) };for (int x = 0; x < mapX; x++){for (int y = 0; y < mapY; y++){if (tileOccupy[x, y] || visited[x, y])continue;var isLand = new List<Vector2I>();var q = new Queue<Vector2I>();q.Enqueue(new Vector2I(x, y));visited[x, y] = true;int size = 0;//队列搜索while (q.Count > 0){var vec = q.Dequeue();size++;bool isEdge = false;foreach (var dir in dirs){var nx = vec.X + dir.x;var ny = vec.Y + dir.y;//判断墙和边界if (nx < 0 || ny < 0 || nx >= mapX || ny >= mapY || tileOccupy[nx, ny]){isEdge = true;continue;}//未访问,入队if (!visited[nx, ny]){visited[nx, ny] = true;q.Enqueue(new Vector2I(nx, ny));}}if (isEdge)isLand.Add(vec);}if (size > maxSize){if (mainLand.Count > 0)otherLands.Add(mainLand);maxSize = size;mainLand = isLand;}elseotherLands.Add(isLand);}}return (mainLand, otherLands);}获取两岛之间最近的两点
这里我声明了一个没有墙的地块权重表
private void ConnectPoint(Vector2I a, Vector2I b, List<(int index, float weight)> weights){int ax = a.X, ay = a.Y;int bx = b.X, by = b.Y;while (ax != bx || ay != by){int width = rng.Next(1, 3);bool dir = rng.NextDouble() < 0.5f;//随机偏移int ox = rng.Next(-1, 2);int oy = rng.Next(-1, 2);var vec = new Vector2I(ax + ox, ay + oy);DrawRoadTile(vec, width, weights);if (dir)ax += ax < bx ? 1 : -1;elseay += ay < by ? 1 : -1;}}连接并绘制路线
这里我加了一点随机偏移,不然直路看起来太奇怪了,以及路的宽度的大小,大家根据需要去调整
private void ConnectPoint(Vector2I a, Vector2I b, List<(int index, float weight)> weights){int ax = a.X, ay = a.Y;int bx = b.X, by = b.Y;while (ax != bx || ay != by){int width = rng.Next(1, 3);bool dir = rng.NextDouble() < 0.5f;//随机偏移int ox = rng.Next(-1, 2);int oy = rng.Next(-1, 2);var vec = new Vector2I(ax + ox, ay + oy);DrawRoadTile(vec, width, weights);if (dir)ax += ax < bx ? 1 : -1;elseay += ay < by ? 1 : -1;}}private void DrawRoadTile(Vector2I point, int width, List<(int index, float weight)> weights){for (int x = -width; x < width; x++){for (int y = -width; y < width; y++){int px = x + point.X; int py = y + point.Y;if (px < 0 || py < 0 || px >= mapX || py >= mapY)continue;tileOccupy[px, py] = false;var tileID = GetWeightIndex(noise[px, py], weights);tileMapLayer.SetCell(new Vector2I(px, py), tileID, Vector2I.Zero);}}}调用函数
private async Task ThroughRoad(){var isLands = DFS_Land();//这里我把我的道路地块取出var weights = weightTable.Where(w => !packDatas[w.index].isWall).ToList();//生成所有岛连接线foreach (var other in isLands.otherLands){var twoPoint = FindClosePoint(isLands.mainLand, other);ConnectPoint(twoPoint.a, twoPoint.b, weights);await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);}}我们将其加入初始化函数内
public async Task Init(){...await ThroughRoad();}最终生成效果

结语
以上是我简单实现的过程,大家可以根据需要自行修改,感谢大家观看
