一.摄像机工作原理
在游戏中,摄像机是玩家的眼睛,他控制了玩家的视点(POV即PointOfView,后面简称POV)位置以及玩家的视野大小(FOV即FieldOfView,后面简称FOV)。一句话,摄像机决定了我们去观察这个游戏世界。
游戏的类型多种多样,有第一人称的FPS游戏,有第三人称的动作游戏,还有需要统筹全局来观察的RTS游戏。简单来说,第一人称就是把POV放在人眼睛的位置,第三人称就是把POV放在人身后一定距离的位置,RTS就是把POV放到离地面很高的位置。这里说POV要比说摄像机更为准确,因为有的时候摄像机只是一个为了方便大家理解与观察的实体,其实他的位置可以随意,不过为了便于大家理解与使用,我们一般将摄像机位置与视点位置同步。下图的摄像机位置即为POV的位置,紫色的框即为FOV。
图1-1值为90的FOV
图1-2值为40的FOV
因为POV在游戏中是会随时变化的,所以我们需要在Tick里面去更新他的位置。所以,从本质上讲,调整摄像机就是不断地更新POV的位置,这样我们也能平滑流畅的观察游戏世界。如果,我们想要切换不同的视角,那就切换我们的POV。如果想要做一些镜头移动特效,就可以利用一些插值的算法来处理POV的位置。如果想要做一些视觉特效,可以直接在摄像机上加一些后处理效果。总之,还是那句话,摄像机决定你如何观察游戏世界。
二.UE4摄像机关系梳理
首先放一张摄像机相关类的关系图,
图2-1摄像机相关类图
下面开始一一分析各个类之间的关系。
由于大家一般会先去看官方文档,这里就按照官方文档的顺序来解释。
2.1 CameraComponent和CameraActor
CameraComponent组件一般放在控制角色的根节点上,常规的行为与属性都可以在其中进行设置包括POV位置(即组件的位置),FOV,观察模式(分为正交模式和透视模式),宽高比,后处理效果等。
而CameraActor的作用更为简单,就是将CameraComponent组件封装到一个Actor里面来使其可以直接放到一个Level里面。
图2-2Mods里面可以放置Camera
CameraComponent组件里面还包括另外两个组件,一个是UDrawFrustumComponent,另一个是UStaticMeshComponent。UStaticMeshComponent大家很好理解,用来渲染Actor的对象模型,而UDrawFrustumComponent是用来显示摄像机视窗的。(图1-1的紫框,只在编辑器下有)。
从名字上理解,我们可以叫他为摄像机管理器,那么他管理的是什么?答案就是视点POV。POV位置真正的计算过程是在这里进行的,你可以在这里处理不同人称视角的POV计算流程,处理摄像机震动与碰撞,也可以对摄像机增加镜头粒子特效(溅血效果等)。
顾名思义,即观察目标,也就是我们想让摄像机跟随的对象。对于一般的第三人称游戏,我们的摄像机一直是对准着我们控制的角色的,这个角色可以认为就是ViewTarget(不过ViewTarget不仅仅是一个Actor)。ViewTarget定义在PlayerCameraManager里面,负责提供给Manger一个理想的POV。一个ViewTarget结构包含一个Actor对象,该Actor对应的POV信息以及当前的PlayerState。换句话讲,我们一般通过ViewTarget包含的Actor的位置来计算摄像机POV的位置,并把计算后的结果再存储到ViewTarget里面的POV。
图2-3ViewTarget内部结构
2.3 CameraComponent,CamerActor与PlayerCameraManger
经过前面的介绍,我们知道PlayerCameraManager通过绑定一个ViewTarget来计算POV的位置。一般来说,我们将CameraComponent放进的Actor就是我们理想的ViewTarget。同理,拥有CameraComponent的CameraActor也同样是ViewTarget。
PlayerCameraManager一般有几个摄像机模式(CameraStyle),如下图
而在构造时他默认的模式是另一个
默认模式下,PlayerCameraManager在更新POV的时候会调用Actor的CalcCamera。CalcCamera先判断是否有CameraComponent并且bFindCameraComponentWhenViewTarget是否为true,是的话就获取CameraComponent的位置与朝向,否则就获取VIewTarget的Actor的坐标与朝向来更新视点POV的信息。
而其他的模式需要玩家去进一步做详细的计算。
如果当前的ViewTarget是一个CameraActor的话,PlayerCameraManager就会直接获取CameraComponent的位置与朝向来更新POV。
2.4 PlayerController与PlayerCameraManger
我们知道,在UE4里面每个Pawn都会有一个对应的Controller,我们自然想让Controller拥有控制Rotation(朝向)的功能(为什么不控制Location?因为Location跟随Viewtarget就可以了)。所以这里有一个UpdateRotation函数来控制朝向。那么他是如何影响到PlayerCameraManager里面对视点的计算呢?
我们可以再回头看一下图2-1,通过观察,我们可以看到一个PlayerController里面有一个PlayerCameraManager属性。当我们旋转鼠标时,这个偏移量会通过AddControllerYawInput与AddControllerPitchInput传入RotationInput。在PlayerController执行UpdateRotation时会获取PlayerCameraManager并通过ProcessViewRotation计算出移动后(也就是将RotationInput计算进去)的Rotation,把这个Rotation赋值给ControlRotation以及Controller所控制的Pawn(也就包括他身上的CameraComponent组件)。
图2-4Controller接收鼠标旋转流程
最后,经过上面流程,我们的CameraComponent朝向已经被修改,PlayerCameraManager在更新POV的Rotation时获取CameraComponent的朝向即可。(这个就是UE4默认情况下的摄像机处理流程)
假设我们没有任何CameraComponent组件呢?这其实也很简单,在图2-1可以看到一个PlayerCameraManager类会有一个PlayerController属性,这样PlayerCameraManager就可以随时获取当前ViewTarget对应的PlayerController。而且我们在图2-4的流程里面其实已经通过SerControlRotation的获取了当前摄像机计算后的朝向,所以在PlayerCameraManager更新POV朝向的时候,获取Controller的ControlRotation既可。这样,我们在更新POV朝向的时候就不需要获取CameraComponent的朝向了,没有借助任何CameraComponent或是CameraActor。
注:我们看到图2.4里面最后的两步是处理角色朝向的。经过该图里面的处理,玩家会随着摄像机的旋转而旋转。但是目前很多流行的游戏是通过玩家方向键来控制朝向的。
三.总结与梳理
通过上面的介绍,我们基本上了解了摄像机相关类之间的关系。这里再重新梳理一下,
如果我们想实现摄像机的控制,有两个基本方案。
方案一、UE4官方教程,将一个CameraComponent放到我们想控制目标的身上。然后通过获取摄像机组件的位置与朝向更新POV。
方案二、控制的目标可以任意,Viewtarget身上没有CameraComponent。PlayercameraManager通过获取ViewTarget的位置+一定的偏移(PlayerCameraManager有一个TPVCameraOffset和一个FreeCamOffset)来确定POV的位置。而对于朝向,PlayerCameraManager通过获取PlayerController的ControlRotation既可。
我们要知道,PlayerCameraManager这里的计算其实是偏底层一些的。有的时候我们不需要去修改这里的计算也可以处理一些摄像机的设置,比如旋转角度限制,自定义的计算POV朝向这些可以写在PlayerController的CalcCamera里,并在UpdateCameraRotation里面调用。总而言之,我们需要弄清的就是视点POV的位置与朝向到底是如何计算的。
最后再总结一下,摄像机POV计算的要点有两个
一、 你要保证你的计算过程是在Update里面进行的
二、 你要在PlayercameraManager计算POV的时候能获取到你更新后的位置与旋转
有了上面两个条件,你就可以平滑的更新POV的位置与朝向了。
注:到这里,我们已经发现了两个CalcCamera,一个在Actor里面,是用来获取Actor或者他身上的CameraComponent组件位置与朝向的。另一个是PlayerController里的,用来进行自定义对摄像机POV进行处理计算的,默认是不调用的。
图3-1摄像机更新视点POV流程
四摄像机使用细节
.
4.1 摄像机位置处理
2.4节我们简单提到过一种摄像机的使用流程。游戏初始化后,PlayerCameraManager会把玩家角色的Pawn设置为ViewTarget,然后获取ViewTarget的坐标作为一个基础值。基础值加上玩家身上的CameraOffset就得到了视点POV的位置。不过这里的CameraOffset是相对于ViewTarget局部坐标系的偏移,还要根据当前的角色的朝向以及Mutiplier来做进一步处理。
如果想让玩家在不同状态下调整不同的视角,可以增加多个CameraOffset(如果射击CameraOffset)。然后在不同的状态下切换不同的CameraOffset即可。
4.1 摄像机FOV处理
在很多游戏里面,如果使用弓箭,枪一类的武器,可以进行瞄准。瞄准的实现方法就是修改摄像机的FOV。可以在武器上设置一个OverrideFOV属性,通过在瞄准时更新当前的FOV可以实现瞄准的效果。
五.摄像机特殊处理
5.1 摄像机震动
图5-1摄像机调整相关类图
这里我们把摄像机相关类图再拿出来看一下。观察到有一个继承于UObject的UCameraModifer类,他的作用就是对摄像机进行相关的调整,这里我们定义任意的Modify类型来继承于UCameraModifer,但实际上我们最常用的就是UCameraModifier_CameraShake(即摄像机震动),他是真正负责执行摄像机调整的类。
在PlayerCameraManager更新POV信息的时候,或根据条件判断是否调用CamraModifier,在该类中有一个TArray<classUCameraModifier*>ModifierList;来保存当前所有的Modifier。遍历的时候如果找到Modifier就会执行对应的修改。
同时,我们还可以看到一个UCameraShake类,这个类就是用来设置摄像机震动参数的类,包括震动时间震动幅度等。比如,在UGameplayStatics::PlayWorldCameraShake里面。我们就需要传入一个TSubclassOf<classUCameraShake>Shake参数。PlayerController里面也有一个ClientPlayCameraShake方法让客户端去调用,里面也需要传入UCameraShake类型。其实,执行摄像机震动的基本原理就是根据传入的UCameraShake参数来添加一个UCameraModifier_CameraShake。
在游戏里面,我们可能调用摄像机震动的情况(及实现方法)有
1. 靠近大型NPC(在NPC的动画蓝图里面添加Notify,通知客户端执行)
2. 开枪(在武器上绑定UCameraShake)
3. 玩家受伤
4. 挥动武器击中目标时(在武器上绑定UCameraShake)
还有很多情况,根据项目需求
5.2 摄像机镜头粒子特效
在PlayerCameraManager类定义中我们可以看到这样一个属性,他通过在摄像机前面绑定一个粒子特效来实现滴血等效果。通过ClientSpawnCameraLensEffect_Implementation可以使用。效果如下
图5-3CameraLensEffect实现效果
5.3 摄像机碰撞
摄像机的碰撞的逻辑可以写在了PlayerCameraManager的UpdateViewTarget里面(也就是更新视点POV信息的地方),他需要在最后做一个盒型的碰撞检测,如果碰到符合通道条件的物体就会更新视点的坐标。
5.4 摄像机模式切换
前面介绍了UE4默认提供了几种摄像机模式给开发者使用,我们常用的有自由摄像机、第一人称、第三人称等。旁观者模式是类似一种自由摄像机的模式。
PlayerCameraManager里面在更新POV时会判断当前的CameraStyle,从而进行不同的计算,比如我们从第三人称视角切换到第一人称视角,POV相对玩家的位置就会改变(从背后变为眼睛附近)。
有一点也要注意一下,不同摄像机的碰撞检测通道可能是不同的。
5.5 摄像机后处理
UE4里面的后处理可以通过两种方法完成(另一篇博客简单的介绍了后处理 UE4后处理简述。)。第一种是通过在场景里面添加PostProcessVolume,然后摄像机处于该体积内才能产生效果。第二种是在摄像机组件里面添加与设置,这样就不需要把摄像机放在某个特别的位置。前面图4-3的第一张图就是经过高斯模糊以及颜色混合的后处理效果。
5.6 摄像机平滑与延迟Lag
在大部分游戏中,为了得到更好的游戏体验,摄像机的移动都是平滑的。因为摄像机是在每帧都进行更新,所以我们只要保持与我们的ViewTarget固定的距离,就可以得到平滑的效果。
但是如果我们想从当前摄像机立刻切换到另一个摄像机机位,或者我们的ViewTarget发生瞬移,这时候如果不做任何处理,摄像机就会突然的切换(有的时候这样也没什么不好)。假如我们觉得这样切换有点突然,我们只要简单的处理一下就可以了。设置一个插值,让当前的位置逐渐插值到目标位置。(FMath::VInterpTo(当前位置,目标位置, DeltaTime,速度);)
同理,如果想做一个摄像机延迟效果(就是玩家可能突然用技能走出一大段位移,为了体现效果,想让摄像机慢慢的追上玩家),也可以使用类似的方法来实现。
如果有兴趣,可以参考一下UE4第三人称官方例程,然后找到弹簧组件,搜索EnableCameraLag属性,试试效果。当然也可以到代码里面看看他实现的细节。
六.其他
1.UE4里面的DoUpdateCamera会处理相机viewTarget切换,如果当前摄像机要切换到另一个就在这里处理插值与混合,这个时候就不应该调用UpdateViewTarget。要切换的对象是PendingViewTarget,混合参数为BlendParams。
2.PlayerCameraManager是通过void APlayerController::SpawnPlayerCameraManager()生成的
3.把CameraManager的位置与旋转与视点同步到一起。这样,我们也可以通过摄像机的Manager获取视点的相关信息。同理,把PlayerController的朝向与视点同步,我们也可以获取PlayerController的朝向来获取视点的朝向。
4.下面流程图描述了角色是如何被PlayerCameraManger设置为ViewTarget的。
图6-1设置玩家控制的Pawn为ViewTarget流程图