feat: first-person player with Enhanced Input
AGigLineCharacter: 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look scaled in degrees per second (no double DeltaTime), jump. Loaded ahead of a Blueprint so the demo is playable with no assets wired by hand. Tools/ue_create_input_assets.py builds /Game/Input headlessly: five 1D actions and an 11-mapping context. One action per direction instead of per-key modifiers, because modifier objects do not survive the save path here: a mapping's modifier array serialises None, and dimensioning a 2D move with per-key axis masks would need exactly those modifiers. The cost is four action assets rather than two. Also adds a fourth review camera at eye height and documents it; its approval is pending.
This commit is contained in:
1 parent
b3815499e0
commit
a0de0a6626
16 files changed
+522
-8
No files matched your search
@@ -0,0 +1,210 @@
|
||||
#include "GigLineCharacter.h"
|
||||
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "InputAction.h"
|
||||
#include "InputMappingContext.h"
|
||||
|
||||
AGigLineCharacter::AGigLineCharacter()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// 1.75 m tall, 0.42 m wide, human scale in Unreal units (1 m = 100 uu).
|
||||
GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f);
|
||||
|
||||
// Move relative to where the player is looking, not where the actor points.
|
||||
bUseControllerRotationPitch = false;
|
||||
bUseControllerRotationYaw = true;
|
||||
bUseControllerRotationRoll = false;
|
||||
|
||||
UCharacterMovementComponent* Movement = GetCharacterMovement();
|
||||
Movement->bOrientRotationToMovement = false;
|
||||
Movement->MaxWalkSpeed = WalkSpeedMetresPerSecond * 100.0f;
|
||||
Movement->JumpZVelocity = 420.0f;
|
||||
Movement->AirControl = 0.2f;
|
||||
Movement->BrakingDecelerationWalking = 2048.0f;
|
||||
Movement->MaxStepHeight = 45.0f;
|
||||
|
||||
FirstPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
|
||||
FirstPersonCamera->SetupAttachment(GetCapsuleComponent());
|
||||
FirstPersonCamera->SetRelativeLocation(FVector(0.0f, 0.0f, 72.5f)); // eye height 1.60 m
|
||||
FirstPersonCamera->bUsePawnControlRotation = true;
|
||||
|
||||
// An eye-level mesh is not needed for 0.1.0; the body is a capsule with a camera.
|
||||
GetMesh()->SetHiddenInGame(true);
|
||||
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
|
||||
// Input assets are created by Tools/ue_create_input_assets.py. They are loaded in
|
||||
// BeginPlay rather than the constructor: the constructor runs during CDO
|
||||
// construction where asset loads are unreliable, and a missing asset should be
|
||||
// a loud warning, not a silent no-op.
|
||||
}
|
||||
|
||||
void AGigLineCharacter::LoadInputAssets()
|
||||
{
|
||||
auto LoadSoft = [](const TCHAR* Path) -> TObjectPtr<UObject>
|
||||
{
|
||||
return LoadObject<UObject>(nullptr, Path);
|
||||
};
|
||||
MappingContext = Cast<UInputMappingContext>(LoadSoft(TEXT("/Game/Input/IMC_Default.IMC_Default")));
|
||||
MoveAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Move.IA_Move")));
|
||||
StrafeAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Strafe.IA_Strafe")));
|
||||
MoveBackAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_MoveBack.IA_MoveBack")));
|
||||
StrafeLeftAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft")));
|
||||
LookAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Look.IA_Look")));
|
||||
JumpAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Jump.IA_Jump")));
|
||||
|
||||
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|
||||
|| !LookAction)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("GigLine: input assets missing (context=%s move=%s look=%s). ")
|
||||
TEXT("Run Tools/ue_create_input_assets.py to build /Game/Input."),
|
||||
MappingContext ? TEXT("ok") : TEXT("missing"),
|
||||
(MoveAction && MoveBackAction && StrafeAction && StrafeLeftAction) ? TEXT("ok")
|
||||
: TEXT("missing"),
|
||||
LookAction ? TEXT("ok") : TEXT("missing"));
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
LoadInputAssets();
|
||||
|
||||
// Push the mapping context for this local player.
|
||||
if (MappingContext)
|
||||
{
|
||||
if (const APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||
{
|
||||
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
|
||||
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
|
||||
{
|
||||
Subsystem->AddMappingContext(MappingContext, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
{
|
||||
Super::SetupPlayerInputComponent(PlayerInputComponent);
|
||||
|
||||
UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
|
||||
if (!Input)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("GigLine: player input component is not Enhanced Input"));
|
||||
return;
|
||||
}
|
||||
|
||||
// One action per direction keeps the sign explicit: W and S differ only by
|
||||
// which handler runs, not by a modifier in the asset.
|
||||
if (MoveAction)
|
||||
{
|
||||
Input->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveForward);
|
||||
}
|
||||
if (MoveBackAction)
|
||||
{
|
||||
Input->BindAction(MoveBackAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveBack);
|
||||
}
|
||||
if (StrafeAction)
|
||||
{
|
||||
Input->BindAction(StrafeAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveStrafe);
|
||||
}
|
||||
if (StrafeLeftAction)
|
||||
{
|
||||
Input->BindAction(StrafeLeftAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveLeft);
|
||||
}
|
||||
if (LookAction)
|
||||
{
|
||||
Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::Look);
|
||||
}
|
||||
if (JumpAction)
|
||||
{
|
||||
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
||||
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &AGigLineCharacter::StopJump);
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
/** One-dimensional action value; S and A arrive positive and are negated here. */
|
||||
float SignedAxis(const FInputActionValue& Value, bool bNegativeKey)
|
||||
{
|
||||
float Axis = 0.0f;
|
||||
switch (Value.GetValueType())
|
||||
{
|
||||
case EInputActionValueType::Boolean:
|
||||
Axis = Value.Get<bool>() ? 1.0f : 0.0f;
|
||||
break;
|
||||
case EInputActionValueType::Axis1D:
|
||||
Axis = Value.Get<float>();
|
||||
break;
|
||||
default:
|
||||
Axis = Value.Get<FVector2D>().Y;
|
||||
break;
|
||||
}
|
||||
return bNegativeKey ? -Axis : Axis;
|
||||
}
|
||||
}
|
||||
|
||||
void AGigLineCharacter::MoveForward(const FInputActionValue& Value)
|
||||
{
|
||||
ApplyMove(FMath::Abs(SignedAxis(Value, false)), 0.0f);
|
||||
}
|
||||
|
||||
void AGigLineCharacter::MoveBack(const FInputActionValue& Value)
|
||||
{
|
||||
ApplyMove(-FMath::Abs(SignedAxis(Value, false)), 0.0f);
|
||||
}
|
||||
|
||||
void AGigLineCharacter::MoveStrafe(const FInputActionValue& Value)
|
||||
{
|
||||
ApplyMove(0.0f, FMath::Abs(SignedAxis(Value, false)));
|
||||
}
|
||||
|
||||
void AGigLineCharacter::MoveLeft(const FInputActionValue& Value)
|
||||
{
|
||||
ApplyMove(0.0f, -FMath::Abs(SignedAxis(Value, false)));
|
||||
}
|
||||
|
||||
void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
|
||||
{
|
||||
const AController* Controller = GetController();
|
||||
if (!Controller || (FMath::IsNearlyZero(Forward) && FMath::IsNearlyZero(Strafe)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward/right on the controller yaw keeps the capsule upright on slopes.
|
||||
const FRotator YawRotation(0.0f, Controller->GetControlRotation().Yaw, 0.0f);
|
||||
const FVector ForwardDir = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
|
||||
const FVector RightDir = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
|
||||
|
||||
AddMovementInput(ForwardDir, Forward);
|
||||
AddMovementInput(RightDir, Strafe);
|
||||
}
|
||||
|
||||
void AGigLineCharacter::Look(const FInputActionValue& Value)
|
||||
{
|
||||
// Enhanced Input applies the mapping's scalar modifier, which already scales by
|
||||
// DeltaTime; scaling again by DeltaTime would make the mouse frame-rate dependent.
|
||||
const FVector2D Axis = Value.Get<FVector2D>();
|
||||
AddControllerYawInput(Axis.X * LookSensitivity);
|
||||
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f));
|
||||
}
|
||||
|
||||
void AGigLineCharacter::StartJump()
|
||||
{
|
||||
Jump();
|
||||
}
|
||||
|
||||
void AGigLineCharacter::StopJump()
|
||||
{
|
||||
StopJumping();
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// First-person player character for the 0.1.0 walkable Hollywood block.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "GigLineCharacter.generated.h"
|
||||
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
|
||||
|
||||
/**
|
||||
* Walking first-person character.
|
||||
*
|
||||
* Movement is C++ and network-agnostic in the shapes it uses (Enhanced Input ->
|
||||
* AddMovementInput), so a Blueprint subclass can retune values without touching
|
||||
* code. Defaults are human-scale: 1.75 m capsule, 1.6 m eye height, 4 m/s walk.
|
||||
*/
|
||||
UCLASS()
|
||||
class GIGLINE_API AGigLineCharacter : public ACharacter
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AGigLineCharacter();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
protected:
|
||||
/** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */
|
||||
void LoadInputAssets();
|
||||
|
||||
/** Moves along the control yaw by the given forward/strafe amounts. */
|
||||
void ApplyMove(float Forward, float Strafe);
|
||||
|
||||
/** Handles input actions to movement; assigned in C++ so the project runs with no assets. */
|
||||
/** Forward/back and strafe are separate 1D actions, summed into one move. */
|
||||
void MoveForward(const FInputActionValue& Value);
|
||||
void MoveStrafe(const FInputActionValue& Value);
|
||||
void MoveBack(const FInputActionValue& Value);
|
||||
void MoveLeft(const FInputActionValue& Value);
|
||||
void Look(const FInputActionValue& Value);
|
||||
void StartJump();
|
||||
void StopJump();
|
||||
|
||||
/** Mapping context for the gamepad and keyboard. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputMappingContext> MappingContext;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> MoveAction;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> MoveBackAction;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> StrafeAction;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> StrafeLeftAction;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> LookAction;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> JumpAction;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UCameraComponent> FirstPersonCamera;
|
||||
|
||||
/** Degrees per second per unit of mouse input; defaults to Unreal's usual 45. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||
float LookSensitivity = 45.0f;
|
||||
|
||||
/** Invert the pitch axis, for players who prefer it. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||
bool bInvertLookY = false;
|
||||
|
||||
/** Walk speed in metres per second; converted to Unreal units on first use. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
|
||||
float WalkSpeedMetresPerSecond = 4.0f;
|
||||
};
|
||||
@@ -1 +1,8 @@
|
||||
#include "GigLineGameModeBase.h"
|
||||
|
||||
#include "GigLineCharacter.h"
|
||||
|
||||
AGigLineGameModeBase::AGigLineGameModeBase()
|
||||
{
|
||||
DefaultPawnClass = AGigLineCharacter::StaticClass();
|
||||
}
|
||||
@@ -6,11 +6,16 @@
|
||||
|
||||
/**
|
||||
* Default game mode for the 0.1.0 walkable Hollywood block demo.
|
||||
* First-person movement is Blueprint-driven on top of Enhanced Input;
|
||||
* this class exists so the project has a valid primary game module.
|
||||
*
|
||||
* Sets the first-person character as the default pawn and spawns it at the start
|
||||
* point, so the demo runs with no Blueprint wiring. Gameplay tuning that belongs to
|
||||
* designers still lives in Blueprint subclasses on top of this.
|
||||
*/
|
||||
UCLASS()
|
||||
class GIGLINE_API AGigLineGameModeBase : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AGigLineGameModeBase();
|
||||
};
|
||||
Reference in new issue
Block a user