一种城市道路网络的随机生成方式(Unity中可视化)

参考:
[1]王元,刘华,李航.基于有限元网格划分的城市道路网建模[J].图学学报,2016,37(03):377-385.
[2]李任君. 关于四边形有限元网格生成算法的研究[D].吉林大学,2008.

1. 说在开头

做毕设的时候,第一步就需要城市道路网络的生成。翻了一些文献,感觉王元等人1所提到的利用有限元网格生成的方式进行的路网生成效果挺不错的,于是打算用这种方式实现。但是论文中并没有过多的提算法本身,所以实现算法的时候主要还是参考李任君2的那篇文献。
论文中路网生成效果图

上图为论文1中路网的生成效果

写这篇博客的时候,我已经大体上把生成路网的初步功能给实现了。当然实现的非常粗糙,而且自身算法能力非常有限,很多地方写的也很暴力。还会有一定几率会生成出质量很差的网格、甚至还有一些BUG没有解决。后续继续做下去的时候应该还是会进一步完善的。现在先写一篇博客,记录一下阶段性的成果。顺便当作是给最后写毕业设计说明书打个草稿。

目前为止可以实现的是,通过给定一个多边形的顶点数据作为路网的边缘,然后自动的生成路网的数据,也可以实现不同区域的路网密度不同。

先放一下演示视频,视频播放有问题或者不对的话,可以跳转到原网页查看。

路网生成效果

稍微讲解一下,目前是在Unity中,简单的通过输入顶点的方式给定路网边缘,然后可以设置的参数有单元格长度(每一小段道路长度会尽可能的贴近单元格的长度),随机种子(用于随机一些长度和角度,种子一样且其他参数一样的情况下每次的结果也会一样),然后就是一些可视化调试用的选项。以及在世界坐标靠近原点的位置那一区块道路密度更高(以后会改成专门制定密度的形式,这个靠近原点密度更高只是暂时用来测试密度变化功能用的)。

当前也存在着一些比较明显的问题,比如有的网格质量非常差,所划分的区域很小很窄,道路也靠的很近,效果不好。有的时候生成会出现死循环的情况(检测到死循环就自动跳出),无法继续向下划分(视频中出现过一次,一般换一个随机种子就有很大几率解决,当然这也是治标不治本),目前认为主要还是网格密度变化导致的一些缺陷,有待优化。

2. 有限元网格划分

2.1. 什么是有限元

百度百科:
在数学中,有限元法(FEM,Finite Element Method)是一种为求解偏微分方程边值问题近似解的数值技术。求解时对整个问题区域进行分解,每个子区域都成为简单的部分,这种简单部分就称作有限元。
它通过变分方法,使得误差函数达到最小值并产生稳定解。类比于连接多段微小直线逼近圆的思想,有限元法包含了一切可能的方法,这些方法将许多被称为有限元的小区域上的简单方程联系起来,并用其去估计更大区域上的复杂方程。它将求解域看成是由许多称为有限元的小的互连子域组成,对每一单元假定一个合适的(较简单的)近似解,然后推导求解这个域总的满足条件(如结构的平衡条件),从而得到问题的解。这个解不是准确解,而是近似解,因为实际问题被较简单的问题所代替。由于大多数实际问题难以得到准确解,而有限元不仅计算精度高,而且能适应各种复杂形状,因而成为行之有效的工程分析手段。

定义还是有点难懂。根据我的一些浅显的了解(可能不太准确),大概就是在工业上,对一些模型进行某些力学分析等处理的时候,需要将模型细分为很多的小块。用到的是有限元分析。

查资料的时候搜到的也基本是对三维模型,特别是CAD等的处理。

在这里插入图片描述

上图为百度中搜的图片

然后一个平面有限元网格划分的结果,的确是有一点像路网的意思,再给他加上一些调整就更像了。

2.2. 前沿推进法/波前推进法(Advancing Front Technique)

进行有限元网格划分的方法并不止一种,比较常见的有Delaunay法、映射法、AFT法等。通常也都是生成三角形,但是也有生成四边形的。

我这里用到的就是AFT法。下面先大概讲一下这个算法的思路,主要参考的是李任君的论文。

  1. 首先是有一个边界的多边形数据(一般别人的边界可以是曲线也可以是直线,我这就不搞那么复杂了。只有直线),如下图。
    在这里插入图片描述
  2. 然后把根据设置的单元格长度把一条长的边离散开来,如下图。在这里插入图片描述
  3. 这一圈蓝色的边就称作当前的前沿,然后开始遍历前沿上的每一个点,判断该点适合往里面生成一个怎样的网格单元。生成一个网格单元就记录起来,并且更新前沿,实其不包括已经生成出去的网格单元。然后一直重复直到整个前沿剩下最后一个小的网格单元,即生成完毕。如下图的蓝色边界是一个已经生成了四个网格单元之后的前沿。在这里插入图片描述下图蓝色边界是生成了更多网格单元之后的前沿。
    在这里插入图片描述下图是生成完之后,已经没有了前沿。在这里插入图片描述
    这个视频就是一个动态的过程,其中也能看到桥边把前沿划分开的情况。(后面会提到桥边的概念)

    AFT动态可视化

3. 实现

3.1. 数据的定义

首先是节点和道路的定义。非常的简单明了,稍微提一下就是,每一个节点和道路都需要一个唯一的ID,然后AftNode中的angle,其实是可以不用的,因为后面实际用到角度的时候我都是用一次当场计算一次的,这里存起来只是当时还在调试算法的时候,可视化角度用的。

    public class AftNode
    {
        // ID
        public readonly uint ID;
        // 位置
        public Vector3 Coord { get; }
        // 该点内角角度
        public float Angle;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="coord">该点的坐标</param>
        /// <param name="id">ID</param>
        public AftNode(Vector3 coord,uint id)
        {
            Coord = coord;
            ID = id;
        }
    }

    /// <summary>
    /// 边界线段
    /// </summary>
    public class AftEdge
    {
        // ID
        public readonly uint ID;
        // 开始的节点
        public AftNode Begin { get; }
        // 结束的节点
        public AftNode End { get; }
        // 所在的标准单元
        public readonly List<StandardUnit> Units = new List<StandardUnit>();

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="begin">开始节点</param>
        /// <param name="end">结束节点</param>
        /// <param name="id">ID</param>
        public AftEdge(AftNode begin, AftNode end,uint id)
        {
            this.Begin = begin;
            this.End = end;
            ID = id;
        }

        /// <summary>
        /// 解除与该边界相关的标准单元格的关系
        /// </summary>
        public void DeleteRelateUit()
        {
            foreach (var unit in Units)
                unit.Edges.Remove(this);
            Units.Clear();
        }
    }

上面还有一个概念——标准单元。所谓的标准单元就是一个矩形,如下图一个灰色的框框就是一个标准单元。在这里插入图片描述
为什么需要引入标准单元这个概念呢?是因为在后面算法的计算中,需要进行非常多次的线段碰撞判断等操作,如果直接与所有的线段边界都比较一次,那显然开销特别大。而且做了很多无用功,因为100条线段中,也许90条都根本不可能发生碰撞,因为他们相隔实在太远了。

标准单元,记录自身所包含的线段,每一个线段也记录着他所相关的所有单元。比如上图中绿色的一条线段,他所在的标准单元就是黄色的三个单元。当他要进行比较的时候只需要与这三个标准单元中所包含的线段比较即可。

下面是标准单元的定义。(X、Y其实不需要,因为后续算法中是用一个二维数组存储所有的标准单元,直接通过计算索引就可以找到标准单元,这里只是我在将他可视化出来成黄色框框的时候,需要用到而已)

/// <summary>
    /// 标准单元
    /// </summary>
    public class StandardUnit
    {
        // 标准单元内包含的边界
        public List<AftEdge> Edges = new List<AftEdge>();
        // TEST 可视化的时候需要知道单元格的坐标,但实际上算法中不需要坐标
        public int X, Y;
    }

这就是标准网格的存储方式。

	// 标准网格(由标准单元组成)
    public static StandardUnit[,] StandardNet { get; private set; }

然后在生成一个小网格的时候,需要把它作为结果存起来。需要存下来的数据有顶点的位置,道路(包含两个顶点),街区(包含多个道路)。我使用字典的方式存储,用他们的ID作为key。

	public static readonly Dictionary<uint, Block> ResultBlocks = new Dictionary<uint, Block>();
	public static readonly Dictionary<uint, Node> ResultNodes = new Dictionary<uint, Node>();
	public static readonly Dictionary<uint, Road> ResultRoads = new Dictionary<uint, Road>();

这是结果类的定义。

	/// <summary>
    /// 一段道路的定义
    /// </summary>
    public class Road
    {
        // 道路的唯一ID
        public uint ID { get; private set; }
        // 起始节点
        public Node Begin { get; private set; }
        // 结束节点
        public Node End { get; private set; }
        // 道路等级
        public ushort Level { get; private set; }
        // TODO 构造函数等未实现
        public Road(uint id, Node begin, Node end)
        {
            ID = id;
            Begin = begin;
            End = end;
        }
    }

    /// <summary>
    /// 一个道路节点的定义
    /// </summary>
    public class Node
    {
        // 节点的唯一ID
        public uint ID { get; private set; }
        // 位置
        public Vector3 Coord { get; private set; }
        
        // 构造函数
        public Node(uint id, Vector3 coord)
        {
            ID = id;
            Coord = coord;
        }
    }

    /// <summary>
    /// 一个街区的定义
    /// </summary>
    public class Block
    {
        // 街区的唯一ID
        public uint ID { get; private set; }

        public readonly List<Road> Edges;
        
        public Block(uint id, List<Road> edges)
        {
            ID = id;
            Edges = edges;
        }
        // TODO 更多内容未实现
    }

然后整个前沿是以一个类对象的形式存在的,类为AdvancingFrontTechnique部分定义如下图所示,后面还有很多函数就不展示了,最后会放上完整的代码。(IsDone可以忽视掉,在之前不完善的算法中是用到这一个变量的,后面改进之后就不需要了,只是遗留在这里没有删除。)在这里插入图片描述

3.2. 边界的分类

在前沿每一次往内推进的过程中,是需要生成新的边界的。论文中是把边界分成了三类,我这里根据我自己的想法稍微改了一下,分成了四类。

在这里插入图片描述
首先先说明一下,如上图所示。图中是整个前沿中截出来的一下段,假设现在正在处理第i个顶点(红色是顶点,蓝色是边和角),此时其他点的名称即分别为点i-1点i+1点i+2点i+3。边为边i-1边i边i+1边i+3边i点i点i+1的连线)。然后点i+1点i+2所在的角度分别为a1a2

然后就是四种分类:

  1. 添加一条边的情况(1)在这里插入图片描述
    如果此时a1<65°,且a2>90°,则将点i点i+2相连作为新的边,然后边i边i+1黄色的新边就作为一个新的小网格存到结果中去,以及所包含的点和边也是。并且把边i边i+1从前沿中去掉,在该位置插入黄色的新边,同时也去掉点i+1
  2. 添加一条边的情况(2)在这里插入图片描述当65°<=a1<115°,且160°<=a1+a2<240°时,将点i点i+3相连作为新的边,与上面的情况一样,删除点i+1点i+2边i边i+1边i+2,并把边i边i+1边i+2黄色新边所构成的网格作为新的结果存起来。
  3. 添加两条边的情况在这里插入图片描述
    当65°<=a1<115°,200°<=a1+a2时。创建一个新顶点点t1,然后连接点i点t1作为边e1,以此类推,将边e1边e2加入到前沿中,点t1也加入到前沿中,其他的该删除删除,该存结果存结果。
  4. 添加三条边的情况在这里插入图片描述和前面三种一个套路,话不多说,懂得都懂。

写了个简单的demo,大概效果就如下,有时候各种情况都不符合的,基本上当i到下一个的时候,或者下一轮生成的时候也就都生成了。

边界分类

3.3. 生成一个小网格

生成一个网格主要是有一个private void GenerateABlock(int i)来搞定,这个也是整个算法中最核心的一个函数,半数代码都在这(当让也是因为我写的比较烂,感觉挺乱的)。

前面的public List<AftNode> Nodes = new List<AftNode>();public List<AftEdge> Edges = new List<AftEdge>();就是按照逆时针的顺序存着当前的前沿中,所有的点和边 (这里我用的List来存这个数据,其实是不妥当的。因为更新前沿时会经常的在中间进行插入和删除的操作,而且需要不断地循环,并且处理顶点的时候也是一个接着一个的处理,几乎不怎么需要随机读取任意位置的数据。所以改用双向循环链表来存前沿的数据才是最合适的。 但是我写到后期才意识到这个问题,而且可能是因为数据量的确不算大,即便用List的时候生成路网也是蛮快的,反正这个速度我可以接受,就懒得改了。不过为了处理跨越结尾的增删操作等等,我还是写了好多函数和求余操作去处理,的确实制造了挺多的麻烦) 。

然后GenerateABlock中的参数i的意思就是处理第i个节点的时候。就是根据前面提到的边界分类,进行判断然后处理。我就挑生成三条边的情况稍微讲一下,其他的情况也是按照思路写就是了。

  1. 首先第一步判断当前前沿中顶点的个数是否是6以下,即小于等于5。如果是的话,直接把剩下的所有顶点作为一个街区,然后清空前沿。结束。(其实论文中是以生成四边形网格为目标,迫不得已就生成一个三角形,但是我觉得我的需求其实无所谓,5边形我觉得也是可以接受的,所以我就这样写了)
	// 当节点剩下的已经足够少,直接组成Block
    if (Nodes.Count < 6)
      {
          // 标记已执行
          _executed = true;
          
          // 添加街区
          AddToResultBlock(Edges.ToArray());

          // 清空
          Nodes.Clear();
          Edges.Clear();

          // 标记结束
          IsDone = true;
          return;
      }
  1. 然后就是按照边界分类那部分讲的一样,计算a1a2的角度,这个AngleOf(int n)函数是我自己写的,返回的是前沿中第n个顶点所在的角度。求余是因为处理正好+1、+2之后跨越边界饶了一圈的情况。
	// 不需要比较的边的ID
	var noCompareIDs = new List<uint>();

	// 求得下两个顶点的角度之和
	var angle1 = AngleOf((i + 1) % Nodes.Count);
	var angle2 = AngleOf((i + 2) % Nodes.Count);
	
	// 新加的边界是否与已存在的边界碰撞
	var isCollide = false;
  1. 然后判断当符合生成三条边的时候,if (angle1 >= 115 && angle1 < 220 && angle2 >= 115 && angle2 < 220),继续执行下面对应的函数。
  2. 然后就是要生成出新的两个点和三个边。边好说,只要有这几个点,按顺序直接连起来就好了。可是点要怎么求呢?我这里用到的方法就是定义向量dir是一个从点i+1点i+2的向量旋转90度之后的向量,那么点i+1加上向量dir就是点t1的位置,点t2同理。当然这个向量dir需要先单位话然后再乘上单元格的长度,这样就可以时得生成的网格中,每一个小段道路都是贴近单元格长度。不过我再此还加上了一个随机数(可以指定随机种子),让路网不至于太规规矩矩。这个时候生成出来的两条边都还是垂直于边i+1的,因为是按照90度旋转的,要是喜欢在这也可以加上一定的随机。代码里面用node1、node2存了以下就是不想后面每次用都写Nodes[(i + 1) % Nodes.Count]这么麻烦,其实node1就是图中的点i+1,node2就是点i+2这里可以留意一下,我是写了一个IdManger来管理ID,确保每一个ID都是唯一的。以及里面的WeightUnitLength函数,这里本来应该是直接写的单元格长度。但是后来我加了一个控制密度的功能。就是我可以指定某一个区域的路网更稀疏或者更密集。我就可以通过这个函数获取当前位置的长度应该是多少。当然目前这个函数背后只是很简单的根据位置判断了以下长度返回回来,还处于测试阶段。
	// 记录一下node1 node2
	AftNode node1 = Nodes[(i + 1) % Nodes.Count], node2 = Nodes[(i + 2) % Nodes.Count];
	// 计算第一个点到第二个点的方向
	var dir = node2.Coord - node1.Coord;
	// 计算旋转矩阵
	var roateMat = Matrix4x4.Rotate(Quaternion.Euler(0, -90, 0));
	// 计算并创建出第一个要添加的点
	var testScale = RandomRange(0.9f, 1.1f);
	Vector3 newDir = (roateMat * dir).normalized * (WeightUnitLength(node1.Coord.x,node1.Coord.z) * testScale);
	var tempNode1 = new AftNode(node1.Coord + newDir, IdManager.GetNodeID());
	
	// 计算第二个点到底一个点的方向(反转即可)
	dir *= -1;
	// 计算旋转矩阵
	roateMat = Matrix4x4.Rotate(Quaternion.Euler(0, 90, 0));
	// 计算并创建出第二个要添加的点
	newDir = (roateMat * dir).normalized * (WeightUnitLength(node1.Coord.x,node1.Coord.z) * testScale);
	var tempNode2 = new AftNode(node2.Coord + newDir, IdManager.GetNodeID());
	
	// 计算新的三条边
	var tempEdge1 = new AftEdge(node1, tempNode1, IdManager.GetRoadID());
	var tempEdge2 = new AftEdge(tempNode1, tempNode2, IdManager.GetRoadID());
	var tempEdge3 = new AftEdge(tempNode2, node2, IdManager.GetRoadID());
  1. 然后就是给新加的这三条边设置与标准单元的关系,用于等会计算碰撞。函数实现可以看最后放的完整代码,反正这里就是设置了关系。
	// 设置关系
	SetUnitRelation(tempEdge1);
	SetUnitRelation(tempEdge2);
	SetUnitRelation(tempEdge3);
  1. 判断新加入的边是否与其他边相互碰撞,如果有碰撞的话就说明无法按这个方式生成。首先我们可以知道,新加的边本身,和相邻的边是不需要判断碰撞的,而且因为他们是点对点的接在一起,甚至真有可能判断为碰撞,所以先把不需要碰撞的边的ID存一下,待会检测的时候只要是这些ID就不需要比较。结合着上面的图片看就会比较容易理解了。然后就是遍历当前边每一个有关系的单元中,所有有关系的边。虽然这里是双重循环,但是一般来说一条边最多只会和三个标准单元有关系(因为一条边的长度就是按照标准单元的长度生成的)。然后比较的时候,除非真的有碰撞,否则大多数情况除了不需要比较的ID,其他也没多少需要比较的,一旦找到一个也直接跳出所有的判断了。所以这里效率其实还算可以。但也不是没法优化了,还是可能会出现再两个单元格内都出现同一条边,然后重复比较的情况,可以记录一下比较过的边的ID。但是这种情况感觉也不多,就算了。
    // 判断碰撞
    // 设置不需要比较碰撞的ID(新加的边以及几个边是不需要比较的)
    noCompareIDs.Clear();
    noCompareIDs.Add(tempEdge1.ID);
    noCompareIDs.Add(tempEdge2.ID);
    noCompareIDs.Add(tempEdge3.ID);
    noCompareIDs.Add(Edges[(i + 1) % Edges.Count].ID);
    
    // 遍历新边所在的所有标准单元
    foreach (var unit in tempEdge2.Units)
        // 遍历每一个标准单元中的其他边
        foreach (var edge in unit.Edges)
            // 如果发生碰撞,就记录并跳出(不比较某些边)
            if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge2))
            {
                isCollide = true;
                // 跳出这个双重循环
                goto outLoop;
            }
    
    noCompareIDs.Add(Edges[i].ID);
    
    foreach (var unit in tempEdge1.Units)
        // 遍历每一个标准单元中的其他边
        foreach (var edge in unit.Edges)
            // 如果发生碰撞,就记录并跳出(不比较某些边)
            if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge1))
            {
                isCollide = true;
                // 跳出这个双重循环
                goto outLoop;
            }

    noCompareIDs.Remove(Edges[i].ID);
    noCompareIDs.Add(Edges[(i + 2) % Edges.Count].ID);
    
    foreach (var unit in tempEdge3.Units)
        // 遍历每一个标准单元中的其他边
        foreach (var edge in unit.Edges)
            // 如果发生碰撞,就记录并跳出(不比较某些边)
            if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge3))
            {
                isCollide = true;
                // 跳出这个双重循环
                goto outLoop;
            }

    outLoop:
  1. 如果检测到碰撞isCollide就会为true。那么说明这三条边这样生成是不行的,所以首先要解除他们与标准单元的关系。因为如果他们的信息还存在里面,以后判断别的边的时候,很可能会将明明不碰撞的判断为了碰撞。所以要把残留的关系清除。同时因为刚才都给新增的顶点和边界分配了唯一ID,如果不将ID收回的话,将会浪费很多ID。(不过说实在的,我大概只需要几千个,顶多上万个街区就已经足够了,uint这么多ID其实根本分配不完,但是能省则省吧)所以还需要跟IdManager说明移除这些分配过的ID,待会就还可以继续分配。
	// 清除关系
	tempEdge1.DeleteRelateUit();
	tempEdge2.DeleteRelateUit();
	tempEdge3.DeleteRelateUit();
	// 移除ID
	IdManager.RemoveNodeID(tempNode1.ID);
	IdManager.RemoveNodeID(tempNode2.ID);
	IdManager.RemoveRoadID(tempEdge1.ID);
	IdManager.RemoveRoadID(tempEdge2.ID);
	IdManager.RemoveRoadID(tempEdge3.ID);
  1. 如果没有碰撞,说明这样的生成方法可行,那么就把该存到结果的存到结果。然后把新的边和顶点加入到前沿中,同时把已经不属于前沿的顶点和边从前沿中删去。
	// 添加新Node到结果中
	AddToResultNode(tempNode1);
	AddToResultNode(tempNode2);
	
	// 添加新Road到结果中
	AddToResultRoad(tempEdge1);
	AddToResultRoad(tempEdge2);
	AddToResultRoad(tempEdge3);
	
	// 添加新Block到结果中
	AddToResultBlock(Edges[(i + 1) % Edges.Count], tempEdge3, tempEdge2, tempEdge1);
	
	// 清除掉无用边界与其标准单元的关系
	Edges[(i + 1) % Edges.Count].DeleteRelateUit();
	
	// 添加新顶点到前沿中
	var tempNodeList = new List<AftNode>()
	{
	    tempNode1,
	    tempNode2
	};
	Nodes.InsertRange((i + 2) % Nodes.Count, tempNodeList);
	
	// 添加新边界到前沿中
	var tempEdgeList = new List<AftEdge>()
	{
	    tempEdge1,
	    tempEdge2,
	    tempEdge3
	};
	Edges.InsertRange((i + 1) % Edges.Count, tempEdgeList);
	
	// 删除无用边界
	RemoveEdgesInList((i + 4) % Edges.Count, 1);

至此,如果没有问题的话,第i个顶点位置的一个小的网格就生成好了,有问题的话就不会生成。如果是因为碰撞导致的不生成,后面会直接用桥边的方式解决,如果是不满足四类的任何一种情况导致的不生成,那么要么就会在下一个点的时候生成,要么在下一轮的时候生成。很少会出现卡死的情况(目前还是有一定记录出现到某些情况的时候会卡住,无法继续往下生成,观察后感觉主要是因为加入了密度改变的功能之后,当有时候一个小前沿中,怎么生成都会碰撞,但有的边有太长而不符合桥边的规则的时候会出现,这个后续有待优化。但是更换以下随机种子还是很大程度上的暂时的解决这个问题)。

3.4. 生成桥边

当前沿两边靠的足够近的时候,很容易就会出现,并且总是会出现怎么生成都会碰撞的情况。如下图红色圈起来的部分,到这里按照前面的分类生成肯定会碰撞,如果没有生成桥边的功能。那么算法到这里将会卡死无法继续生成。
在这里插入图片描述
当一个顶点可以找到一个足够近的其他顶点的时候,可以将两点连接起来,分为两个前沿,然后再分别生成。
在这里插入图片描述

论文2中桥边的图片

比如上图中,C和H两点已经足够近了,可以直接连接生成桥边,然后再分成左边的前沿和右边的前沿继续生成。

只要遍历一下一个点所在的标准单元以及附近的一圈的标准单元中,最近的那个点是否满足足够近的这个原则,就可以找到连接点。(为啥还需要判断附近一圈的标准单元呢?因为很可能出现两个点很近但是又处于相邻的标准单元的情况)。

但是还有一个要考虑的问题就是。假设当前的前沿如下图所示,此时正在寻找第i个顶点适合连接桥边的另一个顶点。如果单纯按距离来算,很可能会找到紫色圈起来的顶点。很明显这不是我们想要的桥边。
在这里插入图片描述
我们想要的是如下图黄色区域所示,这一区域中,最近的且满足距离要求的点。
在这里插入图片描述
我这里是利用向量的计算,判断这个顶点是否属于黄色一边的区域。这是在检测线段碰撞的方法中得到启发所改写的。

	/// <summary>
	/// 找到距离该点,最近的(下限以内)且是左侧的点的坐标
	/// </summary>
	/// <param name="currentIndex">需要判断的点</param>
	/// <param name="noCompareIDs">不需要判断的边的ID</param>
	/// <param name="p1">比较点前一个点坐标</param>
	/// <param name="p2">比较点坐标</param>
	/// <param name="p3">比较点下一个点坐标</param>
	/// <returns></returns>
	private int FindClosestNodeIndex(int currentIndex, ICollection<uint> noCompareIDs, Vector3 p1, Vector3 p2, Vector3 p3)
	{
	    // 计算这个坐标位于的标准网格索引
	    int indexX = (int) Math.Floor((Nodes[currentIndex].Coord.x - StartCoordinate.x) / StandardUintLength),
	        indexY = (int) Math.Floor((Nodes[currentIndex].Coord.z - StartCoordinate.z) / StandardUintLength);
	
	    // 暂存最近距离
	    var tempDistance = StandardUintLength * 100;
	    AftNode tempNode = null;
	    
	    // 遍历附近包括自己所在的9个标准单元
	    for (var x = Math.Max(0, indexX - 1); x <= Math.Min(indexX + 1, StandardNet.GetLength(0) - 1); x++)
	    {
	        for (var y = Math.Max(0, indexY - 1); y <= Math.Min(indexY + 1, StandardNet.GetLength(1) - 1); y++)
	        {
	            // 遍历每个单元内需要比较的边
	            foreach (var edge in StandardNet[x, y].Edges)
	            {
	                // 去除不需要比较的情况
	                if (noCompareIDs.Contains(edge.ID)) continue;
	                
	                var distance = Vector3.Distance(edge.Begin.Coord, Nodes[currentIndex].Coord);
	                // 判断该线段上两个端点有没有足够近的顶点
	                // TODO 1.35有待考究
	                if (distance < tempDistance && distance < WeightUnitLength(Nodes[currentIndex].Coord.x,Nodes[currentIndex].Coord.z) * 1.35f)
	                {
	                    if (Vector3.SignedAngle(p2 - p1, p3 - p2, Vector3.up) > 0)
	                    {
	                        if (CrossVec2(edge.Begin.Coord - p1, p2 - p1) < 0 ||
	                            CrossVec2(edge.Begin.Coord - p2, p3 - p2) < 0)
	                            // 在左侧
	                        {
	                            tempDistance = distance;
	                            tempNode = edge.Begin;
	                        }
	                                
	                    }
	                    else
	                    {
	                        if (CrossVec2(edge.Begin.Coord - p1, p2 - p1) < 0 && 
	                            CrossVec2(edge.Begin.Coord - p2, p3 - p2) < 0) // 在左侧
	                        {
	                            tempDistance = distance;
	                            tempNode = edge.Begin;
	                        }
	                    }
	                }
	
	                distance = Vector3.Distance(edge.End.Coord, Nodes[currentIndex].Coord);
	                if (distance < tempDistance && distance < WeightUnitLength(Nodes[currentIndex].Coord.x,Nodes[currentIndex].Coord.z) * 1.35f)
	                {
	                    if (Vector3.SignedAngle(p1 - p2, p2 - p3, Vector3.up) > 0)
	                    {
	                        if (CrossVec2(edge.End.Coord - p1, p2 - p1) < 0 || 
	                            CrossVec2(edge.End.Coord - p2, p3 - p2) < 0) // 在左侧
	                        {
	                            tempDistance = distance;
	                            tempNode = edge.End;
	                        }
	                    }
	                    else
	                    {
	                        if (CrossVec2(edge.End.Coord - p1, p2 - p1) < 0 && 
	                            CrossVec2(edge.End.Coord - p2, p3 - p2) < 0) // 在左侧
	                        {
	                            tempDistance = distance;
	                            tempNode = edge.End;
	                        }
	                    }
	                }
	            }
	        }
	    }
	
	    if (tempNode != null) return Nodes.IndexOf(tempNode); 
	    
	    // 没有找到
	    return -1;
	}

然后就是在处理完第i个顶点所生成的网格之后,顺带如下面代码一样,判断一下是否有可以生成桥边的情况。如果有则生成桥边,并且将当前前沿根据桥边划分为两部分,一部分继续生成,另一部分存到_edgesLinkedList_nodesLinkedList链表中,直到当前前沿生成结束,节生成链表中的下一个前沿。

// 判断有没有距离该节点很近的其他非邻边节点
	noCompareIDs.Clear();
	noCompareIDs.Add(Edges[i > 0 ? i - 1 : Edges.Count - 1].ID);
	noCompareIDs.Add(Edges[i].ID);
	noCompareIDs.Add(Edges[(i + 1) % Edges.Count].ID);
	noCompareIDs.Add(Edges[(i + 2) % Edges.Count].ID);
	
	// 找到一个最近的,并且位于左侧的一个符合距离的顶点
	var closestIndex = FindClosestNodeIndex((i + 1) % Nodes.Count, noCompareIDs,
	    Nodes[i].Coord,
	    Nodes[(i + 1) % Nodes.Count].Coord,
	    Nodes[(i + 2) % Nodes.Count].Coord);
	
	// 如果找到
	if (closestIndex >= 0)
	{
	    // 标记已执行
	    _executed = true;
	
	    // 【注意】此ID由两个方向的边用,但实际是一条边,任意一条边加入结果即可
	    var brigeEdgeID = IdManager.GetRoadID();
	
	    // 创建桥边
	    var brigeEdge = new AftEdge(Nodes[closestIndex], Nodes[(i + 1)%Nodes.Count], brigeEdgeID);
	
	    // 添加到结果中
	    AddToResultRoad(brigeEdge);
	
	    // 创建桥边分割开来的另一半的顶点和边
	    var newNodes = CopyRangeInNodes((i + 1) % Nodes.Count, closestIndex);
	    var newEdges = CopyRangeInEdges((i + 1) % Edges.Count, closestIndex > 0 ? closestIndex - 1 : Edges.Count - 1);
	    newEdges.Add(brigeEdge);
	
	    // 将新的边界和顶点存到链表后,用于以后继续生成,代替递归
	    _edgesLinkedList.AddLast(newEdges);
	    _nodesLinkedList.AddLast(newNodes);
	
	    // 删除掉已经划分的边和顶点
	    // 计算需要删掉几个边
	    var deleteEdgeCount = (Edges.Count + closestIndex - i - 1) % Edges.Count;
	    // 添加桥边
	    Edges.Insert((i + 1) % Edges.Count, new AftEdge(Nodes[(i + 1) % Nodes.Count], Nodes[closestIndex], brigeEdgeID));
	    // 删除多余边
	    RemoveEdgesInList((i + 2) % Edges.Count, deleteEdgeCount);
	    // 删除多余顶点
	    RemoveNodesInList((i + 2) % Nodes.Count, deleteEdgeCount - 1);
	}

这一部分的时候我遇到一个。一开始我不是这样写的,一开始我是生成桥边之后,用划分出来的另一半生成一个新的AdvancingFrontTechnique对象,然后让他继续划分,直到所有结束。是一种递归的思想。当然这种方法没问题,是对的,实验出来的结果也OK。(那个IsDone也是这个地方用的,当IsDone为true就说明这个前沿生成完毕,就返回到刚才的前沿接着生成)

但是当我单元网格长度越挑调小,也就是密度越来越高,数据越来越大的时候。Unity闪退了。真的是突然就闪退,也没有崩溃日志。就像有一个临界值,只要密度超过那个他就闪退。我被这个问题难了几乎整整一天。也和群里的同学讨论了很久。一开始认为是unity的问题,后来又感觉不是,觉得是不是内存爆了(但是任务管理器看内存是好好的,占用很少,啥都没爆),并且单步调试看到闪退前的数据量也不大,也就几千。真是百思不得其解。甚至当晚我下单了一条8G内存第二天到货(虽然后来在内存到之前我就把问题解决了),企图的解决这个问题。

然后第二天早上我就尝试着把这个算法直接粘到vs的项目里面运行,看看是不是unity的问题。不过由于我还是用了不少uinity给的数学运算,不在unity里面都没法用,好在github上有这一部分源码,我就照着拿了一些过来用。改了以下终于能跑了。然后测试了和在unity中运行时会闪退的数据。VS终于给了我一个结果。
在这里插入图片描述
堆栈溢出。

好家伙终于把问题找到了,原来是堆栈溢出。Unity要早报这个错我早解决了。下图是用Rider调试的时候,闪退前的堆栈情况,左边那个长到令人发指的就是不停的划分桥边生成新对象并且调用函数用到的堆栈。简单的上网查了以下,貌似是c#一个进程的堆栈大小就只给你1m,也没法改,像我这样整肯定就溢出了。
在这里插入图片描述
然后我才把这段代码改成前面那样用链表存着划分出来的另一个前沿。然后用下面这种形式来控制所有的前沿来生成。

public void GenOnce()
{
    while (_edgesLinkedList.Count > 0 && _nodesLinkedList.Count > 0)
    {
        _executed = false;
        
        if(Nodes != null && Edges != null){
            if (Nodes.Count == 0 && Edges.Count == 0)
            {
                _nodesLinkedList.RemoveFirst();
                _edgesLinkedList.RemoveFirst();
                if (_nodesLinkedList.Count > 0 && _edgesLinkedList.Count > 0)
                {
                    Nodes = _nodesLinkedList.First.Value;
                    Edges = _edgesLinkedList.First.Value;    
                }
            }
            if(Nodes != null && Edges!= null)
                for (var i = 0; i < Nodes.Count; i++)
                    GenerateABlock(i);
        }
    
        if (!_executed && _edgesLinkedList.Count > 0 && _nodesLinkedList.Count > 0)
        {
            Debug.LogWarning("检测到死循环,跳出!");
            return;
        }
    }
}

4. 完整代码

基本的思想就在前面讲了一遍,下面是完整的代码。多很多函数的实现,也多了一些细节。注释也很充足。(同时也有很多没用的代码在里面,不用太在意)

当然现在我这个东西其实也还是个半成品,不少东西还不完善,也还处于测试的状态。说不定还会有BUG存在。

有什么问题也欢迎交流。

4.1. 一些定义或辅助

using System.Collections.Generic;
using UnityEngine;

namespace RoadNetwork
{
    /// <summary>
    /// 整个地图的数据
    /// </summary>
    public class RoadMap
    {
        // 所有道路节点
        public Dictionary<uint, Node> Nodes { get; private set; }
        // 所有道路
        public Dictionary<uint, Road> Roads { get; private set; }
        // 所有街区
        public Dictionary<uint, Block> Blocks { get; private set; }
        // TODO 未实现
    }

    /// <summary>
    /// 一段道路的定义
    /// </summary>
    public class Road
    {
        // 道路的唯一ID
        public uint ID { get; private set; }
        // 起始节点
        public Node Begin { get; private set; }
        // 结束节点
        public Node End { get; private set; }
        // 道路等级
        public ushort Level { get; private set; }
        // TODO 构造函数等未实现
        public Road(uint id, Node begin, Node end)
        {
            ID = id;
            Begin = begin;
            End = end;
        }
    }

    /// <summary>
    /// 一个道路节点的定义
    /// </summary>
    public class Node
    {
        // 节点的唯一ID
        public uint ID { get; private set; }
        // 位置
        public Vector3 Coord { get; private set; }
        
        // 构造函数
        public Node(uint id, Vector3 coord)
        {
            ID = id;
            Coord = coord;
        }
    }

    /// <summary>
    /// 一个街区的定义
    /// </summary>
    public class Block
    {
        // 街区的唯一ID
        public uint ID { get; private set; }

        public readonly List<Road> Edges;
        
        public Block(uint id, List<Road> edges)
        {
            ID = id;
            Edges = edges;
        }
        // TODO 更多内容未实现
    }
}

using System.Collections.Generic;

namespace RoadNetwork
{
    public static class IdManager
    {
        // 当前的最大ID(已分配)
        // 0不会被分配
        public static uint currentNodeID { get; private set; }
        public static uint currentRoadID { get; private set; }
        public static uint currentBlcokID { get; private set; }

        // 被移除掉的ID,获取ID时会优先分配这些ID,以免浪费ID
        private static Queue<uint> _removedNodeID = new Queue<uint>();
        private static Queue<uint> _removedRoadID = new Queue<uint>();
        private static Queue<uint> _removedBlockID = new Queue<uint>();

        /// <summary>
        /// 所有都进行初始化
        /// </summary>
        public static void Initialization()
        {
            InitNode();
            InitRoad();
            InitBlock();
        }

        /// <summary>
        /// 仅初始化节点
        /// </summary>
        public static void InitNode()
        {
            currentNodeID = 0;
            _removedNodeID.Clear();
        }

        /// <summary>
        /// 仅初始化道路
        /// </summary>
        public static void InitRoad()
        {
            currentRoadID = 0;
            _removedRoadID.Clear();
        }

        /// <summary>
        /// 仅初始化街区
        /// </summary>
        public static void InitBlock()
        {
            currentBlcokID = 0;
            _removedBlockID.Clear();
        }
        
        /// <summary>
        /// 获取当前的节点唯一ID
        /// </summary>
        /// <returns></returns>
        public static uint GetNodeID()
        {
            // 优先分配已删除的ID
            if (_removedNodeID.Count > 0) return _removedNodeID.Dequeue();
            return ++currentNodeID;
        }

        /// <summary>
        /// 获取当前的道路唯一ID
        /// </summary>
        /// <returns></returns>
        public static uint GetRoadID()
        {
            // 优先分配已删除的ID
            if (_removedRoadID.Count > 0) return _removedRoadID.Dequeue();
            return ++currentRoadID;
        }

        /// <summary>
        /// 获取当前的街区唯一ID
        /// </summary>
        /// <returns></returns>
        public static uint GetBlockID()
        {
            // 优先分配已删除的ID
            if (_removedBlockID.Count > 0) return _removedBlockID.Dequeue();
            return ++currentBlcokID;
        }

        /// <summary>
        /// 去除掉这一个ID
        /// </summary>
        /// <param name="id"></param>
        public static void RemoveNodeID(uint id)
        {
            _removedNodeID.Enqueue(id);
        }
        /// <summary>
        /// 去除掉这一个ID
        /// </summary>
        /// <param name="id"></param>
        public static void RemoveRoadID(uint id)
        {
            _removedRoadID.Enqueue(id);
        }
        /// <summary>
        /// 去除掉这一个ID
        /// </summary>
        /// <param name="id"></param>
        public static void RemoveBlockID(uint id)
        {
            _removedBlockID.Enqueue(id);
        }
    }
}

4.2. AFT算法

using System;
using System.Collections.Generic;
using UnityEngine;

namespace RoadNetwork
{
    public class AdvancingFrontTechnique
    {
        // 该前沿是否生成完成
        public bool IsDone { get; private set; }

        // 当前多边形边界的所有顶点
        private readonly LinkedList<List<AftNode>> _nodesLinkedList = new LinkedList<List<AftNode>>();
        public List<AftNode> Nodes = new List<AftNode>();
        
        // 当前多边形边界的所有边界线段
        private readonly LinkedList<List<AftEdge>> _edgesLinkedList = new LinkedList<List<AftEdge>>();
        public List<AftEdge> Edges = new List<AftEdge>();

        // 单个标准单元的长度
        public float StandardUintLength { get; }
        
        // 标准网格(由标准单元组成)
        public static StandardUnit[,] StandardNet { get; private set; }

        // 网格起始点
        public Vector3 StartCoordinate { get; private set; }

        // 生成的结果
        public static readonly Dictionary<uint, Block> ResultBlocks = new Dictionary<uint, Block>();
        public static readonly Dictionary<uint, Node> ResultNodes = new Dictionary<uint, Node>();
        public static readonly Dictionary<uint, Road> ResultRoads = new Dictionary<uint, Road>();
        
        // 随机数
        private static System.Random _random;

        // TODO 构造函数
        public AdvancingFrontTechnique(IEnumerable<Vector3> coords, float unitLength = 1,int randomSeed = 0)
        {
            // 初始化
            IsDone = false;
            ResultBlocks.Clear();
            ResultNodes.Clear();
            ResultRoads.Clear();

            // TODO 
            _nodesLinkedList.AddLast(Nodes);
            _edgesLinkedList.AddLast(Edges);
            
            _random = new System.Random(randomSeed);

            // 创建节点
            foreach (var point in coords)
                Nodes.Add(new AftNode(point, IdManager.GetNodeID()));

            // 参数传递
            StandardUintLength = unitLength;

            // 离散边界
            DisperseNodes(ref Nodes);

            // 生成边
            for (var i = 0; i < Nodes.Count - 1; i++)
                Edges.Add(new AftEdge(Nodes[i], Nodes[i + 1], IdManager.GetRoadID()));
            // 最后一条边
            Edges.Add(new AftEdge(Nodes[Nodes.Count - 1], Nodes[0], IdManager.GetRoadID()));

            // 生成标准网格
            GenerateStandardNet(Nodes);

            // TEST 计算所有顶点的角度
            for (var i = 0; i < Nodes.Count; i++)
                Nodes[i].Angle = AngleOf(i);

            // 把最初的边界和顶点加入到结果中
            foreach (var node in Nodes)
                AddToResultNode(node);
            foreach (var edge in Edges)
                AddToResultRoad(edge);
        }
        
        /// <summary>
        /// 离散顶点,在两个顶点之间距离过长的中间插入顶点
        /// </summary>
        /// <param name="nodes"></param>
        private void DisperseNodes(ref List<AftNode> nodes)
        {
            for (var i = 0; i < nodes.Count; i++)
            {
                // 计算该顶点与下一顶点的位置
                float distance = Vector3.Distance(nodes[i].Coord, nodes[(i + 1) % nodes.Count].Coord);

                // 当距离超过一定限度就需要进行划分
                // TODO 这个1.5应该还需要考究
                if (distance > StandardUintLength * 1.5f)
                {
                    // 计算中间需要拆分的数量
                    var partsCount = (int) Math.Floor(distance / StandardUintLength) + 1;

                    var tempList = new List<AftNode>();
                    for (var j = 1; j < partsCount; j++)
                    {
                        // 通过插值计算位置并插入新的List中
                        tempList.Add(new AftNode(
                            Vector3.Lerp(
                                nodes[i].Coord,
                                nodes[(i + 1) % nodes.Count].Coord,
                                (float) j / partsCount), IdManager.GetNodeID()));
                    }

                    // 插入到原来的List中
                    nodes.InsertRange(i + 1, tempList);
                    // 新加入的节点已经符合要求,不需要继续判断,可以直接跳过
                    i += partsCount - 1;
                }
            }
        }

        /// <summary>
        /// 生成标准网格
        /// </summary>
        /// <param name="coords">边界数据</param>
        private void GenerateStandardNet(List<AftNode> coords)
        {
            // 找到边界的XY值
            FindEdgePoints(coords, out var minX, out var maxX, out var minZ, out var maxZ);

            // 计算所需的单元个数
            int xCount = (int) Math.Ceiling((maxX - minX) / StandardUintLength),
                zCount = (int) Math.Ceiling((maxZ - minZ) / StandardUintLength);

            // 初始化标准网格
            StandardNet = new StandardUnit[xCount, zCount];
            for (var i = 0; i < xCount; i++)
            {
                for (var j = 0; j < zCount; j++)
                {
                    StandardNet[i, j] = new StandardUnit();
                    // TEST 可视化的时候需要知道单元格的坐标,但实际上算法中不需要坐标
                    StandardNet[i, j].X = i;
                    StandardNet[i, j].Y = j;
                }
            }

            // 初始化网格起始点
            StartCoordinate = new Vector3(minX, 0, minZ);

            // 设置当前所有边的关系
            foreach (var edge in Edges)
                SetUnitRelation(edge);
        }

        // 是否执行了操作,用来检测死循环 // 初始化为false
        private bool _executed;

        //private int counter = 0;
        public void GenOnce()
        {
            // while (_edgesLinkedList.Count > 0 && _nodesLinkedList.Count > 0)
            // // {
            //     _executed = false;
            //     
            //     if(Nodes != null && Edges != null){
            //         if (Nodes.Count == 0 && Edges.Count == 0)
            //         {
            //             _nodesLinkedList.RemoveFirst();
            //             _edgesLinkedList.RemoveFirst();
            //             if (_nodesLinkedList.Count > 0 && _edgesLinkedList.Count > 0)
            //             {
            //                 Nodes = _nodesLinkedList.First.Value;
            //                 Edges = _edgesLinkedList.First.Value;    
            //             }
            //         }
            //         // if(Nodes != null && Edges!= null)
            //         //     for (var i = 0; i < Nodes.Count; i++)
            //                 GenerateABlock((counter++)%Nodes.Count);
            //     }
            //
            //     if (!_executed && _edgesLinkedList.Count > 0 && _nodesLinkedList.Count > 0)
            //     {
            //         Debug.LogWarning("检测到死循环,跳出!");
            //         return;
            //     }
            // //}
            while (_edgesLinkedList.Count > 0 && _nodesLinkedList.Count > 0)
            {
                _executed = false;
                
                if(Nodes != null && Edges != null){
                    if (Nodes.Count == 0 && Edges.Count == 0)
                    {
                        _nodesLinkedList.RemoveFirst();
                        _edgesLinkedList.RemoveFirst();
                        if (_nodesLinkedList.Count > 0 && _edgesLinkedList.Count > 0)
                        {
                            Nodes = _nodesLinkedList.First.Value;
                            Edges = _edgesLinkedList.First.Value;    
                        }
                    }
                    if(Nodes != null && Edges!= null)
                        for (var i = 0; i < Nodes.Count; i++)
                            GenerateABlock(i);
                }
            
                if (!_executed && _edgesLinkedList.Count > 0 && _nodesLinkedList.Count > 0)
                {
                    Debug.LogWarning("检测到死循环,跳出!");
                    return;
                }
            }
        }

        // TODO 待完善
        /// <summary>
        /// 在该点生成一个街区
        /// </summary>
        /// <param name="i">顶点的索引</param>
        private void GenerateABlock(int i)
        {
            // 当节点剩下的已经足够少,直接组成Block
            if (Nodes.Count < 6)
            {
                // 标记已执行
                _executed = true;
                
                // 添加街区
                AddToResultBlock(Edges.ToArray());

                // 清空
                Nodes.Clear();
                Edges.Clear();

                // 标记结束
                IsDone = true;
                return;
            }

            var noCompareIDs = new List<uint>();

            // 求得下两个顶点的角度之和
            var angle1 = AngleOf((i + 1) % Nodes.Count);
            var angle2 = AngleOf((i + 2) % Nodes.Count);

            // 新加的边界是否与已存在的边界碰撞
            var isCollide = false;

            // 只需要添加一条新线段的情况(角度特别小)
            // TODO 角度需要研究
            if (angle1 < 65 && angle2 > 90)
            {
                // 创建出需要添加的边
                var tempEdge = new AftEdge(Nodes[i], Nodes[(i + 2) % Nodes.Count], IdManager.GetRoadID());

                // 判断该边是否与附近的其他边相交
                // 先设置新边与标准单元格的关系
                SetUnitRelation(tempEdge);

                // 设置不需要比较的ID(新加的边以及几个边是不需要比较的 )
                noCompareIDs.Clear();
                noCompareIDs.Add(tempEdge.ID);
                noCompareIDs.Add(Edges[i > 0 ? i - 1 : Edges.Count - 1].ID);
                noCompareIDs.Add(Edges[i].ID);
                noCompareIDs.Add(Edges[(i + 1) % Edges.Count].ID);
                noCompareIDs.Add(Edges[(i + 2) % Edges.Count].ID);

                // 遍历新边所在的所有标准单元
                foreach (var unit in tempEdge.Units)
                {
                    // 遍历每一个标准单元中的其他边
                    foreach (var edge in unit.Edges)
                    {
                        // 如果发生碰撞,就记录并跳出(不比较某些边)
                        if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge))
                        {
                            isCollide = true;
                            // 跳出这个双重循环
                            goto outLoop;
                        }
                    }
                }

                outLoop:

                // 如果没有相交
                if (!isCollide)
                {
                    // 标记已执行
                    _executed = true;
                    
                    // 把新加的边加入结果中
                    AddToResultRoad(tempEdge);

                    // 清除掉无用边界与其标准单元的关系
                    Edges[i].DeleteRelateUit();
                    Edges[(i + 1) % Edges.Count].DeleteRelateUit();

                    // 添加街区到结果中
                    AddToResultBlock(Edges[i], Edges[(i + 1) % Edges.Count], tempEdge);

                    // 把新边界加入到前沿中
                    Edges.Insert(i, tempEdge);

                    // 删除无用边界
                    RemoveEdgesInList((i + 1) % Edges.Count, 2);

                    // 删除无用顶点
                    RemoveNodesInList((i + 1) % Nodes.Count, 1);
                }
                // 如果有相交
                else
                {
                    tempEdge.DeleteRelateUit();
                    IdManager.RemoveRoadID(tempEdge.ID);
                    
                    // 去判断最近
                    //goto checkClose;
                }
            }
            // 只需要添加一条新线段的情况
            // TODO 角度需要研究
            else if (angle1 >= 65 && angle1 < 115 && angle1 + angle2 >= 160 && angle1 + angle2 < 240)
            {
                // TEST
                if(Vector3.Distance(Nodes[i].Coord,Nodes[(i + 3) % Nodes.Count].Coord) < WeightUnitLength(Nodes[i].Coord.x,Nodes[i].Coord.z) * 0.8f) return;
                
                // 创建出需要添加的边
                var tempEdge = new AftEdge(Nodes[i], Nodes[(i + 3) % Nodes.Count], IdManager.GetRoadID());

                // 判断该边是否与附近的其他边相交
                // 先设置新边与标准单元格的关系
                SetUnitRelation(tempEdge);

                // 设置不需要比较的ID(新加的边以及几个边是不需要比较的 )
                noCompareIDs.Clear();
                noCompareIDs.Add(tempEdge.ID);
                noCompareIDs.Add(Edges[i > 0 ? i - 1 : Edges.Count - 1].ID);
                noCompareIDs.Add(Edges[i].ID);
                noCompareIDs.Add(Edges[(i + 1) % Edges.Count].ID);
                noCompareIDs.Add(Edges[(i + 2) % Edges.Count].ID);
                noCompareIDs.Add(Edges[(i + 3) % Edges.Count].ID);

                // 遍历新边所在的所有标准单元
                foreach (var unit in tempEdge.Units)
                {
                    // 遍历每一个标准单元中的其他边
                    foreach (var edge in unit.Edges)
                    {
                        // 如果发生碰撞,就记录并跳出(不比较某些边)
                        if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge))
                        {
                            isCollide = true;
                            // 跳出这个双重循环
                            goto outLoop;
                        }
                    }
                }

                outLoop:

                // 如果没有相交
                if (!isCollide)
                {
                    // 标记已执行
                    _executed = true;
                    
                    // 新加入的边和顶点
                    var newEdges = new List<AftEdge>();
                    var newNodes = new List<AftNode>();
                    // 判断新加入的边是否过长
                    var distance = Vector3.Distance(tempEdge.Begin.Coord, tempEdge.End.Coord); 
                    if (distance > 1.5 * WeightUnitLength(tempEdge.Begin.Coord.x,tempEdge.Begin.Coord.z))
                    {
                        // 如果过长 拆分为多段加入
                        
                        // 取消边的ID 
                        IdManager.RemoveRoadID(tempEdge.ID);
                        
                        // 计算中间需要拆分的数量
                        var partsCount = (int) Math.Floor(distance / WeightUnitLength(tempEdge.Begin.Coord.x,tempEdge.Begin.Coord.z)) + 1;
                        
                        // 计算顶点
                        for (var j = 1; j < partsCount; j++)
                        {
                            // 通过插值计算位置并插入新的List中
                            newNodes.Add(new AftNode(
                                Vector3.Lerp(
                                    tempEdge.Begin.Coord,
                                    tempEdge.End.Coord,
                                    (float) j / partsCount), IdManager.GetNodeID()));
                        }

                        // 计算新边
                        // 第一个边
                        newEdges.Add(new AftEdge(tempEdge.Begin,newNodes[0],IdManager.GetRoadID()));
                        // 中间的边
                        for (var j = 0; j < newNodes.Count - 1; j++)
                            newEdges.Add(new AftEdge(newNodes[j],newNodes[j+1],IdManager.GetRoadID()));
                        // 最后一个边
                        newEdges.Add(new AftEdge(newNodes[newNodes.Count - 1],tempEdge.End,IdManager.GetRoadID()));
                        
                    }
                    else
                    {
                        newEdges.Add(tempEdge);
                    }
                    
                    // 把新加的边和顶点加入结果中
                    foreach (var node in newNodes)
                        AddToResultNode(node);
                    foreach (var edge in newEdges)
                        AddToResultRoad(edge);

                    // 清除掉无用边界与其标准单元的关系
                    Edges[i].DeleteRelateUit();
                    Edges[(i + 1) % Edges.Count].DeleteRelateUit();
                    Edges[(i + 2) % Edges.Count].DeleteRelateUit();

                    // 添加街区到结果中 // TODO
                    var edgesToBlock = new List<AftEdge>();
                    edgesToBlock.Add(Edges[i]);
                    edgesToBlock.Add(Edges[(i + 1) % Edges.Count]);
                    edgesToBlock.Add(Edges[(i + 2) % Edges.Count]);
                    // 反转一下
                    newEdges.Reverse();
                    edgesToBlock.AddRange(newEdges);
                    AddToResultBlock(edgesToBlock.ToArray());
                    // 返回来
                    newEdges.Reverse();
                    //AddToResultBlock(Edges[i], Edges[(i + 1) % Edges.Count], Edges[(i + 2) % Edges.Count], tempEdge);

                    // 把新边界加入到前沿中
                    Edges.InsertRange(i, newEdges);

                    // 删除无用边界
                    RemoveEdgesInList((i + newEdges.Count) % Edges.Count, 3);

                    // 如果有新顶点就加入
                    if (newNodes.Count > 0)
                        Nodes.InsertRange(i + 1, newNodes);
                    
                    // 删除无用顶点
                    RemoveNodesInList((i + 1 + newNodes.Count) % Nodes.Count, 2);
                }
                // 如果有相交
                else
                {
                    tempEdge.DeleteRelateUit();
                    IdManager.RemoveRoadID(tempEdge.ID);
                    
                    // 去判断最近
                    //goto checkClose;
                }
            }
            // 需要添加两条线段的情况
            else if (angle1 < 115 && angle1 > 65 && angle1 + angle2 > 200)
            {
                // 记录一下node1 node2
                AftNode node1 = Nodes[(i + 1) % Nodes.Count], node2 = Nodes[(i + 2) % Nodes.Count];
                // 计算第i点到第一个点的方向
                var dir = node2.Coord -node1.Coord;
                // 计算旋转矩阵
                var roateMat = Matrix4x4.Rotate(Quaternion.Euler(0, -angle1 + RandomRange(-2,2), 0));
                // 计算并创建出第一个要添加的点
                // TEST 加个随机的缩放看效果如何
                //var testScale = UnityEngine.Random.Range(0.95f, 1.05f);
                //var testScale = 1f;
                var testScale = RandomRange(0.9f, 1.1f);
                dir = (roateMat * dir).normalized * (WeightUnitLength(node1.Coord.x,node1.Coord.z) * testScale);
                var tempNode = new AftNode(node2.Coord + dir, IdManager.GetNodeID());

                // 创建俩个新的边
                var tempEdge1 = new AftEdge(Nodes[i], tempNode, IdManager.GetRoadID());
                var tempEdge2 = new AftEdge(tempNode, node2, IdManager.GetRoadID());

                // 设置关系
                SetUnitRelation(tempEdge1);
                SetUnitRelation(tempEdge2);

                // 判断碰撞
                // 设置不需要比较碰撞的ID(新加的边以及几个边是不需要比较的)
                noCompareIDs.Clear();
                noCompareIDs.Add(tempEdge1.ID);
                noCompareIDs.Add(tempEdge2.ID);
                noCompareIDs.Add(Edges[i > 0 ? i - 1 : Edges.Count - 1].ID);
                noCompareIDs.Add(Edges[i].ID);
                noCompareIDs.Add(Edges[(i + 1) % Edges.Count].ID);
                
                // 遍历新边所在的所有标准单元
                foreach (var unit in tempEdge1.Units)
                    // 遍历每一个标准单元中的其他边
                    foreach (var edge in unit.Edges)
                        // 如果发生碰撞,就记录并跳出(不比较某些边)
                        if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge1))
                        {
                            isCollide = true;
                            // 跳出这个双重循环
                            goto outLoop;
                        }
                
                noCompareIDs.Remove(Edges[i > 0 ? i - 1 : Edges.Count - 1].ID);
                noCompareIDs.Remove(Edges[i].ID);
                noCompareIDs.Add(Edges[(i + 2) % Edges.Count].ID);

                foreach (var unit in tempEdge2.Units)
                    // 遍历每一个标准单元中的其他边
                    foreach (var edge in unit.Edges)
                        // 如果发生碰撞,就记录并跳出(不比较某些边)
                        if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge2))
                        {
                            isCollide = true;
                            // 跳出这个双重循环
                            goto outLoop;
                        }
                outLoop:

                // 如果没有碰撞
                if (!isCollide)
                {
                    // 标记已执行
                    _executed = true;

                    // 添加新Node到结果中
                    AddToResultNode(tempNode);

                    // 添加新Road到结果中
                    AddToResultRoad(tempEdge1);
                    AddToResultRoad(tempEdge2);

                    // 添加新Block到结果中
                    AddToResultBlock(Edges[i], Edges[(i + 1) % Edges.Count], tempEdge2, tempEdge1);

                    // 清除掉无用边界与其标准单元的关系
                    Edges[i].DeleteRelateUit();
                    Edges[(i + 1) % Edges.Count].DeleteRelateUit();

                    // 直接替换顶点
                    Nodes[(i + 1) % Nodes.Count] = tempNode;

                    // 直接替换
                    Edges[i] = tempEdge1;
                    Edges[(i + 1) % Edges.Count] = tempEdge2;
                }
                // 如果有碰撞
                else
                {
                    // 清除关系
                    tempEdge1.DeleteRelateUit();
                    tempEdge2.DeleteRelateUit();
                    // 移除ID
                    IdManager.RemoveNodeID(tempNode.ID);
                    IdManager.RemoveRoadID(tempEdge1.ID);
                    IdManager.RemoveRoadID(tempEdge2.ID);
                    
                    // 去判断最近
                    //goto checkClose;
                }

            }
            // 需要添加三条线段的情况
            else if (angle1 >= 115 && angle1 < 220 && angle2 >= 115 && angle2 < 220)
            {
                // 记录一下node1 node2
                AftNode node1 = Nodes[(i + 1) % Nodes.Count], node2 = Nodes[(i + 2) % Nodes.Count];
                // 计算第一个点到第二个点的方向
                var dir = node2.Coord - node1.Coord;
                // 计算旋转矩阵
                var roateMat = Matrix4x4.Rotate(Quaternion.Euler(0, -90, 0));
                // 计算并创建出第一个要添加的点
                // TEST 加个随机的缩放看效果如何
                //float testScale = UnityEngine.Random.Range(0.8f, 1.1f);
                //var testScale = 1;
                var testScale = RandomRange(0.9f, 1.1f);
                Vector3 newDir = (roateMat * dir).normalized * (WeightUnitLength(node1.Coord.x,node1.Coord.z) * testScale);
                var tempNode1 = new AftNode(node1.Coord + newDir, IdManager.GetNodeID());

                // 计算第二个点到底一个点的方向(反转即可)
                dir *= -1;
                // 计算旋转矩阵
                roateMat = Matrix4x4.Rotate(Quaternion.Euler(0, 90, 0));
                // 计算并创建出第二个要添加的点
                // TEST 加个随机的缩放看效果如何
                //testScale = UnityEngine.Random.Range(0.8f, 1.1f);
                newDir = (roateMat * dir).normalized * (WeightUnitLength(node1.Coord.x,node1.Coord.z) * testScale);
                var tempNode2 = new AftNode(node2.Coord + newDir, IdManager.GetNodeID());

                // 计算新的三条边
                var tempEdge1 = new AftEdge(node1, tempNode1, IdManager.GetRoadID());
                var tempEdge2 = new AftEdge(tempNode1, tempNode2, IdManager.GetRoadID());
                var tempEdge3 = new AftEdge(tempNode2, node2, IdManager.GetRoadID());

                // 设置关系
                SetUnitRelation(tempEdge1);
                SetUnitRelation(tempEdge2);
                SetUnitRelation(tempEdge3);

                // 判断碰撞
                // 设置不需要比较碰撞的ID(新加的边以及几个边是不需要比较的)
                noCompareIDs.Clear();
                noCompareIDs.Add(tempEdge1.ID);
                noCompareIDs.Add(tempEdge2.ID);
                noCompareIDs.Add(tempEdge3.ID);
                noCompareIDs.Add(Edges[(i + 1) % Edges.Count].ID);
                
                // 遍历新边所在的所有标准单元
                foreach (var unit in tempEdge2.Units)
                    // 遍历每一个标准单元中的其他边
                    foreach (var edge in unit.Edges)
                        // 如果发生碰撞,就记录并跳出(不比较某些边)
                        if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge2))
                        {
                            isCollide = true;
                            // 跳出这个双重循环
                            goto outLoop;
                        }
                
                noCompareIDs.Add(Edges[i].ID);
                
                foreach (var unit in tempEdge1.Units)
                    // 遍历每一个标准单元中的其他边
                    foreach (var edge in unit.Edges)
                        // 如果发生碰撞,就记录并跳出(不比较某些边)
                        if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge1))
                        {
                            isCollide = true;
                            // 跳出这个双重循环
                            goto outLoop;
                        }

                noCompareIDs.Remove(Edges[i].ID);
                noCompareIDs.Add(Edges[(i + 2) % Edges.Count].ID);
                
                foreach (var unit in tempEdge3.Units)
                    // 遍历每一个标准单元中的其他边
                    foreach (var edge in unit.Edges)
                        // 如果发生碰撞,就记录并跳出(不比较某些边)
                        if (!noCompareIDs.Contains(edge.ID) && IsCollide(edge, tempEdge3))
                        {
                            isCollide = true;
                            // 跳出这个双重循环
                            goto outLoop;
                        }

                outLoop:

                // 如果没有碰撞
                if (!isCollide)
                {
                    // 标记已执行
                    _executed = true;

                    // 添加新Node到结果中
                    AddToResultNode(tempNode1);
                    AddToResultNode(tempNode2);

                    // 添加新Road到结果中
                    AddToResultRoad(tempEdge1);
                    AddToResultRoad(tempEdge2);
                    AddToResultRoad(tempEdge3);

                    // 添加新Block到结果中
                    AddToResultBlock(Edges[(i + 1) % Edges.Count], tempEdge3, tempEdge2, tempEdge1);

                    // 清除掉无用边界与其标准单元的关系
                    Edges[(i + 1) % Edges.Count].DeleteRelateUit();

                    // 添加新顶点到前沿中
                    var tempNodeList = new List<AftNode>()
                    {
                        tempNode1,
                        tempNode2
                    };
                    Nodes.InsertRange((i + 2) % Nodes.Count, tempNodeList);

                    // 添加新边界到前沿中
                    var tempEdgeList = new List<AftEdge>()
                    {
                        tempEdge1,
                        tempEdge2,
                        tempEdge3
                    };
                    Edges.InsertRange((i + 1) % Edges.Count, tempEdgeList);

                    // 删除无用边界
                    RemoveEdgesInList((i + 4) % Edges.Count, 1);
                }
                // 如果有碰撞
                else
                {
                    // 清除关系
                    tempEdge1.DeleteRelateUit();
                    tempEdge2.DeleteRelateUit();
                    tempEdge3.DeleteRelateUit();
                    // 移除ID
                    IdManager.RemoveNodeID(tempNode1.ID);
                    IdManager.RemoveNodeID(tempNode2.ID);
                    IdManager.RemoveRoadID(tempEdge1.ID);
                    IdManager.RemoveRoadID(tempEdge2.ID);
                    IdManager.RemoveRoadID(tempEdge3.ID);
                }
            }
            // 检测近距离顶点
            else
            {
                // 判断有没有距离该节点很近的其他非邻边节点
                noCompareIDs.Clear();
                noCompareIDs.Add(Edges[i > 0 ? i - 1 : Edges.Count - 1].ID);
                noCompareIDs.Add(Edges[i].ID);
                noCompareIDs.Add(Edges[(i + 1) % Edges.Count].ID);
                noCompareIDs.Add(Edges[(i + 2) % Edges.Count].ID);

                // 找到一个最近的,并且位于左侧的一个符合距离的顶点
                var closestIndex = FindClosestNodeIndex((i + 1) % Nodes.Count, noCompareIDs,
                    Nodes[i].Coord,
                    Nodes[(i + 1) % Nodes.Count].Coord,
                    Nodes[(i + 2) % Nodes.Count].Coord);
                
                // 如果找到
                if (closestIndex >= 0)
                {
                    // 标记已执行
                    _executed = true;

                    // TEST
                    //Debug.LogWarning("有桥边" + Nodes[i].ID +" ," + Nodes[closestIndex].ID);
                    
                    // 【注意】此ID由两个方向的边用,但实际是一条边,任意一条边加入结果即可
                    var brigeEdgeID = IdManager.GetRoadID();

                    // 创建桥边
                    var brigeEdge = new AftEdge(Nodes[closestIndex], Nodes[(i + 1)%Nodes.Count], brigeEdgeID);

					// 设置关系
                    SetUnitRelation(brigeEdge);
                    
                    // TODO 这里也许需要加碰撞检测,但是不加的话 好像也不会出现碰撞,但是没有证明过

                    // 添加到结果中
                    AddToResultRoad(brigeEdge);

                    // 创建桥边分割开来的另一半的顶点和边
                    var newNodes = CopyRangeInNodes((i + 1) % Nodes.Count, closestIndex);
                    var newEdges = CopyRangeInEdges((i + 1) % Edges.Count, closestIndex > 0 ? closestIndex - 1 : Edges.Count - 1);
                    newEdges.Add(brigeEdge);

                    // 将新的边界和顶点存到链表后,用于以后继续生成,代替递归
                    _edgesLinkedList.AddLast(newEdges);
                    _nodesLinkedList.AddLast(newNodes);

                    // 删除掉已经划分的边和顶点
                    // 计算需要删掉几个边
                    var deleteEdgeCount = (Edges.Count + closestIndex - i - 1) % Edges.Count;
                    // 添加桥边
                    Edges.Insert((i + 1) % Edges.Count, new AftEdge(Nodes[(i + 1) % Nodes.Count], Nodes[closestIndex], brigeEdgeID));
                    // 删除多余边
                    RemoveEdgesInList((i + 2) % Edges.Count, deleteEdgeCount);
                    // 删除多余顶点
                    RemoveNodesInList((i + 2) % Nodes.Count, deleteEdgeCount - 1);
                }
                
                
                // TEST
                // 当节点剩下的已经足够少,直接组成Block
                if (Nodes.Count < 6)
                {
                    // 标记已执行
                    _executed = true;
                    
                    // 添加街区
                    AddToResultBlock(Edges.ToArray());

                    // 清空
                    Nodes.Clear();
                    Edges.Clear();

                    // 标记结束
                    IsDone = true;
                }
            }
            
        }

        /// <summary>
        /// 找到距离该点,最近的(下限以内)且是左侧的点的坐标
        /// </summary>
        /// <param name="currentIndex">需要判断的点</param>
        /// <param name="noCompareIDs">不需要判断的边的ID</param>
        /// <param name="p1">比较点前一个点坐标</param>
        /// <param name="p2">比较点坐标</param>
        /// <param name="p3">比较点下一个点坐标</param>
        /// <returns></returns>
        private int FindClosestNodeIndex(int currentIndex, ICollection<uint> noCompareIDs, Vector3 p1, Vector3 p2, Vector3 p3)
        {
            // 计算这个坐标位于的标准网格索引
            int indexX = (int) Math.Floor((Nodes[currentIndex].Coord.x - StartCoordinate.x) / StandardUintLength),
                indexY = (int) Math.Floor((Nodes[currentIndex].Coord.z - StartCoordinate.z) / StandardUintLength);

            // 暂存最近距离
            var tempDistance = StandardUintLength * 100;
            AftNode tempNode = null;
            
            // 遍历附近包括自己所在的9个标准单元
            for (var x = Math.Max(0, indexX - 1); x <= Math.Min(indexX + 1, StandardNet.GetLength(0) - 1); x++)
            {
                for (var y = Math.Max(0, indexY - 1); y <= Math.Min(indexY + 1, StandardNet.GetLength(1) - 1); y++)
                {
                    // 遍历每个单元内需要比较的边
                    foreach (var edge in StandardNet[x, y].Edges)
                    {
                        // 去除不需要比较的情况
                        if (noCompareIDs.Contains(edge.ID)) continue;
                        
                        var distance = Vector3.Distance(edge.Begin.Coord, Nodes[currentIndex].Coord);
                        // 判断该线段上两个端点有没有足够近的顶点
                        // TODO 1.35有待考究
                        if (distance < tempDistance && distance < WeightUnitLength(Nodes[currentIndex].Coord.x,Nodes[currentIndex].Coord.z) * 1.35f)
                        {
                            if (Vector3.SignedAngle(p2 - p1, p3 - p2, Vector3.up) > 0)
                            {
                                if (CrossVec2(edge.Begin.Coord - p1, p2 - p1) < 0 ||
                                    CrossVec2(edge.Begin.Coord - p2, p3 - p2) < 0)
                                    // 在左侧
                                {
                                    tempDistance = distance;
                                    tempNode = edge.Begin;
                                }
                                        
                            }
                            else
                            {
                                if (CrossVec2(edge.Begin.Coord - p1, p2 - p1) < 0 && 
                                    CrossVec2(edge.Begin.Coord - p2, p3 - p2) < 0) // 在左侧
                                {
                                    tempDistance = distance;
                                    tempNode = edge.Begin;
                                }
                            }
                        }

                        distance = Vector3.Distance(edge.End.Coord, Nodes[currentIndex].Coord);
                        if (distance < tempDistance && distance < WeightUnitLength(Nodes[currentIndex].Coord.x,Nodes[currentIndex].Coord.z) * 1.35f)
                        {
                            if (Vector3.SignedAngle(p1 - p2, p2 - p3, Vector3.up) > 0)
                            {
                                if (CrossVec2(edge.End.Coord - p1, p2 - p1) < 0 || 
                                    CrossVec2(edge.End.Coord - p2, p3 - p2) < 0) // 在左侧
                                {
                                    tempDistance = distance;
                                    tempNode = edge.End;
                                }
                            }
                            else
                            {
                                if (CrossVec2(edge.End.Coord - p1, p2 - p1) < 0 && 
                                    CrossVec2(edge.End.Coord - p2, p3 - p2) < 0) // 在左侧
                                {
                                    tempDistance = distance;
                                    tempNode = edge.End;
                                }
                            }
                        }
                    }
                }
            }

            if (tempNode != null) return Nodes.IndexOf(tempNode); 
            
            // 没有找到
            return -1;
        }

        /// <summary>
        /// 便捷的移除AftNode中List的节点
        /// </summary>
        /// <param name="begin"></param>
        /// <param name="count"></param>
        private void RemoveNodesInList(int begin, int count)
        {
            // 如果需要跨越结尾进行删除
            if (begin + count > Nodes.Count)
            {
                var frontCount = begin + count - Nodes.Count;
                count = Nodes.Count - begin;
                Nodes.RemoveRange(begin, count);
                Nodes.RemoveRange(0, frontCount);
            }
            // 如果可以直接删除
            else
            {
                Nodes.RemoveRange(begin, count);
            }
        }

        /// <summary>
        /// 便捷的移除AftEdge中List的节点
        /// </summary>
        /// <param name="begin"></param>
        /// <param name="count"></param>
        private void RemoveEdgesInList(int begin, int count)
        {
            // 如果需要跨越结尾进行删除
            if (begin + count > Edges.Count)
            {
                var frontCount = begin + count - Edges.Count;
                count = Edges.Count - begin;
                Edges.RemoveRange(begin, count);
                Edges.RemoveRange(0, frontCount);
            }
            // 如果可以直接删除
            else
            {
                Edges.RemoveRange(begin, count);
            }
        }

        /// <summary>
        /// 从Node的List中复制一部分出来(可跨越)
        /// </summary>
        /// <param name="begin">开始索引</param>
        /// <param name="end">结束索引</param>
        /// <returns></returns>
        private List<AftNode> CopyRangeInNodes(int begin, int end)
        {
            var res = new List<AftNode>();

            // TODO 可优化
            if (end > begin)
            {
                for (var i = begin; i <= end; i++)
                    res.Add(Nodes[i]);
            }
            else
            {
                for (var i = begin; i < Nodes.Count; i++)
                    res.Add(Nodes[i]);
                for (var i = 0; i <= end; i++)
                    res.Add(Nodes[i]);
            }

            return res;
        }

        /// <summary>
        /// 从Edges的List中复制一部分出来(可跨越)
        /// </summary>
        /// <param name="begin">开始索引</param>
        /// <param name="end">结束索引</param>
        /// <returns></returns>
        private List<AftEdge> CopyRangeInEdges(int begin, int end)
        {
            var res = new List<AftEdge>();

            // TODO 可优化
            if (end > begin)
            {
                for (var i = begin; i <= end; i++)
                    res.Add(Edges[i]);
            }
            else
            {
                for (var i = begin; i < Nodes.Count; i++)
                    res.Add(Edges[i]);
                for (var i = 0; i <= end; i++)
                    res.Add(Edges[i]);
            }

            return res;
        }

        /// <summary>
        /// 快捷的构造一个街区并且添加到结果中去,逆时针顺序
        /// </summary>
        /// <param name="edges"></param>
        private void AddToResultBlock(params AftEdge[] edges)
        {
            // 判断是否符合要求
            if (edges.Length < 3)
            {
                Debug.LogWarning("传入的边界不足以构成街区");
                return;
            }

            // 创建街区所需的道路List
            var roads = new List<Road>();
            foreach (var edge in edges)
            {
                roads.Add(ResultRoads[edge.ID]);
            }

            // 加入到结果中
            var tempBlock = new Block(IdManager.GetBlockID(), roads);
            ResultBlocks.Add(tempBlock.ID,tempBlock);
        }

        /// <summary>
        /// 随机一个小数
        /// </summary>
        /// <param name="begin"></param>
        /// <param name="end"></param>
        /// <returns></returns>
        private static float RandomRange(float begin,float end)
        {
            return Mathf.Lerp(begin, end, (float) _random.NextDouble());
        }
        
        /// <summary>
        /// 快捷的把该边界加入到结果中
        /// </summary>
        /// <param name="road"></param>
        private void AddToResultRoad(AftEdge road)
        {
            if (ResultRoads.ContainsKey(road.ID))
            {
                Debug.LogWarning("添加道路边界时 出现重复ID:" + road.ID);
                return;
            }
            ResultRoads.Add(road.ID,new Road(road.ID,ResultNodes[road.Begin.ID],ResultNodes[road.End.ID]));
        }
        
        /// <summary>
        /// 快捷的把该节点加入到结果中
        /// </summary>
        /// <param name="node"></param>
        private void AddToResultNode(AftNode node)
        {
            if (ResultNodes.ContainsKey(node.ID))
            {
                Debug.LogWarning("添加节点时 出现重复ID:" + node.ID);
                return;
            }
            ResultNodes.Add(node.ID,new Node(node.ID,node.Coord));
        }
        
        /// <summary>
        /// 设置边界线段与单元格的关系
        /// </summary>
        /// <param name="edge">边界线段</param>
        private void SetUnitRelation(AftEdge edge)
        {
            // 计算线段两个节点所在的单元格索引
            int node1X = (int)Math.Floor((edge.Begin.Coord.x - StartCoordinate.x) / StandardUintLength),
                node1Y = (int)Math.Floor((edge.Begin.Coord.z - StartCoordinate.z) / StandardUintLength);
            int node2X = (int)Math.Floor((edge.End.Coord.x - StartCoordinate.x) / StandardUintLength),
                node2Y = (int)Math.Floor((edge.End.Coord.z - StartCoordinate.z) / StandardUintLength);

            // 让1为小的值,2为大的值
            if(node1X > node2X) Swap(ref node1X,ref node2X);
            if(node1Y > node2Y) Swap(ref node1Y,ref node2Y);
            
            // 找到需要检测的单元格
            for (var i = node1X; i <= node2X; i++)
            {
                for (var j = node1Y; j <= node2Y; j++)
                {
                    // 检测该单元格与线段是否碰撞
                    if (IsCollide(edge, RectOf(i,j)))
                    {
                        // 在边界中加入单元
                        edge.Units.Add(StandardNet[i,j]);
                        // 在单元中加入边界
                        StandardNet[i,j].Edges.Add(edge);
                    }
                }
            }
        }

        /// <summary>
        /// 返回该位置带权重的单元长度
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        private float WeightUnitLength(float x, float y)
        {
            if (Mathf.Abs(x) < 12 && Mathf.Abs(y) < 12)
            {
                return StandardUintLength / 2;
            }
            
            return StandardUintLength;
        }
        
        /// <summary>
        /// 判断线段与矩形是否碰撞
        /// </summary>
        /// <param name="edge"></param>
        /// <param name="rect"></param>
        /// <returns></returns>
        private static bool IsCollide(AftEdge edge, Vector3[] rect)
        {
            /*参考:https://blog.csdn.net/weixin_43807642/article/details/89243356
             判断两个对角线与直线的交点是否在矩形内。
             虽然这里是线段不是直线,但是经过前面的筛选后不会出现线段不碰撞而直线碰撞的矩形了,所以直接当成直线检测即可*/
            
            // k1 b1为线段的斜率和b,k2 b2为矩形对角线的斜率和b,交点xy

            // 求线段的斜率和b
            var k1 = (edge.End.Coord.z - edge.Begin.Coord.z) / (edge.End.Coord.x - edge.Begin.Coord.x);
            var b1 = edge.Begin.Coord.z - k1 * edge.Begin.Coord.x;
            
            // 求第一个对角线的斜率和b
            var k2 = (rect[1].z - rect[0].z) / (rect[1].x - rect[0].x);
            var b2 = rect[0].z - k2 * rect[0].x;
            // 计算第一个交点
            var x = (b1 - b2) / (k2 - k1);
            var y = (b1 - b2) / (k2 - k1) * k1 + b1;

            // 判断第一个交点是否在矩形内,如果是直接返回true
            if (x > rect[0].x && x < rect[1].x && y > rect[0].z && y < rect[1].z)
                return true;
            
            // 求第二个对角线的斜率和b
            // 交换一下求的另一个对角线
            var temp = rect[0].z;
            rect[0].z = rect[1].z;
            rect[1].z = temp;
            k2 = (rect[1].z - rect[0].z) / (rect[1].x - rect[0].x);
            b2 = rect[0].z - k2 * rect[0].x;
            // 计算第二个交点
            x = (b1 - b2) / (k2 - k1);
            y = (b1 - b2) / (k2 - k1) * k1 + b1;

            // 判断第二个交点是否在矩形内,如果是直接返回true
            return x >= rect[0].x && x <= rect[1].x && y >= rect[1].z && y <= rect[0].z;
        }
        
        /// <summary>
        /// 判断两线段是否相交
        /// </summary>
        /// <param name="edge1"></param>
        /// <param name="edge2"></param>
        /// <returns></returns>
        private static bool IsCollide(AftEdge edge1, AftEdge edge2)
        {
            /* 参考: https://blog.csdn.net/stevenkylelee/article/details/87934320
             基于向量叉乘,判断每个线段的两端是否分别在另一个线段所划分的空间的两端,是则相交*/
            
            // 定义四个点
            var ap1 = new Vector2(edge1.Begin.Coord.x, edge1.Begin.Coord.z);
            var ap2 = new Vector2(edge1.End.Coord.x, edge1.End.Coord.z);
            var bp1 = new Vector2(edge2.Begin.Coord.x, edge2.Begin.Coord.z);
            var bp2 = new Vector2(edge2.End.Coord.x, edge2.End.Coord.z);

            // 判断是否异号
            if (CrossVec2(ap2 - ap1, bp1 - ap1) * CrossVec2(ap2 - ap1, bp2 - ap1) < 0 && 
                CrossVec2(bp1 - bp2, ap1 - bp2) * CrossVec2(bp1 - bp2, ap2 - bp2) < 0)
                return true;

            return false;
        }
        
        /// <summary>
        /// 二维向量叉乘
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        private static float CrossVec2(Vector2 a, Vector2 b)
        {
            return (a.x * b.y) - (b.x * a.y);
        }
        private static float CrossVec2(Vector3 a, Vector3 b)
        {
            return (a.x * b.z) - (b.x * a.z);
        }

        /// <summary>
        /// 计算该顶点的角度
        /// </summary>
        /// <param name="n">顶点索引</param>
        /// <returns>角度</returns>
        private float AngleOf(int n)
        {
            // 计算该点的两个向量
            var vec1 = Nodes[n > 0 ? n - 1 : Nodes.Count - 1].Coord - Nodes[n].Coord;
            var vec2 = Nodes[(n + 1) % Nodes.Count].Coord - Nodes[n].Coord;

            // 计算角度
            var angle = Vector3.SignedAngle(vec1, vec2, Vector3.up);

            // 将负的角度改成正数
            if (angle < 0) angle = 360 + angle;

            return angle;
        } 
        
        /// <summary>
        /// 返回对应单元格的矩形数据
        /// </summary>
        /// <param name="x">索引x</param>
        /// <param name="y">索引y</param>
        /// <returns>矩形数据(2维,只有对角的坐标)</returns>
        private Vector3[] RectOf(int x, int y)
        {
            var res = new Vector3[2];

            res[0].x = StartCoordinate.x + x * StandardUintLength;
            res[0].z = StartCoordinate.z + y * StandardUintLength;
            res[1].x = res[0].x + StandardUintLength;
            res[1].z = res[0].z + StandardUintLength;
            
            return res;
        }
        
        /// <summary>
        /// 交换
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <typeparam name="T"></typeparam>
        private static void Swap<T>(ref T a, ref T b)
        {
            var temp = a;
            a = b;
            b = temp;
        }
        
        /// <summary>
        /// 查找多边形的最边界的顶点的坐标
        /// </summary>
        /// <param name="coords">顶点List</param>
        /// <param name="minX">x方向上最小的顶点的坐标</param>
        /// <param name="maxX">x方向上最大的顶点的坐标</param>
        /// <param name="minZ">z方向上最小的顶点的坐标</param>
        /// <param name="maxZ">z方向上最大的顶点的坐标</param>
        private static void FindEdgePoints(IReadOnlyList<AftNode> coords, out float minX, out float maxX, out float minZ, out float maxZ)
        {
            // 遍历一遍找到对应的索引
            int minXIndex = 0, maxXIndex = 0, minZIndex = 0, maxZIndex = 0;
            for (var i = 1; i < coords.Count; i++)
            {
                if (coords[i].Coord.x < coords[minXIndex].Coord.x)
                    minXIndex = i;
                if (coords[i].Coord.x > coords[maxXIndex].Coord.x)
                    maxXIndex = i;
                if (coords[i].Coord.z < coords[minZIndex].Coord.z)
                    minZIndex = i;
                if (coords[i].Coord.z > coords[maxZIndex].Coord.z)
                    maxZIndex = i;
            }

            // 赋值
            minX = coords[minXIndex].Coord.x;
            maxX = coords[maxXIndex].Coord.x;
            minZ = coords[minZIndex].Coord.z;
            maxZ = coords[maxZIndex].Coord.z;
        }
    }
    /// <summary>
    /// 标准单元
    /// </summary>
    public class StandardUnit
    {
        // 标准单元内包含的边界
        public List<AftEdge> Edges = new List<AftEdge>();
        // TEST 可视化的时候需要知道单元格的坐标,但实际上算法中不需要坐标
        public int X, Y;
    }

    /// <summary>
    /// 节点
    /// </summary>
    public class AftNode
    {
        // ID
        public readonly uint ID;
        // 位置
        public Vector3 Coord { get; }
        // 该点内角角度
        public float Angle;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="coord">该点的坐标</param>
        /// <param name="id">ID</param>
        public AftNode(Vector3 coord,uint id)
        {
            Coord = coord;
            ID = id;
        }
    }

    /// <summary>
    /// 边界线段
    /// </summary>
    public class AftEdge
    {
        // ID
        public readonly uint ID;
        // 开始的节点
        public AftNode Begin { get; }
        // 结束的节点
        public AftNode End { get; }
        // 所在的标准单元
        public readonly List<StandardUnit> Units = new List<StandardUnit>();

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="begin">开始节点</param>
        /// <param name="end">结束节点</param>
        /// <param name="id">ID</param>
        public AftEdge(AftNode begin, AftNode end,uint id)
        {
            this.Begin = begin;
            this.End = end;
            ID = id;
        }

        /// <summary>
        /// 解除与该边界相关的标准单元格的关系
        /// </summary>
        public void DeleteRelateUit()
        {
            foreach (var unit in Units)
                unit.Edges.Remove(this);
            Units.Clear();
        }
    }
}

4.3. 可视化

using System;
using System.Collections.Generic;
using UnityEngine;
using RoadNetwork;
using System.Threading;

namespace Test
{
    public class TestAFT : MonoBehaviour
    {
        public List<Transform> polygon = new List<Transform>();
        public float unitLength = 1.5f;

        public float nodeShpereSize = 0.2f;
        
        public int randomSeed = 0;
        
        public enum ShowType
        {
            Input,
            Front,
            Result,
            FrontAndResult
        }
        
        public enum LableType
        {
            NodeAndRoad,
            Node,
            Road
        }

        public LableType idType;

        public int genType;

        public ShowType showType;
        
        private List<Vector3> polygonCoords = new List<Vector3>();

        public AdvancingFrontTechnique _boundary;

        // TEST 可视化选择边界用到
        [Range(1,100)]
        public int edgeIndex = 1;

        public void Creat()
        {
            // 获取多边形顶点位置
            polygonCoords.Clear();
            foreach (var point in polygon)
                polygonCoords.Add(point.position);
            
            Generate();
        }

        public void GenAndGen()
        {
            Generate();
            _boundary.GenOnce();
            Debug.Log("Done");
            Debug.Log(AdvancingFrontTechnique.ResultNodes.Count);
        }

        private void OnDrawGizmos()
        {
            

            // 画出标准网格
            if (_boundary != null)
            {
                // 显示起始点
                Gizmos.color = Color.red;
                Gizmos.DrawSphere(_boundary.StartCoordinate,0.4f);
                // 显示标砖网格
                Gizmos.color = Color.gray;
                for (int i = 0; i < AdvancingFrontTechnique.StandardNet.GetLength(0); i++)
                {
                    for (int j = 0; j < AdvancingFrontTechnique.StandardNet.GetLength(1); j++)
                    {
                        Vector3 p1 = _boundary.StartCoordinate, p2 = _boundary.StartCoordinate;
                
                        p1.x += i * _boundary.StandardUintLength;
                        p1.z += j * _boundary.StandardUintLength;
                        
                        p2.x = p1.x + _boundary.StandardUintLength;
                        p2.z = p1.z + _boundary.StandardUintLength;
                
                        DrawRect(p1,p2);
                    }
                }
            }
            switch (showType)
                {
                    case ShowType.Input:
                        // 画出所构建的多边形
                        if (polygon.Count > 0)
                        {
                            Gizmos.color = Color.blue;
                            for (var i = 0; i < polygon.Count; i++)
                            {
                                Gizmos.DrawLine(polygon[i].position, polygon[(i + 1) % polygon.Count].position);
                            }
                        }
                        break;
                    case ShowType.Front:
                        if (_boundary != null)
                        {
                            // 显示离散之后的各个顶点
                            Gizmos.color = Color.magenta;
                            foreach (var node in _boundary.Nodes)
                                Gizmos.DrawSphere(node.Coord,nodeShpereSize);

                            // 显示前沿
                            Gizmos.color = Color.blue;
                            foreach (var edge in _boundary.Edges)
                            {
                                Gizmos.DrawLine(edge.Begin.Coord, edge.End.Coord);
                            }
                            // 显示一个边以及对应的单元格
                            // 线条
                            Gizmos.color = Color.green;
                            edgeIndex %= _boundary.Edges.Count;
                            Gizmos.DrawLine(_boundary.Edges[edgeIndex].Begin.Coord,_boundary.Edges[edgeIndex].End.Coord);
                            // 单元格
                            Gizmos.color = Color.yellow;
                            foreach (var unit in _boundary.Edges[edgeIndex].Units)
                            {
                                Vector3 p1 = _boundary.StartCoordinate, p2 = _boundary.StartCoordinate;
                    
                                p1.x += unit.X * _boundary.StandardUintLength;
                                p1.z += unit.Y * _boundary.StandardUintLength;
                            
                                p2.x = p1.x + _boundary.StandardUintLength;
                                p2.z = p1.z + _boundary.StandardUintLength;
                        
                                DrawRect(p1,p2);
                            }
                        }
                        break;
                    case ShowType.Result:
                        if (_boundary != null && _boundary.IsDone)
                        {
                            // 绘制结果顶点
                            Gizmos.color = Color.magenta;
                            foreach (var node in AdvancingFrontTechnique.ResultNodes)
                                Gizmos.DrawSphere(node.Value.Coord,nodeShpereSize);
                            // 绘制结果边界
                            Gizmos.color = Color.green;
                            foreach (var road in AdvancingFrontTechnique.ResultRoads)
                            {
                                Gizmos.DrawLine(road.Value.Begin.Coord,road.Value.End.Coord);
                            }
                        }
                        break;
                    case ShowType.FrontAndResult:
                        if (_boundary != null)
                        {
                           
                            // 绘制结果边界
                            Gizmos.color = Color.green;
                            foreach (var road in AdvancingFrontTechnique.ResultRoads)
                            {
                                Gizmos.DrawLine(road.Value.Begin.Coord,road.Value.End.Coord);
                            }
                            
                            // 显示前沿
                            Gizmos.color = Color.blue;
                            foreach (var edge in _boundary.Edges)
                            {
                                Gizmos.DrawLine(edge.Begin.Coord, edge.End.Coord);
                            }
                            
                            // 绘制结果顶点
                            Gizmos.color = Color.white;
                            foreach (var node in AdvancingFrontTechnique.ResultNodes)
                                Gizmos.DrawSphere(node.Value.Coord,nodeShpereSize);
                            
                            // 显示离散之后的各个顶点
                            Gizmos.color = Color.red;
                            foreach (var node in _boundary.Nodes)
                                Gizmos.DrawSphere(node.Coord,nodeShpereSize);
                        }
                        break;
                }
        }

        // 画四边形
        private void DrawRect(Vector3 p1, Vector3 p3)
        {
            Vector3 p2 = new Vector3(p1.x,0,p3.z), p4 = new Vector3(p3.x,0,p1.z);
            Gizmos.DrawLine(p1,p2);
            Gizmos.DrawLine(p2,p3);
            Gizmos.DrawLine(p3,p4);
            Gizmos.DrawLine(p4,p1);
        }
        
        public void Generate()
        {
            // 初始化IDManager
            IdManager.Initialization();
            
            _boundary = new AdvancingFrontTechnique(polygonCoords, unitLength, randomSeed);
        }

        // TEST 测试用的函数
        private Thread _thread;
        public void TEST()
        {
            // 获取多边形顶点位置
            polygonCoords.Clear();
            foreach (var point in polygon)
                polygonCoords.Add(point.position);
            _thread = new Thread(new ThreadStart(GenAndGen));
            _thread.Start();
        }

        public void StopThread()
        {
            _thread.Abort();
        }
        
        private static float CrossVec2(Vector3 a, Vector3 b)
        {
            return (a.x * b.z) - (b.x * a.z);
        }
    }
}
using System.Collections.Generic;
using RoadNetwork;
using UnityEditor;
using UnityEngine;

namespace Test.Editor
{
    [CustomEditor(typeof(TestAFT))]
    public class TestAftEditor : UnityEditor.Editor
    {
        private bool showLable;

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            showLable = GUILayout.Toggle(showLable, "ShowLable");
        
            TestAFT script = target as TestAFT;
        
            if (GUILayout.Button("生成"))
            {
                script.Creat();
                SceneView.RepaintAll();
            }

            if (script._boundary!=null && GUILayout.Button("生成一次网格"))
            {
                script._boundary.GenOnce();
                Debug.Log(AdvancingFrontTechnique.ResultBlocks.Count);
                SceneView.RepaintAll();
            }
        
            if (GUILayout.Button("TEST"))
            {
                script.TEST();
            }
        
            if (GUILayout.Button("STOP"))
            {
                script.StopThread();
            }
        }

        private void OnSceneGUI()
        {
            TestAFT script = target as TestAFT;
        
        
            if (showLable && script._boundary != null)
            {
                switch (script.showType)
                {
                    case TestAFT.ShowType.Front:
                        // TEST 可视化角度 (更新过前沿之后这个角度就不对了,因为每次用角度都是重新计算的,并没有更新这个角度)
                        foreach (var node in script._boundary.Nodes)
                            Handles.Label(node.Coord,"Node: " + node.ID.ToString());
                        foreach (var road in script._boundary.Edges)
                            Handles.Label(Vector3.Lerp(road.Begin.Coord,road.End.Coord,0.5f),"Road: "+road.ID.ToString());
                        break;
                    case TestAFT.ShowType.Result:
                        // 可视化ID
                        Handles.color = Color.white;
                        foreach (var node in AdvancingFrontTechnique.ResultNodes)
                            Handles.Label(node.Value.Coord,"Node: " + node.Value.ID.ToString());
                        Handles.color = Color.black;    
                        foreach (var road in AdvancingFrontTechnique.ResultRoads)
                            Handles.Label(Vector3.Lerp(road.Value.Begin.Coord,road.Value.End.Coord,0.5f),"Road: "+road.Value.ID.ToString());
                        break;
                    case TestAFT.ShowType.FrontAndResult:
                        // 可视化ID
                        Handles.color = Color.white;
                        if(script.idType == TestAFT.LableType.NodeAndRoad || script.idType == TestAFT.LableType.Node)
                            foreach (var node in AdvancingFrontTechnique.ResultNodes)
                                Handles.Label(node.Value.Coord,"Node: " + node.Value.ID.ToString());    
                        Handles.color = Color.black;    
                        if(script.idType == TestAFT.LableType.NodeAndRoad || script.idType == TestAFT.LableType.Road)
                            foreach (var road in AdvancingFrontTechnique.ResultRoads)
                                Handles.Label(Vector3.Lerp(road.Value.Begin.Coord,road.Value.End.Coord,0.5f),"Road: "+road.Value.ID.ToString());
                        break;
                }
            }
        }

        private void TestLinkedList()
        {
            var linkedList = new LinkedList<MyClass>();
            linkedList.AddLast(new MyClass("1",1));
            linkedList.AddLast(new MyClass("2",2));
            linkedList.AddLast(new MyClass("3",3));
            linkedList.AddLast(new MyClass("4",4));
            linkedList.AddLast(new MyClass("5",5));
            linkedList.AddLast(new MyClass("6",6));
            linkedList.AddLast(new MyClass("7",7));
            linkedList.AddLast(new MyClass("8",8));

            MyClass temp = linkedList.First.Value;
            temp.str = "liu";
        
            ShowLinked(linkedList);
        }

        private void RemoveRange(ref LinkedList<MyClass> link, LinkedList<MyClass>.Enumerator begin, LinkedList<MyClass>.Enumerator end)
        {
            var i = begin;
            link.Remove(i.Current);

        }

        private void ShowLinked(LinkedList<MyClass> targetList)
        {
            foreach (var node in targetList)
            {
                Debug.Log(node);
            }
        }
    
        public class MyClass
        {
            public string str;
            public int inter;

            public MyClass(string str, int inter)
            {
                this.inter = inter;
                this.str = str;
            }

            public override string ToString()
            {
                return str;
            }
        }
    }
}


  1. 王元,刘华,李航.基于有限元网格划分的城市道路网建模[J].图学学报,2016,37(03):377-385. ↩︎

  2. 李任君. 关于四边形有限元网格生成算法的研究[D].吉林大学,2008. ↩︎

  • 6
    点赞
  • 17
    收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 3

打赏作者

lxbhahaha

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值