[Catlike Coding][Hex Map 1] 创建六边形网格

把正方形变成六边形。

三角化六边形网格。

使用立方体坐标。

与网格单元交互。

制作游戏内编辑器。

本教程是关于六边形贴图系列的第一部分。许多游戏使用六边形网格,尤其是战略游戏,包括奇迹时代3、文明5和无尽的传奇。我们将从基础开始,逐步添加功能,直到最终得到一个复杂的基于六边形网格的地形。

本教程假设您已完成“网格基础”系列,该系列从“Procedural Grid.”开始。它是根据Unity 5.3.1创建的。整个系列通过Unity的多个版本进行。最后一部分由Unity 2017.3.0p3制作。

一个基础的六边形地图

1. 关于六边形

为什么使用六边形?如果你需要一个网格,只使用正方形是有意义的。正方形确实很容易绘制和定位,但它们也有缺点。看看网格中的一个正方形。然后看看与它相邻的网格。

 一个正方形网格和与它相邻的网格

一共有八个邻居。穿过正方形网格的边缘可以到达四个。它们在水平方向和垂直方向相邻。穿过正方形网格的一角就可以到达另外四个。这些是对角邻居。

网格中相邻方形单元中心之间的距离是多少?如果边长度为1,则水平和垂直邻域的距离为1。但对于对角线邻居来说,答案是√2.

这两种相邻方式之间的差异会导致一些问题。如果你使用离散运动,你如何处理对角线运动?你允许吗?你如何创造一个更有机的外观?不同的游戏使用不同的方法,有不同的优点和缺点。一种方法是根本不使用正方形网格,而是使用六边形。

  一个六边形网格和与它相邻的网格

与正方形相比,六边形只有六个邻居,而不是八个。所有这些邻居都是边缘邻居。没有对角邻居。所以只有一种邻居,它简化了很多事情。当然,六边形网格的构造不如正方形网格简单,但我们可以解决这个问题。

在开始之前,我们必须确定六边形的尺寸。让我们选择一个边缘长度是10的网格单元。因为六边形由六个等边三角形组成,所以从中心到任何一个角的距离也是10。这定义了六边形单元格的外半径。

 一个六边形的外半径和内半径

还有一个内半径,即从中心到每个边的距离。这个值很重要,因为到每个邻居中心的距离等于这个值的两倍。内径等于(√3)/2乘以外半径,所以边长是10时, 内半径是5√3。让我们将这些值放在一个静态类中,以便于使用。

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)
};

unitypackage

2. 网格创建

要创建六边形网格,我们需要网格单元(grid cell)。为此目的创建一个HexCell组件。暂时将其留空,因为我们尚未使用任何单元格数据。

using UnityEngine;
public class HexCell : MonoBehaviour { }

开始很简单,请创建一个默认plane对象,将HexCell组件添加到其中,然后将其转换为预设。

 使用Plane作为六边形单元预制

接下来是网格(grid)。创建具有公共宽度、高度和单元预制变量的简单构件。然后将带有此组件的游戏对象添加到场景中。

using UnityEngine;
public class HexGrid : MonoBehaviour {
    public int width = 6;
    public int height = 6;
    public HexCell cellPrefab;
}

  

让我们从创建一个规则的正方形网格开始,因为我们已经知道如何做到这一点。将单元格存储在一个数组中,以便我们以后可以访问它们。

由于默认平面为10乘10个单位,因此将每个单元偏移该数量。

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[i] = Instantiate<HexCell>(cellPrefab);
	cell.transform.SetParent(transform, false);
	cell.transform.localPosition = position;
}

 plane的正方形网格

这为我们提供了一个由无缝方形单元组成的漂亮网格。但哪个单元在哪里?当然,这对我们来说很容易检查,但六边形会变得更棘手。如果我们能同时看到所有的单元坐标,那就方便了。

2.1 显示坐标

通过GameObject/UI/canvas将画布添加到场景中,并使其成为网格对象的子对象。由于这是一个纯粹的信息画布,请删除其raycaster组件。您还可以删除自动添加到场景中的事件系统对象,因为我们还不需要它。

将Rendre Mode设置为World Space,并围绕X轴旋转90度,以便画布覆盖网格。将其pivot和position设置为零。给它一个轻微的Y轴方向上的偏移,使其内容显示在顶部。它的宽度和高度无关紧要,因为我们将自己定位它的内容。可以将它们设置为零,以消除场景视图中的大矩形。

最后,将 Canvas Scaler 的 Dynamic Pixels Per Unit 增加到10。这将确保文本对象使用合适的字体纹理分辨率。

 

六边形网格坐标

要显示坐标,请通过GameObject/UI/text创建一个文本对象,并将其转换为预设。确保其anchors和pivort居中,并将其大小设置为5 x 15。文本的对齐方式也应水平和垂直居中。将字体大小设置为4。最后,我们不需要默认文本,也不使用富文本。不管是否启用了Raycast目标,因为我们的画布无论如何都不会这样做。

 

 创建文本预制

现在,我们的网格需要了解画布和预制件。使用UnityEngine.UI添加;在脚本的顶部,可以方便地访问UnityEngine.UI.Text类型。文本预置需要一个公共变量,而画布可以通过调用getComponentChildren找到。

public Text cellLabelPrefab;
Canvas gridCanvas;
void Awake () {
    gridCanvas = GetComponentInChildren<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();
}

 显示坐标

2.2 六边形坐标

现在我们可以直观地识别每个单元,让我们开始移动它们。我们知道,X方向上相邻六边形单元之间的距离等于内径的两倍。让我们用这个。此外,到下一行单元格的距离应为外半径的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);

// position.x = x * HexMetrics.innerRadius * 2 + HexMetrics.innerRadius * z

规律的六边形位置产生菱形网格。

 当把cells放置在六边形的适当位置时,我们的网格现在填充的是菱形而不是矩形。因为使用矩形网格更方便,所以让我们强制单元格重新对齐。我们通过撤消部分偏移来实现这一点。每第二行,所有单元格都应后退一步。在相乘之前将Z的整数除法减去2就可以了。

position.x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f);

// position.x = x * HexMetrics.innerRadius * 2 + HexMetrics.innerRadius * (z % 2);

矩形区域中的六边形间距。

 unitypackage

3.渲染六边形

正确定位单元格后,我们可以继续显示实际六边形。我们必须首先摆脱平面,所以从单元预制中移除除HexCell 之外的所有组件。

 不再是Plane 

就像在 Mesh Basics 教程中一样,我们使用一个简单的 mesh渲染整个网格。但是,这次我们不打算预先确定需要多少顶点和三角面。我们将使用列表代替。

创建一个新的 HexMesh组件来处理mesh。它需要一个网格过滤器(Mesh Filter)和渲染器(Mesh Render),有一个网格,并有其顶点和三角面的列表。

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>();
	}
}

使用此组件为网格创建新的子对象。它将自动获得网格渲染器,但不会为其指定材质。因此,向其添加默认材质。

 

 六边形mesh object

现在,HexGrid可以关联他的六边形Mesh,与查找画布的方式相同。

HexMesh hexMesh;

void Awake () {
	gridCanvas = GetComponentInChildren<Canvas>();
	hexMesh = GetComponentInChildren<HexMesh>();
	
	…
}

网格Awake()后,它必须告诉网格对其单元进行三角切分。必须确保在六边形网格组件Awake之后也会对三角形切分。当Start稍后被调用时,让我们在那里执行它。

void Start () {
	hexMesh.Triangulate(cells);
}

这个hextmesh.Triangulate()方法可以在任何时候调用,即使单元格已经在前面进行了三角剖分。因此,我们应该从清除旧数据开始。然后循环遍历所有单元,分别对它们进行三角剖分。完成后,将生成的顶点和三角形指定给网格,并通过重新计算网格法线结束。

public void Triangulate (HexCell[] cells) {
	hexMesh.Clear();
	vertices.Clear();
	triangles.Clear();
	for (int i = 0; i < cells.Length; i++) {
		Triangulate(cells[i]);
	}
	hexMesh.vertices = vertices.ToArray();
	hexMesh.triangles = triangles.ToArray();
	hexMesh.RecalculateNormals();
}

void Triangulate (HexCell cell) {
}

由于六边形是由三角形构成的,所以让我们创建一个方便的方法来添加三角形,给定三个顶点位置。它只是按顺序添加顶点。它还添加这些顶点的索引以形成三角形。在添加新顶点之前,第一个顶点的索引等于顶点列表的长度。因此,在添加顶点之前请记住这一点。

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);
}

现在我们可以把我们的Cell三角化了。让我们从第一个三角形开始。它的第一个顶点是六边形的中心。其他两个顶点是相对于其中心的第一个和第二个角点。

void Triangulate (HexCell cell) {
	Vector3 center = cell.transform.localPosition;
	AddTriangle(
		center,
		center + HexMetrics.corners[0],
		center + HexMetrics.corners[1]
	);
}

 每个cell的第一个三角形

 它生效了,接着循环六次。

Vector3 center = cell.transform.localPosition;
for (int i = 0; i < 6; i++) {
	AddTriangle(
		center,
		center + HexMetrics.corners[i],
		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)
};

完整的六边形

 unitypackage

4. 六边形坐标

让我们在六边形网格的上下文中再次查看单元坐标。Z坐标看起来很好,但X坐标呈之字形。这是偏移行以覆盖矩形区域的副作用。

 偏移坐标,高亮显示零。

处理六边形时,这些偏移坐标不容易处理。让我们添加一个HexCoordinates结构,我们可以使用它转换为不同的坐标系。使其可序列化,以便Unity可以存储它,从而允许它们在播放模式下经受重新编译。另外,使用公共只读属性使这些坐标不可变。

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[i] = 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();

现在让我们确定这些X坐标,使它们沿直轴对齐。我们可以通过取消水平移动来实现这一点。结果通常称为轴向坐标。

	public static HexCoordinates FromOffsetCoordinates (int x, int z) {
		return new HexCoordinates(x - z / 2, z);
	}

 

 轴坐标

这个二维坐标系使我们能够一致地描述四个方向上的运动和偏移。然而,剩下的两个方向仍然需要特殊处理。这表明存在第三维度。事实上,如果我们水平翻转X维度,我们会得到缺失的Y维度。

 显示Y维度

由于这些X和Y尺寸相互镜像,如果保持Z恒定,将它们的坐标相加将始终产生相同的结果。事实上,如果你把三个坐标加在一起,你总是会得到零。如果增加一个坐标,则必须减少另一个坐标。事实上,这产生了六种可能的运动方向。这些坐标通常称为立方体坐标,因为它们是三维的,拓扑结构类似于立方体。

因为所有坐标加起来等于零,所以始终可以从其他两个坐标导出每个坐标。因为我们已经存储了X和Z坐标,所以不需要存储Y坐标。我们可以包括一个按需计算它的属性,并在string方法中使用它。

	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里显示坐标

在Play模式下选择一个网格单元。事实证明,Inspector里面没有显示其坐标。仅显示HexCell.coordinates的前缀标签。

 Inspector没有显示坐标

虽然这没什么大不了的,但如果坐标真的出现了,那就太好了。Unity当前不显示坐标,因为它们未标记为序列化字段。为此,我们必须为X和Z显式定义可序列化字段。

	[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类型定义自定义属性抽屉,我们可以做得更好。创建一个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());

 有标签的坐标。

unitypackage

5. 触碰单元格

如果我们不能与六边形网格交互,它就不是很有趣。最基本的交互是触摸一个细胞,所以让我们添加对它的支持。现在,只需将此代码直接放在HexGrid中。一旦一切正常,我们就把它搬到别的地方去。

要触摸细胞,我们可以从鼠标位置向场景中发射光线。我们可以使用与Mesh Deformation 教程中相同的方法。

	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>();
		…
	}

完成三角剖分后,将Mesh绑定给碰撞器。

	public void Triangulate (HexCell[] cells) {
		…
		meshCollider.sharedMesh = hexMesh;
	}

我们就不能用一个box collider吗?

我们可以,但它不能完全符合我们网格的轮廓。我们的网格也不会长时间保持平坦,尽管这是未来教程的一部分。

我们现在可以接触网格了!但我们接触的是哪个细胞?要知道这一点,我们必须将触摸位置转换为hex coordinates。这是一个用于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为零,这只能给我们正确的坐标。再一次,当我们沿着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);

结果看起来很有希望,但坐标正确吗?仔细研究会发现,我们有时会得到不等于零的坐标!发生这种情况时,让我们记录一个警告,以确保它确实发生。

		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;

 Cell颜色选择。

将公共颜色字段添加到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添加颜色信息。

	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();
		…
	}

在进行三角剖分时,我们现在还必须为每个三角形添加颜色数据。为此添加一个单独的方法。

	void Triangulate (HexCell cell) {
		Vector3 center = cell.transform.localPosition;
		for (int i = 0; i < 6; i++) {
			AddTriangle(
				center,
				center + HexMetrics.corners[i],
				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创建新的默认着色器。它只需要两个改变。首先,将颜色数据添加到其输入结构中。第二,用这个颜色乘以反照率。我们只关心RGB通道,因为我们的材质是不透明的。

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-轴抖动。调整平行光的阴影偏移应足以解决此问题。

unitypackage

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方法移动到那里。给它一个公共字段以引用六边形网格,一个颜色数组,以及一个私有字段以跟踪激活的颜色。最后,添加一个公共方法来选择颜色,并确保最初选择第一种颜色。

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];
	}
}

添加另一个Canvas,这次保留其默认设置。添加一个HexMapEditor组件,给它一些颜色,然后连接hex网格。这一次我们确实需要一个事件系统对象,它再次被自动创建。

有四种颜色的六边形编辑器

通过GameObject/UI/panel向画布添加一个面板以容纳颜色选择器。通过Components/UI/toggle group给它一个切换组。把它做成一个小面板,放在屏幕的一角。

 Color panel with toggle group.

 现在,通过GameObject/UI/toggle,用每种颜色的toggle填充面板。目前,我们不需要为一个花哨的用户界面而烦恼,只需要一个看起来足够好的手动设置。

ui

hierarchy

每个颜色对应的toggle 

确保仅启用了第一个toggle 。同时使它们都成为ToggleGroup 的一部分,以便同时只选择其中一个。最后,将它们连接到编辑器的SelectColor方法。您可以通过On Value Changed事件UI的加号按钮执行此操作。选择Hex Map Editor对象,然后从下拉列表中选择正确的方法。

 第一个toggle

此事件提供一个布尔参数,表明每次切换时切换是打开还是关闭。但我们不在乎这个。相反,我们必须手动提供一个整数参数,它对应于我们想要使用的颜色索引。因此,第一个Toggle将其设置为0,第二个Toggle将其设置为1,依此类推。

什么时候调用toggle事件方法?

每次toggle的状态更改时,它都会调用该方法。如果该方法有一个布尔参数,它将告诉我们toggle是打开还是关闭的。

由于我们的toggle是组的一部分,选择不同的toggle将首先关闭当前活动的toggle,然后打开选定的toggle。这意味着SelectColor将被调用两次。这没关系,因为第二次调用是我们关心的。

使用多种颜色涂色 

虽然UI功能正常,但有一个恼人的细节。要查看它,请移动面板,使其覆盖六边形网格。选择新颜色时,还将绘制UI下方的单元格。因此,我们同时与UI和十六进制网格交互。这是不可取的。

这可以通过询问事件系统是否检测到光标位于某个对象上方来解决。因为它只知道UI对象,这表明我们正在与UI交互。因此,只有在情况并非如此时,我们才应该自己处理输入。

using UnityEngine;
using UnityEngine.EventSystems;
	
	…
	
	void Update () {
		if (
			Input.GetMouseButton(0) &&
			!EventSystem.current.IsPointerOverGameObject()
		) {
			HandleInput();
		}
	}

下一个教程是 Blending Cell Colors.

unitypackage

PDF

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值