using UnityEngine; public static class HexMetrics { public const float outerRadius = 10f; public const float innerRadius = outerRadius * 0.866025404f; }
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) };
HexCell
component. For now, let's leave it empty, because we are not using any of these cells yet. using UnityEngine; public class HexCell : MonoBehaviour { }
using UnityEngine; public class HexGrid : MonoBehaviour { public int width = 6; public int height = 6; public HexCell cellPrefab; }
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; }
using UnityEngine.UI;
to conveniently access the UnityEngine.UI.Text
type. The prefab label needs a shared variable, and the canvas can be found by calling GetComponentInChildren
. public Text cellLabelPrefab; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); … }
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(); }
position.x = x * (HexMetrics.innerRadius * 2f); position.y = 0f; position.z = z * (HexMetrics.outerRadius * 1.5f);
position.x = (x + z * 0.5f) * (HexMetrics.innerRadius * 2f);
position.x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f);
HexCell
except HexCell
.HexMesh
component that will deal with our mesh. It will require a mesh filter and renderer, it has a mesh and lists for vertices and triangles. 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>(); } }
HexGrid
can get its mesh of hexagons in the same way that it found the canvas. HexMesh hexMesh; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); … }
Start
is called later, we insert the appropriate code there. void Start () { hexMesh.Triangulate(cells); }
HexMesh.Triangulate
method can be called at any time, even if the cells have already been triangulated. Therefore, we should start by clearing old data. When traversing a loop through all cells, we triangulate them separately. After completing this operation, let's assign the generated vertices and triangles to the mesh, and finish by recalculating the normals of the mesh. 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); }
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[i], center + HexMetrics.corners[i + 1] ); }
IndexOutOfRangeException
. This is because the last triangle is trying to get the seventh corner, which does not exist. Of course, he must go back and use as the last vertex of the first corner. Or we can duplicate the first corner in HexMetrics.corners
, so as not to go beyond the boundaries. 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) };
HexCoordinates
, which can be used to convert to another coordinate system. Let's make it serializable so that Unity can store it and it experienced recompilation in Play mode. We will also make these coordinates immutable, using the public readonly properties. 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
method by default returns the name of the type struct, which is not very useful to us. Redefine it so that it returns the coordinates on one line. We will also add a method for outputting coordinates to individual lines, because we already use such a scheme. public override string ToString () { return "(" + X.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Z.ToString(); }
HexCell
component. public class HexCell : MonoBehaviour { public HexCoordinates coordinates; }
HexGrid.CreateCell
so that he can use the new coordinates. 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();
public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x - z / 2, z); }
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(); }
HexCell.coordinates
. [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; }
HexCoordinates
type. Create a HexCoordinatesDrawer
script and paste it into the Editor folder, because this is an editor-only script.UnityEditor.PropertyDrawer
and it requires the UnityEditor.CustomPropertyDrawer
attribute to associate it with the appropriate type. using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(HexCoordinates))] public class HexCoordinatesDrawer : PropertyDrawer { }
OnGUI
method. This method allowed us to draw inside the screen rectangle the serializable property data and the label of the field to which they belong. public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { }
HexCoordinates.ToString
method. 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
method. As a bonus, it returns an aligned rectangle that corresponds to the space to the right of this label. position = EditorGUI.PrefixLabel(position, label); GUI.Label(position, coordinates.ToString());
HexGrid
. When he starts working, we will move it to another place. 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
collider mesh. MeshCollider meshCollider; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); … }
public void Triangulate (HexCell[] cells) { … meshCollider.sharedMesh = hexMesh; }
HexCoordinates
, so HexCoordinates
's declare that it has a static FromPosition
method. public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); Debug.Log("touched at " + coordinates.ToString()); }
public static HexCoordinates FromPosition (Vector3 position) { float x = position.x / (HexMetrics.innerRadius * 2f); float y = -x; }
float offset = position.z / (HexMetrics.outerRadius * 3f); x -= offset; y -= offset;
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);
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
custom cell colors to the default and the affected cell. public Color defaultColor = Color.white; public Color touchedColor = Color.magenta;
HexCell
general field of color. public class HexCell : MonoBehaviour { public HexCoordinates coordinates; public Color color; }
HexGrid.CreateCell
to the default color. void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.color = defaultColor; … }
HexMesh
the color information. 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
. First, we transform the coordinates of the cell into the corresponding array index. For a square grid, this would simply be X plus Z multiplied by the width, but in our case we will have to add an offset to half Z. Then we take a cell, change its color and again triangulate the mesh. 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); }
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 = ca; } ENDCG } FallBack "Diffuse" }
HexGrid
, so we will turn it TouchCell
into a general method with an additional color parameter. Also remove the field 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
and move the methods Update
and into it HandleInput
. Add a common field to it to reference the hexagonal grid, the array of colors, and the private field to track the active color. Finally, we add a general method for choosing a color and force it to initially select the first color. 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
, set several colors and connect to the grid of hexagons. This time we need the event system object, and it was automatically created again.SelectColor
our editor. This can be done with the "+" UI event of On Value Changed . Select the map editor object, then select the desired method from the drop-down list.SelectColor
. , . using UnityEngine; using UnityEngine.EventSystems; … void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } }
public enum HexDirection { NE, E, SE, SW, W, NW }
enum
, . . , . , .HexCell
array. Although we can make it general, we will instead make it private and will provide access to methods using directions. We also make it serializable, so that the links are not lost during recompilation. [SerializeField] HexCell[] neighbors;
public HexCell GetNeighbor (HexDirection direction) { return neighbors[(int)direction]; }
public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; }
public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; cell.neighbors[(int)direction.Opposite()] = this; }
HexDirection
. To get the opposite direction you need to add to the original 3. However, this works only for the first three directions, for the rest you have to subtract 3. public enum HexDirection { NE, E, SE, SW, W, NW } public static class HexDirectionExtensions { public static HexDirection Opposite (this HexDirection direction) { return (int)direction < 3 ? (direction + 3) : (direction - 3); } }
this
. , .HexGrid.CreateCell
. When traversing cells line by line, from left to right, we know which cells have already been created. These are the cells with which we can connect. void CreateCell (int x, int z, int i) { … cell.color = defaultColor; if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } Text label = Instantiate<Text>(cellLabelPrefab); … }
if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } if (z > 0) { }
if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); } }
&&
— , &
— . , . 1, 1. , 10101010 & 00001111
00001010
. if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - width - 1]); } } }
if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - width - 1]); } } else { cell.SetNeighbor(HexDirection.SW, cells[i - width]); if (x < width - 1) { cell.SetNeighbor(HexDirection.SE, cells[i - width + 1]); } } }
void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } } void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[(int)direction], center + HexMetrics.corners[(int)direction + 1] ); AddTriangleColor(cell.color); }
AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) );
HexMetrics
two static methods. As a bonus, this allows us to make the array of corners private. 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) }; public static Vector3 GetFirstCorner (HexDirection direction) { return corners[(int)direction]; } public static Vector3 GetSecondCorner (HexDirection direction) { return corners[(int)direction + 1]; }
HexMesh.AddTriangleColor
has only one color argument. It can create a triangle with only solid color. Let's create an alternative that supports separate colors for each vertex. void AddTriangleColor (Color c1, Color c2, Color c3) { colors.Add(c1); colors.Add(c2); colors.Add(c3); }
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) ); HexCell neighbor = cell.GetNeighbor(direction); AddTriangleColor(cell.color, neighbor.color, neighbor.color); }
NullReferenceException
, because the cells on the border do not have six neighbors. What should we do if there is a shortage of a neighbor? Let's be pragmatic and use the cell itself as a replacement. HexCell neighbor = cell.GetNeighbor(direction) ?? cell;
a ?? b
— a != null ? a : b
.null
. . HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Color edgeColor = (cell.color + neighbor.color) * 0.5f; AddTriangleColor(cell.color, edgeColor, edgeColor);
HexDirectionExtensions
two addition methods for easy transition to the previous and next directions. public static HexDirection Previous (this HexDirection direction) { return direction == HexDirection.NE ? HexDirection.NW : (direction - 1); } public static HexDirection Next (this HexDirection direction) { return direction == HexDirection.NW ? HexDirection.NE : (direction + 1); }
HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f );
public const float solidFactor = 0.75f; public const float blendFactor = 1f - solidFactor;
public static Vector3 GetFirstSolidCorner (HexDirection direction) { return corners[(int)direction] * solidFactor; } public static Vector3 GetSecondSolidCorner (HexDirection direction) { return corners[(int)direction + 1] * solidFactor; }
HexMesh.Triangulate
it to use these solid fill corners instead of the original angles. For now, the colors are the same. AddTriangle( center, center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) );
void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3); } void AddQuadColor (Color c1, Color c2, Color c3, Color c4) { colors.Add(c1); colors.Add(c2); colors.Add(c3); colors.Add(c4); }
HexMesh.Triangulate
triangle to receive one color, and the quadrilateral to mix between solid color and the colors of the two corners. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); Vector3 v3 = center + HexMetrics.GetFirstCorner(direction); Vector3 v4 = center + HexMetrics.GetSecondCorner(direction); AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor( cell.color, cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); }
v3
and v4
, starting with v1
and v2
, and then moving along the bridge directly to the edge of the cell. What will be the displacement of the bridge? We can find it by taking the midpoint between the two corresponding angles, and then applying a mixing factor to it. This will do HexMetrics
. public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * 0.5f * blendFactor; }
HexMesh
, it will now be logical to add an option AddQuadColor
that needs only two colors. void AddQuadColor (Color c1, Color c2) { colors.Add(c1); colors.Add(c1); colors.Add(c2); colors.Add(c2); }
Triangulate
so that it creates correctly mixing bridges between neighbors. Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor(cell.color, (cell.color + neighbor.color) * 0.5f);
Color bridgeColor = (cell.color + neighbor.color) * 0.5f; AddQuadColor(cell.color, bridgeColor); AddTriangle(v1, center + HexMetrics.GetFirstCorner(direction), v3); AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, bridgeColor );
AddTriangle(v2, v4, center + HexMetrics.GetSecondCorner(direction)); AddTriangleColor( cell.color, bridgeColor, (cell.color + neighbor.color + nextNeighbor.color) / 3f );
HexMetrics.GetBridge
. Instead, we simply add them, and then multiply by the blending factor. public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * blendFactor; }
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); TriangulateConnection(direction, cell, v1, v2); } void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); }
if (direction == HexDirection.NE) { TriangulateConnection(direction, cell, v1, v2); }
if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); }
TriangulateConnection
when neighbors are absent. That is, we no longer need to replace the missing neighbors with the cell itself. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } … }
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { AddTriangle(v2, v4, v2); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } }
v2
, but this is obviously not true. Since each edge of these triangles is connected to a bridge, we can find it by passing over the bridge to the next neighbor. AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next()));
if (direction <= HexDirection.E && nextNeighbor != null) { AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next())); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); }
HexCell
. public int elevation;
HexMetrics
. We will use a step of five units so that the transitions are clearly visible. In a real game, I would use a smaller step. public const float elevationStep = 5f;
HexGrid.ColorCell
is not enough for us. In addition, in the future we can add other cell editing options, so we need a new approach.ColorCell
to GetCell
and make it so that instead of setting the color of the cell, it returns the cell in the specified position. Since this method does not change anything else, we need to immediately triangulate the cells. public HexCell GetCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; return cells[index]; }
HexGrid.Refresh
. public void Refresh () { hexMesh.Triangulate(cells); }
HexMapEditor
it so that it can work with new methods. We give it a new method EditCell
that will take care of all the changes in the cell, after which it will update the grid. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCell(hexGrid.GetCell(hit.point)); } } void EditCell (HexCell cell) { cell.color = activeColor; hexGrid.Refresh(); }
int activeElevation; void EditCell (HexCell cell) { cell.color = activeColor; cell.elevation = activeElevation; hexGrid.Refresh(); }
public void SetElevation (float elevation) { activeElevation = (int)elevation; }
SetElevation
of the Hex Map Editor object . The method must be selected from the dynamic list so that it is called with the slider value.HexCell.elevation
private and add a common property HexCell.Elevation
. public int Elevation { get { return elevation; } set { elevation = value; } } int elevation;
set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; }
HexMapEditor.EditCell
. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation; hexGrid.Refresh(); }
HexCell
link to RectTransform
its UI tags so that you can update it later. public RectTransform uiRect;
HexGrid.CreateCell
. void CreateCell (int x, int z, int i) { … cell.uiRect = label.rectTransform; }
HexCell.Elevation
so that it also changes the position of the UI cell. Since the canvas of the grid of hexagons is rotated, the labels must be moved in the negative direction along the Z axis, and not in the positive direction of the Y axis. set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = elevation * -HexMetrics.elevationStep; uiRect.localPosition = uiPosition; }
HexMesh.TriangulateConnection
. In the case of edge connections, we need to redefine the height of the other end of the bridge. Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep;
if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; AddTriangle(v2, v4, v5); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); }
HexMetrics
and calculate the number of stages based on this. public const int terracesPerSlope = 2; public const int terraceSteps = terracesPerSlope * 2 + 1;
HexMetrics
. public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { return a; }
public const float horizontalTerraceStepSize = 1f / terraceSteps; public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; return a; }
public const float verticalTerraceStepSize = 1f / (terracesPerSlope + 1); public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; float v = ((step + 1) / 2) * HexMetrics.verticalTerraceStepSize; ay += (by - ay) * v; return a; }
public static Color TerraceLerp (Color a, Color b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; return Color.Lerp(a, b, h); }
HexMesh.TriangulateConnection
and place it in a separate method. In the comments I will save the source code to refer to it in the future. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep; TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); // AddQuad(v1, v2, v3, v4); // AddQuadColor(cell.color, neighbor.color); … } void TriangulateEdgeTerraces ( Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft, Vector3 endRight, HexCell endCell ) { AddQuad(beginLeft, beginRight, endLeft, endRight); AddQuadColor(beginCell.color, endCell.color); }
void TriangulateEdgeTerraces ( Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft, Vector3 endRight, HexCell endCell ) { Vector3 v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, 1); Vector3 v4 = HexMetrics.TerraceLerp(beginRight, endRight, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); }
AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color);
AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c2; v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, i); v4 = HexMetrics.TerraceLerp(beginRight, endRight, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2); } AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color);
HexMetrics.terracesPerSlope
. Of course, until we created the ledges for the corner joints, we will leave this for later. public enum HexEdgeType { Flat, Slope, Cliff }
HexMetrics
method using two levels of height. public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { }
public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } }
public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } int delta = elevation2 - elevation1; if (delta == 1 || delta == -1) { return HexEdgeType.Slope; } return HexEdgeType.Cliff; }
HexCell.GetEdgeType
to get the type of cell edge in a certain direction. public HexEdgeType GetEdgeType (HexDirection direction) { return HexMetrics.GetEdgeType( elevation, neighbors[(int)direction].elevation ); }
NullReferenceException
. , , - . , . .NullReferenceException
.HexMesh.TriangulateConnection
so that it creates ledges only for slopes. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } // AddQuad(v1, v2, v3, v4); // AddQuadColor(cell.color, neighbor.color);
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); }
HexMesh
triangulation to the new method. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); }
TriangulateConnection
have to determine which of the cells is the lowest. First we check whether the triangulated cell is below its neighbors or is flush with the lowest one. If so, then we can use it as the lowest cell. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } } } }
if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } }
if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v4, neighbor, v5, nextNeighbor, v2, cell); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); }
HexCell
another convenient slope recognition method between any two cells. public HexEdgeType GetEdgeType (HexCell otherCell) { return HexMetrics.GetEdgeType( elevation, otherCell.elevation ); }
HexMesh.TriangulateCorner
to determine the types of left and right edges. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); }
TriangulateCornerTerraces
. After that, we will return from the method. Insert this check before the old triangulation code, so that it replaces the original triangle. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } } AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { }
TriangulateCornerTerraces
, some corner joints with two slopes will become voids. Whether the connection becomes empty or not depends on which of the cells is the bottom. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); Color c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, 1); AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); }
AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color);
AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2, c3, c4); } AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color);
if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } }
void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { }
TriangulateCorner
, when the left edge is a slope. if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } }
void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); }
float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); AddTriangle(begin, left, boundary); AddTriangleColor(beginCell.color, leftCell.color, boundaryColor);
float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor);
AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor);
AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor);
void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); } void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } }
TriangulateCornerTerracesCliff
and modify it accordingly. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, left, b); Color boundaryColor = Color.Lerp(beginCell.color, leftCell.color, b); TriangulateBoundaryTriangle( right, rightCell, begin, beginCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } }
TriangulateCorner
. if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; }
TriangulateCorner
, calling methods TriangulateCornerCliffTerraces
and TriangulateCornerTerracesCliff
with different cell turns. if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { … } if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } return; }
void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … } void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … }
TriangulateCorner
getting rid of the operators return
and using blocks instead else
. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } else if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); } else { TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } } else { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } }
else
covers all remaining cases that are not yet covered. These cases are PPP (plane-plane-plane), OOP, OOOR and OOOL. All of them are covered by one triangle.Source: https://habr.com/ru/post/424257/
All Articles