5-9 摄像机移动控制

  在上一章中,我们完成了摄像机视距控制的功能,通过鼠标滚轮可以控制摄像机视距的远近。接下来我们继续完善摄像机控制的相关功能。
  要让摄像机在水平和垂直方向移动,与调整摄像机视距思路相似,可以检测水平和垂直的输入信息,对应Hex Map Camera在X和Z方向的运动。代码如下

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void Update()
{


//检测 水平 和 垂直 方向的输入
float xDelta = Input.GetAxis("Horizontal");
float zDelta = Input.GetAxis("Vertical");
if (xDelta != 0f || zDelta != 0f)
{
AdjustPosition(xDelta, zDelta);
}
}

/// <summary>
/// 控制摄像机移动
/// </summary>
/// <param name="xDelta">X轴输入增量</param>
/// <param name="zDelta">Z轴输入增量</param>
private void AdjustPosition(float xDelta, float zDelta)
{
}

  使Hex Map Camera移动最简单的方式,就是先获取当前位置,然后再加上X和Z轴的输入增量,这样的出来的结果就是移动后的位置。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
private void AdjustPosition(float xDelta, float zDelta)
{
//先获取当前位置
Vector3 position = transform.localPosition;

//当前位置加上位移增量,得出新的位移位置,此方法会受到帧率影响
position += new Vector3(xDelta, 0f, zDelta);
transform.localPosition = position;
}

  现在我们就可以通过WSAD来控制摄像机在X和Z轴方向上的移动了。但是这样移动的速度并不是恒定的,会受到帧率的影响。为了避免受到帧率的影响,我们改变一下思路,使用方向乘以位移距离的方式来描述摄像机的移动。而距离则是由时间增量和速度所组成的。我们添加moveSpeed变量,用来控制摄像机的移动速度,初始值为100。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HexMapCamera : MonoBehaviour
{
//摄像机移动速度
public float moveSpeed;



private void AdjustPosition(float xDelta, float zDelta)
{
//通过实践增量和速度,计算出位移距离
float distance = moveSpeed * Time.deltaTime;



//这里的 new Vector3是作为方向来使用的distance表示移动的距离, new Vector3表示移动的方向
position += new Vector3(xDelta, 0f, zDelta) * distance;


}
}

  通过勾股定理我们可以发现,摄像机在斜向移动的时候,会比只沿着X或Z轴速度更快。所以需要将移动方向进行归一化处理,这样就保证摄像机向着各个方向移动时速度相同。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
10
11
private void AdjustPosition(float xDelta, float zDelta)
{
//camera的移动方向
Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized;



position += direction * distance;


}

  修复了移动速度的问题后,还存在一个问题,就是当我们持续按下一个方向键一段时间,抬起后摄像机的移动不会立刻就停止,而是会持续移动一段时间。这是因为当我们按下方向键后摄像机位置不会立即变为计算后的位置,而是需要一个过渡时间,抬起按键也是相同的原理,而且我们将表示方向的向量进行了归一化,所以在这段时间内,摄像机都会以一个恒定的速度运动一段时间。
  解决这个问题的方式,就是增加一个阻尼值,阻尼值取X或Z轴向两个输入值中绝对值最大的那个,这样既保留了开始和停止时的平滑感,又解决了抬起按键还会持续移动的问题。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
10
11
private void AdjustPosition(float xDelta, float zDelta)
{
//阻尼系数,取X或Z的绝对值中最大的一个,这样避免抬起按键后还会移动,又保留了平滑感
//这个值是慢慢减少的,所以移动距离也会是慢慢变小,最终到0。与按键的按下和抬起同步
float damping = Mathf.Max(Mathf.Abs(xDelta), Mathf.Abs(zDelta));

//通过实践增量和速度,计算出位移距离
float distance = moveSpeed * damping * Time.deltaTime;


}

  现在摄像机移动的相关代码已经完成了,但是当我们将镜头视距在默认位置时没没有什么问题,拉至最远的时候会感觉摄像机运动的比较慢。那就需要我们有两个速度值,一个是视距最远的时候,一个是视距最近的时候。按照这个思路,将moveSpeed拆分为2个变量,默认值分别为400和100,对应视距最远和最近时的移动速度,通过zoom变量进行插值计算。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HexMapCamera : MonoBehaviour
{
//camera的两个移动值,分别对应视距最远和最近
[SerializeField] private float moveSpeedMinZoom;
[SerializeField] private float moveSpeedMaxZoom;



private void AdjustPosition(float xDelta, float zDelta)
{
//根据当前视距计算移动速度
float moveSpeed = Mathf.Lerp(moveSpeedMinZoom, moveSpeedMaxZoom, zoom);


}
}

  至此摄像机终于可以正确的移动了。但是还存在一个问题,就是摄像机不应该移动到地图的边界之外。为了让摄像机只能在地图范围内移动,我们首先要知道地图的范围,在HexMapCamera中创建HexGrid变量,并在Unity中将HexGrid物体拖入这个变量对应栏位中。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
public class HexMapCamera : MonoBehaviour
{
//为了控制camera的移动范围,要获取地图的实例
public HexGrid grid;


}

  获取到地图实例后,使用一个新的方法来计算当前摄像机的位置是否在地图的范围内,并将这个计算后的位置赋值为摄像机当前的位置。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void AdjustPosition(float xDelta, float zDelta)
{


//对camera的移动范围进行限制
transform.localPosition = ClampPosition(position);
}

/// <summary>
/// 限制camera的移动范围在地图尺寸内
/// </summary>
/// <param name="position">当前camera的位置</param>
/// <returns>计算是否在地图范围内后的位置</returns>
private Vector3 ClampPosition(Vector3 position)
{
return position;
}

  X的范围最小值是0,最大值则需要获取地图的实际尺寸。

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
private Vector3 ClampPosition(Vector3 position)
{
//获取地图的实际宽度
float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius);
//将camera的位置限制在宽度范围内
position.x = Mathf.Clamp(position.x, 0f, xMax);


}

  Z的范围取值也是相同的原理,代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
10
11
private Vector3 ClampPosition(Vector3 position)
{


//获取地图的实际宽度
float zMax = grid.chunkCountZ * HexMetrics.chunkSizeZ * (1.5f * HexMetrics.outerRadius);
//将camera的Z限制在宽度范围内
position.z = Mathf.Clamp(position.z, 0f, zMax);


}

  实际上只是获取地图的宽度并不精确,我们希望摄像机最终会停在最右边地图单元的中心上,因此需要在X的最大值上减去半个地图单元的宽度。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
private Vector3 ClampPosition(Vector3 position)
{
//这里为了使camera镜头中心在最右侧cell的中心,这里要减去半个cell的宽度
float xMax = (grid.chunkCountX * HexMetrics.chunkSizeX - 0.5f) * (2f * HexMetrics.innerRadius);


}

  Z的取值范围也是相同,不过因为地图单元的排列在横纵坐标的度量方式不同,所以这里要减去整个地图单元的高度。代码如下:

HexMapCamera.cs
1
2
3
4
5
6
7
8
9
private Vector3 ClampPosition(Vector3 position)
{


//因为cell排列方式,这里Z方向是减去一个cell的Z
float zMax = (grid.chunkCountZ * HexMetrics.chunkSizeZ - 1) * (1.5f * HexMetrics.outerRadius);


}

  当我们现在移动摄像机时,就不会超出地图范围了。这里还有一个细节需要注意,因为我们使用的是检测水平和垂直的输入,所以Unity会响应WSAD和方向键上下左右的输入。但是同时UI也会响应方向键的输入。这样的结果就是当我们点击UI并把鼠标停留在UI上,同时使用方向键控制摄像机移动的时候,UI中的滑动条也会跟着移动。可以取消选择EventSystem上的Send Navigation Event选项来禁止UI监听按键事件来解决这个问题。

  这样,我们就完成了摄像机的移动控制功能,在接下来的一章中,我们将完成通过鼠标旋转摄像机视角的功能。

Github代码