文章目录
- 前言
- 柏林噪声
- 素材导入
- Rule Tile配置
- 生成随机地图
- 问题
- 扩展问题
- 添加植被
- 源码
- 参考
- 完结
前言
我的上一篇文章介绍了TileMap的使用,主要是为我这篇做一个铺垫,看过上一篇文章的人,应该已经很好的理解TileMap的使用了,这里我就不需要过多的解释一些繁琐而基础的知识了,省去很多时间。所有没看过上一篇文章的小伙伴我强烈建议先去看看:
【Unity小技巧】Unity2D TileMap的探究(最简单,最全面的TileMap使用介绍)
先来看看本文实现的最终效果
源码在文章末尾
柏林噪声
柏林噪声(Perlin noise
)是由Ken Perlin于1983年提出的一种随机数生成算法,常用于计算机图形学中的纹理、地形和粒子系统等领域。它产生了一种平滑、连续的随机分布,常用于生成自然风格的纹理和地形。
柏林噪声和直接随机有以下几个区别:
-
平滑性:柏林噪声生成的值在空间上变化连续平滑,不会出现剧烈的跳变。而直接随机生成的值可能会出现突然的变化,不够平滑。
-
一致性:柏林噪声的生成结果是基于一个固定的种子值,因此每次使用相同的种子值生成的结果都是一致的。而直接随机生成的结果每次都不同。
-
纹理性:柏林噪声生成的值可以用来模拟自然纹理,如山脉、云彩等。而直接随机生成的值没有这种纹理性,更加随机。
比如随机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; // 地面的Tilepublic TileBase waterTile; // 水域的Tileprivate 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~1for (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;// topif (IsInMapRange(x, y + 1) && IsGround(x, y + 1)) count += 1;// bottomif (IsInMapRange(x, y - 1) && IsGround(x, y - 1)) count += 1;// leftif (IsInMapRange(x - 1, y) && IsGround(x - 1, y)) count += 1;// rightif (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://www.bilibili.com/video/BV1Js4y117C6?p=1
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~