1-8 获取鼠标点击位置

  在之前的章节中,我们完成了一个正六边形地图系统的基本框架。但是目前这个地图系统是无法与玩家产生任何交互的。在一般策略游戏中,地图系统最基本的交互方式,就是响应玩家的鼠标点击事件。即玩家鼠标左键单击一个地图单元,这个地图单元便会改变颜色,或者播放一段预置的动画,来响应玩家的操作。我们可以通过鼠标向场景中发射一条射线的方式,来检测鼠标是否点击在了某个地图单元上。

  目前,我们先把交互代码放在HexGrid.cs脚本里,随着项目在之后的章节中不断完善,将会把与玩家交互的代码移动到其他的脚本中。代码如下:

HexGrid.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void Update()
{
//之后鼠标点击交互相关代码会移动到其他脚本中
//检测鼠标左键是否点击
if (Input.GetMouseButton(0))
{
HandleInput();
}
}

/// <summary>
/// 鼠标左键单击会调用此方法,以鼠标为发射点,经过主摄像机练成射线
/// 检测射线穿过Collider的位置
/// </summary>
private void HandleInput()
{
//射线起点为鼠标位置,经过主摄像机
Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);

//检测射线是否碰撞到了collider
RaycastHit hit;
if (Physics.Raycast(inputRay, out hit))
{
TouchCell(hit.point);
}
}

/// <summary>
/// 将射线的触碰点转换到自身的坐标系中
/// </summary>
/// <param name="position">触碰到的collider的位置</param>
private void TouchCell(Vector3 position)
{
//将触碰点的坐标系,转换到自身的坐标系
position = transform.InverseTransformPoint(position);
Debug.Log("touched at " + position);
}

  现在,我们完成了鼠标左键单击后发射一条射线的功能,这条射线如果穿过了一个带有Collider组件的模型,那么将会返回一个Vector3的位置信息。但是,现在的地图单元是没有Collider组件的,我们在HexMesh.cs脚本中为它添加MeshCollider组件。代码如下:

HexMesh.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//为了检测射线碰撞Collider
private MeshCollider meshCollider;

private void Awake()
{
//初始化MeshFilter组件的,实例化hexMesh,并给其命名
GetComponent<MeshFilter>().mesh = hexMesh = new Mesh();
hexMesh.name = "Hex Mesh";

//为HexMesh物体添加MeshCollider组件
meshCollider = gameObject.AddComponent<MeshCollider>();


}

  为Hex Mesh这个物体添加了MeshCollider组件后,我们需要将创建的Mesh数据赋值给MeshCollider组件,这样它就可以根据Mesh信息成成碰撞网格了。代码如下:

HexMesh.cs
1
2
3
4
5
6
public void Triangulate(HexCell[] cells)
{


meshCollider.sharedMesh = hexMesh;
}

  这样,射线穿过Hex Mesh这个物体时,通过MeshCollider组件就可以返回射线的碰撞信息了。

  可能会有小伙伴问,为什么不使用更简单高效的Box Collider?这是因为Box Collider不能很准确的吻合正六边形地图单元的轮廓,尤其是在3个地图单元相邻的情况下,Box Collider可能会重叠到一起,最终导致我们无法判断鼠标到底点击在了哪个地图单元上。而且随着项目的不断深入,我们的地形单元并不会一直保持在同意水平面上。所以使用MeshCollider会更加方便计算和判断。

  以上代码完成后,我们在Unity的Scene窗口中点击任意一个地图单元,Console窗口中就会输出鼠标点击的坐标信息了。但是这个信息并没有体现出我们具体点击的是哪一个地图单元,所以需要将鼠标的点击的坐标信息,转换成六边形的坐标信息。这个步骤需要在HexCoordinates.cs中进行。

  我们需要在首先要在HexGrid.TouchCell方法内,添加对HexCoordinates.cs中发发的调用,这个方法命名为FromPosition,专门用来将射线触碰MeshCollider的坐标,转换成正六边形地图的坐标。代码如下:

HexGrid.cs
1
2
3
4
5
6
7
8
9
10
private void TouchCell(Vector3 position)
{
//将触碰点的坐标系,转换到自身的坐标系
position = transform.InverseTransformPoint(position);

//调用转换坐标的方法,定位具体点击到哪个cell上了
HexCoordinates coordinates = HexCoordinates.FromPosition(position);

Debug.Log("touched at " + coordinates.ToString());
}

  现在,我们来思考FromPosition这个方法要处理的事情。要将原有的射线触碰Collider信息转换成正六边形地图信息,可以将X、Y、Z几个轴向分开处理。这里首先处理X轴坐标,只需要将转换前的坐标除以地图单元的宽度即可,而且当Z值为0的时候,X和Y是护卫相反数的。在这里我们先假设Z值为0,所以很容易就能得出X和Y的值。代码如下:

HexCoordinates.cs
1
2
3
4
5
6
7
public static HexCoordinates FromPosition(Vector3 position)
{
//当Z为0的时候,X和Y互为相反数
//X的值可以通过 实际X的值除以2倍内切圆半径来得到
float x = position.x / (HexMetrics.innerRadius * 2f);
float y = -x;
}

  接下来,Z不为0的时候,我们需要对X和Y进行偏移,才能得出正确的结果。代码如下:

HexCoordinates.cs
1
2
3
float offset = position.z / (HexMetrics.outerRadius * 3f);
x -= offset;
y -= offset;

  当计算出X和Y的值后,我们可以利用X+Y+Z=0这个特性,求出Z的坐标。然后将这些坐标进行四舍五入,就可以得到转换后的正六边形地图坐标了。代码如下:

HexCoordinates.cs
1
2
3
4
5
6
//对得出的坐标进行四舍五入,得到转换后的Hexmap坐标
int iX = Mathf.RoundToInt(x);
int iY = Mathf.RoundToInt(y);
int iZ = Mathf.RoundToInt(-x - y);

return new HexCoordinates(iX, iZ);

  以上这些步骤虽然看起来没什么问题,但是仔细想一下就会发现,这样计算的最终坐标,很可能相加并不为0.让我们来加一段验证这个想法的代码:

HexCoordinates.cs
1
2
3
4
5
6
7
8
9
10
11
12
//对得出的坐标进行四舍五入,得到转换后的Hexmap坐标
int iX = Mathf.RoundToInt(x);
int iY = Mathf.RoundToInt(y);
int iZ = Mathf.RoundToInt(-x - y);

//验证X+Y+Z是否为0
if (iX + iY + iZ != 0)
{
Debug.LogWarning("rounding error!");
}

return new HexCoordinates(iX, iZ);

  我们再次运行程序的时候发现,这个报错信息确实会弹出。而且是发生在鼠标点击的位置接近正六边形边界的时候。所以应该是在四舍五的过程中出现了问题,因为离地图单元的中心越远,四舍五入时舍去的值就越多,所以我们做一个合理的假设:舍去值更大的坐标是错误的。

  知道了产生错误的原因,解决起来就比较简单了。解决方法就是废弃具有最大舍去增量的坐标值,然后用其它的两个坐标去重新构建它。这里我们只需要去重建X和Z,不需要关注Y,因为Y本来就是由X和Z求得的。代码如下:

HexCoordinates.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (iX + iY + iZ != 0)
{
float dX = Mathf.Abs(x - iX);
float dY = Mathf.Abs(y - iY);
float dZ = Mathf.Abs(-x - y - iZ);

//判断哪个舍去的值最多
//利用X+Y+Z=0的特性,使用两个舍去较小的值得,求出社区较大的那个
if (dX > dY && dX > dZ)
{
iX = -iY - iZ;
}
else if (dZ > dY)
{
iZ = -iX - iY;
}
}

  通过判断,我们重新计算四舍五入中,舍去的值最多的那个坐标。这样我们就得到了最终正确结果。

  在下一章中,我们利用本章判断鼠标点击在哪个地图单元上的功能,给被点击的地图单元改变颜色,让地图系统拥有最基本的交互。