官方商店里有个PlatformerGame整个免费的游戏,是一个卷轴类的跑酷游戏。整个项目的角色控制器很有意思,可以跑、跳、滑行,很酷。这里来分析下它具体是如何实现的。
UCharacterMovementComponent这个类实现了角色控制器的大部分功能,PlatformerGame实现了一个自定义的MovmentComponent,叫UPlatformerPlayerMovementComp,它从标准的UCharacterMovementComponent继承,并扩展了它的行为。
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; }
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(); } }
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方向的输入。就好像你一直按着方向键一样。
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); }
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); }
const float FloorDotVelocity = FVector::DotProduct(CurrentFloor.HitResult.ImpactNormal, Velocity.SafeNormal());
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; }
void UPlatformerPlayerMovementComp::StartSlide() { if (!bInSlide) { bInSlide = true; CurrentSlideVelocityReduction = 0.0f; SetSlideCollisionHeight(); APlatformerCharacter* MyOwner = Cast<APlatformerCharacter>(PawnOwner); if (MyOwner) { MyOwner->PlaySlideStarted(); } } }
void UPlatformerPlayerMovementComp::TryToEndSlide() { // end slide if collisions allow if (bInSlide) { if (RestoreCollisionHeightAfterSlide()) { bInSlide = false; APlatformerCharacter* MyOwner = Cast<APlatformerCharacter>(PawnOwner); if (MyOwner) { MyOwner->PlaySlideFinished(); } } } }
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);
void UPlatformerPlayerMovementComp::PauseMovementForObstacleHit() { SavedSpeed = Velocity.Size() * ModSpeedObstacleHit; StopMovementImmediately(); DisableMovement(); TryToEndSlide(); } void UPlatformerPlayerMovementComp::PauseMovementForLedgeGrab() { SavedSpeed = Velocity.Size() * ModSpeedLedgeGrab; StopMovementImmediately(); DisableMovement(); TryToEndSlide(); }PauseMovementForObstacleHit()处理当遇到障碍时的情况。
void UPlatformerPlayerMovementComp::RestoreMovement() { SetMovementMode(MOVE_Walking); if (SavedSpeed > 0) { Velocity = PawnOwner->GetActorRotation().Vector() * SavedSpeed; } }