六边形地图

六边形地图相较四方地图的优势:只有6个邻居而且每个邻居到中心的距离都是一样的。而四方地图有8个邻居包含2种情况,一种是边邻居,一种是角邻居,难以统一处理。


六边形地图和他的邻居

开始之前先确定一下六边形的大小。假定边长是10个单位。因为六边形由6个等边三角形组成,所以外径就是边长。内径就是三角的高,√3/2*10=5√3,这些值用静态变量存起来。


六边形的内径和外径.png
using UnityEngine;

public static class HexMetrics {

    public const float outerRadius = 10f;

    public const float innerRadius = outerRadius * 0.866025404f;
}

接下来确定6个点相对中心的位置。注意到有2种摆放六边形的方式,角朝上或者边朝上。我们选择角朝上。从这个角开始,其它角顺时针摆放。顺着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)
    };
  1. 网格构造
    按最简单的方式来,创建一个默认的plane,把cell组件加上去,然后做成prefab。
using UnityEngine;

public class HexCell : MonoBehaviour
{
}
用一个plane来做六边形prefab

然后来做网格。创建一个空对象把HexGrid组件给它。

using UnityEngine;

public class HexGrid : MonoBehaviour
{

    public int width = 6;
    public int height = 6;

    public HexCell cellPrefab;

}
HexGrid对象

我们从一个常规的方形网格开始。把单元存在数组里方便访问。

默认的单元大小是10X10,把每个格子依次加上偏移量。

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;
}
方块网格

一个严丝合缝的10X10网格。但是看不出哪是哪。

2.1显示坐标
创建一个Canvas然后将他变成Grid的子对象。渲染模式变成World Space,绕X轴旋转90度让Canvas躺在地上。


显示坐标用的canvas

为了显示坐标,创建一个文本对象通过GameObject/ui/text然后变成prefab。

单元格标签prefab

在HexGrid里创建一个变量CellLablePrefab


把标签预设关联给脚本
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轴的距离等于内径的2倍。而距离下一行的距离是1.5倍的外径。

六边形相邻的几何图
position.x = x * (HexMetrics.innerRadius * 2f);
position.y = 0f;
position.z = z * (HexMetrics.outerRadius * 1.5f);
使用六边形距离,不加偏移量

当然格子们不是直直的放成一行行的而是交错放的,每行的在X轴的偏移量是内径

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

3.渲染六边形
我们用一个Mesh去画整个网格。创建一个HexMesh组件来创建Mesh。需要一个mesh filter, mesh renderer, mesh,有顶点和三角面列表。

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

创建一个新的子对象绑上HexMesh。然后给一个默认材质。


HexMesh对象
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]
    );
}
每个格子的第一个三角形

循环画6个,但是i+1会超。所以存corner的时候复制第一个元素在最后面,就省得判断有没有超出了。

Vector3 center = cell.transform.localPosition;
        for (int i = 0; i< 6; i++) {
            AddTriangle(
                center,
                center + HexMetrics.corners[I],
                center + HexMetrics.corners[i + 1]
            );
        }
完成的六边形

六边形坐标
上面那张图,Z轴表现得挺好的,但是 X轴弯弯曲曲的。

坐标偏移,高亮第0行

我们来添加一个六边形坐标系结构用来转换不同的坐标系

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

X轴的偏移。EX:(0,2)原本会在(0,0)隔一行的正上方,而正确的位置应该是往右偏一格。以此类推,偶行+Z/2(注意Z是整型,结果会取整)
    public static HexCoordinates FromOffsetCoordinates(int x, int z)
    {
        return new HexCoordinates(x - z / 2, z);
    }
}
轴坐标

二维坐标可以很明确的用来描述6个方向中的4个。变化X就是X轴方向的变化 ,变化Z就是左下到右上的位置。这意味着我们需要第三维,只要把X坐标取反就得到了Y坐标

红色代表X,绿色代表Y

由于X,Y轴是对方的镜像,所以保持Z不变,把坐标加起来总是得到同一个值。实际上,如果把所有坐标都相加会得到0.如果你增加一个坐标,就要减少另外一个。这个属性非常像立方体坐标系,同样都是3个维度,拓扑结构类似于立方体。

因为总共为0,所以得知其中两个就能推算出剩下那个,所以Y不必存。

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里的坐标
定义一个属性绘制器。创建HexCoordinatesDrawer脚本然后放在Editor文件夹下。类继承自 UnityEditor.PropertyDrawer而且需要UnityEditor.CustomPropertyDrawer把它关联到正确的类型上。

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(HexCoordinates))]
public class HexCoordinatesDrawer : PropertyDrawer
{
}

Property drawers通过OnGUI渲染其内容。提供了屏幕坐标、系列化属性和属性名称标签。

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    HexCoordinates coordinates = new HexCoordinates(
        property.FindPropertyRelative("x").intValue,
        property.FindPropertyRelative("z").intValue
    );
    position = EditorGUI.PrefixLabel(position, label);
    GUI.Label(position, coordinates.ToString());
}
坐标和标签

EditorGUI.PrefixLabel用来加前缀标签,并且返回新的位置

  1. 触摸格子

Physics.Raycast实现,前提是给HexMesh一个mesh collider。


MeshCollider meshCollider;

void Awake()
{
    GetComponent<MeshFilter>().mesh = hexMesh = new Mesh();
    meshCollider = gameObject.AddComponent<MeshCollider>();
        …
    }

在 triangulating后把mesh给collider

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

如果Z等于0的话,X,Y互为相反数。

public static HexCoordinates FromPosition(Vector3 position)
{
    float x = position.x / (HexMetrics.innerRadius * 2f);
    float y = -x;
}

沿着z轴移动,每二行就出现X-1,Y-1的情况。

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

加个LOG验证

if (iX + iY + iZ != 0) {
   Debug.LogWarning("rounding error!");
}
        
return new HexCoordinates(iX, iZ)

发生问题的是当点是靠近在格子边缘的时候(注:X和Z算行数的时候2行之前是有交叠的部分的),取整导致的问题。离格子中心越远则误差越大。那可以认为误最大的那个方向是错的。

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;
选择颜色设置.png

HexCell一个颜色字段。并在创建格子的时候把默认色给它


public class HexCell : MonoBehaviour
{

    public HexCoordinates coordinates;

    public Color color;

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

在triangulating的时候。我们顺便把颜色信息设置了。另外写个方法AddTriangleColor来处理这事

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)*WIDTH。但我们这种情况,还需要加上半个Z的偏移。然后取出相应的格子,改变颜色 。再重新triangulate一遍。其实也并不用重新triangulate,之后优化的教程会说到

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不会用的顶点颜色。创建 Assets / Create / Shader / Default Surface Shader。改两个地方: input加上color属性,albedo* color。只关心RGB,因为我们是不透明的。然后新建个材质用这个shader。

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"
}

注:如果阴影扭曲或者动来动去的话,是因为Z值冲突。调整方向光的shadow bias 就能解决。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343