5-5 修复地图编辑器

  在上一章中,我们已经可以创建更大尺寸的地图了。但是因为删除了一些代码的原因,现在地图编辑器处于失效的状态中。在这一章中,我们将根据新的生成地图单元方式,修复地图编辑器。在上一章中我们删除了HexGrid中的Refresh,如果现在要改变某个地图单元的高度或者颜色,应该刷新其所在地图块的网格,而不是所有的地图单元网格。所以在HexGridChunk中添加新的Refresh方法。代码如下:

HexGridChunk.cs
1
2
3
4
5
6
7
/// <summary>
/// 重新构建当前chunk内的所有cell
/// </summary>
public void Refresh()
{
hexMesh.Triangulate(cells);
}

  那应该在什么时候调用这个方法?之前我们是在改变地图单元的高度或者颜色的时候对mesh进行重新构建,因为那时只有一个mesh,但现在我们有很多的地图块,每个地图块都包含了一个mesh组件。就不能每个地图块都刷新,而是仅当地图块被改变时再刷新效率会更高,否则编辑较大地图时会感觉很卡。
  接下来问题就变成了如何知道哪个地图块需要刷新。一个比较简单的方法是确保每个地图单元都知道它是属于哪一个地图块,这样地图单元就能在其被改变时刷新它所在的地图块,所以给HexCell一个地图块的引用。代码如下:

HexCell.cs
1
2
3
4
5
6
7
public class HexCell : MonoBehaviour
{
//引用当前其所在的地图块
public HexGridChunk chunk;


}

  当添加地图单元实例时HexGridChunk可以直接把自己赋值给它。代码如下:

HexCell.cs
1
2
3
4
5
6
7
8
9
public void AddCell(int index, HexCell cell)
{
cells[index] = cell;

//将chunk自身实例添加到cell中,这样cell就知道自己属于哪个chunk了
cell.chunk = this;


}

  将这些引用连接建立之后,在HexCell里创建一个Refresh方法,地图单元刷新时就同步刷新自己所在的地图块。代码如下:

HexCell.cs
1
2
3
4
5
6
7
8
9


/// <summary>
/// 当自身状态改变时,刷新自身所在chunk的所有cell
/// </summary>
private void Refresh()
{
chunk.Refresh();
}

  我们之所以不需要把HexCell.Refresh设置为public方法,因为只有地图单元自己清楚它什么时候发生了变化。例如,在高度改变之后。

HexCell.cs
1
2
3
4
5
6
7
8
9
10
11
12
public int Elevation
{


set
{


//设置高度后刷新当前chunk
Refresh();
}
}

  其实只有当前地图单元的高度被设置成了一个不同值时才需要刷新,并不需要在赋了一个相同的高度值后重新计算,所以新的高度值相同时,可以在set属性的一开始就跳出。代码如下:

HexCell.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int Elevation
{


set
{
//当新的高度值赋值是,与旧的相同,直接返回,不执行之后的代码
if (elevation == value)
{
return;
}


}
}

  然而这会跳过第一次设置高度为0时的计算,因为0是地图网格的默认高度,为预防这一点,确保初始值是永远都不会用到的值。代码如下:

HexCell.cs
1
2
3
4
5
6
7
public class HexCell : MonoBehaviour
{
//为高度赋初始值,这样避免了初始值为0,新输入的值也为0,不会刷新mesh的问题
private int elevation = int.MinValue;


}

什么是int.MinValue?
这是int所能表示的最小值,在C#中int是一个32位的数字,它有2的32次方种可能的整数值,分成正值和负值和0,其中一位用来指出这个值是不是负的。
最小值是负的2的31次方=-2147483648,我们永远不会使用这个高度等级。
最大值是2的31次方减1=2147483647,比2的31次方少1是因为还有0存在。
MSDN连接

  除了改变高度会刷新当前的地图块,改变颜色也会。为了检测颜色是否被改变,我们也要把颜色设置成一个属性。重命名成首字母大写的Color,接着改成属性并使用私有的color变量。颜色的默认值是标准黑色,这里就不用再添加赋初始值的代码了。

HexCell.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//cell颜色
private Color color;

//cell颜色
public Color Color
{
get
{
return color;
}
set
{
//当新颜色与现在颜色相同时,不再进行赋值和刷新
if (color == value)
{
return;
}

color = value;

Refresh();
}
}

  回到Unity中运行,我们发现会报空引用异常,这是因为在把地图单元赋值给它所在的地图块之前,就设置了其默认的颜色和高度。最好的办法是在这里先不刷新,因为我们会在初始化完成之后三角化它们。换句话说就是只有在地图块被赋值完成后才进行刷新。代码如下:

HexCell.cs
1
2
3
4
5
6
7
private void Refresh()
{
if (chunk != null)
{
chunk.Refresh();
}
}

  现在又可以使用地图编辑器了,但是我们发现一个问题,就是在两个地图块的交界处,如果地图单元的颜色不是白色, 那就会产生很明显的一个边界。如下图:

  这个问题很好理解,因为一个地图单元发生变化后,所有与它相邻的地图单元也会发生改变,而这些相邻的地图单元有可能在不同的地图块中。最简单的解决方案是当地图单元与其相邻地图单元不在一个地图块时,也刷新一下相邻单元格的地图块。代码如下:

HexCell.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void Refresh()
{
if (chunk != null)
{
chunk.Refresh();

//遍历自身当前所有相邻的cell
for (int i = 0; i < neighbors.Length; i++)
{
HexCell neighbor = neighbors[i];

//当自身与相邻cell不在同一个chunk时,刷新相邻的chunk
if (neighbor != null && neighbor.chunk != chunk)
{
neighbor.chunk.Refresh();
}
}
}
}

  这样修改虽然视觉效果正确,但我们要刷新单个地图块多次,一旦我们在一次绘制横跨多个地图单元时,情况就更糟糕了。我们没必要在地图块刷新信息时直接三角化,我们可以通知这个地图块需要刷新,然后在编辑完成时一次性三角化。
  因为HexGridChunk没有用来做其它的事情,我们可以用脚本的enable状态作为需要刷新的信号,当开始刷新时,给脚本设置enable状态,就算多次设置也没关系,因为不会有变化。稍后脚本更新时,我们就在这里进行三角化,然后再次设置状态为disable。
  因我们使用LateUpdate,这样就能确保三角化发生在当前帧编辑完成之后。代码如下:

HexGridChunk.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void Refresh()
{
//hexMesh.Triangulate(cells);

//需要对当前Chunk刷新时,就启用当前脚本
enabled = true;
}

private void LateUpdate()
{
//完成三角构建后,就停用当前脚本,这样就不会发生重复刷新的问题了
hexMesh.Triangulate(cells);
enabled = false;
}

Update与LateUpdate有什么区别?
每一帧中, 所有enabled状态的组件中的Update会在随机时候调用。在这结束之后,LateUpdate方法也是同样的逻辑。所以这是两个更新步骤,一个早一些一个晚一些。Unity官方文档
Unity内脚本的生命周期如下图:

  因为脚本组件默认状态就是enabled,所以不再需要在Start里进行三角构建,现在就可以删掉这个方法了。

HexGridChunk.cs
1
2
3
4
5
6
//private void Start()
//{
// cell实例会由HexGrid创建
// 之后会将实例分配到各个HexGridChunk的数组中,这样再进行mesh的构建
// hexMesh.Triangulate(cells);
//}

  至此,我们就修复了地图编辑器的所有功能了,当我们编辑一个在地图块边缘的地图单元时,相邻地图块也会随之刷新。在接下来的章节中,我们会对现有代码进行一些优化。

Github代码