把正方形变成六边形。
三角化六边形网格。
使用立方体坐标。
与网格单元交互。
制作游戏内编辑器。
本教程是关于六边形贴图系列的第一部分。许多游戏使用六边形网格,尤其是战略游戏,包括奇迹时代3、文明5和无尽的传奇。我们将从基础开始,逐步添加功能,直到最终得到一个复杂的基于六边形网格的地形。
本教程假设您已完成“网格基础”系列,该系列从“Procedural Grid.”开始。它是根据Unity 5.3.1创建的。整个系列通过Unity的多个版本进行。最后一部分由Unity 2017.3.0p3制作。
一个基础的六边形地图
为什么使用六边形?如果你需要一个网格,只使用正方形是有意义的。正方形确实很容易绘制和定位,但它们也有缺点。看看网格中的一个正方形。然后看看与它相邻的网格。
一个正方形网格和与它相邻的网格
一共有八个邻居。穿过正方形网格的边缘可以到达四个。它们在水平方向和垂直方向相邻。穿过正方形网格的一角就可以到达另外四个。这些是对角邻居。
网格中相邻方形单元中心之间的距离是多少?如果边长度为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
要创建六边形网格,我们需要网格单元(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(cellPrefab);
cell.transform.SetParent(transform, false);
cell.transform.localPosition = position;
}
plane的正方形网格
这为我们提供了一个由无缝方形单元组成的漂亮网格。但哪个单元在哪里?当然,这对我们来说很容易检查,但六边形会变得更棘手。如果我们能同时看到所有的单元坐标,那就方便了。
通过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
关联文本预制
在绑定文本预置之后,我们可以实例化它们并显示单元坐标。在X和Z之间放置一个换行符,使它们在单独的行上结束。
void CreateCell (int x, int z, int i) {
…
Text label = Instantiate(cellLabelPrefab);
label.rectTransform.SetParent(gridCanvas.transform, false);
label.rectTransform.anchoredPosition =
new Vector2(position.x, position.z);
label.text = x.ToString() + "\n" + z.ToString();
}
显示坐标
现在我们可以直观地识别每个单元,让我们开始移动它们。我们知道,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
正确定位单元格后,我们可以继续显示实际六边形。我们必须首先摆脱平面,所以从单元预制中移除除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 vertices;
List triangles;
void Awake () {
GetComponent().mesh = hexMesh = new Mesh();
hexMesh.name = "Hex Mesh";
vertices = new List();
triangles = new List();
}
}
使用此组件为网格创建新的子对象。它将自动获得网格渲染器,但不会为其指定材质。因此,向其添加默认材质。
六边形mesh object
现在,HexGrid可以关联他的六边形Mesh,与查找画布的方式相同。
HexMesh hexMesh;
void Awake () {
gridCanvas = GetComponentInChildren
网格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
让我们在六边形网格的上下文中再次查看单元坐标。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(cellPrefab);
cell.transform.SetParent(transform, false);
cell.transform.localPosition = position;
cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z);
Text label = Instantiate(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
如果我们不能与六边形网格交互,它就不是很有趣。最基本的交互是触摸一个细胞,所以让我们添加对它的支持。现在,只需将此代码直接放在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().mesh = hexMesh = new Mesh();
meshCollider = gameObject.AddComponent();
…
}
完成三角剖分后,将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;
}
}
现在我们可以触摸到正确的细胞,是时候进行一些真正的互动了。让我们更改所点击的每个单元格的颜色。为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 colors;
void Awake () {
…
vertices = new List();
colors = new List();
…
}
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
现在我们知道了如何编辑颜色,让我们升级到一个简单的游戏编辑器。此功能超出了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填充面板。目前,我们不需要为一个花哨的用户界面而烦恼,只需要一个看起来足够好的手动设置。
每个颜色对应的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