第三人称物理相机

前言

第三人称相机虽然说是以第三人称视角锁定角色,但是其表现形式也不尽相同,比如UE5中创建的默认第三人称模板中,相机只会随着角色的平移而平移,但是在很多游戏中,在角色进行平移时,相机还会有旋转操作,如图1所示:

图1 - 左侧是随着角色移动进行平移的相机,右侧是随着角色移动进行旋转的相机。

另外,由于第三人称相机的特性,相机本身也是一个独立的物体,因此可能出现相机与目标之间出现其他阻挡物,而如果不处理这种情况,这个时候目标就会被阻挡物挡住,图2展示了这种可能:

图2 - 相机被阻挡或者相机进入了其他物体

遇到这种情况的大部分处理方式是将相机放置到一个”合理”的仍然能看到角色的位置,如图3所示:

图3 - 将相机放到合理位置

这种处理方式至少不会让玩家看不到目标,但是也有可能导致相机与角色距离过近,让玩家近乎占满整个屏幕,但是这种做法更加符合物理,因此一般也称为物理相机。

也有少数游戏的做法,是将所有阻挡物改为半透明,让玩家仍然能在理想的距离下观察到目标,也有用XRay来显示被阻挡的角色的轮廓等实现方式。

本文主要阐述一下个人对物理相机的一些实现思路。

基本定义

首先要实现这样一个相机,我们需要一个相机控制器,所有相机相关的移动会在控制器中实现。

那么控制器需要定义哪些可调整参数呢?

  • 偏移offset:默认情况下会认为相机是看向目标位置的,不过为了更通用一些,我们可以添加一个偏移offset来调整相机实际观察的位置,也就是相机最终看向的点是:

    1
    Vector3 lookingAt = target.position + offset;

  • 理想距离ideal_distance:这个就是定义的相机与目标的距离,如果不考虑相机的弹性效果,那么相机将始终与目标保持这个距离,但是一般为了让相机的效果更加出色,会给相机添加“弹性”。

以上两个参数非常基本,接下来先来看一下相机的视角,如图4所示:

图4 - 相机的Yaw,Pitch,Roll

其中Yaw表示你摇头的方向,Pitch表示你点头的方向,Roll表示你摆头的方向。大部分情况下,我们只需要用到Yaw和Pitch(因为角色一般都是沿着重力方向站着的,即使站在斜坡上),少数特殊情况中会用到Roll(比如角色沿着法线方向站立),不过这里我们只考虑Yaw和Pitch。

Yaw和Pitch的更新一般通过鼠标滑动或者右摇杆来控制,而这两种输入都可以理解成一个二维的矢量,其中横轴用于控制Yaw,纵轴用于控制Pitch。

一般情况下,并不会对Yaw的范围做限制,也就是Yaw的范围就是-180度到180度,而Pitch会做限制,因为你抬头或者低头的时候应该是做不到超过某个角度的吧。

综上所述,下面整理出和视角相关的参数。

  • min_pitch:指相机能到达的最小Pitch,范围一般在-90度到0度之间。
  • max_pitch:指相机能到达的最大Pitch,范围一般在0度到90度之间。
  • yaw_speed:指相机进行Yaw旋转时的速度。
  • pitch_speed:指相机进行Pitch旋转时的速度。

另外一个可选的功能则是刚才有提到过的“弹性”,其实很多游戏中,相机会随着角色的跑动适当拉远或靠近,这样每次角色在开始跑动或者停止跑动时带来一种弹性的感觉,因为我不会截动图,可以自己去观察一些游戏中相机的表现,简而言之就是,如果角色朝前跑动,那么相机会有个往外弹的感觉,相反如果角色朝后跑动,那么相机会有个往内弹的感觉,如图5所示:

图5 - 相机的弹性移动

这里我们可以定义一个速度的阈值,如果目标的速度超过这个阈值,则相机开始产生弹性。

  • speed_threshold:相机发生弹性的参考速度。
  • bounce_duration:弹性从发生到停止所需时间,一般是一个较小的值。
  • bounce_distance:弹性发生的距离(假设内外一致)。

这里再简述一下过程,如果角色在相机正向的投影速度超过speed_threshold,则相机往后产生弹性,反之如果角色在相机背面的投影速度超过speed_threshold,则相机往前产生弹性,其他情况则会尝试复原回原本的理想距离。

至此,一些可调整参数就定义完毕了,接下来讲一下我的实现。

实现思路

虽然听起来逻辑有点复杂,但实际上可以建立一个非常简单的模型来实现这些功能,首先假想目标周围有一个球面,球面的半径假设为sphere_radius,而相机的最终位置就会落在这个球面上(没有阻挡时),那么如果以相机的观察点为起点往外发射射线(射线的方向向量为相机当前的背面方向),射线长度为sphere_radius,此时如果发生了碰撞,那么就将相机置于碰撞点,否则就将相机置于球面相交点,如图6所示:

图6 - 左侧无碰撞,右侧有碰撞。

因此,我们维护一个当前的相机朝向向量camera_direction,当出现视角输入时,我们只需要旋转这个朝向向量(注意设置的视角限制),然后做一次射线检测即可计算出相机的正确位置。

在角色移动时,根据你选择的是旋转策略还是平移策略,由于平移策略不需要旋转相机,因此比较简单,如果是旋转策略,则需要计算角色与相机在xz平面上的新向量,因为角色的高度可能发生变化,但是我们不希望Pitch受到角色高度的影响,因此只需要计算xz平面上的分量,然后再与原来的y分量(Pitch分量)合并成新的朝向向量即可。

在裁剪Pitch时,可以简单使用asin(camera_direction.y)得到当前向量的Pitch值,将其裁剪后再绕右向量旋转(始终在Local Space内旋转)。

因此我们定义了一个更新相机的函数update_camera(),输入为sphere_radius, camera_direction,输出为相机位置。

那么很显然,如果不考虑弹性的情况下,sphere_radius应该始终是ideal_distance。

而发生弹性时要做的事就很简单了,我们只需要根据目标当前的速度在camera_direction上的投影值来判断弹性方向,我的做法是维护一个累加器来动态增减弹性参数:

1
2
3
4
5
6
7
8
9
10
11
Vector3 velocity = trackingNode->get_velocity();
real_t projection = velocity.dot(_cameraForward);
if (projection > _bounceSpeed) {
_bouncingAcc = Math::min(real_t(_bouncingAcc + dt), _bounceDuration);
} else if (projection < -_bounceSpeed) {
_bouncingAcc = Math::max(real_t(_bouncingAcc - dt), -_bounceDuration);
} else if (_bouncingAcc > 0.0) {
_bouncingAcc = Math::max(real_t(_bouncingAcc - dt), DECIMAL_CORRECTION(0.0));
} else if (_bouncingAcc < 0.0) {
_bouncingAcc = Math::min(real_t(_bouncingAcc + dt), DECIMAL_CORRECTION(0.0));
}

将_bouncingAcc除以_bounceDuration后就可以得到一个-1到1范围的值(正负表示弹性方向),这样我们就可以选择不同的插值方式来更新sphere_radius,然后再update_camera()即可。

总结

实现方式当然多种多样,我所阐述的也只是我所能想到的比较容易理解的模型了。

具体的相机逻辑还是要根据自己的游戏类型去定义,这里我只是提供了一种较为常用的实现方式。