准备工作
如果大佬对ECS版的海克斯无限地图感兴趣,不妨参与进来,欢迎Star/Folk源码
0下载Unity编辑器(2019.1.12f1 or 更新的版本),if(已经下载了)continue;
1克隆:git clone https://github.com/cloudhu/HexMapMadeInUnity2019ECS.git --recurse
或下载Zip压缩包
2如果下载的是压缩包,需要先将压缩包解压。然后将HexMapMadeInUnity2019ECS添加到Unity Hub项目中;
3用Unity Hub打开的开源项目:HexMapMadeInUnity2019ECS,等待Unity进行编译工作;
4打开项目后,启动场景在Scenes目录下,打开AdvancedHexMap场景(优化重构的版本)。
前言
在CSDN偶然发现了Andrew翻译的ECS官方文档,很有参考价值,这里推荐一下!如果之前有看到的话,可以少走不少弯路!
这一篇的河流比较复杂,花了两天时间来做,主要是水平有限,很多东西不理解。最终还是选择先实现出来,效果还可以,原作者的知识点这里就不说了,可以看原文,这里只说下河流的随机算法。
在生成单元的时候,根据单元的海拔来获取源头,毕竟河流的源头一般都是高处,水往低处流嘛。所以我暂时设定一个常量来控制源头的产生,顺便设置河床海拔和水面海拔偏移量的常数,都写在HexMetrics脚本里:
#region River河流
/// <summary>
/// 河流源头的海拔值
/// </summary>
public const int RiverSourceElevation = 5;
/// <summary>
/// 河床海拔偏移量
/// </summary>
public const float StreamBedElevationOffset = -1.75f;
/// <summary>
/// 水面海拔偏移量
/// </summary>
public const float RiverSurfaceElevationOffset = -0.5f;
#endregion
接下来在单元生成系统CellSpawnSystem中计算河流的随机算法,先申请两个容器来保存参数:
//河流的源头
NativeList<int> riverSources = new NativeList<int>(totalCellCount/15,Allocator.Temp);
//流入的河流索引
NativeArray<int> riverIn = new NativeArray<int>(totalCellCount, Allocator.Temp);
//若干代码
//释放内存
riverSources.Dispose();
riverIn.Dispose();
这里使用原生的容器,方便申请内存和释放,河流源头产生的几率大概是2/7,还有一些源头是需要闭源的。首先把第0个单元设置为源头,这样方便去观察生成的效果,0是比较好的参照:
//使第0个单元成为河流的源头
Colors[0] = Color.green;
Elevations[0] = HexMetrics.RiverSourceElevation;
riverSources.Add(0);
之后的每个单元都随机生成,我们只需要观察一个源头即可:
//后面将从服务器获取这些数据,现在暂时随机生成
for (int i = 1; i < cellCountZ* cellCountX; i++)
{
Colors[i]= new Color(random.NextFloat(), random.NextFloat(), random.NextFloat());
int elevtion = random.NextInt(6);
Elevations[i]= elevtion;
if (elevtion >= HexMetrics.RiverSourceElevation) riverSources.Add(i);
}
这里的i是从1开始的,因为0已经被占位了,如果随机海拔满足河源产生的海拔,就将其索引添加进列表。这样我们手里就有原始数据了,这些数据将成为地图生成的关键,从这里开始,后面的地图都带有一定的必然性。
这里涉及到河流相关的数据,我们保存在Data组件里面:
/// <summary>
/// 河流数据
/// </summary>
public struct River : IComponentData
{
/// <summary>
/// 是否有河流进入
/// </summary>
public bool HasIncomingRiver;
/// <summary>
/// 是否有河流出
/// </summary>
public bool HasOutgoingRiver;
/// <summary>
/// 流入方向
/// </summary>
public int IncomingRiver;
/// <summary>
/// 流出方向
/// </summary>
public int OutgoingRiver;
}
在计算的时候先初始化这些字段,再通过算法来赋值,下面是出版的算法:
- 大河向东流,所以我们尽量使河流的走向是由西向东,未来地图的东面会做成湖或海;
- 水往低处走,所以流经的单元必然比河源的海拔低;
- 一个单元最多通过一条河流,这是为了使简化算法;
- 优先流向与自身海拔相近的单元,这是为了源远流长,避免过早流入低海拔的单元;
- 如果没有出口,就此闭源,这是为了减少河源,避免河流过短;
基于这些算法,我们来写代码:
//初始化河流数据
bool hasRiver = false;
bool hasOutgoingRiver = false;
bool hasIncomingRiver = false;
int incomingRiver = int.MinValue;
int outgoingRiver = int.MinValue;
//如果当前单元是河源
if (riverSources.Contains(i))
{
hasRiver = true;//河源单元必然有河流
//上一个单元海拔,用来做比较
int lastElevation = int.MinValue;
//从六个方向寻找河床
for (int j = 0; j < 6; j++)
{
if (directions[j] != int.MinValue)//如果是最小值,说明没有相邻单元
{
int elevationR = Elevations[directions[j]];//获取相邻单元的海拔
//先判断出水口:水向东流,则当前所以必然小于相邻索引,否则就在西面
if (i<directions[j])
{
//如果已经是源头了,则无法在流入了,一个单元最多有一条河流经,且海拔必然低于河源
if (!riverSources.Contains(directions[j]) && elevationR <= Elevations[i])
{
//为了源远流长,选择与自身海拔相近的单元
if (elevationR > lastElevation)
{
hasOutgoingRiver = true;
outgoingRiver = directions[j];
lastElevation = elevationR;
}
}
}
//判断入水口,但凡入水口都保存在数组中了
if (riverIn.Contains(directions[j]))
{
//方向校正,当前单元的入水方向即是相邻单元的出水方向
if (directions[j] == riverIn[i])
{
incomingRiver = riverIn[i];
hasIncomingRiver = true;
}
}
}
}
//有出水口则保存起来
if (hasOutgoingRiver)
{
//当前单元的出水口,正是相邻单元的入水口
riverIn[outgoingRiver]=i;
riverSources.Add(outgoingRiver);
}
else
{//出水口没有,进水口也没有,则闭源
hasRiver = hasIncomingRiver;
}
}
因为有算法和详细注释,所以应该不难理解,当然这个算法并不完善。还没有考虑到河流汇聚的情况,例如两条河都往一个方向走,必然会相交,从而汇聚成更大的江或湖泊。这些留到后面的去完善,这里先保留这些bug。
还有河道的三角顶点/UV/Shader等计算都在原版教程里,中文版点这里,我会尽量不讲原版的知识点,因为原作者真的写得太详细了,在此特别感谢Jasper Flick的无私分享,以及大佬沈琰的翻译!
ECS专题目录
ECS更新计划
作者的话
如果喜欢可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
技术难题?加入开发者联盟:566189328(QQ付费群)提供有限技术探讨,以及,心灵鸡汤Orz!
当然,不需要技术探讨也欢迎加入进来,在这里劈柴、遛狗、聊天、撸猫!( ̄┰ ̄*)