Skip to content

Instantly share code, notes, and snippets.

@LuviKunG
Created March 6, 2025 18:00
Show Gist options
  • Save LuviKunG/ebdbdec4372c1a591044862659ba8532 to your computer and use it in GitHub Desktop.
Save LuviKunG/ebdbdec4372c1a591044862659ba8532 to your computer and use it in GitHub Desktop.
A simple spectator pawn class for Unreal Engine 5.X C++ which replacement of a default pawn. Using Enhanced Inputs and implement move speed changing and zooming by change camera field of view.
// Copyright 2025 Thanut Panichyotai (@LuviKunG) All Rights Reserved.
#include "SimpleSpectatorPawn.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
ASimpleSpectatorPawn::ASimpleSpectatorPawn(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
bUseControllerRotationYaw = true;
bUseControllerRotationPitch = true;
SphereComponent = ObjectInitializer.CreateDefaultSubobject<USphereComponent>(this, TEXT("SphereComponent"));
SphereComponent->InitSphereRadius(20.0f);
SphereComponent->SetCollisionProfileName(TEXT("Pawn"));
SphereComponent->SetGenerateOverlapEvents(false);
SphereComponent->CanCharacterStepUpOn = ECB_No;
SphereComponent->bApplyImpulseOnDamage = false;
SphereComponent->bReplicatePhysicsToAutonomousProxy = false;
RootComponent = SphereComponent;
CameraComponent = ObjectInitializer.CreateDefaultSubobject<UCameraComponent>(this, TEXT("CameraComponent"));
CameraComponent->SetupAttachment(SphereComponent.Get());
FloatingPawnMovement = ObjectInitializer.CreateDefaultSubobject<UFloatingPawnMovement>(this, TEXT("FloatingPawnMovement"));
FloatingPawnMovement->UpdatedComponent = SphereComponent;
MinSpeed = 10.0f;
MaxSpeed = 1000.0f;
SpeedIncrement = 10.0f;
NormalFieldOfView = 90.0f;
ZoomFieldOfView = 30.0f;
bIsZooming = false;
CurrentFieldOfView = 0.0f;
}
void ASimpleSpectatorPawn::BeginPlay()
{
Super::BeginPlay();
CurrentFieldOfView = NormalFieldOfView;
ResetInputValue();
}
void ASimpleSpectatorPawn::TickActor(const float DeltaTime, const ELevelTick TickType, FActorTickFunction& ThisTickFunction)
{
Super::TickActor(DeltaTime, TickType, ThisTickFunction);
const float TargetFieldOfView = bIsZooming ? ZoomFieldOfView : NormalFieldOfView;
if (FMath::IsNearlyEqual(CameraComponent->FieldOfView, TargetFieldOfView, 0.1f))
{
CameraComponent->FieldOfView = TargetFieldOfView;
}
else
{
CameraComponent->FieldOfView = FMath::InterpEaseOut(CameraComponent->FieldOfView, TargetFieldOfView, DeltaTime, 10.0f);
}
}
void ASimpleSpectatorPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInputComponent->BindAction(InputActionMove.Get(), ETriggerEvent::Triggered, this, &ASimpleSpectatorPawn::OnInputMoveTriggered);
EnhancedInputComponent->BindAction(InputActionLook.Get(), ETriggerEvent::Triggered, this, &ASimpleSpectatorPawn::OnInputLookTriggered);
EnhancedInputComponent->BindAction(InputActionSpeed.Get(), ETriggerEvent::Triggered, this, &ASimpleSpectatorPawn::OnInputSpeedTriggered);
EnhancedInputComponent->BindAction(InputActionZoom.Get(), ETriggerEvent::Started, this, &ASimpleSpectatorPawn::OnInputZoomPressed);
EnhancedInputComponent->BindAction(InputActionZoom.Get(), ETriggerEvent::Canceled, this, &ASimpleSpectatorPawn::OnInputZoomReleased);
EnhancedInputComponent->BindAction(InputActionZoom.Get(), ETriggerEvent::Completed, this, &ASimpleSpectatorPawn::OnInputZoomReleased);
}
}
void ASimpleSpectatorPawn::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
check(InputMappingContext);
PlayerController = Cast<APlayerController>(NewController);
if (!PlayerController)
return;
if (UEnhancedInputLocalPlayerSubsystem* EnhancedInputLocalPlayerSubsystem = PlayerController->GetLocalPlayer()->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
EnhancedInputLocalPlayerSubsystem->AddMappingContext(InputMappingContext.Get(), 0);
}
}
void ASimpleSpectatorPawn::UnPossessed()
{
if (!PlayerController)
{
Super::UnPossessed();
return;
}
check(InputMappingContext);
if (UEnhancedInputLocalPlayerSubsystem* EnhancedInputLocalPlayerSubsystem = PlayerController->GetLocalPlayer()->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
EnhancedInputLocalPlayerSubsystem->RemoveMappingContext(InputMappingContext.Get());
}
ResetInputValue();
Super::UnPossessed();
}
float ASimpleSpectatorPawn::GetSpeed() const
{
return FloatingPawnMovement->MaxSpeed;
}
float ASimpleSpectatorPawn::GetFieldOfView() const
{
return CameraComponent->FieldOfView;
}
void ASimpleSpectatorPawn::ResetInputValue()
{
bIsZooming = false;
FloatingPawnMovement->MaxSpeed = FMath::Lerp(MinSpeed, MaxSpeed, 0.5f);
CameraComponent->FieldOfView = NormalFieldOfView;
}
// ReSharper disable once CppMemberFunctionMayBeConst - Delegate Event
void ASimpleSpectatorPawn::OnInputMoveTriggered(const FInputActionValue& InputActionValue)
{
const FVector2D InputValue = InputActionValue.Get<FVector2D>();
const FVector ForwardVector = GetActorForwardVector();
const FVector RightVector = GetActorRightVector();
const FVector MovementDirection = ForwardVector * InputValue.Y + RightVector * InputValue.X;
FloatingPawnMovement->AddInputVector(MovementDirection);
}
// ReSharper disable once CppMemberFunctionMayBeConst - Delegate Event
void ASimpleSpectatorPawn::OnInputLookTriggered(const FInputActionValue& InputActionValue)
{
const FVector2D InputValue = InputActionValue.Get<FVector2D>();
const FRotator CurrentRotation = GetControlRotation();
const FRotator DeltaRotation = FRotator(-InputValue.Y, InputValue.X, 0.0f);
const FRotator NewRotation = CurrentRotation + DeltaRotation;
if (Controller)
{
Controller->SetControlRotation(NewRotation);
}
}
// ReSharper disable once CppMemberFunctionMayBeConst - Delegate Event
void ASimpleSpectatorPawn::OnInputSpeedTriggered(const FInputActionValue& InputActionValue)
{
const float InputValue = InputActionValue.Get<float>();
FloatingPawnMovement->MaxSpeed = FMath::Clamp(FloatingPawnMovement->MaxSpeed + InputValue * SpeedIncrement, MinSpeed, MaxSpeed);
if (OnSpeedChanged.IsBound())
OnSpeedChanged.Broadcast(FloatingPawnMovement->MaxSpeed);
#if WITH_EDITOR
UE_LOG(LogTemp, Log, TEXT("Speed: %f"), FloatingPawnMovement->MaxSpeed);
#endif
}
void ASimpleSpectatorPawn::OnInputZoomPressed()
{
bIsZooming = true;
if (OnZoomChanged.IsBound())
OnZoomChanged.Broadcast(true);
#if WITH_EDITOR
UE_LOG(LogTemp, Log, TEXT("Zoom: true"));
#endif
}
void ASimpleSpectatorPawn::OnInputZoomReleased()
{
bIsZooming = false;
if (OnZoomChanged.IsBound())
OnZoomChanged.Broadcast(false);
#if WITH_EDITOR
UE_LOG(LogTemp, Log, TEXT("Zoom: false"));
#endif
}
// Copyright 2025 Thanut Panichyotai (@LuviKunG) All Rights Reserved.
// ReSharper disable CppUEBlueprintCallableFunctionUnused
#pragma once
#include "CoreMinimal.h"
#include "InputMappingContext.h"
#include "Camera/CameraComponent.h"
#include "Components/SphereComponent.h"
#include "GameFramework/FloatingPawnMovement.h"
#include "GameFramework/Pawn.h"
#include "SimpleSpectatorPawn.generated.h"
UCLASS(Abstract)
class ASimpleSpectatorPawn : public APawn
{
GENERATED_BODY()
public:
explicit ASimpleSpectatorPawn(const FObjectInitializer& ObjectInitializer);
virtual void BeginPlay() override;
virtual void TickActor(float DeltaTime, ELevelTick TickType, FActorTickFunction& ThisTickFunction) override;
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
virtual void PossessedBy(AController* NewController) override;
virtual void UnPossessed() override;
UDELEGATE()
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpeedChangeDelegate, const float&, Speed);
UPROPERTY(BlueprintAssignable, Category = "Pawn")
FSpeedChangeDelegate OnSpeedChanged;
UDELEGATE()
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZoomChangeDelegate, const bool, bIsZooming);
UPROPERTY(BlueprintAssignable, Category = "Pawn")
FZoomChangeDelegate OnZoomChanged;
UFUNCTION(BlueprintPure, Category = "Pawn")
float GetSpeed() const;
UFUNCTION(BlueprintPure, Category = "Pawn")
float GetFieldOfView() const;
protected:
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "Components")
TObjectPtr<USphereComponent> SphereComponent;
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "Components")
TObjectPtr<UCameraComponent> CameraComponent;
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "Components")
TObjectPtr<UFloatingPawnMovement> FloatingPawnMovement;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inputs")
TObjectPtr<UInputMappingContext> InputMappingContext;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inputs")
TObjectPtr<UInputAction> InputActionMove;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inputs")
TObjectPtr<UInputAction> InputActionLook;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inputs")
TObjectPtr<UInputAction> InputActionSpeed;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inputs")
TObjectPtr<UInputAction> InputActionZoom;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pawn|Speed")
float MinSpeed;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pawn|Speed")
float MaxSpeed;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pawn|Speed")
float SpeedIncrement;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pawn|Zoom")
float NormalFieldOfView;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pawn|Zoom")
float ZoomFieldOfView;
UPROPERTY(BlueprintReadOnly, Category = "Pawn|Controller")
TObjectPtr<APlayerController> PlayerController;
bool bIsZooming;
float CurrentFieldOfView;
void ResetInputValue();
private:
UFUNCTION()
void OnInputMoveTriggered(const FInputActionValue& InputActionValue);
UFUNCTION()
void OnInputLookTriggered(const FInputActionValue& InputActionValue);
UFUNCTION()
void OnInputSpeedTriggered(const FInputActionValue& InputActionValue);
UFUNCTION()
void OnInputZoomPressed();
UFUNCTION()
void OnInputZoomReleased();
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment