Unreal Engine 4 —— 在UE4中實現真實第一人稱相機
這篇部落格來自於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
的值,我通過下面三步來進行處理:
- 在
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
需要注意的是,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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
函式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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
(譯者按:使用尤拉角真的沒問題嗎?永珍的話該怎麼辦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
還沒更新。
為了解決這個問題,我並沒有在Character
的Tick
函式中執行PreUpdateCamera()
方法,我將這個方法的呼叫放在PlayerController
的Tick
函式中。通過這樣的方法,我確保了對應的值是實時最新的。
播放Montages
整體來講,這個系統已經可以工作了。下一步就是去播放一個可以作用於整個身體的動畫。為了做到這一點,我們使用AnimMontage。在這個專案中,我需要讓人物在落地後,播放一個重力緩衝的動畫。該動畫如下:
程式碼很簡單,可能在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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
這段程式碼做的事是取消玩家的輸入,然後播放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 finish
FRotator TargetControl = EPC->GetControlRotation();
TargetControl.Pitch = 0.0f;
float BlenSpeed = 300.0f;
TargetControl = FMath::RInterpConstantTo( EPC->GetControlRotation(), TargetControl, DeltaTime, BlenSpeed);
EPC->SetControlRotation( TargetControl );
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
以上的程式碼只是在下落過程中,在本地相機的旋轉值計算之前,將其Pitch值通過RInterpConstantTo()
函式逐漸設為0.以下是最終效果:
相比來講好多了。在此之外,可以再做一個在Montage結尾的時候,將其設回最初的Rotation,但是這個在這個專案中並不太重要。
防止運動眩暈的方法
最後一點,當使用全身動畫時,需要注意那些針對頭部的運動操作。不停點頭、快速轉身之類的快速動畫容易使得玩家感到噁心。因此跑步和走路的動畫需要儘可能的穩定。這一點和VR中的眩暈很類似——產生這種眩暈的原因是玩家的感覺和看到的東西並不一致。
在我的專案中,我針對了大部分的重複動畫(例如跑步)使用了一個方法——將玩家的角色進行約束,讓其總是看著很遠處的一個固定點。這樣的方法能夠使得頭部儘量聚焦於一點,從而穩定相機。
在AnimationBP的這一層之後,你可以使用一些額外的處理來進行身體動畫的操作。這樣做的好處是可以很好的進行狀態之間的切換,並且減少眩暈感。
<全文完>