用ECS做HexMap:河流

31 篇文章 2 订阅
31 篇文章 3 订阅

基于Unity2019最新ECS架构开发MMO游戏笔记25

河流
最终效果

准备工作

如果大佬对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更新计划

作者的话

Alt

如果喜欢可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
技术难题?加入开发者联盟:566189328(QQ付费群)提供有限技术探讨,以及,心灵鸡汤Orz!
当然,不需要技术探讨也欢迎加入进来,在这里劈柴、遛狗、聊天、撸猫!( ̄┰ ̄*)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CloudHu1989

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值