【unity小技巧】Unity2D TileMap+柏林噪声生成随机地图(附源码)

前言

我的上一篇文章介绍了TileMap的使用,主要是为我这篇做一个铺垫,看过上一篇文章的人,应该已经很好的理解TileMap的使用了,这里我就不需要过多的解释一些繁琐而基础的知识了,省去很多时间。所有没看过上一篇文章的小伙伴我强烈建议先去看看:
【Unity小技巧】Unity2D TileMap的探究(最简单,最全面的TileMap使用介绍)

先来看看本文实现的最终效果
在这里插入图片描述
源码在文章末尾

柏林噪声

柏林噪声(Perlin noise)是由Ken Perlin于1983年提出的一种随机数生成算法,常用于计算机图形学中的纹理、地形和粒子系统等领域。它产生了一种平滑、连续的随机分布,常用于生成自然风格的纹理和地形。

柏林噪声和直接随机有以下几个区别:

  1. 平滑性:柏林噪声生成的值在空间上变化连续平滑,不会出现剧烈的跳变。而直接随机生成的值可能会出现突然的变化,不够平滑。

  2. 一致性:柏林噪声的生成结果是基于一个固定的种子值,因此每次使用相同的种子值生成的结果都是一致的。而直接随机生成的结果每次都不同。

  3. 纹理性:柏林噪声生成的值可以用来模拟自然纹理,如山脉、云彩等。而直接随机生成的值没有这种纹理性,更加随机。

比如随机0-1可能生成跳动比较大的数据:0,0.8,0.1
而使用柏林噪声生成的数据大概率是:0,0.3,0.5

素材导入

在这里插入图片描述
在这里插入图片描述

Rule Tile配置

Rule Tile的使用我这里就不再解释了,不清楚的可以看我前面发的文章链接,这里就直接贴出配置图了,节省大家时间,配置起来也不难就是要多测试,费点时间
在这里插入图片描述

在这里插入图片描述

效果演示,可以看到无论我们如何绘制地图,都可以做很好的兼容
在这里插入图片描述

生成随机地图

新建脚本MapCreate,先定义两个方法

public class MapCreate : MonoBehaviour
{
    
    
    // 创建地图数据
    public void GenerateMap()
    {
    
    
        
    }

    // 清除地图数据
    public void CleanTileMap()
    {
    
    
        
    }
}

一直启动才生成地图,太慢了,为了加快我们的调试节奏,可以实现未启动unity生成地图效果,我们需要新建一个Editor文件夹
在这里插入图片描述
书写MapCreateEditor脚本

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(MapCreate))]// 自定义编辑器,目标为我们前面创建的MapCreate
public class MapCreateEditor : Editor
{
    
    
    public override void OnInspectorGUI()// 重写OnInspectorGUI方法
    {
    
    
        base.DrawDefaultInspector();// 绘制默认的检查器
        if (GUILayout.Button("创建地图"))// 如果GUILayout的按钮被按下,按钮名为"创建地图"
        {
    
    
            ((MapCreate)target).GenerateMap();// 目标MapGenerator生成地图
        }
        if (GUILayout.Button("清除地图"))// 如果GUILayout的按钮被按下,按钮名为"清除地图"
        {
    
    
            ((MapCreate)target).CleanTileMap();// 目标MapGenerator清理地图
        }
    }
}

效果
在这里插入图片描述
继续完善我们的MapCreate代码,代码我加了详细的中文注释,这里不过多解释了

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Tilemaps;

// 创建地图的类
public class MapCreate : MonoBehaviour
{
    
    
    public Tilemap groundTileMap; // 地图的Tilemap组件

    public int width; // 地图的宽度
    public int height; // 地图的高度

    public int seed; // 生成地图的种子
    public bool useRandomSeed; // 是否使用随机种子

    public float lacunarity; // 柏林噪声的频率,决定地形的空隙度

    [Range(0, 1f)]
    public float waterProbability; // 水域的概率

    public TileBase groundTile; // 地面的Tile
    public TileBase waterTile; // 水域的Tile

    private bool[,] mapData; // 地图数据,True表示地面,False表示水域

    // 创建地图数据
    public void GenerateMap()
    {
    
    
        GenerateMapData(); // 生成地图数据
        GenerateTileMap(); // 生成Tile地图
    }

    // 生成地图数据
    private void GenerateMapData()
    {
    
    
        // 对于种子的应用
        if (!useRandomSeed) seed = Time.time.GetHashCode(); // 如果不使用随机种子,则使用当前时间的哈希值作为种子
        UnityEngine.Random.InitState(seed); // 初始化随机状态

        float randomOffset = UnityEngine.Random.Range(-10000, 10000); // 随机偏移量
        mapData = new bool[width, height]; // 初始化地图数据
        for (int x = 0; x < width; x++)
        {
    
    
            for (int y = 0; y < height; y++)
            {
    
    
                // 使用柏林噪声生成地图数据
                float noiseValue = Mathf.PerlinNoise(x * lacunarity + randomOffset, y * lacunarity + randomOffset);
                mapData[x, y] = noiseValue < waterProbability ? false : true; // 如果噪声值小于水域概率,则该位置为水域,否则为地面
            }
        }
    }

    // 生成Tile地图
    private void GenerateTileMap()
    {
    
    
        CleanTileMap(); // 清除地图数据

        // 生成地面
        for (int x = 0; x < width; x++)
        {
    
    
            for (int y = 0; y < height; y++)
            {
    
    
                // 如果地图数据为True,则该位置为地面,否则为空
                TileBase tile = mapData[x, y] ? groundTile : waterTile;
                groundTileMap.SetTile(new Vector3Int(x, y), tile); // 设置Tile
            }
        }
    }

    // 清除地图数据
    public void CleanTileMap()
    {
    
    
        groundTileMap.ClearAllTiles(); // 清除所有的Tile
    }
}

挂载脚本,配置数据
在这里插入图片描述
运行效果
在这里插入图片描述

问题

这里还有一个问题,如果我们把lacunarity值设置很小,且把水的比例调的比较高或者比较低的时候,你会发现没有按我们要的比例生成效果
在这里插入图片描述
Mathf.PerlinNoise(x, y)函数在Unity中用于生成柏林噪声,它的返回值是在0到1之间的浮点数。这个函数的两个参数通常是在一个连续的范围内变化的,例如时间或者空间的坐标。

前面我们使用了x * lacunarity + randomOffset和y * lacunarity + randomOffset作为输入。lacunarity是一个控制频率的参数,randomOffset是一个随机偏移量。

当lacunarity很小的时候,x * lacunarity和y * lacunarity的值会非常接近,这意味着我们在查询柏林噪声的时候,查询的点非常接近。柏林噪声的特性是,查询的点越接近,返回的值越接近。所以,当lacunarity很小的时候,我们得到的噪声值的范围可能会小于0到1。

如果你想让噪声值的范围更接近0到1,你可以尝试增大lacunarity的值。这样,你查询柏林噪声的点就会更分散,返回的噪声值的范围就会更大。但是,这也会影响到生成的地图的样子,可能会使地图的特征更大或者更小,这取决于你的需求。

另外,我们也可以在得到噪声值之后,对其进行一些数学处理,例如缩放或者偏移,来使其范围更接近0到1。例如,可以使用Mathf.InverseLerp来确保噪声值在0到1之间。

Mathf.InverseLerp 是 Unity 中的一个函数,用于反向插值计算。它接受三个参数:a,b 和 value。

函数的工作原理是这样的:首先,它会找到 value 在 a 和 b 之间的相对位置。然后,它会返回一个介于 0 和 1 之间的值,这个值表示 value 在 a 和 b 之间的相对位置。如果 value 等于 a,则返回 0;如果 value 等于 b,则返回 1。如果 value 在 a 和 b 之间,则返回一个介于 0 和 1 之间的值。

例如,Mathf.InverseLerp(0, 10, 5) 将返回 0.5,因为 5 是 0 和 10 之间的中点。

这个函数在需要将一个值映射到 0 到 1 的范围时非常有用,例如在归一化操作中。

修改MapCreate代码

private float[,] mapData; // 地图数据

private void GenerateMapData()
{
    
    
   	//。。。
    mapData = new float[width, height]; // 初始化地图数据
    float minValue = float.MaxValue;
    float maxValue = float.MinValue;
    for (int x = 0; x < width; x++)
    {
    
    
        for (int y = 0; y < height; y++)
        {
    
    
            // 使用柏林噪声生成地图数据
            float noiseValue = Mathf.PerlinNoise(x * lacunarity + randomOffset, y * lacunarity + randomOffset);
            mapData[x, y] = noiseValue;
            if (noiseValue < minValue) minValue = noiseValue;
            if (noiseValue > maxValue) maxValue = noiseValue;
        }
    }
    // 平滑到0~1
    for (int x = 0; x < width; x++)
    {
    
    
        for (int y = 0; y < height; y++)
        {
    
    
            mapData[x, y] = Mathf.InverseLerp(minValue, maxValue, mapData[x, y]);
        }
    }
}

// 生成Tile地图
private void GenerateTileMap()
{
    
    
   //。。。
   // 如果地图数据为True,则该位置为地面,否则为水
   TileBase tile = mapData[x, y] > waterProbability ? groundTile : waterTile;
}

float.MaxValue是C#中浮点数类型(float)可以表示的最大值,大约为3.4E+38。float.MinValue是浮点数类型(float)可以表示的最小负值,大约为-3.4E+38。

运行效果,可以看到,现在的效果就是我们的预期,水域显示占比没有问题了
在这里插入图片描述

扩展问题

我这里的地图瓦片是比较全面的,各个方位形状的都有,所有直接生成出来的地形不会出现什么问题,不过有时候我们的输出可能只包括常见的四方向瓦片,那么生成的地图多多少少会出现一些问题

比如:
在这里插入图片描述消除错误没有意义的瓦片,你可以去寻找他们的一个共性,比如就是瓦片都只有一个邻居,消除的大概思路就是遍历每个瓦片进行判断,如果他只有一个邻居就把它变为水。

参考代码

public void GenerateMap()
{
    
    
    // 地图处理 处理次数
    for (int i = 0; i < 3; i++)
    {
    
    
        if (!RemoveSeparateTile()) // 如果本次操作什么都没有处理,则不进行循环
        {
    
    
            break;
        }
    }
}

//移除孤立的瓷砖    
private bool RemoveSeparateTile()
{
    
    
    bool res = false; // 是否是有效的操作
    for (int x = 0; x < width; x++)
    {
    
    
        for (int y = 0; y < height; y++)
        {
    
    
            // 是地面且只有一个邻居也是地面
            if (IsGround(x, y) && GetFourNeighborsGroundCount(x, y) <= 1)
            {
    
    
                groundTileMap.SetTile(new Vector3Int(x, y), tile);// 设置为水
                res = true;
            }
        }
    }
    return res;
}

// 获取四方向地面邻居的数量
private int GetFourNeighborsGroundCount(int x, int y)
{
    
    
    int count = 0;
    // top
    if (IsInMapRange(x, y + 1) && IsGround(x, y + 1)) count += 1;
    // bottom
    if (IsInMapRange(x, y - 1) && IsGround(x, y - 1)) count += 1;
    // left
    if (IsInMapRange(x - 1, y) && IsGround(x - 1, y)) count += 1;
    // right
    if (IsInMapRange(x + 1, y) && IsGround(x + 1, y)) count += 1;
    return count;
}

// 是否在地图范围内
public bool IsInMapRange(int x, int y)
{
    
    
    return x >= 0 && x < width && y >= 0 && y < height;
}

// 是否是地面
public bool IsGround(int x, int y)
{
    
    
    return mapData[x, y] > waterProbability;
}

这样就可以消除错误或者没有意义的瓦片啦。

添加植被

定义一个类存放植被瓦片和权重

[Serializable]
public class ItemData
{
    
    
    public TileBase tile;
    public int wegith;
}

逻辑代码

public Tilemap itemTileMap;//植被的Tilemap组件
public List<ItemData> ItemData;//植被列表

//生成植被
public void CreateItemData()
{
    
    
    // 植被权重和
    int weightTotal = 0;
    for (int i = 0; i < ItemData.Count; i++)
    {
    
    
        weightTotal += ItemData[i].wegith;
    }
    //生成植被
    for (int x = 0; x < width; x++)
    {
    
    
        for (int y = 0; y < height; y++)
        {
    
    
            //只有地面可以生成物品
            if (IsGround(x, y))
            {
    
    
                float randValue = UnityEngine.Random.Range(1, weightTotal + 1);
                float temp = 0;
                for (int i = 0; i < ItemData.Count; i++)
                {
    
    
                    temp += ItemData[i].wegith;
                    if (randValue < temp)
                    {
    
    
                        // 命中
                        if (ItemData[i].tile) itemTileMap.SetTile(new Vector3Int(x, y), ItemData[i].tile);
                        break;
                    }
                }
            }

        }
    }
}

挂载脚本,配置权重参数,可以像我一样配置一个为空,及控制无植被的占比权重
在这里插入图片描述

运行效果
在这里插入图片描述

源码

https://gitcode.net/unity1/unity2d-randommap
在这里插入图片描述

参考

【视频】https://www.bilibili.com/video/BV1Js4y117C6?p=1

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,以便我第一时间收到反馈,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_36303853/article/details/132423642