您需要 登录 才可以下载或查看,没有账号?注册
x
本帖最后由 成林 于 2018-5-16 20:43 编辑
从正方形到六边形 三角化六边形网格 使用立方体坐标 与网格单元相作用 制作一个游戏内部编辑器
此教程为六边形地图系列的第一篇。许多游戏使用六边形网格,尤其是一些策略游戏,例如奇迹时代3,文明5,以及无尽传奇。我们从基础开始,逐渐地加入不同的功能直到得到一个复杂的六边形地形。
此教程假设你已经完成了使用 程式化网格的Mesh Basic 系列。本教程使用Unity5.3.1。
一个基本的六边形地图
1 关于六边形 为什么使用六边形?如果你需要一个网格,使用正方形貌似很合理。正方形容易画而且容易设定位置,但是它们也有弊端。看看网格中央的正方形。然后看看它周围。 一个正方形和它的邻居 它总共有八个邻居。其中四个可以通过穿过正方形的边到达,其中有水平方向和垂直方向。另外四个可以通过穿过正方形的角到达,它们被称为对角邻居。
中间正方形和临近正方形单元的距离是多少呢?如果边长为1,那么对于水平邻居和垂直邻居答案就是1。但是对于对角邻居答案是 。
两种邻居的差别使得问题变得复杂。如果使用离散运动,你怎样处理对角线方向的运动呢?允许还是不允许对角线运动?如何看上去更合理?不同的游戏使用不同的方法,各有利弊。其中一个方法就是根本不使用正方形网格,而是使用六边形代替它。 一个六边形和它的邻居
和正方形相比,六边形只有六个邻居而不是八个。所有这些邻居都是边邻居。没有角邻居。只有一种邻居会简化许多问题。当然六边形网格要比正方形网格直观性差些,但是我们会处理好这个问题。
在开始前,我们需要确定我们的六边形的大小。我们设置边长为10个单位。因为一个六边形是由六个等边三角形得到的,它们外接于外接圆,所以从中心到任何一个角的距离也是10。这决定了六边形的外径。 一个六边形的外径和内径
它还有一个内径,即从中心到各边的距离。这个度量非常重要,因为相邻单元中心之间的距离都是这个值的二倍。内径等于 乘以外径,在我们的情况中结果为5 。让我们将这些度量值都放在一个静态类中以便于处理。 | using UnityEngine;
public static class HexMetrics {
public const float outerRadius = 10f;
public const float innerRadius = outerRadius * 0.866025404f;
}
|
如何推导出内径长度? 以六边形中六个三角形任意一个为例。内径是该三角形的高。我们通过将该三角形分为两个直角三角形,然后使用 毕达哥拉斯定理得到这个高。
既然我们都到这一步了,让我们也确定一下六个角相对于单元中心的位置。注意有两种方法可以为六边形定向。不是尖朝上就是边朝上。我们在顶部放置一个角。从这个角开始以顺时针方向加入剩余的部分。将它们放在XZ平面内,这样六边形就会与地面相平了。 可能的方向
| public static Vector3[] corners = {
new Vector3(0f, 0f, outerRadius),
new Vector3(innerRadius, 0f, 0.5f * outerRadius),
new Vector3(innerRadius, 0f, -0.5f * outerRadius),
new Vector3(0f, 0f, -outerRadius),
new Vector3(-innerRadius, 0f, -0.5f * outerRadius),
new Vector3(-innerRadius, 0f, 0.5f * outerRadius)
};
|
2 构建网格 想要构建一个六边形网格,我们需要网格单元。创建一个HexCell组件。因为我们还没有使用任何单元数据,先让它保持空白。 | using UnityEngine;
public class HexCell : MonoBehaviour {
}
|
开始很简单,创建一个默认平面对象,加入单元组件,将它转为一个预设体(prefab)。 使用平面作为六边形单元预设体
接下来是网格。创建一个简单的组件,包含公有的宽、高和单元预设体变量。然后在场景中加入一个拥有该组件的游戏对象。 | using UnityEngine;
public class HexGrid : MonoBehaviour {
public int width = 6;
public int height = 6;
public HexCell cellPrefab;
}
|
六边形网格对象
让我们从一个普通正方形网格开始构建,因为我们已经知道该怎样做了。将单元保存在数组中以便我们以后使用。 因为默认平面是10*10单位,将每个单元偏移那么多。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| HexCell[] cells;
void Awake () {
cells = new HexCell[height * width];
for (int z = 0, i = 0; z < height; z++) {
for (int x = 0; x < width; x++) {
CreateCell(x, z, i++);
}
}
}
void CreateCell (int x, int z, int i) {
Vector3 position;
position.x = x * 10f;
position.y = 0f;
position.z = z * 10f;
HexCell cell = cells = Instantiate<hexcell>(cellPrefab);
cell.transform.SetParent(transform, false);
cell.transform.localPosition = position;
}</hexcell>
|
平面的正方形网格
我们得到了非常好的无缝正方形单元。但是它们都在哪呢?当然我们很容易查到,但是对于六边形就比较难了。如果我们可以看到每个单元的坐标会很方便。
2.1 显示坐标 通过GameObject/UI/Canvas向场景中加入一个画布然后将它设置为你的网格对象的子级。因为这个画布只是为了提供信息的,所以我们移除它的射线组件。你还可以删除自动加入场景中的事件对象系统,因为我们不需要它。
将渲染模式设置为世界空间然后按X轴旋转90°以便让画布覆盖我们的网格。将它的支点和位置设为0。给它一个很小的垂直方向的偏移量,它的内容会出现在顶部。它的宽和高不重要,因为我们会手动调整它内容的位置。你可以将它们设置为0来去除掉场景视角中的大方块。
最后,将Dynamic Pixels PerUnit 增加到10。这可以保证文字对象使用合适的字体纹理分辨率。 六边形网格系统的画布
为了显示坐标,我们通过GameObject/UI/Text创建一个对象,然后将它转为一个预设体。确认它的支点和锚(anchor)都位于中心,然后将它们的大小设为5*15。文字在水平和垂直方向上也要对齐中心。将字体大小设置为4。最后,我们不需要默认的文字并且不使用Rich Text。Raycast Target开启与否并不重要,因为我们的画布不使用它。 单元标签预设体
现在我们的网格需要知道画布和预设体的信息。加入using UnityEngine.UI; 可以在脚本的顶部方便地处理UnityEngine.UI.Text类型。标签预设体需要一个公有变量,同时通过调用GetComponentInChildren我们可以找到画布。 | public Text cellLabelPrefab;
Canvas gridCanvas;
void Awake () {
gridCanvas = GetComponentInChildren<canvas>();
…
}
</canvas>
|
连接标签预设体
在将标签预设体设置完毕后,我们可以将它们实例化然后显示单元坐标了。在X和Z之间放置一个换行符,这样它们就能出现在不同行了。 | void CreateCell (int x, int z, int i) {
…
Text label = Instantiate<text>(cellLabelPrefab);
label.rectTransform.SetParent(gridCanvas.transform, false);
label.rectTransform.anchoredPosition =
new Vector2(position.x, position.z);
label.text = x.ToString() + "\n" + z.ToString();
}
</text>
|
可视化坐标
2.2 六边形的位置 由于我们可以在视觉上区分每个单元了,让我们开始移动它们。我们知道相邻六边形单元X方向的距离是内径的2倍。我们使用这一点。另外,到下一行的距离应该是1.5倍的外径。 六边形邻居的几何 | position.x = x * (HexMetrics.innerRadius * 2f);
position.y = 0f;
position.z = z * (HexMetrics.outerRadius * 1.5f);
|
使用六边形距离,没有偏移
当然每一行六边形不是在下一行的正上方。每一行沿着X方向都有一个内径大小的偏移。我们可以在乘以两倍内径之前加上Z到X距离的一半。
position.x = (x + z * 0.5f) * (HexMetrics.innerRadius* 2f); 正确的六边形距离产生一个菱形网格
虽然我们的单元处于正确的六边形位置,我们的网格现在是菱形而不是方形。因为方形网格更容易操作些,让我们将单元限制在行内。我们需要取消一部分的偏移。每隔一行所有的单元应该向后移动一格。在做乘法之前先减去Z除以2的商。
position.x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f); 方形区域内的六边形间隔
3 渲染六边形 在正确放置单元以后,我们可以接着显示真正的六边形了。我们需要先清空平面,所以首先从单元预设体中移除除了HexCell的所有的组件。 没有任何平面了
就像在 网格物体基础教程中一样,我们使用一个单独的网格物体去渲染整个网格。然而,这一次我们不提前决定需要多少顶点和三角。我们使用列表(list)来代替。
创建一个HexMesh组件来管理你的网格物体。它需要一个网格物体过滤器和一个渲染器,需要有一个网格物体,以及需要有网格物体顶点和三角的列表。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using UnityEngine;
using System.Collections.Generic;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class HexMesh : MonoBehaviour {
Mesh hexMesh;
List<vector3> vertices;
List<int> triangles;
void Awake () {
GetComponent<meshfilter>().mesh = hexMesh = new Mesh();
hexMesh.name = "Hex Mesh";
vertices = new List<vector3>();
triangles = new List<int>();
}
}</int></vector3></meshfilter></int></vector3>
|
为我们的网格创建一个使用该组件的新子对象。它会自动拥有一个网格物体渲染器,但是不会有材料分配给它。那么让我们为它加入默认的材料。 六边形网格物体对象
现在HexGrid可以得到它的六边形网格物体了,和它找到画布是同样的原理。 | HexMesh hexMesh;
void Awake () {
gridCanvas = GetComponentInChildren<canvas>();
hexMesh = GetComponentInChildren<hexmesh>();
…
}</hexmesh></canvas>
|
在网格被唤醒后,它需要让网格物体三角化它的单元。我们必须要确认这发生在六边形网格物体组件被唤醒之后。由于之后我们会调用Start,让我们在这里实现它。 | void Start () {
hexMesh.Triangulate(cells);
}
|
这个HexMesh.Triangulate方法可以在任何时候被调用,即使是单元之前已经被三角化。所以我们首先应该清除旧的数据。然后循环所有的单元,将它们分别三角化。这一步完成后,将产生的顶点和三角分配给网格物体,最后重新计算网格物体的法线。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public void Triangulate (HexCell[] cells) {
hexMesh.Clear();
vertices.Clear();
triangles.Clear();
for (int i = 0; i < cells.Length; i++) {
Triangulate(cells);
}
hexMesh.vertices = vertices.ToArray();
hexMesh.triangles = triangles.ToArray();
hexMesh.RecalculateNormals();
}
void Triangulate (HexCell cell) {
}
|
因为六边形是由三角形组成的,给定三个顶点位置,我们创造一个简便的方法去加一个三角。它仅仅按顺序将顶点相加。它还将这些顶点的索引(index)相加形成一个三角形。第一个顶点的索引在加入新顶点前等于顶点列表的长度。所以将顶点相加之前我们记录它。 | void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) {
int vertexIndex = vertices.Count;
vertices.Add(v1);
vertices.Add(v2);
vertices.Add(v3);
triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
}
|
现在我们可以三角化我们的单元了。让我们从第一个三角开始。它的第一个顶点是六边形的中心。另外两个顶点是相对于中心的第一个和第二个角。 | void Triangulate (HexCell cell) {
Vector3 center = cell.transform.localPosition;
AddTriangle(
center,
center + HexMetrics.corners[0],
center + HexMetrics.corners[1]
);
}
|
每一个单元的第一个三角
就是这样,接着循环所有六个三角。 | Vector3 center = cell.transform.localPosition;
for (int i = 0; i < 6; i++) {
AddTriangle(
center,
center + HexMetrics.corners,
center + HexMetrics.corners[i + 1]
);
}
|
为什么我们不能分享顶点呢? 我们能。实际上,我们可以做得更好,仅仅使用四个三角去渲染一个六边形,而不是使用六个。但是我们不这样做是为了让事情简单些。目前那是个好主意,因为在之后的教程中难度会逐渐增加。优化顶点和三角目前没有太大必要。
不幸的是,这会产生IndexOutOfRangeException。这是因为最后一个三角形试图找到不存在的第七个角。当然它应该返回来使用第一个角作为它最后的顶点。所以我们在HexMetrics.corners中复制第一个角,这样就不需要担心超出界限的问题了。 | public static Vector3[] corners = {
new Vector3(0f, 0f, outerRadius),
new Vector3(innerRadius, 0f, 0.5f * outerRadius),
new Vector3(innerRadius, 0f, -0.5f * outerRadius),
new Vector3(0f, 0f, -outerRadius),
new Vector3(-innerRadius, 0f, -0.5f * outerRadius),
new Vector3(-innerRadius, 0f, 0.5f * outerRadius),
new Vector3(0f, 0f, outerRadius)
};
|
完整的六边形
4 六边形坐标 让我们在六边形网格存在的情况下再看看我们的单元坐标。Z坐标还好,但是X坐标呈锯齿状分布。这是想要覆盖一个方形区域从而对行进行偏移带来的副作用。 偏移坐标,高亮了0行
使用这些偏移坐标处理六边形不太方便。让我们加入一个HexCoordinate结构体,我们可以使用它来转换成不同的坐标系统。将它设置为可序列化这样Unity可以存储它,这样它们可以在游戏模式中成功重编译。另外,通过使用公有的只读属性使这些坐标不能被改变。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| using UnityEngine;
[System.Serializable]
public struct HexCoordinates {
public int X { get; private set; }
public int Z { get; private set; }
public HexCoordinates (int x, int z) {
X = x;
Z = z;
}
}
|
加入一个静态方法来创造一个使用普通偏移坐标的坐标集。现在只需逐个复制这些坐标。 | public static HexCoordinates FromOffsetCoordinates (int x, int z) {
return new HexCoordinates(x, z);
}
|
再加入方便的字符串转换方法。默认的ToString方法返回结构体的类型名字,这没什么用。将它覆盖返回某一行的坐标。另外加入一个将坐标放在不同行上的方法,和我们之前使用的格式相同。 | public override string ToString () {
return "(" + X.ToString() + ", " + Z.ToString() + ")";
}
public string ToStringOnSeparateLines () {
return X.ToString() + "\n" + Z.ToString();
}
|
现在我们向HexCell组件加入一个坐标集。 public class HexCell : MonoBehaviour { public HexCoordinates coordinates;}
调整HexGrid.CreateCell来配合新坐标。 | HexCell cell = cells = Instantiate<hexcell>(cellPrefab);
cell.transform.SetParent(transform, false);
cell.transform.localPosition = position;
cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z);
Text label = Instantiate<text>(cellLabelPrefab);
label.rectTransform.SetParent(gridCanvas.transform, false);
label.rectTransform.anchoredPosition =
new Vector2(position.x, position.z);
label.text = cell.coordinates.ToStringOnSeparateLines();</text></hexcell>
|
现在让我们修正那些X坐标让它们沿直线排开。想要这样我们可以取消水平调整。结果我们得到了轴坐标。 public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x - z / 2, z); } 轴坐标
这个二维坐标系统可以使我们一致地描述四个方向上的运动和偏移。然而两个剩下的方向仍需要特殊对待。这意味着还有第三个维度。事实上,如果我们想要在水平方向上翻转X维度,我们会得到丢失的Y维度。
由于这些X和Y维度彼此对称,如果保持Z不变,将它们坐标相加总会产生相同的结果。实际上,如果你将三个坐标加起来你总会得到0。如果一个坐标增加了,你必须要减少一个坐标。实际上,这会产生六个可能的运动方向。这些坐标被称为立方体坐标,因为它们是三维的且形成的拓扑类似一个立方体。
因为所有的坐标相加得0,你总可以从两个坐标推导出另外一个。因为我们已经存储了X和Z坐标,所以不需要存储Y坐标了。我们可以包含一个变量用来在需要的时候计算它,然后在字符串方法中使用它。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public int Y {
get {
return -X - Z;
}
}
public override string ToString () {
return "(" +
X.ToString() + ", " + Y.ToString() + ", " + Z.ToString() + ")";
}
public string ToStringOnSeparateLines () {
return X.ToString() + "\n" + Y.ToString() + "\n" + Z.ToString();
}
|
立方体坐标
4.1 审查器(Inspector)中的坐标 在游戏模式中选择一个网格单元。我们会发现审查器不显示它的坐标。只显示HexCell.coordinates的前标。 审查器不显示坐标
虽然这不是大问题,但如果能显示坐标的话看上去会很工整。Unity目前不显示坐标因为它们不被标为序列化的域。为了显示坐标,我们必须显式为X和Z定义序列化的域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| [SerializeField]
private int x, z;
public int X {
get {
return x;
}
}
public int Z {
get {
return z;
}
}
public HexCoordinates (int x, int z) {
this.x = x;
this.z = z;
}
|
看上去丑陋并且可以被编辑
X和Z轴现在显示出来了,但它们可以被编辑。我们不希望这样,因为坐标应该是固定的。另外显示在下方看上去很丑陋。
我们可以为HexCoordinates类型定义一个自定义性质绘制器(custom property drawer)。创建一个HexCoordinatesDrawer脚本然后将它放置在编辑器文件夹中,因为它是只在编辑器中运行的脚本。 这个类需要拓展UnityEditor.PropertyDrawer并需要UnityEditor.CustomPropertyDrawer性质将它和正确的类型联系起来。 using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(HexCoordinates))]public class HexCoordinatesDrawer :PropertyDrawer {}
性质绘制器通过OnGUI方法渲染它们的内容。屏幕中的方形提供这个方法来绘制内部内容、性质的序列化数据以及它属于的域的标签。 public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { }
将x和z值从性质中提取出来,使用它们来创造一个新的坐标集。然后在特定地点使用HexCoordinates.ToString方法绘制一个GUI标签。 | public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
HexCoordinates coordinates = new HexCoordinates(
property.FindPropertyRelative("x").intValue,
property.FindPropertyRelative("z").intValue
);
GUI.Label(position, coordinates.ToString());
}
|
没有前标的坐标
我们的坐标显示出了,但是却没有显示域名。通常这些名字是由EditorGUI.PrefixLabel方法绘制的。除此之外,它返回一个调整过的长方形,与标签右侧的空白相匹配。
position = EditorGUI.PrefixLabel(position, label); GUI.Label(position,coordinates.ToString()); 拥有标签的坐标
5 触碰单元 如果我们不能与六边形网格互动那么就很没意思了。最基本的互动是触碰单元,所以让我们加入这个功能。目前只需将这个代码放在HexGrid中。以后我们会将它移动到其他位置。
我们可以使用从鼠标位置向场景发射射线的方法来触碰一个单元。我们可以使用和 网格物体变形教程中相同的方法。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| void Update () {
if (Input.GetMouseButton(0)) {
HandleInput();
}
}
void HandleInput () {
Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(inputRay, out hit)) {
TouchCell(hit.point);
}
}
void TouchCell (Vector3 position) {
position = transform.InverseTransformPoint(position);
Debug.Log("touched at " + position);
}
|
这还什么都没做。我们需要向网格加入一个碰撞器,这样射线就有东西可碰撞了。所以向HexMesh加入一个网格物体碰撞器。 | MeshCollider meshCollider;
void Awake () {
GetComponent<meshfilter>().mesh = hexMesh = new Mesh();
meshCollider = gameObject.AddComponent<meshcollider>();
…
}</meshcollider></meshfilter>
|
在完成三角化之后将我们的网格物体设置给碰撞器。
public void Triangulate (HexCell[] cells) { … meshCollider.sharedMesh = hexMesh; }
为什么我们不可以使用一个长方体碰撞器呢? 我们可以,但是它与我们网格的外形并不相称。而且我们的网格不会一直那么宽,尽管这是后话了。
我们现在可以触碰网格了!但是我们在触碰哪一个网格呢?为了知道这一点,我们需要将触碰位置转换为六边形坐标。这是HexCoordinates的工作,所以让我们在其中声明一个静态的FromPosition方法。
public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); Debug.Log("touched at " + coordinates.ToString()); }
这个方法如何计算出哪个坐标属于某一位置呢?首先将x除以六边形的水平宽。因为Y坐标与X坐标相对陈,x的负数就是y。
public static HexCoordinates FromPosition (Vector3 position) { float x = position.x / (HexMetrics.innerRadius * 2f); float y = -x; }
但是只有Z为0的时候我们会得到正确的坐标。沿着Z运动我们需要再一次偏移。每隔两行需要向左偏移一整个单位。 float offset = position.z / (HexMetrics.outerRadius * 3f); x -= offset; y -= offset;
现在我们的x和y值作为整数出现在每一单元的中央了。所以通过对它们取整我们应该得到坐标。我们还可以推导出Z坐标,然后构建最后的坐标。 int iX = Mathf.RoundToInt(x); int iY = Mathf.RoundToInt(y); int iZ = Mathf.RoundToInt(-x -y); return new HexCoordinates(iX, iZ);
结果看上去不错,但是坐标正确吗?仔细思考我们会发现有些坐标加起来不等于0!每当这发生时我们记录一个警告来确保这真实发生了。 if (iX + iY + iZ != 0) { Debug.LogWarning("rounding error!"); } return new HexCoordinates(iX, iZ);
事实上,我们会得到警告,那么我们如何解决这个问题呢?问题貌似只会出现在靠近六边形相邻边的位置。所以是对坐标的取整导致了问题。哪个坐标取整错误呢?嗯,你离一个单元的中心距离越远,取整的幅度越大。所以应该假设取整幅度最大的坐标是错误的。
解法就变成了要摒弃取整幅度最大的坐标,然后从另外两个重新计算它。但是由于我们只需要X和Z,我们不需要重建Y。 if (iX + iY + iZ != 0) { float dX = Mathf.Abs(x - iX); float dY = Mathf.Abs(y - iY); float dZ = Mathf.Abs(-x -y - iZ); if (dX > dY && dX > dZ) { iX = -iY - iZ; } else if (dZ > dY) { iZ = -iX - iY; } }
5.1 为六边形着色 现在我们可以触碰正确的单元了,是时候做些真正的互动了。让我们每一次触碰单元的时候都会改变它的颜色。为HexGrid添加一个可配置的默认状态的和触碰时的颜色。
public Color defaultColor = Color.white; public Color touchedColor = Color.magenta; 单元颜色选择
向HexCell添加一个公有颜色域。
public class HexCell : MonoBehaviour { public HexCoordinates coordinates; public Color color;}
在HexGrid.CreateCell中将默认颜色分配给它。
void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.color = defaultColor; … }
我们还要为HexMesh添加一个颜色信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| List<color> colors;
void Awake () {
…
vertices = new List<vector3>();
colors = new List<color>();
…
}
public void Triangulate (HexCell[] cells) {
hexMesh.Clear();
vertices.Clear();
colors.Clear();
…
hexMesh.vertices = vertices.ToArray();
hexMesh.colors = colors.ToArray();
…
}</color></vector3></color>
|
当三角化时,我们必须为每一个三角加入颜色数据。为此添加一个新的方法。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| void Triangulate (HexCell cell) {
Vector3 center = cell.transform.localPosition;
for (int i = 0; i < 6; i++) {
AddTriangle(
center,
center + HexMetrics.corners,
center + HexMetrics.corners[i + 1]
);
AddTriangleColor(cell.color);
}
}
void AddTriangleColor (Color color) {
colors.Add(color);
colors.Add(color);
colors.Add(color);
}
|
回到HexGrid.TouchCell。首先将单元坐标转换为合适的数组索引。对于一个正方形网格就是X加Z乘以宽度,但是在我们的情况中我们还需要加入半-Z偏移量。然后得到单元,改变它的颜色,然后再一次三角化网格物体。
我们真的需要再一次三角化整个网格物体吗? 我们可以聪明点,但是现在不是进行此类优化的时间。在后面的教程中网格物体会变得越来越复杂。现在所做的任何假设和捷径在后面都会无效。这个简单粗暴的方法总是管用的。 | public void TouchCell (Vector3 position) {
position = transform.InverseTransformPoint(position);
HexCoordinates coordinates = HexCoordinates.FromPosition(position);
int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2;
HexCell cell = cells[index];
cell.color = touchedColor;
hexMesh.Triangulate(cells);
}
|
尽管我们现在可以为单元着色了,我们看不见任何视觉的改变。这是因为默认的着色器没有使用顶点颜色。我们需要自己创建。通过Assets/Create/Shader/Default Surface Shader创建一个新的默认着色器。它仅需要两处改变。首先,在它的输入结构体中加入颜色数据。第二,将albedo乘以这个颜色。我们只在乎RGB频道,因为我们的材料是不透明的。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| Shader "Custom/VertexColors" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
float4 color : COLOR;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb * IN.color;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
|
创建一个使用该着色器的新材料,然后确认网格物体使用该材料。这样单元的颜色就可以显示出来了。 着色的单元
我得到了奇怪的阴影效果 在一些Unity版本中自定义表面着色器会产生阴影问题。如果你的阴影抖动或者呈带状,那么需要做些Z方向的调整了。调整阴影定向光的阴影偏向足够解决该问题了。
6 地图编辑器 现在我们知道如何编辑颜色了,让我们进一步制作一个游戏内的编辑器。这个功能超出了HexGrid的范围,所以将TouchCell变为一个含有额外颜色参数的公有方法。另外将touchedColor域移除。 | public void ColorCell (Vector3 position, Color color) {
position = transform.InverseTransformPoint(position);
HexCoordinates coordinates = HexCoordinates.FromPosition(position);
int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2;
HexCell cell = cells[index];
cell.color = color;
hexMesh.Triangulate(cells);
}
|
创建一个HexMapEditor组件然后将Update和HandleInput方法移动到那。加入一个公有域来引用一个六边形网格,一个颜色数组,以及一个私有域去记录可用的颜色。最后,加入一个选择颜色的公有方法,并确认初始下选择第一个颜色。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| using UnityEngine;
public class HexMapEditor : MonoBehaviour {
public Color[] colors;
public HexGrid hexGrid;
private Color activeColor;
void Awake () {
SelectColor(0);
}
void Update () {
if (Input.GetMouseButton(0)) {
HandleInput();
}
}
void HandleInput () {
Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(inputRay, out hit)) {
hexGrid.ColorCell(hit.point, activeColor);
}
}
public void SelectColor (int index) {
activeColor = colors[index];
}
}
|
加入另一个画布,这次保留它的默认值。加入一个HexMapEditor组件,加入一些颜色,然后与六边形网格相关联。这一次我们需要一个事件系统对象,它已经自动生成了。 拥有四种颜色的六边形地图编辑器
通过GameObject/UI/Panel向画布加入一个面板来显示颜色选择器。通过Components/UI/Toggle Group为它添加一个切换组。将它设置为一个小的面板,放置在屏幕的角落处。 拥有切换组的颜色面板
现在通过GameObject/UI/Toggle在面板中加入每种颜色的切换选项。我们现在不需要考虑UI的美化,仅仅一个手动的设置就已经很好了。 每种颜色一个切换选项
要确认只有第一个切换选项是打开的。另外将它们都设为切换组的一部分,所以同一时间只能选择其中一个。最后将它们和我们编辑器的SelectColor方法联系起来。你可以选择On Value Changed 事件UI中的加号按钮。选择六边形地图编辑器对象,然后从下拉列表中选择正确的方法。 第一个切换选项
这个事件提供了一个布尔型参数,它指明每一次变化时切换是打开的还是关闭的。但是我们不考虑那些。我们只需手动提供一个整形参数,它对应着我们想要使用的颜色的索引。所以第一个切换选项保持为0,第二个设为1,等等。
切换事件方法什么时候被调用? 每次切换状态改变时,该方法会被调用。如果方法有一个布尔型参数,它会告诉我们切换是打开还是关闭的。 由于我们的切换选项是组的一部分,选择一个不同的选项会首先将目前打开的选项关闭,然后打开选择的切换选项。这意味着SelectColor会被调用两次。这没问题,因为我们只在乎第二次调用。 使用多种颜色着色
虽然UI是功能性的,有一个令人讨厌的细节问题。想要查看它,移动画布至覆盖六边形网格。当选择一个新颜色时,在UI下面的单元也会被着色。所以我们和两个UI、两个六边形都在互动。这不是我们想要的。
我们可以通过询问事件系统是否检测到鼠标在某对象上来修正这个问题。由于它只知道UI对象,这意味着我们正和UI互动。所以只有当此情况没发生的时候我们才需手动处理输入。 [size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
[size=1em]7
[size=1em]8
[size=1em]9
[size=1em]10
[size=1em]11
[size=1em]12
[size=1em]13
| [size=1em][size=1em]using UnityEngine;
[size=1em]using UnityEngine.EventSystems;
[size=1em]
[size=1em] …
[size=1em]
[size=1em] void Update () {
[size=1em] if (
[size=1em] Input.GetMouseButton(0) &&
[size=1em] !EventSystem.current.IsPointerOverGameObject()
[size=1em] ) {
[size=1em] HandleInput();
[size=1em] }
[size=1em] }
|
|