Created a working minimal implementation using UMovementComponent. Some getter / setters, IsValid checks instead of ptr checks, decoupling could still be improved on but that is all actually exactly copied from UFloatingPawnMovementComponent just as is. Quite surprised that THIS much had to be implemented over UMovementComponent and that movement can not be decoupled from APawn. Tis what it is…
.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/MovementComponent.h"
#include "MyMovementComponent.generated.h"
class USceneComponent;
class AActor;
class APawn;
struct FHitResult;
/*
* Gravity is not implemented.
*/
UCLASS(editinlinenew, BlueprintType, Blueprintable, Within = "Pawn", meta = (BlueprintSpawnableComponent))
class MY_API UMyMovementComponent : public UMovementComponent {
GENERATED_BODY()
private:
// Pawn
/** Pawn that owns this component. */
UPROPERTY(Transient, DuplicateTransient)
TObjectPtr<APawn> PawnOwner;
// Movement
/** Maximum velocity magnitude allowed for the controlled Pawn. */
UPROPERTY(EditAnywhere, Category = "Movement")
float MaxSpeed = 1200.f;
/** Acceleration applied by input (rate of change of velocity) */
UPROPERTY(EditAnywhere, Category = "Movement")
float Acceleration = 4000.f;
/** Deceleration applied when there is no input (rate of change of velocity) */
UPROPERTY(EditAnywhere, Category = "Movement")
float Deceleration = 8000.f;
/**
* Setting affecting extra force applied when changing direction, making turns have less drift and become more responsive.
* Velocity magnitude is not allowed to increase, that only happens due to normal acceleration. It may decrease with large direction changes.
* Larger values apply extra force to reach the target direction more quickly, while a zero value disables any extra turn force.
*/
UPROPERTY(EditAnywhere, Category = "Movement", meta = (ClampMin = "0", UIMin = "0"))
float TurningBoost = 8.0f;
protected:
// Movement
/** Set to true when a position correction is applied. Used to avoid recalculating velocity when this occurs. */
UPROPERTY(Transient)
bool bPositionCorrected = false;
public:
private:
// Tick
void UpdateMovementInput();
void UpdateVelocity(float InDeltaTime);
protected:
// Tick
void ApplyControlInputToVelocity(float InDeltaTime);
// Pawn
/** Returns this component's associated controller, typically from the owning Pawn. May be null. May be overridden for special handling when the controller isn't paired with the Pawn that owns this component. */
virtual AController* GetController() const;
// Camera
/**
* Attempts to mark the PlayerCameraManager as dirty, if the controller has one.
* This will have no effect if called from the server.
*/
void MarkForClientCameraUpdate();
// Movement
/** Prevent Pawn from leaving the world bounds (if that restriction is enabled in WorldSettings) */
virtual bool LimitWorldBounds();
virtual bool ResolvePenetrationImpl(const FVector& InAdjustment, const FHitResult& InHit, const FQuat& InNewRotation) override;
public:
// Setup
virtual void Serialize(FArchive& Ar) override;
// Tick
virtual void TickComponent(float InDeltaTime, enum ELevelTick InTickType, FActorComponentTickFunction* InThisTickFunction) override;
// Pawn
/** Overridden to only allow registration with components owned by a Pawn. */
virtual void SetUpdatedComponent(USceneComponent* NewUpdatedComponent) override;
/**
* Adds the given vector to the accumulated input in world space. Input vectors are usually between 0 and 1 in magnitude.
* They are accumulated during a frame then applied as acceleration during the movement update.
*
* @param WorldDirection Direction in world space to apply input
* @param ScaleValue Scale to apply to input. This can be used for analog input, ie a value of 0.5 applies half the normal value.
* @param bForce If true always add the input, ignoring the result of IsMoveInputIgnored().
* @see APawn::AddMovementInput()
*/
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|PawnMovement")
virtual void AddInputVector(FVector WorldVector, bool bForce = false);
/**
* Return the pending input vector in world space. This is the most up-to-date value of the input vector, pending ConsumeMovementInputVector() which clears it.
* PawnMovementComponents implementing movement usually want to use either this or ConsumeInputVector() as these functions represent the most recent state of input.
* @return The pending input vector in world space.
* @see AddInputVector(), ConsumeInputVector(), GetLastInputVector()
*/
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|PawnMovement", meta = (Keywords = "GetInput"))
FVector GetPendingInputVector() const;
/**
* Return the last input vector in world space that was processed by ConsumeInputVector(), which is usually done by the Pawn or PawnMovementComponent.
* Any user that needs to know about the input that last affected movement should use this function.
* @return The last input vector in world space that was processed by ConsumeInputVector().
* @see AddInputVector(), ConsumeInputVector(), GetPendingInputVector()
*/
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|PawnMovement", meta = (Keywords = "GetInput"))
FVector GetLastInputVector() const;
/* Returns the pending input vector and resets it to zero.
* This should be used during a movement update (by the Pawn or PawnMovementComponent) to prevent accumulation of control input between frames.
* Copies the pending input vector to the saved input vector (GetLastMovementInputVector()).
* @return The pending input vector.
*/
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|PawnMovement")
virtual FVector ConsumeInputVector();
/** Helper to see if move input is ignored. If there is no Pawn or UpdatedComponent, returns true, otherwise defers to the Pawn implementation of IsMoveInputIgnored(). */
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|PawnMovement")
virtual bool IsMoveInputIgnored() const;
/** Return the Pawn that owns UpdatedComponent. */
UFUNCTION(BlueprintCallable, Category = "Pawn|Components|PawnMovement")
class APawn* GetPawnOwner() const;
virtual void OnTeleported() override;
// Movement
virtual float GetMaxSpeed() const override;
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "Movement")
FVector GetForwardMovementInputDirection() const;
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "Movement")
FVector GetRightMovementInputDirection() const;
};
.cpp
#include "MyMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "Camera/PlayerCameraManager.h"
#include "Kismet/KismetMathLibrary.h"
#include "GameFramework/Actor.h"
#include "InputState.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/HitResult.h"
#include "GameFramework/Pawn.h"
// Setup
void UMyMovementComponent::Serialize(FArchive& Ar) {
APawn* CurrentPawnOwner = PawnOwner;
Super::Serialize(Ar);
if (Ar.IsLoading()) {
// This was marked Transient so it wont be saved out, but we need still to reject old saved values.
PawnOwner = CurrentPawnOwner;
}
}
// Tick
void UMyMovementComponent::TickComponent(float InDeltaTime, enum ELevelTick InTickType, FActorComponentTickFunction* InThisTickFunction) {
Super::TickComponent(InDeltaTime, InTickType, InThisTickFunction);
// TODO, other movement components call Super only if not ShouldSkipUpdate. Seems wrong.
if (!ShouldSkipUpdate(InDeltaTime)) {
UpdateVelocity(InDeltaTime);
}
}
void UMyMovementComponent::ApplyControlInputToVelocity(float InDeltaTime) {
const FVector ControlAcceleration = GetPendingInputVector().GetClampedToMaxSize(1.f);
const float AnalogInputModifier = (ControlAcceleration.SizeSquared() > 0.f ? ControlAcceleration.Size() : 0.f);
const float MaxPawnSpeed = GetMaxSpeed() * AnalogInputModifier;
const bool bExceedingMaxSpeed = IsExceedingMaxSpeed(MaxPawnSpeed);
if (AnalogInputModifier > 0.f && !bExceedingMaxSpeed) {
// Apply change in velocity direction
if (Velocity.SizeSquared() > 0.f) {
// Change direction faster than only using acceleration, but never increase velocity magnitude.
const float TimeScale = FMath::Clamp(InDeltaTime * TurningBoost, 0.f, 1.f);
Velocity = Velocity + (ControlAcceleration * Velocity.Size() - Velocity) * TimeScale;
}
}
else {
// Dampen velocity magnitude based on deceleration.
if (Velocity.SizeSquared() > 0.f) {
const FVector OldVelocity = Velocity;
const float VelSize = FMath::Max(Velocity.Size() - FMath::Abs(Deceleration) * InDeltaTime, 0.f);
Velocity = Velocity.GetSafeNormal() * VelSize;
// Dont allow braking to lower us below max speed if we started above it.
if (bExceedingMaxSpeed && Velocity.SizeSquared() < FMath::Square(MaxPawnSpeed))
{
Velocity = OldVelocity.GetSafeNormal() * MaxPawnSpeed;
}
}
}
// Apply acceleration and clamp velocity magnitude.
const float NewMaxSpeed = (IsExceedingMaxSpeed(MaxPawnSpeed)) ? Velocity.Size() : MaxPawnSpeed;
Velocity += ControlAcceleration * FMath::Abs(Acceleration) * InDeltaTime;
Velocity = Velocity.GetClampedToMaxSize(NewMaxSpeed);
ConsumeInputVector();
}
void UMyMovementComponent::UpdateVelocity(float InDeltaTime) {
UpdateMovementInput();
if (!IsValid(PawnOwner) || !IsValid(UpdatedComponent)) {
return;
}
const AController* Controller = PawnOwner->GetController();
if (IsValid(Controller) && Controller->IsLocalController()) {
// apply input for local players but also for AI thats not following a navigation path at the moment
if (Controller->IsLocalPlayerController()) {
ApplyControlInputToVelocity(InDeltaTime);
}
// if its not player controller, but we do have a controller, then its AI
// (thats not following a path) and we need to limit the speed
else if (IsExceedingMaxSpeed(MaxSpeed)) {
Velocity = Velocity.GetUnsafeNormal() * MaxSpeed;
}
LimitWorldBounds();
bPositionCorrected = false;
// Move actor
FVector DeltaVelocity = Velocity * InDeltaTime;
if (!DeltaVelocity.IsNearlyZero(1e-6f)) {
const FVector OldLocation = UpdatedComponent->GetComponentLocation();
const FQuat Rotation = UpdatedComponent->GetComponentQuat();
FHitResult Hit(1.f);
SafeMoveUpdatedComponent(DeltaVelocity, Rotation, true, Hit);
if (Hit.IsValidBlockingHit()) {
HandleImpact(Hit, InDeltaTime, DeltaVelocity);
// Try to slide the remaining distance along the surface.
SlideAlongSurface(DeltaVelocity, 1.f - Hit.Time, Hit.Normal, Hit, true);
}
// Update velocity
// We dont want position changes to vastly reverse our direction (which can happen due to penetration fixups etc)
if (!bPositionCorrected) {
const FVector NewLocation = UpdatedComponent->GetComponentLocation();
Velocity = ((NewLocation - OldLocation) / InDeltaTime);
}
}
// Finalize
UpdateComponentVelocity();
}
}
void UMyMovementComponent::UpdateMovementInput() {
// Calculate whatever movement input vector you desire, could be read from input on the owning pawns input component.
const ForwardVal = OwnerPawn->InputComponent->GetAxisValue("MoveForward");
OwnerPawn->AddMovementInput(GetForwardMovementInputDirection(), ForwardVal);
}
// Pawn
AController* UMyMovementComponent::GetController() const {
if (PawnOwner) {
return PawnOwner->GetController();
}
return nullptr;
}
void UMyMovementComponent::SetUpdatedComponent(USceneComponent* NewUpdatedComponent) {
if (NewUpdatedComponent) {
if (!ensureMsgf(Cast<APawn>(NewUpdatedComponent->GetOwner()), TEXT("%s must update a component owned by a Pawn"), *GetName())) {
return;
}
}
Super::SetUpdatedComponent(NewUpdatedComponent);
PawnOwner = UpdatedComponent ? CastChecked<APawn>(UpdatedComponent->GetOwner()) : NULL;
}
APawn* UMyMovementComponent::GetPawnOwner() const {
return PawnOwner;
}
bool UMyMovementComponent::IsMoveInputIgnored() const {
if (UpdatedComponent) {
if (PawnOwner) {
return PawnOwner->IsMoveInputIgnored();
}
}
// No UpdatedComponent or Pawn, no movement.
return true;
}
void UMyMovementComponent::AddInputVector(FVector WorldAccel, bool bForce) {
if (PawnOwner)
{
PawnOwner->Internal_AddMovementInput(WorldAccel, bForce);
}
}
FVector UMyMovementComponent::GetPendingInputVector() const {
return PawnOwner ? PawnOwner->Internal_GetPendingMovementInputVector() : FVector::ZeroVector;
}
FVector UMyMovementComponent::GetLastInputVector() const {
return PawnOwner ? PawnOwner->Internal_GetLastMovementInputVector() : FVector::ZeroVector;
}
FVector UMyMovementComponent::ConsumeInputVector() {
return PawnOwner ? PawnOwner->Internal_ConsumeMovementInputVector() : FVector::ZeroVector;
}
void UMyMovementComponent::OnTeleported() {
if (PawnOwner && PawnOwner->IsNetMode(NM_Client) && PawnOwner->IsLocallyControlled()) {
MarkForClientCameraUpdate();
}
}
// Camera
void UMyMovementComponent::MarkForClientCameraUpdate() {
if (APlayerController* PlayerController = Cast<APlayerController>(GetController())) {
APlayerCameraManager* PlayerCameraManager = PlayerController->PlayerCameraManager;
if (PlayerCameraManager != nullptr && PlayerCameraManager->bUseClientSideCameraUpdates) {
PlayerCameraManager->bShouldSendClientSideCameraUpdate = true;
}
}
}
// Movement
bool UMyMovementComponent::LimitWorldBounds() {
AWorldSettings* WorldSettings = PawnOwner ? PawnOwner->GetWorldSettings() : NULL;
if (!WorldSettings || !WorldSettings->AreWorldBoundsChecksEnabled() || !UpdatedComponent) {
return false;
}
const FVector CurrentLocation = UpdatedComponent->GetComponentLocation();
if (CurrentLocation.Z < WorldSettings->KillZ) {
Velocity.Z = FMath::Min<FVector::FReal>(GetMaxSpeed(), WorldSettings->KillZ - CurrentLocation.Z + 2.0f);
return true;
}
return false;
}
bool UMyMovementComponent::ResolvePenetrationImpl(const FVector& InAdjustment, const FHitResult& InHit, const FQuat& InNewRotationQuat) {
bPositionCorrected |= Super::ResolvePenetrationImpl(InAdjustment, InHit, InNewRotationQuat);
return bPositionCorrected;
}
float UMyMovementComponent::GetMaxSpeed() const {
return MaxSpeed;
}
FVector UMyMovementComponent::GetForwardMovementInputDirection() const {
APawn* OwnerPawn = Cast<APawn>(GetOwner());
APlayerController* PC = IsValid(OwnerPawn) ? Cast<APlayerController>(OwnerPawn->GetController()) : nullptr;
if (!IsValid(PC) || !IsValid(PC->PlayerCameraManager)) {
return UKismetMathLibrary::GetForwardVector(OwnerPawn->GetActorRotation());
}
// Top down camera, so we get the up vector.
// Todo is some finetuning required when camera tilts or different axis are used.
return UKismetMathLibrary::GetUpVector(PC->PlayerCameraManager->GetCameraRotation());
}
FVector UMyMovementComponent::GetRightMovementInputDirection() const {
APawn* OwnerPawn = Cast<APawn>(GetOwner());
APlayerController* PC = IsValid(OwnerPawn) ? Cast<APlayerController>(OwnerPawn->GetController()) : nullptr;
if (!IsValid(PC) || !IsValid(PC->PlayerCameraManager)) {
return UKismetMathLibrary::GetRightVector(OwnerPawn->GetActorRotation());
}
return UKismetMathLibrary::GetRightVector(PC->PlayerCameraManager->GetCameraRotation());
}