Unreal Engine 4

您所在的位置:网站首页 第一人称睁眼游戏视频 Unreal Engine 4

Unreal Engine 4

2024-06-12 21:59| 来源: 网络整理| 查看: 265

这篇博客来自于Fabrice Piquet,翻译工作已获得作者授权,原文传送门。

我决定分享一下我在当前项目中处理真实第一人称相机的方法。针对真实第一人称视角,目前没有太多相关的文档。因此研究一段时间过后,尤其是当前项目中我花了不少时间去解决一些存在的难题过后,我决定写一篇相关的文章。项目中最终的效果如下:

FirstPersonCamera

真实第一人称视角?

何为真实第一人称(True First Person,TFP)呢?在某些场景下,它也被称为“身体意识(Body Awareness)”。相对于仅仅是一个悬浮的相机来说,它是一个真实运动的,模拟角色真实身体运动的身体的第一人称视角。拥有这类视角的游戏有:

超世纪战警:暗黑雅典娜 The Chronicles of Riddick: Assault on Dark Athena 暴力辛迪加 Syndicate 镜之边缘 Mirror's Edge Mirror's Edge2

我避免的东西:分开手臂和身体

在一个手臂和身体分离的系统中,角色的两只手和身体是分开的,从而直接将手臂attach到相机上。这样可以在确保手是跟随相机进行运动的同时,还能够对手臂进行操作。身体的剩下的部分通常也是分开的,它们通常也会有自己的动画系统。

这个系统的问题在于在做一个全身动画(例个重力缓冲效果)的时候,它要求这两个独立的动画系统进行严格的同步(这对动画的制作以及在引擎中的逻辑都有着对应的要求)。有时候游戏使用一个只对操纵玩家可见的模型来模拟,全身的模型用于渲染角色的阴影(以及在多人游戏中对于其他玩家显示,在最近的使命召唤系列游戏中,这种方法运用的比较多)。

如果对于优化以及特殊表现有着比较高的要求,那么这种方法是适合的。但是如果游戏追求可信度和沉浸感,那么我并不推荐这种方式。由于这并不是我需要的方法,因此针对于这种方式我并不准备介绍过多。再说了现在互联网上有很多很多相关的教程,这里就不再赘述了。

Arms Arms_Weapon

全身模型的设置

针对于全身模型,我们不使用隔离的动画系统。相反,我们使用一整个的全身模型来表现角色。对应的相机attach在头部,这也就意味着由你的身体动画来驱动它。我们不直接进行相机的位置或朝向的数值修改,最终,整个类的架构如下:

PlayerControler -> Character -> Mesh -> AnimBlueprint -> Camera

针对PlayerController,其实没什么好说的,在UE中它总是在Character或者Pawn之上。Character有一个表示身体的Mesh,而这个Mesh有一个针对全身骨骼进行操作的AnimBlueprint。最后,我们有一个在Constructor中attach到头上的相机。

那么现在相机已经attach到头上了,我们完成了吗?当然没有。因为相机是由骨骼驱动的,我们需要实现基本的相机操作:向上下左右看。可以通过使用Additive animation来制作。所谓的Additive animation是一帧的动画,用于把各个骨骼的offset给apply上去。总体来说,我是用了10个动画,当然你可以使用更多的pose,但是我发现更多的动画就不再必要了。

在我们的项目中,我设置当玩家向左/右看时,整个人的身体也会向左/右转(就像上面的镜之边缘的gif图一样)。此外,还有一个专门为角色idle设置的additional animation,这层动画在这些动画层级之上。效果如下:

Aim offset

当这些动画被成功导入引擎中后,我们需要设置一些东西。首先起一个好名字,来确保自己日后能够找到它。在我们的项目中,我将其命名为“anim_idle_additive_base”。针对其他的pose动画,我将其进行Additive Setting。具体来讲就是将Additive Anim Type参数设定为Mesh Space,并且将Base Pose Type设定为Selected Animation。最后,将Base Pose Animation设定好即可。针对每个Pose重复以上过程即可。

Additive Settings

将动画资源准备好后,就可以创建Aim Offset了。Aim Offset指的是允许开发者依据输入的参数,在多个动画中进行平滑Blending操作的东西。针对更多的内容,可以参考官方的文档:Aim Offset。当设定完毕后,效果如下: Aim Offset

我自己的Aim Offset使用两个参数进行驱动:Pitch和Yaw。这两个数值在代码内进行逻辑更新,细节如下: Aim input Aim Graph

更新动画的Blending

我们需要将玩家针对相机的输入转化为驱动Aim Offset的值,我通过下面三步来进行处理:

在PlayerController里将游戏输入转化为旋转值在Character中将世界空间下的旋转值转化为本地空间根据本地空间的旋转值来驱动Anim Blueprint 1. PlayerController Input

当玩家移动鼠标或者手柄摇杆时,我需要将这些值在PlayerController中接收,并通过重写UpdateRotation()函数转化为对应的旋转值。

void AExedrePlayerController::UpdateRotation(float DeltaTime) { if( !IsCameraInputEnabled() ) return; float Time = DeltaTime * (1 / GetActorTimeDilation()); FRotator DeltaRot(0,0,0); DeltaRot.Yaw = GetPlayerCameraInput().X * (ViewYawSpeed * Time); DeltaRot.Pitch = GetPlayerCameraInput().Y * (ViewPitchSpeed * Time); DeltaRot.Roll = 0.0f; RotationInput = DeltaRot; Super::UpdateRotation(DeltaTime); }

需要注意的是,UpdateRotation方法在PlayerController类中每帧都会调用一次。我考虑了GetActorTimeDilation()函数,因此当使用slomo方法时,相机转动的速度不会变动。

2. 在Character中的相机控制

我的Character类中有一个PreUpdateCamera()函数,该函数如下:

void AExedreCharacter::PreUpdateCamera( float DeltaTime ) { if( !FirstPersonCameraComponent || !EPC || !EMC ) return; //------------------------------------------------------- // Compute rotation for Mesh AIM Offset //------------------------------------------------------- FRotator ControllerRotation = EPC->GetControlRotation(); FRotator NewRotation = ControllerRotation; // Get current controller rotation and process it to match the Character NewRotation.Yaw = CameraProcessYaw( ControllerRotation.Yaw ); NewRotation.Pitch = CameraProcessPitch( ControllerRotation.Pitch + RecoilOffset ); NewRotation.Normalize(); // Clamp new rotation NewRotation.Pitch = FMath::Clamp( NewRotation.Pitch, -90.0f + CameraTreshold, 90.0f - CameraTreshold); NewRotation.Yaw = FMath::Clamp( NewRotation.Yaw, -91.0f, 91.0f); //Update loca variable, will be retrived by AnimBlueprint CameraLocalRotation = NewRotation; }

函数CameraProcessYaw()和CameraProcessPitch()将Controller的世界坐标系旋转值转化为本地坐标系下的旋转值。这两个函数如下:

float AExedreCharacter::CameraProcessPitch( float Input ) { //Recenter value if( Input > 269.99f ) { Input -= 270.0f; Input = 90.0f - Input; Input *= -1.0f; } return Input; } float AExedreCharacter::CameraProcessYaw( float Input ) { //Get direction vector from Controller and Character FVector Direction1 = GetActorRotation().Vector(); FVector Direction2 = FRotator(0.0f, Input, 0.0f).Vector(); //Compute the Angle difference between the two dirrection float Angle = FMath::Acos( FVector::DotProduct(Direction1, Direction2) ); Angle = FMath::RadiansToDegrees( Angle ); //Find on which side is the angle difference (left or right) FRotator Temp = GetActorRotation() - FRotator(0.0f, 90.0f, 0.0f); FVector Direction3 = Temp.Vector(); float Dot = FVector::DotProduct( Direction3, Direction2 ); //Invert angle to switch side if( Dot > 0.0f ) { Angle *= -1; } return Angle; }

(译者按:使用欧拉角真的没问题吗?万象的话该怎么办orz)

3. AnimBlueprint 更新逻辑

最后一步也是最简单的一步,我通过Event Blueprint Update Animation节点来获取上述的值,并且将其作为Aim Offset的控制变量:

AnimationBP_Update AnimationBP_Anim

如何避免帧延迟

这个问题有时很多人并不重视,但是这的确是个问题。如果你是按照上面的设置走下来的并且你不是太清楚Tick()函数在UE中是怎么运作的,你会遇到这个问题:有一帧的延迟。

这一帧的延迟会很蛋疼,而且有可能会造成很糟糕的游戏体验——基本上来讲这一帧的相机总是会基于上一帧的数据。这意味着如果你快速移动鼠标然后突然停止,那么实际上你会在下一帧才停止。无论你的帧率是多少,这个问题都会存在。

解决这个问题的方案需要对Tick函数有一些理解,在默认状况下,Tick函数执行顺序如下:

_ _ _ _ _ UpdateTimeAndHandleMaxTickRate (Engine function) _ _ _ _ _ Tick_PlayerController _ _ _ _ _ Tick_SkeletalMeshComponent _ _ _ _ _ Tick_AnimInstance _ _ _ _ _ Tick_GameMode _ _ _ _ _ Tick_Character _ _ _ _ _ Tick_Camera

那么在这里发生了什么事呢?可以看见Character类的Tick顺序是在AnimBlueprint之后的,这意味着在这一帧的AnimBlueprint更新时,对应的Character还没更新。

为了解决这个问题,我并没有在Character的Tick函数中执行PreUpdateCamera()方法,我将这个方法的调用放在PlayerController的Tick函数中。通过这样的方法,我确保了对应的值是实时最新的。

播放Montages

整体来讲,这个系统已经可以工作了。下一步就是去播放一个可以作用于整个身体的动画。为了做到这一点,我们使用AnimMontage。在这个项目中,我需要让人物在落地后,播放一个重力缓冲的动画。该动画如下:

Fall_Overview

代码很简单,可能在Blueprint中更简单:

void AExedreCharacter::PlayAnimLanding() { if( MeshBody != nullptr ) { if( EPC != nullptr ) { EPC->SetMovementInputEnabled( false ); EPC->SetCameraInputEnabled( false ); EPC->ResetFallingTime(); } //Snap mesh FRotator TargetRotation = FRotator::ZeroRotator; if( EPC != nullptr ) { TargetRotation.Yaw = EPC->GetControlRotation().Yaw; } else { TargetRotation.Yaw = GetActorRotation().Yaw; } SetActorRotation( TargetRotation ); //Start anim SetPerformingMontage(true); TotalMontageDuration = MeshBody->AnimScriptInstance->Montage_Play(AnmMtgLandingFall, 1.0f); LatestMontageDuration = TotalMontageDuration; //Set Timer to the end of the duration FTimerHandle TimeHandler; this->GetWorldTimerManager().SetTimer(TimeHandler, this, &AExedreCharacter::PlayAnimLandingExit, TotalMontageDuration - 0.01f, false); } }

这段代码做的事是取消玩家的输入,然后播放Montage。我设定了一个Timer,从而在动画结束的时候重新开启输入。如果你是这么做的,那么你会获得这样的结果: Fall_Wrong

这并不是我们想要的效果。发生这种情况的原因是Anim slot先于Anim Offset节点就被设置了。因此当播放全身动画时,这个aim offset就直接被加上去了。因此如果玩家看着地面再播放这个动画,那么这个偏移就会变成双份。

那么我们为什么要将Aim offset放在之后进行计算呢?实际上这只是为了在状态之间进行更顺滑的切换。如果在Aim offset之后再进行montage的播放,那么整个的切换会非常尖锐。

为了解决这个问题,我将Camera Rotation值进行了一次重置。我在PreUpdateCamera函数中加入了如下代码:

//------------------------------------------------------- // Blend Pitch to 0.0 if we are performing a montage (input are disabled) //------------------------------------------------------- if( IsPerformingMontage() ) { //Reset camera rotation to 0 for when the Montage finish FRotator TargetControl = EPC->GetControlRotation(); TargetControl.Pitch = 0.0f; float BlenSpeed = 300.0f; TargetControl = FMath::RInterpConstantTo( EPC->GetControlRotation(), TargetControl, DeltaTime, BlenSpeed); EPC->SetControlRotation( TargetControl ); }

以上的代码只是在下落过程中,在本地相机的旋转值计算之前,将其Pitch值通过RInterpConstantTo()函数逐渐设为0.以下是最终效果:

Fall_Good

相比来讲好多了。在此之外,可以再做一个在Montage结尾的时候,将其设回最初的Rotation,但是这个在这个项目中并不太重要。

防止运动眩晕的方法

最后一点,当使用全身动画时,需要注意那些针对头部的运动操作。不停点头、快速转身之类的快速动画容易使得玩家感到恶心。因此跑步和走路的动画需要尽可能的稳定。这一点和VR中的眩晕很类似——产生这种眩晕的原因是玩家的感觉和看到的东西并不一致。

在我的项目中,我针对了大部分的重复动画(例如跑步)使用了一个方法——将玩家的角色进行约束,让其总是看着很远处的一个固定点。这样的方法能够使得头部尽量聚焦于一点,从而稳定相机。 Animation Constraint

在AnimationBP的这一层之后,你可以使用一些额外的处理来进行身体动画的操作。这样做的好处是可以很好的进行状态之间的切换,并且减少眩晕感。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3