地图生成的方式有很多种,这里给大家讲一下我最近实现的基于噪声的TileMap地图生成
噪声生成
首先,我们需要生成一个噪声,为了可复现,我们设置了种子,以及噪声的频率,我为了方便调整用了一个变量,大家根据需要去写吧,具体函数如下
cs
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类型的二维数组,方便我们接下来可以去生成不同的地块,我们声明一个变量用于存储他
cs
private float[,] noise;
地块生成
接下来,我们使用一个TileMapLayer去渲染地块,我这里因为需要,还写了一个bool类型的二维数组作为占用网格,来存储墙
cs
private bool[,] tileOccupy;
为了生成不同的地块,我添加了一个权重表,至于如何加载,大家就根据需要去写吧,我这里就不专门讲了
cs
private List<(int index, float weight)> weightTable = new();
最重要的,不要忘记声明地图的长和宽的变量
cs
[Export] private int mapX; //长
[Export] private int mapY; //宽
生成代码
cs
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);
}
}
通过权重获取索引
cs
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函数
cs
public async Task Init()
{
Seed = seed;
noise = GenerateNoise();
rng = new Random(Seed);
//这里是关于地块数据加载的,里面还有权重表加载,大家根据需要去写吧
LoadTilePacksData();
await GenerateMap();
}
变量的声明我就不一一列举了,接下来我们可以看看效果
生成效果
ps:我只有两种颜色的地块,看起来有很多颜色是因为我开启了碰撞体显示,用来看墙的生成状况,这个图使用的噪声频率是0.008

打通所有路
接下来,我们需要将所有的路都打通,这里我使用的是DFS来获取所有岛,得到主岛(地块最多的岛)和其他岛,刚开始我是直接获取所有岛的集合,但是经过思考我发现我只需要获得岛的外圈即可连接两个最近点,函数如下
DFS获取主岛和外岛
cs
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;
}
else
otherLands.Add(isLand);
}
}
return (mainLand, otherLands);
}
获取两岛之间最近的两点
这里我声明了一个没有墙的地块权重表
cs
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;
else
ay += ay < by ? 1 : -1;
}
}
连接并绘制路线
这里我加了一点随机偏移,不然直路看起来太奇怪了,以及路的宽度的大小,大家根据需要去调整
cs
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;
else
ay += 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);
}
}
}
调用函数
cs
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);
}
}
我们将其加入初始化函数内
cs
public async Task Init()
{
...
await ThroughRoad();
}
最终生成效果

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