洗牌算法学习笔记
洗牌算法
Quad vs Plane
-
Quad:
- 两个单位三角形组成的四边形
- 两个单位三角形组成的四边形
-
Plane
- Plane是一个10行10列一共200个单位三角形组成的
- Plane是一个10行10列一共200个单位三角形组成的
三角形越多也就意味着模型顶点越多,所制作的动画和模型也就越精细;同理,如果三角形少,那么模型顶点也就越少,所能制作的效果会很具有局限性。
这里只是制作地图上的单位瓦片,所以使用Quad即可。
Z-fighting 深度冲突
在项目中,你可以试着把一个白色立方体,放在一个红色立方体的内部,当它们的一个面足够接近时,会出现很奇怪的效果。
原因是:2个模型足够接近时,系统不知道该渲染哪一个。
随机生成地图的制作
- 首先,我们需要创建一个瓦片预制体(让它来成为构成我们地图的一部分),还有地图的大小,为了方便管理,把它们统一放在一个父物体底下,还有瓦片与瓦片间生成的空隙
[Header("瓦片预制体")]public GameObject tilePrefab;
[Header("地图大小")]public Vector2 mapSize;
[Header("瓦片生成的父物体")]public Transform mapHolder;
[Header("瓦片与瓦片之间的空隙")][Range(0,1)]public float outlinePercent;
- 接着就是生成地图,这里构建的想法就是以地图中心点为原点,向四周进行扩充。
/// <summary>
/// 生成地图
/// 以第一个点为原点,向四周排列组合
/// </summary>
private void GenerateMap()
{
for (int i = 0; i < mapSize.x; i++)
{
for (int j = 0; j < mapSize.y; j++)
{
//向左平移一半的长度,还要扣除瓦片一半的长度(单位长度为1),让原点为中心点
Vector3 newPos = new Vector3(-mapSize.x / 2 + 0.5F + i, 0, -mapSize.x / 2 + 0.5F + j);
//生成物体需要先旋转90度
GameObject spawnTile = Instantiate(tilePrefab, newPos, Quaternion.Euler(90, 0, 0));
spawnTile.transform.SetParent(mapHolder);
spawnTile.transform.localScale *= (1 - outlinePercent);
allTilesCoord.Add(new Coord(i,j));
}
}
}
- 接着就是障碍物的生成,这里我们设置了一个存储瓦片坐标(x,y)的结构体进行存储,然后以位置的形式让障碍物进行生成。
/// <summary>
/// 坐标结构体
/// 存储瓦片的信息
/// </summary>
[System.Serializable]
public struct Coord
{
public int x;
public int y;
public Coord(int x,int y)
{
this.x = x;
this.y = y;
}
}
- 在生成瓦片的最后进行随机障碍物生成(障碍物的预制体和障碍物的个数)
...
[Header("障碍物")] public GameObject obsPrefab;
[Header("生成障碍物的数量")] public int obsCount;
public List<Coord> allTilesCoord = new List<Coord>();
/// <summary>
/// 生成地图
/// 以第一个点为原点,向四周排列组合
/// </summary>
private void GenerateMap()
{
...
//生成障碍物
for (int i = 0; i < obsCount; i++)
{
Coord randomCoord = allTilesCoord[UnityEngine.Random.Range(0, allTilesCoord.Count)];
Vector3 newPos = new Vector3(-mapSize.x / 2 + 0.5F + randomCoord.x, 0, -mapSize.x / 2 + 0.5F + randomCoord.y);
GameObject spawnObs = Instantiate(obsPrefab, newPos, Quaternion.identity);
spawnObs.transform.SetParent(mapHolder);
spawnObs.transform.localScale *= (1 - outlinePercent);
}
}
普通随机遇到的问题
通过上面的方法后,我们经过一番调试后,发现地图确实可以生成,障碍物也是,但是,当我们在6*6的地图中生成9个障碍物时,发现了一个问题。
障碍物的数量和我们设置的障碍物的数量并不一致,原因是有2个障碍物重复出现在了相同的地方;我们不可能通过调整地图大小来解决出错的概率,所以下面就要用到"洗牌算法"。
洗牌算法介绍
- 基本思路:
- 比如说这有10张牌,我们随机抽取了一张,每次取值的范围就是Range(0,10)之间,这也就是导致我们随机到之前抽取过牌的元凶;
- 洗牌算法在于,我们第一次随机到一张卡牌后,会与集合中第一张牌的位置进行互换;互换完成后我们下一次遍历就只会在Range(1,10)之间进行随机抽取。以此来避免抽到相同的卡牌。
卡牌算法的实现
通过上面"卡牌算法"的思路,我们可以根据队列"先进先出"的特点进行完成。
- 首先,我们需要完成的是,卡片的互换,把随机得到的卡牌与数组中第一个卡片进行互换。最后并返回随机的数组。
/// <summary>
/// 对所有瓦片的位置重新排序后,返回瓦片集合
/// </summary>
/// <returns></returns>
public static Coord[] ShuffleCoords(Coord[] _dateArray)
{
for (int i = 0; i < _dateArray.Length; i++)
{
int randomNum = Random.Range(i, _dateArray.Length);
//Swap思想:AB互换
//Coord temp = _dateArray[randomNum];
// _dateArray[randomNum] = _dateArray[i];
// _dateArray[i] = temp;
(_dateArray[randomNum], _dateArray[i]) = (_dateArray[i], _dateArray[randomNum]);
}
return _dateArray;
}
- 之后我们创建一个队列,把拿到的随机数组依次从头部取出,再从尾部放入;我们来重改一下我们之前的代码。
...
private Queue<Coord> shuffledQueue;
...
/// <summary>
/// 生成地图
/// 以第一个点为原点,向四周排列组合
/// </summary>
private void GenerateMap()
{
...
//生成障碍物
shuffledQueue = new Queue<Coord>(Utilities.ShuffleCoords(allTilesCoord.ToArray()));
for (int i = 0; i < obsCount; i++)
{
Coord randomCoord = GetRandomCoord();
Vector3 newPos = new Vector3(-mapSize.x / 2 + 0.5F + randomCoord.x, 0.5F, -mapSize.x / 2 + 0.5F + randomCoord.y);
GameObject spawnObs = Instantiate(obsPrefab, newPos, Quaternion.identity);
spawnObs.transform.SetParent(mapHolder);
spawnObs.transform.localScale *= (1 - outlinePercent);
}
}
/// <summary>
/// 移除第一个坐标并放入队列的最后
/// 队列的先进先出的特性
/// </summary>
/// <returns></returns>
private Coord GetRandomCoord()
{
Coord randomCoord = shuffledQueue.Dequeue();
shuffledQueue.Enqueue(randomCoord);//将移除的元素放入队列的最后,保证队列的完整性。
return randomCoord;
}
...
- 最后我们在6*6的地图上生成33个障碍物,看看有没有什么问题。
以上就是"卡牌算法"核心思路的全部实现了。
sharedMaterial
感觉障碍物的颜色有些单一,我们可以设置渐变色和随机高度,来增加障碍物的多样性。
- 设置渐变色的开始和结束的值,并设置随机高度。
[Header("前场景颜色,和后场景颜色")] public Color foregroundColor, backgroundColor;
[Header("随机高度")] public float minObsHeight, maxObsHeight;
- 再生成障碍物的地方添加颜色和高度。
...
/// <summary>
/// 生成地图
/// 以第一个点为原点,向四周排列组合
/// </summary>
private void GenerateMap()
{
...
//生成障碍物
shuffledQueue = new Queue<Coord>(Utilities.ShuffleCoords(allTilesCoord.ToArray()));
for (int i = 0; i < obsCount; i++)
{
Coord randomCoord = GetRandomCoord();
//随机高度
float obsHeight = Mathf.Lerp(minObsHeight, maxObsHeight, UnityEngine.Random.Range(0F, 1F));
Vector3 newPos = new Vector3(-mapSize.x / 2 + 0.5F + randomCoord.x, obsHeight / 2, -mapSize.x / 2 + 0.5F + randomCoord.y);
GameObject spawnObs = Instantiate(obsPrefab, newPos, Quaternion.identity);
spawnObs.transform.SetParent(mapHolder);
spawnObs.transform.localScale = new Vector3(1 - outlinePercent,obsHeight,1 - outlinePercent);
#region 渐变色
MeshRenderer meshRenderer = spawnObs.GetComponent<MeshRenderer>();
Material material = meshRenderer.material;
//颜色渐变,传入Lerp时需要Y轴的百分比
//因为Color.Lerp方法是0到1之间的一个数值
float colorPercent = randomCoord.y / mapSize.y;
material.color = Color.Lerp(foregroundColor,backgroundColor,colorPercent);
meshRenderer.material = material;
#endregion
}
}
...
接着我们调整一下,参数后,看看随机出来的地图如何。
优化为泛型
在日常项目中,类一般都是不确定的,所以我们一般使用泛型来保证每一种类型的卡牌都可以使用。
/// <summary>
/// 对所有瓦片的位置重新排序后,返回瓦片集合
/// </summary>
/// <returns></returns>
public static T[] ShuffleCoords<T>(T[] _dateArray)
{
for (int i = 0; i < _dateArray.Length; i++)
{
int randomNum = Random.Range(i, _dateArray.Length);
//Swap思想:AB互换
//T temp = _dateArray[randomNum];
// _dateArray[randomNum] = _dateArray[i];
// _dateArray[i] = temp;
(_dateArray[randomNum], _dateArray[i]) = (_dateArray[i], _dateArray[randomNum]);
}
return _dateArray;
}