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


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

真实第一人称视角?

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

超世纪战警:暗黑雅典娜

暴力辛迪加

镜之边缘

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

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

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

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


全身模型的设置

针对于全身模型,我们不使用隔离的动画系统。相反,我们使用一整个的全身模型来表现角色。对应的相机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,这层动画在这些动画层级之上。效果如下:

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

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

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

更新动画的Blending

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

  1. PlayerController里将游戏输入转化为旋转值
  2. Character中将世界空间下的旋转值转化为本地空间
  3. 根据本地空间的旋转值来驱动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 CharacterNewRotation.Yaw             = CameraProcessYaw( ControllerRotation.Yaw );NewRotation.Pitch           = CameraProcessPitch( ControllerRotation.Pitch + RecoilOffset );NewRotation.Normalize();// Clamp new rotationNewRotation.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 AnimBlueprintCameraLocalRotation = NewRotation;
}

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

float AExedreCharacter::CameraProcessPitch( float Input )
{//Recenter valueif( 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 CharacterFVector Direction1 = GetActorRotation().Vector();FVector Direction2 = FRotator(0.0f, Input, 0.0f).Vector();//Compute the Angle difference between the two dirrectionfloat 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 sideif( Dot > 0.0f ){Angle *= -1;}return Angle;
}

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

3. AnimBlueprint 更新逻辑

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


如何避免帧延迟

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

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

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

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

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

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

播放Montages

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

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

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

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

这并不是我们想要的效果。发生这种情况的原因是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 finishFRotator 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.以下是最终效果:

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

防止运动眩晕的方法

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

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

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

<全文完>

Unreal Engine 4 —— 在UE4中实现真实第一人称相机相关推荐

  1. ue4 离线渲染_[译]Real Shading in Unreal Engine 4(UE4中的真实渲染)(2)

    利用假期接着把剩下的部分搞完吧,其实,纯粹的翻译应该还是比较简单的,但是,为了更好地理解,我一般都会多找一些资料来进行互相印证.在上面一部分Shader Model的改变过程中,我主要是参考了一些PB ...

  2. UE4中使用真实天空插件——TrueSky

    UE4中使用真实天空插件--TrueSky 1.简述 TrueSKY是一个软件开发套件,可以在各种平台上实时渲染逼真的天空,云彩和大气效果.经过多年研究和开发,它基于光散射和吸收的物理原理,并针对速度 ...

  3. 零基础Unreal Engine 4(UE4)图文笔记之准备篇(一)

    简介:十篇文章介绍虚幻4引擎的入门和基本内容,蓝图.材质.粒子效果.UI界面等 系列目录 准备篇 基本操作 材质入门 初步运行 简易交互门 多种开门方式 相对坐标.绝对坐标.世界坐标的含义 UI 粒子 ...

  4. 【UE4 第一人称射击游戏】01-真实的第一人称相机

    步骤: 1.首先在虚幻商城中下载动画初学者内容包 2.创建一个工程,命名为"FPSTutorial",然后将内容包添加到该工程中 大约20M 3.双击打开"ThirdPe ...

  5. python357左轮枪模内部结构图_CODOL武器与COD4678代中原型的第一人称枪模对比(手枪篇)...

    先说明一下,本贴选用了CODonline里的大部分武器的第一人称枪模和正作的MW.MW2.BO.MW3这四代中的原型枪进行对比(没有原型枪的就不上图了),由于我这会儿手头没有online所以借用了ca ...

  6. 零基础Unreal Engine 4(UE4)图文笔记之粒子系统(九)

    目录 创建粒子材质 创建粒子 Spawn(持续生成) 和 Burst(爆发生成) LiftTime属性 Inital size Inital Velocity Color over Life和Alph ...

  7. 零基础Unreal Engine 4(UE4)图文笔记之粒子系统

    1.我们需要创建两个东西,一个材质一个粒子.先打开材质,在制作粒子之前,我们首先需要自己创建一个粒子效果能用的材质 在材质编辑器中,修改细节中Blend Mode类型为Translucent,Shad ...

  8. 零基础Unreal Engine 4(UE4)图文笔记之基础篇-基本操作(二)

    目录 场景漫游 运用Geometry进行简单的场景 移动物体(W) 旋转物体(E) 缩放(R) 细节面板 Brush Settings--Brush Type Brush实例 组合(CTRL+G) 解 ...

  9. Unreal Engine 4(UE4)下载教程

    首先登陆到UE官方网站https://www.unrealengine.com  下载EpicGamesLauncherInstaller-2.1.3-2533468.msi文件 按照提示进行安装 如 ...

最新文章

  1. 难忘的一天——装操作系统(一)
  2. 产品线的长度宽度深度_LED照明经销商该如何规划自己的产品线
  3. 从工业云到工业互联网平台演进的五个阶段
  4. PHP大批量正则,php – 正则表达式匹配无限数量的选项
  5. [C++STL]list容器用法介绍
  6. P4320-道路相遇,P5058-[ZJOI2004]嗅探器【圆方树,LCA】
  7. oracle基本笔记整理
  8. vSphere Esxi5.1 创建共享磁盘
  9. sql top加不加括号的区别_SQL易错点大作战
  10. 完整简单c语言程序代码,初学C语言常用简单程序代码
  11. JanusGraph 数据模型
  12. html5电子时钟怎么往上移动,html5旋转 怎样用HTML5制作旋转时钟
  13. am调制解调仿真matlab,AM调制与解调仿真matlab
  14. Unity Kinect添加自定义姿势识别
  15. oracle dbms_lob trim,DBMS_LOB包基础应用
  16. 我的msn的博客 欢迎大家点击
  17. python制作自己的二维码
  18. 三菱服务器绝对位置,绝对位置控制指令
  19. 2022年Work-Life Balance能实现吗?
  20. 开源的在线html编辑器,22个国外的Web在线编辑器收集

热门文章

  1. 山东大学2020计算机二级考试,山东省2020年3月计算机二级考试报名通知
  2. 6个Java项目UML反向工程工具
  3. iapp进度条倒计时_人生进度条app(人生时间倒计时)V1.1 安卓版
  4. 【Linux】Linux脚本编程
  5. oracle oui异常
  6. kettle案例22-剪切字符串
  7. IDEA如何开始护眼模式
  8. 兄弟连每集观后感,一些经典镜头的回顾
  9. 全球各种域名后缀注册量TOP100排行榜
  10. 4个免费短视频素材网站,帮你提升90%效率