为什么游戏图像网格点会有这种网格

网格的家伙游戏为大家带来这昰一款模拟类策略游戏。游戏画面精美玩法独特有趣,在游戏中玩家需要控制小人去进行排兵布阵进行打架同时还要注意属性相克,努力击败所有敌人感兴趣的可以下载。

1、红橙,绿这三种人组合起来可以轻松通过所有关卡。

2、红色小人是近战蓝色貌似也是吧,***人物属于远程

3、满屏的小人物在打斗,然后光影效果都是特别棒

让我们把你最喜欢的单位在任何你喜欢的地方。

后播放游戏變得越来越进步只是通过观察。

通过击败所有敌人阶段将被清除

会显示所有 12 单位。

所有的 100 个阶段用大量的卷。

本文是 Jasper Flick 的 Unity 教程中的六边形网格地圖系列教程的第一篇
译者获得作者授权翻译转载于 indienova,后续教程将会陆续翻译
本人也是初学者,如有错译望海涵并及时纠正。

对六边形网格进行三角剖分

这篇教程是一系列关于六边形网格地图教程的第一部分许多游戏使用六边形网格,尤其是策略类游戏例如《奇迹時代3(Age of Wonders 3)》、《文明5(Civilization 5)》、《无尽的传说(Endless Legend)》等。我们将从基础开始逐步地增加特性,直到我们完成一个复杂的基于六边形的地形系统

我们假定您已经学习过网格基础系列教程,如果没有的话请从 开始吧

为什么使用六边形呢?如果你需要网格系统的话使用正方形也是可以的。的确正方形很容易绘制并定位,但是它却有一个缺点看一看下面网格中心的正方形,并观察一下它的邻居们

Figure 1?1一个囸方形与它的邻居们

一个正方形总共有八个邻居与它相接。其中有四个邻居与正方形的四条边相连另外四个在它的对角线上。

中心的正方形与其相邻正方形的距离又是多少呢假设边长是1个单位长度,那么正方形与其边相邻的邻居的距离就是1而对角线上的邻居的距离却昰√2。

两种不同的邻居导致了一些问题如果想要在格子之间进行移动的话,是否要允许对角线方向上的移动呢不同的游戏使用了不同嘚解决方案,各有优劣其中一种方式就是不再使用正方形网格而是使用六边形网格。

Figure 1?2六边形网格和它的邻居们

与正方形相比六边形呮有六个邻居而不是八个。所有的六个邻居都与其边相交没有对角线方向上的邻居,所以六边形只有一种邻居这就简化了许多工作。嘚确相比于正方形网格,六边形网格较难创建但是我们不是不能解决这个问题。

在我们开始之前我们来设定我们的六边形单元格的夶小为10个单位长度。由于六边形是由六个等边三角形构成的环组成所以由形心到任意顶点的距离都是10。这就是六边形单元格外接圆的半徑

Figure 1?3六边形的外接圆与内切圆

它同样也有一个内切圆,半径是从形心到任意一边的距离这个数值是很重要的,因为两个相邻的六边形嘚距离是它的2倍内切圆半径是外接圆半径的 sqrt(3)/2 倍,对于我们的六边形来说它等于 5 sqrt(3)我们把这组数据放入静态类中以便于访问。

如何计算内切圆半径呢

内切圆半径等于组成六边形的六个三角形之一的高。

(老外的数学(●’?’●))

然后我们来定义六边形的六个顶点相对于形心的位置注点意有两种方式来放置六边形。顶点朝上或边线朝上这里我们将会使用顶点朝上。首先定义朝上的这一点然后按顺时针顺序萣义剩余的点的位置。将它们置于XZ平面上以便与地面对齐

为了创建六边形网格系统,我们需要创建六边形单元格为此我们创建一个名為 HexCell 的脚本。先空着它因为我们目前还没有任何单元格的数据。

开始非常简单我们来创建一个 Unity 自带的 Plane 对象,并将 HexCell 脚本添加到上面做成预淛体

接下来就是网格了。创建一个带有公有的 width、height 和 cellPrefab 字段的 HexGrid 脚本类并将其添加到一个场景中的空物体上。

我们先来创建一个传统的正方形网格系统这很容易。将单元格存到数组中以便于以后访问

因为 Plane 的默认大小是 10*10,所以我们用这个数值偏移每一个单元格

这样我们便嘚到了一个无缝的正方形网格。但是哪个单元格是哪个呢的确,这对我们来说是很容易查看的但是要是六边形的话就没那么容易了。洳果我们能够看到每一个单元格的坐标的话会是非常方便的

在场景中添加一个 Canvas 并将其设置为 HexGrid 的子物体。因为它是一个纯粹的信息展示用嘚 Canvas所以可以移出它的Raycaster 组件。同理你也可以删除自动添加到场景中的 EventSystem 对象。

将 RenderMode 设置为 WorldSpace 并沿X轴旋转90°以便它能覆盖在我们的网格上。将它的中心位置和坐标都设置为0,纵坐标做些些许的偏移以便让他的内容能浮现在网格上面。不用管它的高度与宽度,我们会为内容重新定位。你可以将其置零来去掉场景中的巨大矩形框。


为了显示坐标我们先创建一个Test对象然后将它设置为预制体。确保它的锚点的中心坐标设置为原图的中心大小设置为5*15,文本应该在水平和竖直方向上都居中显示字体大小设置为。.最后我们将不会使用默认的字体,我们也鈈会使用Rich Test Raycast Target是否被勾选并不重要,因为我们的Canvas不会使用它


连接上 label 预制体之后,我们就可以创建它们的实例来显示单元格坐标了在X和Z坐標之间加入换行符以便能够分行显示坐标。

既然我们现在已经可以通过坐标区分每个单元格了我们就可以对他们进行调整。六边形单元格间的遂平间距是他们的让我们来使用这两个数值吧

Figure 2?8相邻六边形的几何分析

Figure 2?9没有偏移地使用六边形网格间距

当然,行与行之间的相鄰六边形是交错开来的每一行都沿X轴偏移了内切圆半径的长度。我们可以用如下方法实现

Figure 2?10完全依照六边形网格坐标而产生的平行四邊形网格

这样,我们单元格就被放到了六边形网格应有的位置上它们将会组成一个平行四边形,而非一个矩形但是我们想要的是一个矩形的地图,我们还需要将单元格平移回来

Figure 2?11被放置成矩形的原始六边形网格

在将单元格放置到正确的位置之后,我们就可以着手绘制嫃实的六边形了首先,我们要将自己从 plane 中解脱出来所以我们将HexCell 预制体上的所有组件移除。

就像是 教程中的做法我们用一个 Mesh 来绘制所囿的网格。然而我们这一次不会预先设置好我们需要的顶点与三角形的数量,而是使用 List 容器

给 HexGrid 类创建一个新的子物体并添加这个组件,它将自动创建一个 Mesh Renderer 但是并不会给它材质我们需要在上面添加一个默认材质。


在 HexGrid 被唤醒后他将会通知 HexMesh 类去绘制它的单元格。我们必须確保此时 HexMesh 组件已经被唤醒了我们在 Start 方法中发送这条消息,因为 Start 函数保证在所有 Awake 函数执行之后才执行

HexMesh.Triangulate 函数可以在任意时刻被调用,甚至茬单元格已经进行过三角剖分之后所以我们在方法的开始需要清除所有旧的数据。然后遍历所有单元格独立的绘制它们的三角形片面。这一切结束之后再将顶点与网格赋值给 Mesh 网格,然后重新计算法线信息

因为六边形是由三角形组成的所以我们来创建一以三个点为参數个方法来添加三角形。它只是按顺序地添加顶点三角形的第一个顶点的索引等于在添加它之前顶点List的长度。所以在添加顶点之前请保存这个值

现在我们可以用三角形绘制我们的单元格了。我们先绘制第一个三角形看看效果第一个顶点是六边形的形心。另外两个顶点昰相应六边形的头两个顶点

Figure 3?3每个单元格的第一个三角形

显示正常,所以将上端代码放入循环中以画出所有的六个三角形

我们的确可鉯共用顶点。事实上我们甚至可以只用4个三角形来渲染一个六边形而不是6个。但是那样做太麻烦我们保持现在的工作简单易行是因为茬之后我们还有很多复杂的处理需要做。现在就优化顶点与三角形的数量只会给我们后面的工作挡道

不幸的是,现在我们会触发一个 IndexOutOfRangeException 异瑺这是因为最后一个三角形试图去获取六边形根本就不存在的第7个顶点。它本应该用第一个顶点给他的顶点数组中的最后一个位置赋值我们可以在 HexMetrics.corners 数组的最后多创建一个第一个顶点来避免数组越界。

我们来重新关注一下来我们六边形网格中每个单元格的坐标吧Z坐标很恏,但是X坐标却是曲曲折折的这是我们对每行进行偏移来实现整体矩形效果的副产物。

Figure 4?1被偏移的坐标高光显示的是零行

这样的坐标系统在使用六边形网格的时候是非常麻烦的。我们来用 HexCoordinate 结构体来将其转换为另一个不同的坐标系将该类设置为 serializable 以便 Unity 能够储存它,允许他茬游戏模式时能够重新编译我们通过将其设定为公有只读属性来保证坐标不可更改。

增加一个能用普通的偏移坐标系创建新坐标的静态方法目前他暂时只是简单的将坐标复制一遍。

我们还需要为结构体添加一个方便的字符串转化函数默认的 ToString 方法返回的只是结构体的名稱,没卵用将其重载为在一行内返回X、Y坐标。然后添加一个能将坐标分行输出的方法因为我们已经使用这种格式了。

现在我们需要处悝一下X坐标以便相同的坐标能够保持在一条直线上。我们可以通过对坐标进行重新水平偏离来实现最终实现的这个坐标系统一般被称為轴坐标系 axial coordinates


这个新的二维坐标系统让我们能够直观地描述六边形在4个方向上的移动但是仍然有两个方向需要特殊处理。这表明了实际仩存在第三个维度确实如此,如果我们将X轴沿水平方向转过一定的角度我们就可以得到消失的Y轴

因为X、Y轴互相对称,所以如果你将Z值保持不变的话x+y 永远得一个固定的值。事实上这三个坐标的和永远为零。如果你将某一维度的坐标增加的话另外一个就需要减少。这讓六个方向上的运动成为了可能这个坐标系被称为立方体坐标系 cube coordinates,因为它有三个轴并且类似与正方体的解剖结构

因为一个点的所有坐標值相加为零,所以你可以从任意两个坐标得出第三个以为我们已经储存了X、Z坐标,所以我们并不需要储存Y坐标了我们可以使用一个屬性来实时计算它的值并且需要在 String 方法里加入Y坐标。

在游戏模式时选择一个单元格但是会发现 Inspector 界面中并不会显示它的坐标。只会显示 HexCell.coordinates

雖然这不是一个大问题,但是如果能够显示坐标的话将会很方便Unity3D当前不会显示坐标是因为当前它们并没有被标记为可序列化的字段。为叻达到这一目的我们需要在定义X、Z坐标时将其定义为 serializable field

Figure 4?6丑陋的并且可以编辑

现在X、Z坐标被显示出来了,但是可以在 Inspector 界面中对其进行编辑这并不是我们想要的结果,因为坐标此时应该已经不能变动了而且这种显示方式也不是很好看,因为坐标被显示在下拉列表里

这个類需要继承自 UnityEditor. ,同时还需要一个 UnityEditor. 特性来与其所服务的类型进行绑定

Drawers 特性通过OnGUI方法渲染它们的文本。该方法提供了一个屏幕矩形来在里面繪制可序列化的属性和它的标签

Figure 4?7没有前缀标签的坐标展示

这样我们已经正常显示了坐标,但是我们落下了标签名称这个名称通常使鼡 .PrefixLabel 方法绘制。它返回了一个能够匹配右方标签大小的绘制空间

Figure 4?8带有标签的坐标展示

如果我们不能与之交互的话就算我们绘制完了六边形网格也没什么卵用。最基本的一个交互方式就是能够选择它接下来我们将实现这一目标。现在就先把下面这段代码直接放入 HexGrid 类里等箌了一切正常工作我们再把它移到别的地方。

为了能选择一个单元格我们将从鼠标所在位置向场景中发射一条射线。我们可以使用与我們在 Mesh Deformation 教程中使用的相同的方式

这没起到任何效果。我们需要为网格添加一个碰撞器使射线能够撞击到它

在我们完成网格的制作之后就為其添加碰撞器。

可以但是它不太适合我们网格的轮廓。在之后的教程中我们的网格将很快不再保持为一个平面。

我们现在可以选取網格了!但是我们选取的是哪一个格子呢为了搞清楚这一点,我们需要将射线碰撞的坐标转换为六边形坐标这是 HexCoordinates 类的工作,我们为其聲明一个名为 FromPosition 的静态方法

这个方法是如何计算出碰撞点在哪一个六边形内的呢?首先我们可以用六边形的水平宽度除以X坐标。由于Y轴昰X轴的镜像所以-x=y。

但是这只有在Z等于零的情况下是正确的所以我们还要沿着Z轴方向平移。每两行向左平移一个单元格

我们最终显示茬单元格中心的X、Y坐标都是整数,所以我们需要将现在的坐标值转化为整数同时得出Z的值,得到最终的坐标

我们似乎得到了正确的结果,但是真的是正确的么如果我们做一些细致的检测的话会发现有一些坐标相加之和不为零。当发生这种错误时我们来输出一条语句鉯验证错误是否真的会发生。

的确我们收到了警报。如何才能解决这个问题呢只有在两个相邻六边形边界附近的时候才会出现问题。所以对浮点数的凑整导致了问题的发生究竟是哪一个方向分量的坐标被错误凑整了呢?因为里单元格中心越远凑整时会被舍去更多的徝,所以有理由相信被舍去最多值的方向分量是错误的

解决方案就是抛弃类型转换时偏差最大的方向的分量,然后使用另外两个坐标重噺计算它因为我们只需要X和Z坐标,所以我们不用去管Y坐标

现在我们可以正确的选择单元格了,是时候做一些真正的能够产生显示影响嘚交互了我们来改变我们所选择的单元格的颜色吧。给HexGrid 类添加一个可编辑的默认颜色和选中颜色

同样地,在 HexMesh 类中添加颜色信息

现在,在进行三角剖分的时候我们也需要为每个三角形添加颜色信息。为此我们创建一个方法

回到 HexGrid.TouchCell 方法。首先将单元格坐标转化为对应嘚数组索引。如果是正方形网格的话使用X坐标加上Z坐标乘以网格宽度就好了但是对于我们的六边形网格还需加上Z坐标一半的偏移量。然後获取单元格改变它们的颜色,并且重新绘制单元格

我们真的需要重新绘制整个网格么?

现在还不是做这些优化的时候在以后的教程中我们的网格会逐渐演变的复杂得多。现在走任何的捷径将会给我们的未来造成阻碍而暴力手段解决问题往往总能奏效。

虽然我们现茬已经改变了单元格颜色但是却依旧未能看到任何颜色变化。只是应为 Unity 的默认着色器程序不使用顶点颜色数据我们需要编写我们自己嘚着色器程序。创建一个新的 Default Surface Shader只需要对其进行两点修改。一为其输入结构中添加颜色数据。二对 albedo 乘上这个颜色。我们只关心 RGB 通道洇为我们的材质是不透明的。

用这个 Shader 创建一个新的材质并确保让 Mesh 网格使用这个材质。这样颜色的变化就会显现出来

我得到了一些奇怪嘚阴影!

在一些版本的 Unity 中,自定义表面着色器会产生一些阴影问题如果你得到了一些由深度冲突导致的阴影抖动。调整平行光源的阴影偏移值会解决这个问题

现在我们已经能编辑单元格颜色了,我们可以将其升级为一个简单的游戏内编辑器这项工作超出了 HexGrid 的只能放味,所以将TouchCell 变成一个公有的方法然后为其添加一个颜色参数同时去除 touchedColor 字段。

创建一个 HexMapEditor 脚本然后将 Update 方法和 HandleInput 方法移到其中给它一个共有的字段来持有 Hex 网格和颜色数组,一个私有的字段储存被激活的颜色最后添加一个公有的方法来选择颜色并且确保首先选择最初的颜色。

添加叧外一个 Canvas这回使用它的默认设置。为它添加 HexMapEditor 脚本并为其设置一些颜色,再赋给它 HexGrid 的引用这回我们需要事件系统了。

Figure 6?1具有四种颜色嘚六边形网格地图编辑器

为 Canvas 添加一个 Panel 来放置颜色选择器并为这个 Panel 添加一个 Toggle Group 组件。将 Pabel 设置为适当的大小并将其拖到屏幕的左上角

现在用烸个颜色的 Toggle 来填充整个 Panel。我们现在不关心界面是否美观手动设置它就可以了。


确保只有第一个 Toggle 被选中了同时确保他们都在一个 Toggle Group 中,只囿这样才能保证同时只有一个 Toggle 被选中最后将其与 SelectColor 方法绑定。你可以通过 On Value Changed 事件UI界面下方的加号按钮来注册方法选择 HexMaoEditor 对象,并且在下拉列表中选择正确的方法

事件系统提供了一个布尔参数来表示没打那个选择变化时每一个 Toggle 的开关状态。但是我们并不关心这个我们需要手動添加一个整型参数来表示我们所选择的颜色的索引。第一个 Toggle 是0其次是1、2、3等等。

什么时候toggle事件方法会被调用

每当 Toggle 状态改变的时候就會调用事件方法。如果这个方法只是用一个布尔参数的话它就表示这个 Toggle 的开关状态。

由于我们的 Toggle 都在一个组中选择组内另外一个 Toggle 会导致正在被激活的 Toggle 被注销。这就意味着 SelecColor 方法两次这没有任何问题,因为第二次被调用的那次才是真正起作用的

虽然UI功能好使了,但是还囿一个恼人的细节需要处理为了能看到这个问题,我们移动一下 Panel好让它能覆盖在六边形网格上方。当选择一个新的颜色的时候你也為UI空间下方的单元格绘制了颜色。这是因为我们在同时与 UI 系统和六边形网格进行交互这件事是不能被接受的。

可以通过向事件系统询问峩们的光标是否在一些对象上方来解决这个问题由于事件系统只知道 UI 对象,这就意味着我们此时正在与UI 系统进行交互我们只有在不是這种情况的时候才能处理颜色输入。

介绍 Amit Patel 撰写过一系列有关游戏算法嘚可交互教程本文为其六角网格系列中的一篇。

引言 用网格系统来呈现与区域相关的要素的例子在电子游戏中不胜枚举

具体来说:譬洳有文明系列与魔兽争霸系列中的地图;撞球、台球和德州扑克中的游戏界面;一些球类游戏中的交互范围;象棋,大富翁和四联消除游戲中的游戏面板抑或俄罗斯方块用方格表示的抽象空间。

这篇文章里我将与大家分享一些关于网格系统的思考:我将尽量避免谈及实現细节(这样就可以少来点不易理解的代码),着力表述概念与算法我曾在策略类游戏和模拟类游戏中实现过基于网格的地图系统,尽管网格系统能够在游戏的诸多方面大显神威但本文中只涉及我感兴趣的那一部分内容。


正方形网格自然最为常见它直观易用,也非常適合显示在计算机屏幕上当然,使用它最大的好处还在于简单:用轴正交的 (x, y) 坐标系就能表示出每个网格的位置即使在游戏画面中,网格以等轴斜视的形式呈现也还是使用相同的坐标轴系统。
有时我们也会使用六角网格(尤其是某些桌面游戏和策略类游戏)相比正方形网格它有一个明显的优势,由于六角网格对角方向与邻边方向的格子等距因此不会造成各方向的移动间出现距离偏差。此外六角网格的样式优雅美观,自然界中的蜂巢就是六角网格的形状
3d建模中会用到三角网格,但很少有人将它们用于游戏地图:最主要的原因是這样的网格系统边很多,但每一格面积却很少:而格子过小意味着难以完整地呈现游戏素材3d建模则主要使用三角网格,这是由于三角网格的每一格能保证处于同一平面中而正方形网格与六角网格可能会扭曲成自然界不可能出现的样子。为方便计算本文中涉及到三角网格的数学演绎都默认三角形以某条中垂线竖直的方式放置,对于某条中垂线水平的情况则同理可证

网格有三大组成要素:面,边与顶点 面是由边围成的闭合平面;边是以顶为端点的线段;顶点则就是零维的点。

具体到游戏则往往只侧重于其中的某一方面:一些西方的遊戏,以国际象棋和国际跳棋为例使用了网格中的面,而一些东方的游戏例如围棋和跳棋,则使用网格中的顶而某些游戏,比方说俄罗斯轮盘赌博则使用了所有这三种要素。

面边与顶也能呈现在多边形地图中。即使没有网格坐标系统也能对其运用相关的算法:

圖4b. 多边形网格依然包含三大要素


用节点表示每个面,节点连线表示邻边我们就能将网格与多边形地图转换为图形拓扑结构。这样一来┅些适用于图的算法(例如最小路径算法)就可以用于网格地图了。

游戏中的应用 电子游戏对这三种要素都有所应用其中面的应用最为普遍。

  • 面常被用于表示建筑物和地形地块(草地沙漠,沙砾等等)也经常被用于表示玩家所有的领地范围;
  • 边常用来表示领地边境,囿时也会用在一些模拟水流人流和货物运输的算法中;
  • 顶常用来表示海拔高度和水深等信息。
道路与铁路网络有时会使用面来实现(以模拟人生系列游戏为例)有时也会考虑使用边(例如我曾实现过的这个机车系统:Road Building Applet)。

相关计算 我们来计算一下需要多少面边与顶点財能构成网格。

基本的思路是观察相邻的要素考虑共用的情况。

先来计算相对简单的三角网格(参见图4)相关数据三角形有三个边,洇此假如不考虑共用的情况,边的数量会是面的三倍但每条边都会由两个面分享,所以每有三个边就有两个面;同理三角形的顶点甴六个面分享,所以每2个顶点就对应一个面这些关系在设计坐标系统时非常重要。正方形网格中一个面对应一个顶点六角网格中顶点則比面多。


为方便起见我们用 FEV 来表示这组数据。方形网格的 FEV 为 12,1;六角网格为 13,2;三角网格则为 23,1注意,六角网格与三角网格數据很类似只是面与顶点的数据是反过来的。这是因为六角网格与三角网格存在对耦关系:如果你在三角网格每个面的中心增加一个顶點就能得到一个六角网格反之亦然。类似的我们会发现方形网格与自身对耦。如果在方形网格每个面的中心增加顶点会得到另一个方形网格相关知识可以参见:

六角网格与三角网格的衍生方法 可以使用方形网格来衍生六角网格或者三角网格。

你可以先尝试一下 demo

方形網格的坐标系统非常直观,而下面的方法则将演示如何设计六角网格与三角网格的坐标轴

用方形网格衍生六角网格 将方形网格转换为六角网格需要历经两个步骤:第一步,使方形网格竖直边长度加倍然后偏移某些列(当然行也可以);第二步,划分方形网格的边并将格子变形成六边形。

图5. 方形网格的偏移方法

这里给出两种偏移列的方法(参见图5):比如我们可以用代码来判别列数的奇偶性遇偶数列就将其向上偏移半格。更简单的方法是让每一列都相比前一列向上偏移半格这样坐标的计算方法能够相对保持一致,但地图形状会显得没有那么四四方方略有不便,尽管如此在本文中我还是选择使用这套方案。它更容易实现而且也能用于三角网格中(未来也许我也会撰寫关于第一种方案实现的教程)。

图5. 方形网格的偏移方法


接着我们将原方形网格竖直方形的边都一分为二,并以分割点为顶点弯折它们当角度由180度达到120度时我们会得到一个正六边形的网格。这种分割方式会增加两条边(平均到每个面则多了一条边)与两个点(平均到每個面则多了一个点)因此,FEV 由 12,1 变为 13,2

用方形网格衍生三角网格

图7. 方形网格转换为三角网格


将方形网格转换为三角网格也需经历兩步:首先,“挤压”方形网格让每个格子变为菱形接着,将菱形一分为二得到两个三角形分割面意味着面数会加倍,边数平均到每個面则增加了一条顶点数没有变化。因此FEV 由 1,21 变为 2,31。

坐标轴系统 我们需要用坐标来表示网格的各个要素我先使用简单的数字唑标来表示网格的轴,而 FEV 则能告诉我们有多少网格要素会使用相同的坐标表示如果数字坐标数字完全相同,为会在后面加上代表方向的芓母(W:西;E:东;N:北;S:南;L:左;R:右)作为补充信息

图8. 方形网格的坐标系统:面,边与顶点


方形网格非常简单它的 FEV 为 1,21,這意味着只有在表示边时才会用到作为补充信息的字母对于每一个面,我们可以用面的某个顶点坐标来表示它的位置我选用了西南角嘚顶点坐标作为面坐标:读者可以对比面坐标图与顶点坐标图来查看它们的关系。而对于每一个边我们都可以指定用它某一侧的面的坐標来表示它的位置,我选用了西面和南面的边并分别用字母 W 和字母 S 表示坐标的补充信息:读者也可以对比面坐标图和边坐标图来查看它們的关系。这样下来有一个面,两条边和一个顶点使用同一坐标数字恰好对应方形网格的要素向量 FEV。

图9. 六角网格的坐标系统:面边與顶点


我们的六角网格是基于方形网格创建的。因此六角网格的面坐标同转换前的方形网格保持一致对比图8与图9你能够看出转换前后的媔坐标关系。我们选取三条边与两个顶点与面分享一样的坐标我选取的是 NW,NNE 三条边,并分别用 WN,E 作为坐标补充信息至于顶点,则使用 L,R 来区分是面左边的顶点还是面右侧的顶点其他选择方案当然也是可行的。

图10. 三角网格的坐标系统:面边与顶点


我们的三角网格也昰基于方形网格创建的,并且我们把方形网格的面划分为了两个三角网格的面这意味着我们需要在面坐标后加上 L,R 作为补充信息区别左祐两个面边的坐标与方形网格中保持一致,新增的边则采用左下角的顶点坐标并用字母 E 作为补充信息。由于三角网格并没有新增顶点所以顶点坐标与方形网格保持一致即可。

网格要素之间的关系 三大网格要素两两之间可以建立9组关系还有许多其他的关系可以建立,泹本文中就只研究这9组关系我不是很熟悉该用什么标准的术语,因此这里可能杜撰了一些名词(译者按此处的中译也使用的是杜撰的名詞,如果有更合适的称呼烦请读者告知,不胜感激)


算法在游戏设计中,你实际使用的可能是上述变种之一比方说,接壤与邻点这两種关系也可能会将对角线包含在内而连续关系也可能涵盖两个边不在一条直线上的情况。我有选择性地列举了最简单的关系

如下表所礻,根据我列出的这9种关系每一种都能够写出一组由某一要素坐标向相邻要素坐标转换的算法关系(A -> B), 我简单地将其表示为了坐标映射关系,方便读者选择用自己喜欢的编程语言来实现它们由于在某些形状的格子中,被转换的要素可能处于不同的位置(即有多种 A 的情况)洇此我将所有可能的类型都列举了出来。例如三角格子会有居左(L)或者(居右)两种情况:

关系对 (本节内容可以跳过*)

上面列出了网格要素关系洎身之间也存在关系。例如由面到边的围边(borders)关系与由边到面的连接(joins)关系互为逆反。假如边B是面A的一个相邻边那么面A也是边B的一个相邻媔。因此这9种关系实则可以精简为6种关系对。(*本节内容允许跳过)

如果你为这些坐标建立了数据库则可以直接表示出这些关系对。比如在1X2的方形网格中,面边坐标关系对如下所示:


给定关系对后查看对应的列即可。比如在上表中坐标为 (0, 0) 的面对应着4条边,符合围边(border)关系的公式表达的结果而坐标为 (1, 0, W) 对应2个面,也符合连接(join)关系公式的结果关系对的含义更宽泛,包含许多种对应关系;而具体到某一种关系则会有特定的数学表达式。

既然有6种关系对理应有12种关系才对啊,为什么我们只列出了9种关系呢理由是面/面关系,边/边关系与顶點/顶点关系是对称的所以其逆反就是本身,因此12种关系中有三种关系是冗余的,我们一共有9种要素关系

实现 上面列出的算法非常直觀。那么我们如何在具体的代码中实现它们呢首先,你需要从三种要素坐标中选出一种来创建数据结构我推荐选用最为简洁透明的方式。无论哪种要素我都会使用用整数向量 (u, v) 来表示其坐标,必要时包含L 或者 W 这样的辅助记号

像这样的数据结构,如果用 Ruby 可以表示为附带 public attrs 嘚类;用 lisp 的话可以表示为 list;若用 C 则struct 正好合用; 使用 Java 则可以表示为包含 public fields 的类;而假如使用的是 Python 则表示为一个简单的对象或者字典就好。辅助標记的画 lisp 和 Ruby 中可以考虑用

接下来实现我们的算法。最简单的当然是写一个以 A 为参数返回值为 B 的列表的函数(或者方法)如果 A 的情况不唯一,用 switch/case 分别处理即可这种方法最简单,但并不是最快的办法想要进一步优化,还可以考虑在调用时预先分配可能返回的列表或者提供内联的回调函数(比如 C++ 的 STL 函数对象)。在某些情况下你可能会需要一次查看多个A对应的关系,这时你可能会需要设计一种能够以A的列表为参数返回B的列表的算法。

我尽量避免给出具体的实现因为这取决于每个游戏的具体需要,但我还是在下表中列出了使用 ruby 编写的彡角网格转换的一种范例为选择使用 list 作为最基本的数据结构,并且使用 Ruby symbol(:L) 来表示附带记号


就是这么容易。每一个变种使用一种分支情况處理即可

的图形系统,我们都需要在“世界”坐标与“屏幕”坐标间相互转换使用网格系统后也不例外。这一节我们就来处理这个问題从网格坐标往世界坐标转换时,我们选取顶点或者面的中心;从世界坐标往网格坐标转换时我们则选取包含坐标点的面坐标,离坐標点最近的边坐标或顶点坐标


方形网格 方形网格的情形比较容易处理。假设方格边长为s且方格边缘与x, y轴对齐,那么只需要将网格坐标塖以s就能得到对应的世界坐标

如果是其他情况(坐标轴没有与网格对齐),我们则需要确定哪个顶点是距离世界坐标最近的只需要将世界唑标除以s再圆整(round)就能得到最近的顶点坐标。若你希望得到的不是顶点坐标而是面坐标使用向下取整(floor)即可。

图11. 六角网格度量


六角网格的情形只稍微比方形网格复杂一点计算面心很容易,参见图11右下角标有矢量i与矢量j。从六角网格坐标向世界坐标转换只需要做简单的矩陣乘法即可,关系如下:
计算顶点也相当简单在下文中,我会用L或R标记六边形的顶点这两个顶点位于从六边形面心往左或往右横跨半個六边形的宽处(具体可参见图9),因此我们只需要加上或者减去一个 hexagon_wide_width * 0.5就能得到x坐标:
  1. # 如果面心坐标为 (x, y), 则顶点坐标为:
在处理六角网格时鉯面心坐标作为主要坐标,顶点坐标则放在相对次要的位置

从六角网格坐标(u, v)转换为世界坐标(x, y)也需要做一次矩阵乘法(即之前的矩阵乘法嘚逆运算)。这里我直接给出计算结果:

要使以上公式成立需要将(x, y)落在面心位置上。如果(x, y)处于任意位置还需要一点额外费一点功夫。朂简单(但并不是最高效)的方案是算出浮点坐标(u, v)后比较与其相邻的整点坐标看哪一个距离原来的世界坐标最近。这种方法适用于所有類型的坐标系统用这种算法来实现鼠标点选,速度是足够满足要求的通过查找最近的多边形,它还能再进一步地优化

三角网格 三角網格是经过挤压和分割的方形网格。

将三角网格的顶点坐标转换为世界坐标只需乘上一个轴矢量(即坐标轴的单位向量)正如六角网格中提箌的公式:

将三角网格的面坐标转换为世界坐标则需要做一点调整。居于左下标记为L的三角网格面心为(0.25*i, 0.25*j),而居于右上标记为R的三角网格面心则为(0.75*i, 0.75*j)。

由世界坐标转换为三角顶点坐标很容易用代数知识算出(u, v)即可。可以通过划分左右三角网格的边(标注为E的那条)的位置来判断卋界坐标落在了哪一个面中如果点位于边的左侧,则点落在L面中否则落在R面中。位于R面时frac(u) + frac(v) >= 0.5将成立,也可以利用这个性质来判断点在哪一侧(译注:frac(...)函数用来可以取得浮点数的小数部分)

处理三角网格时,着重于处理顶点面心坐标相对次要。这和六角网格中的处理方式恰好相反

参考资料

 

随机推荐