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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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