《乡村铁路》开发日志2-摄像机

前言

这次说摄像机,还是把对应的Wiki地址发出来:摄像机系统

基本概念

我这个游戏采用的视角是一种类似各大RTS游戏的操作方式,玩家可以操作的参数有镜头平移镜头高度环视角度三个。其中镜头的实际平移的方向由输入的原始平移向量和环视角度决定,而镜头高度决定了镜头的俯视角度。

操作方式

游戏中所有的操作都尽量避免绑定具体按键,而是在原始输入(即UnityEngine.Input)和具体业务逻辑(如操作摄像机)之间增加一个输入管理层,来实现操作和按键的解耦,一方面可以方便用户修改键位配置,另一方面是降低了跨平台的难度(即不同的输入设备,也包括PC上的手柄)。

虽说现在这个游戏类型其实并不适合手柄操作,但是好像触屏操作是可行的(移植到移动平台的话怕不是只有真三7psv版的同屏人数了233)

然后是具体到这个系统的操作,包括以下几种:

操作类型作用PC默认键位
MapPanAxis2地图平移WSAD
MapRotateAxis视角旋转QE
MapZoomAxis地图缩放鼠标滚轮
MapResetKey视角重置鼠标中键

其中,Axis2是二维的轴,比如手柄摇杆,鼠标平移等;Axis是一维的轴,比如手柄扳机,鼠标滚轮等;Key是普通按键,比如手柄普通按钮,键盘上任意按键等。

聚焦点

聚焦点是指摄像机注视的点,玩家的镜头平移操作就会影响到这个点的位置(而不是摄像机的位置),只不过根据当前镜头的环视角度,同样按下W键,聚焦点的移动方向可能会不一样,期望的效果是不管摄像机在XZ平面上的投影往哪个方向看,按下W都会向当前镜头方向漫游,所以体现在地图坐标上,计算公式即为delta=mr*inp

其中delta为计算出的实际坐标变化,inp为输入的方向,mr为旋转角对应的旋转矩阵。

镜头高度和俯角

上面简单提到,镜头的俯角均由镜头高度这一参数决定。这个也不是很复杂,简单的线性插值就可以实现不错的效果。分别规定镜头和高度最大最小值和俯角的最大最小值,将当前的镜头高度从高度最大最小值重新映射到俯角的最大最小值即可。具体公式可以在wiki页面上看到,博客网站没装LaTeX插件,就不贴公式了233。

实现细节

输入系统

输入系统比较简单,准备一个InputSystem,从UnityEngine.Input类中收集该帧的输入信息,保存到ComponentData里,供其他逻辑系统使用(最好是以只读方式)。

为了保证该帧执行的所有逻辑都有一个固定的,正确的输入信息,我选择把输入系统的更新时机放到所有逻辑系统之前,加上[UpdateInGroup(typeof(InitializationSystemGroup))]这一句就可以了,关于Unity ECS内置的Group啥的,这里就不细讲了,感兴趣的可以参考这篇文章

摄像机系统

首先设计好数据结构,摄像机这块很简单:

1
2
3
4
5
6
7
8
9
10
11
public struct CameraTransform : IComponentData
{
public float2 Center; // 聚焦点
public float Height; // 镜头高度
public float Yaw; // 环视角度

// 下面的三个是用于摄像机平滑缓动效果的
public float2 RealCenter;
public float RealHeight;
public float RealYaw;
}

接下来看看CameraSystemOnUpdate()方法里做了些什么事情

平移

首先是平移操作的处理。我想让镜头在离地面近的时候减慢平移速度,拉远之后再恢复正常,于是我根据当前摄像机的高度计算了一个速度因数:

1
var factor = math.remap(MinHeight, MaxHeight, 0.2f, 1, curHeight);

然后根据当前环视角度构建旋转矩阵:

1
2
3
cos(yaw), 0, sin(yaw)
0, 1, 0
-sin(yaw), 0, cos(yaw)

将这个矩阵左乘原始的输入值(h, 0, v)(h和v分别是水平和竖直方向上的输入),再乘以本帧的DeltaTime,即可得到这一帧的聚焦点平移量,累加到CameraTransform.Center即可。最后不要忘记做限制,Center的值不能超过最大地图范围。

镜头高度

这里只针对输入信息进行处理,从高度计算俯角的过程不在这里,所以需要的操作就很简单,做一下范围限制即可:

1
camCom.Height = math.clamp(camCom.Height - inputData.MapZoom, MinHeight, MaxHeight);

环视角度

同样只做范围限制:

1
2
3
4
5
6
camCom.Yaw += inputData.MapRotate * CameraRotateSpeed * dt;
camCom.Yaw %= 360f;
if (camCom.Yaw < 0)
{
camCom.Yaw += 360;
}

考虑到如果用按住鼠标中键来操作视角的话,在一帧内的变化量有可能会超过360°,超过的话总不能直接用clamp截断,于是就用取模运算把结果转换为[0-360°)的区间内,方便处理和调试。

平滑处理

简单做了一个插值,没有仔细调试效果,总之现在看上去能满足需求,就先不改了233

1
2
3
4
var s = math.clamp(CameraSmoothSpeed * dt, 0, 1);
camCom.RealCenter = math.lerp(camCom.RealCenter, camCom.Center, s);
camCom.RealHeight = math.lerp(camCom.RealHeight, camCom.Height, s);
camCom.RealYaw = MathUtils.LerpAngle(camCom.RealYaw, camCom.Yaw, s);

其中CameraSmoothSpeed应该小于目标帧率,如果超过60的话那就相当于没有平滑,我采用的数值是10,当然这个并不能代表什么具体的含义,只是一个速度系数而已。。而MathUtils.LerpAngle则用于在0~360度范围内插值。

应用摄像机坐标

最后比对一下这一帧摄像机组件里Real开头的三个属性相对上一帧有没有变化,如果有的话就执行摄像机坐标的更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static float3 CalcPosition(float h, float pitch, float yaw)
{
var p = math.radians(pitch);
var y = math.radians(yaw);
var x = -h / math.tan(p) * math.sin(y);
var z = -h / math.tan(p) * math.cos(y);
return new float3(x, h, z);
}
// ---分割线---
var h = camCom.RealHeight;
var y = camCom.RealYaw;
var center = new float3(camCom.RealCenter.x, 0, camCom.RealCenter.y);
var p = math.remap(MinHeight, MaxHeight, MinPitch, MaxPitch, h);
var pos = center + CalcPosition(h, p, y);
var rot = new float3(p, y, 0);
mainCamera.transform.position = pos;
mainCamera.rotation = quaternion.Euler(math.radians(rot));

存在的问题

在最前面”操作方式”一节提到,我现在旋转摄像机视角是用的QE两个键,其实本来按照习惯的话我是想用按住鼠标中键然后移动鼠标的。然而根据现在的插值方式,当鼠标移动越来越快时,视角会突然往反方向旋转,因为当前的RealYawYaw的差值已经达到了180°,插值算法的结果就是会反方向插值。解决方式可能是增加一个旋转方向的参数来固定插值方向。

总结

大概是用ECS的思想实现了一套RTS类的摄像机操作逻辑(外加一套输入系统),目前来看是满足需求了,之后有需要的话再继续迭代。

嗯。。通篇没图😂估计是没人感兴趣了233

接下来的话要准备做角色这一块了,也是个大头,先实现个简单的版本,之后再慢慢扩展~