Pawn movement, what am I missing?

Trying to make sense of the APawn’s movement implementation. I want to create a simple movement component (no pathing, no preconfigured movement behaviors), just with networking and depenetration in place. Inheriting from UMovementComponent seems to be the only option then.

APawn::AddMovementInput does absolutely nothing. I actually expected this class to be possessable by a controller and nothing else, but it contains a ton of methods related to movement… while it doesn’t move. Alright… odd but whatever.

So I add a UMovementComponent deriving class to my pawn, nothing fancy:

#pragma once

#include "CoreMinimal.h" 	
#include "GameFramework/MovementComponent.h"

#include "MyMovementComponent.generated.h"


/*
*
*/
UCLASS(editinlinenew, BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent))
class UMyMovementComponent : public UMovementComponent {
	GENERATED_BODY()

public:

	// Setup

	UMyMovementComponent(const FObjectInitializer& InObjectInitializer)
	: Super(InObjectInitializer) {}

	// Movement

	virtual float GetGravityZ() const override { return 0.f; } ;

	virtual float GetMaxSpeed() const override { return 2500.f; } ;

};

You’d think the component would take over movement logic at this point except, it does absolutely nothing. No movement. Typical unreal spaghetti…

At a loss what is expected from me, so I take a look at the next best deriving class which implements a ton of things I don’t need (nav pathing), the UPawnMovementComponent.
Which… doesn’t do sh*t but call methods on its Pawn

Seriously… What am I missing to just make a pawn move at a constant rate in a direction using the UMovementComponent? There’s so much junk I must be looking past an override or something.

1 Like

Try inheriting from ADefaultPawn, it allows for replacement of the movement component with a custom one because it has the needed FName for the component replacement

then you can use code like this:

ACustomPawn::ACustomPawn(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer.SetDefaultSubobjectClass<UCustomMovementComponent>(MovementComponentName))
{
	PrimaryActorTick.bCanEverTick = true;
}

1 Like

Didn’t do it sadly, I believe it expects a UPawnMovementComponent, not a UMovementComponent. ADefaultPawn does a lot of other things like hardcoded engine input bindings. I’ll dig a bit further.

Currently I’m copying UMovementComponent and copying over some minimal logic from UFloatingPawnMovementComponent to see if the velocity is properly processed.

Doing the same thing. Got the base velocity moving but add input is the problem for now :stuck_out_tongue:

in my custom class calling

PawnOwner->Internal_AddMovementInput(WorldAccel, bForce);

seems to not have any effect. (PawnOwner is valid)

edit:
during TickComponent of the custom movement the FVector Velocity gets zeroed out somewhere along the way.
Will try to track it down.

1 Like

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());
}

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.