[Godot] C#基于噪声的简单TileMap地图生成

地图生成的方式有很多种,这里给大家讲一下我最近实现的基于噪声的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();
    }

最终生成效果

结语

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

相关推荐
作孽就得先起床7 小时前
unity UnauthorizedAccessException: 拒绝访问路径
unity·游戏引擎
tealcwu9 小时前
【Unity踩坑】Unity项目提示文件合并有冲突
elasticsearch·unity·游戏引擎
tealcwu1 天前
【Unity小技巧】如何将3D场景转换成2D场景
3d·unity·游戏引擎
全栈陈序员1 天前
用Rust和Bevy打造2D平台游戏原型
开发语言·rust·游戏引擎·游戏程序
神秘的土鸡2 天前
【CS创世SD NAND征文】为无人机打造可靠数据仓:工业级存储芯片CSNP32GCR01-AOW在飞控系统中的应用实践
嵌入式硬件·游戏引擎·无人机·cocos2d·雷龙
jtymyxmz2 天前
《Unity Shader》6.4.3 半兰伯特模型
unity·游戏引擎
AA陈超2 天前
ASC学习笔记0001:处理目标选择系统中当Actor拒绝目标确认时的调用
c++·笔记·学习·游戏·ue5·游戏引擎·虚幻
我的golang之路果然有问题2 天前
mac配置 unity+vscode的坑
开发语言·笔记·vscode·macos·unity·游戏引擎
HahaGiver6662 天前
Unity Shader Graph 3D 实例 - 一个简单的红外线扫描全身效果
3d·unity·游戏引擎