Unreal Engine 4 C++ PlatformerGame自定义角色控制器源代码分析

官方商店里有个PlatformerGame整个免费的游戏,是一个卷轴类的跑酷游戏。整个项目的角色控制器很有意思,可以跑、跳、滑行,很酷。这里来分析下它具体是如何实现的。

UCharacterMovementComponent这个类实现了角色控制器的大部分功能,PlatformerGame实现了一个自定义的MovmentComponent,叫UPlatformerPlayerMovementComp,它从标准的UCharacterMovementComponent继承,并扩展了它的行为。

1.构造函数


[cpp]  
1. UPlatformerPlayerMovementComp::UPlatformerPlayerMovementComp(const class FPostConstructInitializeProperties& PCIP)   
2.     : Super(PCIP)  
3. {  
4.     MaxAcceleration = 200.0f;  
5.     BrakingDecelerationWalking = MaxAcceleration;  
6.     MaxWalkSpeed = 900.0f;  
7.   
8.     SlideVelocityReduction = 30.0f;  
9.     SlideHeight = 60.0f;  
10.     SlideMeshRelativeLocationOffset = FVector(0.0f, 0.0f, 34.0f);  
11. true;  
12.     MinSlideSpeed = 200.0f;  
13.     MaxSlideSpeed = MaxWalkSpeed + 200.0f;  
14.   
15.     ModSpeedObstacleHit = 0.0f;  
16.     ModSpeedLedgeGrab = 0.8f;  
17. }

MaxAcceleration是继承来的变量,它代表角色控制器的最大的加速度;

BrakingDecelerationWalking,这是角色在行走时,受到摩擦力的影响,产生的阻碍的加速度;

MaxWalkSpeed,角色行走的最大速度;

SlideVelicityReduction,角色在滑行时,单位时间内速度损失的量;

SlideHeight,角色在滑行的时候,角色控制器的高度;跟碰撞有关,滑行了,当人角色会矮一点,这样可以通过一些站立时不能通过的障碍;

SlideMeshRelativeLocationOffset,这是控制角色滑行,模型相对角色控制器的位移;

bWantsSlideMeshRelativeLocationOffset ,是否需要位移角色的模型;

MinSlideSpeed,角色必须初始有一个速度,才能够滑行起来,这是能够滑行的最小速度,低于这个速度是不能滑行;

MaxSlideSpeed,角色滑行所能够达到的最大速度;

ModSpeedObstacleHit,

ModSpeedLedgeGrab

2.重载StartFalling()

[cpp]  
1. void UPlatformerPlayerMovementComp::StartFalling(int32 Iterations, float remainingTime, float timeTick, const FVector& Delta, const FVector& subLoc)  
2. {  
3.     Super::StartFalling(Iterations, remainingTime, timeTick, Delta, subLoc);  
4.   
5. if (MovementMode == MOVE_Falling && IsSliding())  
6.     {  
7.         TryToEndSlide();  
8.     }  
9. }

此方法是继承来的,它在角色从Walking模式切换到Falling模式时被调用。

我们假设,角色正在滑行,碰到前面一个悬崖,它开始下落,这时候,角色必须停止滑行,因为它马上就要开始下落了。以上代码便是实现此功能,在下落的时候,如果发现当前在滑行,就尝试停止滑行。

3.重载ScaleInputAcceleration,实现角色向右跑动

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. FVector UPlatformerPlayerMovementComp::ScaleInputAcceleration(const FVector& InputAcceleration) const  
2. {  
3.     FVector NewAccel = InputAcceleration;  
4.   
5.     APlatformerGameMode* GI = GetWorld()->GetAuthGameMode<APlatformerGameMode>();  
6. if (GI && GI->IsRoundInProgress())  
7.     {  
8.         NewAccel.X = 1.0f;  
9.     }  
10.   
11. return Super::ScaleInputAcceleration(NewAccel);  
12. }

4.重载PhysWalking()

[cpp]  
     view plain 
      copy 
     
 
     
 
      
   
1. void UPlatformerPlayerMovementComp::PhysWalking(float deltaTime, int32 Iterations)  
2. {  
3.     APlatformerCharacter* MyPawn = Cast<APlatformerCharacter>(PawnOwner);  
4. if (MyPawn)  
5.     {  
6. const bool bWantsToSlide = MyPawn->WantsToSlide();  
7. if (IsSliding())  
8.         {  
9.             CalcCurrentSlideVelocityReduction(deltaTime);  
10.             CalcSlideVelocity(Velocity);  
11.   
12. const float CurrentSpeedSq = Velocity.SizeSquared();  
13. if (CurrentSpeedSq <= FMath::Square(MinSlideSpeed))  
14.             {  
15. // slide has min speed - try to end it  
16.                 TryToEndSlide();  
17.             }  
18.         }  
19. else if (bWantsToSlide)  
20.         {  
21. if (!IsFlying() &&  
22. // make sure pawn has some velocity  
23.             {  
24.                 StartSlide();  
25.             }  
26.         }  
27.     }  
28.   
29.     Super::PhysWalking(deltaTime, Iterations);  
30. }

每当需要更新角色的Walking状态时,PhysWalking都会被调用。这个方法的调用会比较频繁。它会根据角色的输入和当前的状态来更新当前角色的速度、是否滑行等状态。

首先,我们读取角色的输入bWantsToSlide,看是不是要滑行。

如果正在滑行,CalcCurrentSlideVelocityReduction(deltaTime)计算当前角色因为滑行而造成的速度损失。CalcSlideVelocity(),计算出当前滑行的速度。然后对当前的速度和能够滑行的最小速度进行比较,速度损失到无法再进行滑行的话,就停止滑行状态。

用户有输入,说开始滑行吧,那么检查下,看速度是不是达到了可以滑行的条件,再排除是不是在飞行,不是,那么StartSlide(),开始滑行吧。

5.CalcCurrentSlideVelocityReduction()计算滑行的速度损失

[cpp]  
     view plain 
      copy 
     
 
     
 
      
   
1. void UPlatformerPlayerMovementComp::CalcCurrentSlideVelocityReduction(float DeltaTime)  
2. {  
3. float ReductionCoef = 0.0f;  
4.   
5. const float FloorDotVelocity = FVector::DotProduct(CurrentFloor.HitResult.ImpactNormal, Velocity.SafeNormal());  
6. const bool bNeedsSlopeAdjustment = (FloorDotVelocity != 0.0f);  
7.   
8. if (bNeedsSlopeAdjustment)  
9.     {  
10. const float Multiplier = 1.0f + FMath::Abs<float>(FloorDotVelocity);  
11. if (FloorDotVelocity > 0.0f)  
12.         {  
13. // increasing speed when sliding down a slope  
14.         }  
15. else  
16.         {  
17. // reducing speed when sliding up a slope  
18.         }+  
19.     }  
20. else  
21.     {  
22. // reducing speed on flat ground  
23.     }  
24.   
25. float TimeDilation = GetWorld()->GetWorldSettings()->GetEffectiveTimeDilation();  
26.     CurrentSlideVelocityReduction += (ReductionCoef * TimeDilation * DeltaTime);  
27. }

先说下CurrentFloor是做什么用的,角色在Walking模式下,始终会站立在一个平面上,这个平面的数据就用CurrentFloor来描述。

[cpp]  
     view plain 
      copy 
     
 
     
 
      
   
1. const float FloorDotVelocity = FVector::DotProduct(CurrentFloor.HitResult.ImpactNormal, Velocity.SafeNormal());

这一行,其实是在求解当前速度的方向和平面垂直方向的夹角的余弦,向量点乘,应该不难理解。

滑行时速度的损失的程度,和地面与当前速度方向的夹角有关系。你往坡上滑行,速度损失多点,往坡下滑行,相应的损失会少点。如果两个是垂直的,也就说是在一个平坦的面上,那就不用调整了。bNeedsSlopeAdjustment,就是做这个用的。

ReductionCoef是计算出的速度损失系数,最后把它叠加到CurrentSlideVelocityReduction。

6.CalcSlideVelocity()计算滑行速度

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. void UPlatformerPlayerMovementComp::CalcSlideVelocity(FVector& OutVelocity) const  
2. {  
3. const FVector VelocityDir = Velocity.SafeNormal();  
4.     FVector NewVelocity = Velocity + CurrentSlideVelocityReduction * VelocityDir;  
5.       
6. const float NewSpeedSq = NewVelocity.SizeSquared();  
7. if (NewSpeedSq > FMath::Square(MaxSlideSpeed))  
8.     {  
9.         NewVelocity = VelocityDir * MaxSlideSpeed;  
10.     }  
11. else if (NewSpeedSq < FMath::Square(MinSlideSpeed))  
12.     {  
13.         NewVelocity = VelocityDir * MinSlideSpeed;  
14.     }  
15.   
16.     OutVelocity = NewVelocity;  
17. }

上面计算出了滑行的速度损失,保存在了CurrentSlideVelocityReduction。在叠加出新的速度后,这个速度可能大于最大速度,也可能达不到最小速度,需要对其分别操作。大于,新的速度就是最大滑行速度,小于,新的速度就是最小滑行速度。

7.StartSlide()开始滑行

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. void UPlatformerPlayerMovementComp::StartSlide()  
2. {  
3. if (!bInSlide)  
4.     {  
5. true;  
6.         CurrentSlideVelocityReduction = 0.0f;  
7.         SetSlideCollisionHeight();  
8.   
9.         APlatformerCharacter* MyOwner = Cast<APlatformerCharacter>(PawnOwner);  
10. if (MyOwner)  
11.         {  
12.             MyOwner->PlaySlideStarted();  
13.         }  
14.     }  
15. }

初始一些变量,设置角色的碰撞高度,然后通知Character播放开始滑行的动画,等等。

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. void UPlatformerPlayerMovementComp::TryToEndSlide()  
2. {  
3. // end slide if collisions allow  
4. if (bInSlide)  
5.     {  
6. if (RestoreCollisionHeightAfterSlide())  
7.         {  
8. false;  
9.   
10.             APlatformerCharacter* MyOwner = Cast<APlatformerCharacter>(PawnOwner);  
11. if (MyOwner)  
12.             {  
13.                 MyOwner->PlaySlideFinished();  
14.             }  
15.         }  
16.     }  
17. }

这个方法尝试结束滑行状态,需要把之前的碰撞高度还原,然后通知Character播放滑行结束的动画。

8.SetSlideCollisionHeight()和RestoreCollisionHeightAfterSlide()

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. void UPlatformerPlayerMovementComp::SetSlideCollisionHeight()  
2. {  
3. if (!CharacterOwner || SlideHeight <= 0.0f)  
4.     {  
5. return;  
6.     }  
7.   
8. // Do not perform if collision is already at desired size.  
9. if (CharacterOwner->CapsuleComponent->GetUnscaledCapsuleHalfHeight() == SlideHeight)  
10.     {  
11. return;  
12.     }  
13.   
14. // Change collision size to new value  
15.     CharacterOwner->CapsuleComponent->SetCapsuleSize(CharacterOwner->CapsuleComponent->GetUnscaledCapsuleRadius(), SlideHeight);  
16.   
17. // applying correction to PawnOwner mesh relative location  
18. if (bWantsSlideMeshRelativeLocationOffset)    
19.     {  
20.         ACharacter* DefCharacter = CharacterOwner->GetClass()->GetDefaultObject<ACharacter>();  
21. const FVector Correction = DefCharacter->Mesh->RelativeLocation + SlideMeshRelativeLocationOffset;  
22.         CharacterOwner->Mesh->SetRelativeLocation(Correction);  
23.     }  
24. }

此方法修改碰撞高度,然后根据需要,调整模型的偏移。

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. bool UPlatformerPlayerMovementComp::RestoreCollisionHeightAfterSlide()  
2. {  
3.     ACharacter* CharacterOwner = Cast<ACharacter>(PawnOwner);  
4. if (!CharacterOwner)  
5.     {  
6. return false;  
7.     }  
8.   
9.     ACharacter* DefCharacter = CharacterOwner->GetClass()->GetDefaultObject<ACharacter>();  
10. const float DefHalfHeight = DefCharacter->CapsuleComponent->GetUnscaledCapsuleHalfHeight();  
11. const float DefRadius = DefCharacter->CapsuleComponent->GetUnscaledCapsuleRadius();  
12.   
13. // Do not perform if collision is already at desired size.  
14. if (CharacterOwner->CapsuleComponent->GetUnscaledCapsuleHalfHeight() == DefHalfHeight)  
15.     {  
16. return true;  
17.     }     
18.   
19. const float HeightAdjust = DefHalfHeight - CharacterOwner->CapsuleComponent->GetUnscaledCapsuleHalfHeight();  
20. const FVector NewLocation = CharacterOwner->GetActorLocation() + FVector(0.0f, 0.0f, HeightAdjust);  
21.   
22. // check if there is enough space for default capsule size  
23. "FinishSlide"), false, CharacterOwner);  
24.     FCollisionResponseParams ResponseParam;  
25.     InitCollisionParams(TraceParams, ResponseParam);  
26. const bool bBlocked = GetWorld()->OverlapTest(NewLocation, FQuat::Identity, UpdatedComponent->GetCollisionObjectType(), FCollisionShape::MakeCapsule(DefRadius, DefHalfHeight), TraceParams);  
27. if (bBlocked)  
28.     {  
29. return false;  
30.     }  
31.   
32. // restore capsule size and move up to adjusted location  
33. false, true);  
34.     CharacterOwner->CapsuleComponent->SetCapsuleSize(DefRadius, DefHalfHeight);  
35.   
36. // restoring original PawnOwner mesh relative location  
37. if (bWantsSlideMeshRelativeLocationOffset)  
38.     {  
39.         CharacterOwner->Mesh->SetRelativeLocation(DefCharacter->Mesh->RelativeLocation);  
40.     }  
41.   
42. return true;  
43. }
  1.  

在还原碰撞高度的时候,需要测试当前是不是能够恢复,比如头顶上有个障碍,是不能够恢复的。

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. const bool bBlocked = GetWorld()->OverlapTest(NewLocation, FQuat::Identity, UpdatedComponent->GetCollisionObjectType(), FCollisionShape::MakeCapsule(DefRadius, DefHalfHeight), TraceParams);
  1.   

这一行,对要恢复碰撞的那个位置进行测试,看有没有东西覆盖,没有就把角色传送到此位置。

最后恢复角色模型的偏移。

9.碰到障碍和碰到抓取边缘的处理

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. void UPlatformerPlayerMovementComp::PauseMovementForObstacleHit()  
2. {  
3.     SavedSpeed = Velocity.Size() * ModSpeedObstacleHit;  
4.   
5.     StopMovementImmediately();  
6.     DisableMovement();  
7.     TryToEndSlide();   
8. }  
9.   
10. void UPlatformerPlayerMovementComp::PauseMovementForLedgeGrab()  
11. {  
12.     SavedSpeed = Velocity.Size() * ModSpeedLedgeGrab;  
13.   
14.     StopMovementImmediately();  
15.     DisableMovement();  
16.     TryToEndSlide();  
17. }

PauseMovementForLedgeGrab()处理当抓取障碍边缘时的情况。

都是保存当前的速度,然后停止移动,停止滑行。

10.RestoreMovement()恢复移动

[cpp]  
    view plain 
     copy 
    
 
    
 
     
  
1. void UPlatformerPlayerMovementComp::RestoreMovement()  
2. {  
3.     SetMovementMode(MOVE_Walking);  
4.   
5. if (SavedSpeed > 0)  
6.     {  
7.         Velocity = PawnOwner->GetActorRotation().Vector() * SavedSpeed;  
8.     }  
9. }

在当前角色面对的方向上赋予保存的速度值。