前言
我的上一篇文章介绍了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; // 地面的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。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~