Unreal Engine 四 C++ PlatformerGame自定义角色控制器源代码分析
Unreal Engine 4 C++ PlatformerGame自定义角色控制器源代码分析
此方法是继承来的,它在角色从Walking模式切换到Falling模式时被调用。
每当需要更新角色的Walking状态时,PhysWalking都会被调用。这个方法的调用会比较频繁。它会根据角色的输入和当前的状态来更新当前角色的速度、是否滑行等状态。
先说下CurrentFloor是做什么用的,角色在Walking模式下,始终会站立在一个平面上,这个平面的数据就用CurrentFloor来描述。
上面计算出了滑行的速度损失,保存在了CurrentSlideVelocityReduction。在叠加出新的速度后,这个速度可能大于最大速度,也可能达不到最小速度,需要对其分别操作。大于,新的速度就是最大滑行速度,小于,新的速度就是最小滑行速度。
初始一些变量,设置角色的碰撞高度,然后通知Character播放开始滑行的动画,等等。
8.SetSlideCollisionHeight()和RestoreCollisionHeightAfterSlide()
此方法修改碰撞高度,然后根据需要,调整模型的偏移。
在还原碰撞高度的时候,需要测试当前是不是能够恢复,比如头顶上有个障碍,是不能够恢复的。
在当前角色面对的方向上赋予保存的速度值。
Unreal Engine 4 C++ PlatformerGame自定义角色控制器源代码分析
官方商店里有个PlatformerGame整个免费的游戏,是一个卷轴类的跑酷游戏。整个项目的角色控制器很有意思,可以跑、跳、滑行,很酷。这里来分析下它具体是如何实现的。
UCharacterMovementComponent这个类实现了角色控制器的大部分功能,PlatformerGame实现了一个自定义的MovmentComponent,叫UPlatformerPlayerMovementComp,它从标准的UCharacterMovementComponent继承,并扩展了它的行为。
1.构造函数
UPlatformerPlayerMovementComp::UPlatformerPlayerMovementComp(const class FPostConstructInitializeProperties& PCIP) : Super(PCIP) { MaxAcceleration = 200.0f; BrakingDecelerationWalking = MaxAcceleration; MaxWalkSpeed = 900.0f; SlideVelocityReduction = 30.0f; SlideHeight = 60.0f; SlideMeshRelativeLocationOffset = FVector(0.0f, 0.0f, 34.0f); bWantsSlideMeshRelativeLocationOffset = true; MinSlideSpeed = 200.0f; MaxSlideSpeed = MaxWalkSpeed + 200.0f; ModSpeedObstacleHit = 0.0f; ModSpeedLedgeGrab = 0.8f; }
MaxAcceleration是继承来的变量,它代表角色控制器的最大的加速度;
BrakingDecelerationWalking,这是角色在行走时,受到摩擦力的影响,产生的阻碍的加速度;
MaxWalkSpeed,角色行走的最大速度;
SlideVelicityReduction,角色在滑行时,单位时间内速度损失的量;
SlideHeight,角色在滑行的时候,角色控制器的高度;跟碰撞有关,滑行了,当人角色会矮一点,这样可以通过一些站立时不能通过的障碍;
SlideMeshRelativeLocationOffset,这是控制角色滑行,模型相对角色控制器的位移;
bWantsSlideMeshRelativeLocationOffset ,是否需要位移角色的模型;
MinSlideSpeed,角色必须初始有一个速度,才能够滑行起来,这是能够滑行的最小速度,低于这个速度是不能滑行;
MaxSlideSpeed,角色滑行所能够达到的最大速度;
ModSpeedObstacleHit,
ModSpeedLedgeGrab
2.重载StartFalling()
void UPlatformerPlayerMovementComp::StartFalling(int32 Iterations, float remainingTime, float timeTick, const FVector& Delta, const FVector& subLoc) { Super::StartFalling(Iterations, remainingTime, timeTick, Delta, subLoc); if (MovementMode == MOVE_Falling && IsSliding()) { TryToEndSlide(); } }
此方法是继承来的,它在角色从Walking模式切换到Falling模式时被调用。
我们假设,角色正在滑行,碰到前面一个悬崖,它开始下落,这时候,角色必须停止滑行,因为它马上就要开始下落了。以上代码便是实现此功能,在下落的时候,如果发现当前在滑行,就尝试停止滑行。
3.重载ScaleInputAcceleration,实现角色向右跑动
FVector UPlatformerPlayerMovementComp::ScaleInputAcceleration(const FVector& InputAcceleration) const { FVector NewAccel = InputAcceleration; APlatformerGameMode* GI = GetWorld()->GetAuthGameMode<APlatformerGameMode>(); if (GI && GI->IsRoundInProgress()) { NewAccel.X = 1.0f; } return Super::ScaleInputAcceleration(NewAccel); }游戏一开始,角色就会一直朝右开始跑,玩家只需要控制在合适的时候跳和滑行就行了。那怎么实现不用输入,就一直朝右跑呢?一直给角色一个加速的输入即可。这里判断当前是不是在游戏过程中,是的话,就始终给原始的InputAcceleration参数一个X方向的输入。就好像你一直按着方向键一样。
4.重载PhysWalking()
void UPlatformerPlayerMovementComp::PhysWalking(float deltaTime, int32 Iterations) { APlatformerCharacter* MyPawn = Cast<APlatformerCharacter>(PawnOwner); if (MyPawn) { const bool bWantsToSlide = MyPawn->WantsToSlide(); if (IsSliding()) { CalcCurrentSlideVelocityReduction(deltaTime); CalcSlideVelocity(Velocity); const float CurrentSpeedSq = Velocity.SizeSquared(); if (CurrentSpeedSq <= FMath::Square(MinSlideSpeed)) { // slide has min speed - try to end it TryToEndSlide(); } } else if (bWantsToSlide) { if (!IsFlying() && Velocity.Size() > MinSlideSpeed * 2.0f) // make sure pawn has some velocity { StartSlide(); } } } Super::PhysWalking(deltaTime, Iterations); }
每当需要更新角色的Walking状态时,PhysWalking都会被调用。这个方法的调用会比较频繁。它会根据角色的输入和当前的状态来更新当前角色的速度、是否滑行等状态。
首先,我们读取角色的输入bWantsToSlide,看是不是要滑行。
如果正在滑行,CalcCurrentSlideVelocityReduction(deltaTime)计算当前角色因为滑行而造成的速度损失。CalcSlideVelocity(),计算出当前滑行的速度。然后对当前的速度和能够滑行的最小速度进行比较,速度损失到无法再进行滑行的话,就停止滑行状态。
用户有输入,说开始滑行吧,那么检查下,看速度是不是达到了可以滑行的条件,再排除是不是在飞行,不是,那么StartSlide(),开始滑行吧。
5.CalcCurrentSlideVelocityReduction()计算滑行的速度损失
void UPlatformerPlayerMovementComp::CalcCurrentSlideVelocityReduction(float DeltaTime) { float ReductionCoef = 0.0f; const float FloorDotVelocity = FVector::DotProduct(CurrentFloor.HitResult.ImpactNormal, Velocity.SafeNormal()); const bool bNeedsSlopeAdjustment = (FloorDotVelocity != 0.0f); if (bNeedsSlopeAdjustment) { const float Multiplier = 1.0f + FMath::Abs<float>(FloorDotVelocity); if (FloorDotVelocity > 0.0f) { ReductionCoef += SlideVelocityReduction * Multiplier; // increasing speed when sliding down a slope } else { ReductionCoef -= SlideVelocityReduction * Multiplier; // reducing speed when sliding up a slope }+ } else { ReductionCoef -= SlideVelocityReduction; // reducing speed on flat ground } float TimeDilation = GetWorld()->GetWorldSettings()->GetEffectiveTimeDilation(); CurrentSlideVelocityReduction += (ReductionCoef * TimeDilation * DeltaTime); }
先说下CurrentFloor是做什么用的,角色在Walking模式下,始终会站立在一个平面上,这个平面的数据就用CurrentFloor来描述。
const float FloorDotVelocity = FVector::DotProduct(CurrentFloor.HitResult.ImpactNormal, Velocity.SafeNormal());
这一行,其实是在求解当前速度的方向和平面垂直方向的夹角的余弦,向量点乘,应该不难理解。
滑行时速度的损失的程度,和地面与当前速度方向的夹角有关系。你往坡上滑行,速度损失多点,往坡下滑行,相应的损失会少点。如果两个是垂直的,也就说是在一个平坦的面上,那就不用调整了。bNeedsSlopeAdjustment,就是做这个用的。
ReductionCoef是计算出的速度损失系数,最后把它叠加到CurrentSlideVelocityReduction。
6.CalcSlideVelocity()计算滑行速度
void UPlatformerPlayerMovementComp::CalcSlideVelocity(FVector& OutVelocity) const { const FVector VelocityDir = Velocity.SafeNormal(); FVector NewVelocity = Velocity + CurrentSlideVelocityReduction * VelocityDir; const float NewSpeedSq = NewVelocity.SizeSquared(); if (NewSpeedSq > FMath::Square(MaxSlideSpeed)) { NewVelocity = VelocityDir * MaxSlideSpeed; } else if (NewSpeedSq < FMath::Square(MinSlideSpeed)) { NewVelocity = VelocityDir * MinSlideSpeed; } OutVelocity = NewVelocity; }
上面计算出了滑行的速度损失,保存在了CurrentSlideVelocityReduction。在叠加出新的速度后,这个速度可能大于最大速度,也可能达不到最小速度,需要对其分别操作。大于,新的速度就是最大滑行速度,小于,新的速度就是最小滑行速度。
7.StartSlide()开始滑行
void UPlatformerPlayerMovementComp::StartSlide() { if (!bInSlide) { bInSlide = true; CurrentSlideVelocityReduction = 0.0f; SetSlideCollisionHeight(); APlatformerCharacter* MyOwner = Cast<APlatformerCharacter>(PawnOwner); if (MyOwner) { MyOwner->PlaySlideStarted(); } } }
初始一些变量,设置角色的碰撞高度,然后通知Character播放开始滑行的动画,等等。
void UPlatformerPlayerMovementComp::TryToEndSlide() { // end slide if collisions allow if (bInSlide) { if (RestoreCollisionHeightAfterSlide()) { bInSlide = false; APlatformerCharacter* MyOwner = Cast<APlatformerCharacter>(PawnOwner); if (MyOwner) { MyOwner->PlaySlideFinished(); } } } }
这个方法尝试结束滑行状态,需要把之前的碰撞高度还原,然后通知Character播放滑行结束的动画。
8.SetSlideCollisionHeight()和RestoreCollisionHeightAfterSlide()
void UPlatformerPlayerMovementComp::SetSlideCollisionHeight() { if (!CharacterOwner || SlideHeight <= 0.0f) { return; } // Do not perform if collision is already at desired size. if (CharacterOwner->CapsuleComponent->GetUnscaledCapsuleHalfHeight() == SlideHeight) { return; } // Change collision size to new value CharacterOwner->CapsuleComponent->SetCapsuleSize(CharacterOwner->CapsuleComponent->GetUnscaledCapsuleRadius(), SlideHeight); // applying correction to PawnOwner mesh relative location if (bWantsSlideMeshRelativeLocationOffset) { ACharacter* DefCharacter = CharacterOwner->GetClass()->GetDefaultObject<ACharacter>(); const FVector Correction = DefCharacter->Mesh->RelativeLocation + SlideMeshRelativeLocationOffset; CharacterOwner->Mesh->SetRelativeLocation(Correction); } }
此方法修改碰撞高度,然后根据需要,调整模型的偏移。
bool UPlatformerPlayerMovementComp::RestoreCollisionHeightAfterSlide() { ACharacter* CharacterOwner = Cast<ACharacter>(PawnOwner); if (!CharacterOwner) { return false; } ACharacter* DefCharacter = CharacterOwner->GetClass()->GetDefaultObject<ACharacter>(); const float DefHalfHeight = DefCharacter->CapsuleComponent->GetUnscaledCapsuleHalfHeight(); const float DefRadius = DefCharacter->CapsuleComponent->GetUnscaledCapsuleRadius(); // Do not perform if collision is already at desired size. if (CharacterOwner->CapsuleComponent->GetUnscaledCapsuleHalfHeight() == DefHalfHeight) { return true; } const float HeightAdjust = DefHalfHeight - CharacterOwner->CapsuleComponent->GetUnscaledCapsuleHalfHeight(); const FVector NewLocation = CharacterOwner->GetActorLocation() + FVector(0.0f, 0.0f, HeightAdjust); // check if there is enough space for default capsule size FCollisionQueryParams TraceParams(TEXT("FinishSlide"), false, CharacterOwner); FCollisionResponseParams ResponseParam; InitCollisionParams(TraceParams, ResponseParam); const bool bBlocked = GetWorld()->OverlapTest(NewLocation, FQuat::Identity, UpdatedComponent->GetCollisionObjectType(), FCollisionShape::MakeCapsule(DefRadius, DefHalfHeight), TraceParams); if (bBlocked) { return false; } // restore capsule size and move up to adjusted location CharacterOwner->TeleportTo(NewLocation, CharacterOwner->GetActorRotation(), false, true); CharacterOwner->CapsuleComponent->SetCapsuleSize(DefRadius, DefHalfHeight); // restoring original PawnOwner mesh relative location if (bWantsSlideMeshRelativeLocationOffset) { CharacterOwner->Mesh->SetRelativeLocation(DefCharacter->Mesh->RelativeLocation); } return true; }
在还原碰撞高度的时候,需要测试当前是不是能够恢复,比如头顶上有个障碍,是不能够恢复的。
const bool bBlocked = GetWorld()->OverlapTest(NewLocation, FQuat::Identity, UpdatedComponent->GetCollisionObjectType(), FCollisionShape::MakeCapsule(DefRadius, DefHalfHeight), TraceParams);
这一行,对要恢复碰撞的那个位置进行测试,看有没有东西覆盖,没有就把角色传送到此位置。
最后恢复角色模型的偏移。
9.碰到障碍和碰到抓取边缘的处理
void UPlatformerPlayerMovementComp::PauseMovementForObstacleHit() { SavedSpeed = Velocity.Size() * ModSpeedObstacleHit; StopMovementImmediately(); DisableMovement(); TryToEndSlide(); } void UPlatformerPlayerMovementComp::PauseMovementForLedgeGrab() { SavedSpeed = Velocity.Size() * ModSpeedLedgeGrab; StopMovementImmediately(); DisableMovement(); TryToEndSlide(); }PauseMovementForObstacleHit()处理当遇到障碍时的情况。
PauseMovementForLedgeGrab()处理当抓取障碍边缘时的情况。
都是保存当前的速度,然后停止移动,停止滑行。
10.RestoreMovement()恢复移动
void UPlatformerPlayerMovementComp::RestoreMovement() { SetMovementMode(MOVE_Walking); if (SavedSpeed > 0) { Velocity = PawnOwner->GetActorRotation().Vector() * SavedSpeed; } }
在当前角色面对的方向上赋予保存的速度值。