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
@@ -92,7 +92,9 @@ New content that needs a different camera angle gets a new entry in the shot lis
|
||||
|
||||
The living roadmap with per-version task checklists is in **[ROADMAP.md](ROADMAP.md)**.
|
||||
|
||||
- Planned work is listed as checkbox items in `ROADMAP.md`, each marked **open** (`- [ ]`), **in progress** (`- [-]`), or **done** (`- [x]`). Update the marks as work changes state; a checkbox that no longer matches reality is worse than none.
|
||||
- `ROADMAP.md` is the single, central TODO and progress list for all agents. Put planned work and actionable follow-ups there rather than creating parallel task lists.
|
||||
- When working on a level-three (`###`) section, append ` - active - <sessionId>` to its heading, using your current session ID. Remove the suffix when you stop working on that section. Mark checklist items currently being worked on `- [-]` or `- [~]`; update the marks as their status changes.
|
||||
- Planned work is listed as checkbox items in `ROADMAP.md`, marked **open** (`- [ ]`), **in progress/active** (`- [-]` or `- [~]`), or **done** (`- [x]`). Keep the marks accurate.
|
||||
|
||||
- Each version is a `##` heading; task groups are `###` headings with `- [ ]` steps (`[-]` in progress, `[x]` done).
|
||||
- When **all** steps in a version section are done: write a short descriptive entry to `CHANGELOG.md`, then remove the version section from `ROADMAP.md` to keep it concise.
|
||||
@@ -101,7 +103,7 @@ The living roadmap with per-version task checklists is in **[ROADMAP.md](ROADMAP
|
||||
|
||||
## Memory
|
||||
|
||||
**[MEMORY.md](MEMORY.md)** is the shared working memory between sessions and collaborators. Write to it whenever you:
|
||||
**[MEMORY.md](MEMORY.md)** is shared session memory for agents and collaborators. Write to it whenever you:
|
||||
|
||||
- discover a fact about the environment, a tool, or a data source that cost time to establish;
|
||||
- hit a gotcha, trap, or surprising failure mode, and what actually caused it;
|
||||
|
||||
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();
|
||||
};
|
||||
@@ -44,6 +44,27 @@ its baked skin material and A-pose bind.
|
||||
Close three-quarter view of the cube row. Each cube is exactly 1 m — use it
|
||||
to judge scale against the character.
|
||||
|
||||
## review-03-first-person.png — generated 2026-11-07, approval: ☐ pending
|
||||
|
||||

|
||||
|
||||
New shot for the first-person player work. Camera is at 165 cm — the player's
|
||||
eye height — looking from behind and slightly south-east of the spawn point.
|
||||
It shows where the player lands (the character stands at origin, which is also
|
||||
the default spawn) and how the capsule/camera relate to the 1 m cubes.
|
||||
|
||||
**What changed in the game since the last review:** the project now has a
|
||||
first-person `AGigLineCharacter` (1.75 m capsule, 1.60 m eye camera, 4 m/s
|
||||
walk, Enhanced Input with WASD + mouse look + jump) wired as the default pawn
|
||||
by `AGigLineGameModeBase`. Nothing visible from these cameras should differ:
|
||||
the review map spawns its own pawn-free staging, so this shot is the reference
|
||||
for the new pawn's position and eye height, not of the pawn itself.
|
||||
|
||||
**Not shown, needs a human check:** walking, mouse look and jump can only be
|
||||
judged interactively. Launch the editor, press Play on `Test/ReviewMap`, and
|
||||
confirm W/A/S/D move in the expected directions, the mouse looks around with no
|
||||
acceleration, and Space jumps once per press.
|
||||
|
||||
## What to look for
|
||||
|
||||
- **Character**: proportions, bind pose, skin material readability.
|
||||
|
||||
@@ -100,6 +100,9 @@ try:
|
||||
unreal.Vector(900, -1100, 500),
|
||||
unreal.Vector(200, -300, 160),
|
||||
unreal.Vector(200, 100, 200),
|
||||
# first-person eye height, just south-east of the character: this is the
|
||||
# view a player has at spawn, capsule and camera stack in frame
|
||||
unreal.Vector(120, -260, 165),
|
||||
])
|
||||
shots.set_editor_property("shot_rotations", [
|
||||
unreal.MathLibrary.find_look_at_rotation(
|
||||
@@ -108,10 +111,13 @@ try:
|
||||
unreal.Vector(200, -300, 160), unreal.Vector(0, 0, 85)),
|
||||
unreal.MathLibrary.find_look_at_rotation(
|
||||
unreal.Vector(200, 100, 200), unreal.Vector(0, 600, 50)),
|
||||
unreal.MathLibrary.find_look_at_rotation(
|
||||
unreal.Vector(120, -260, 165), unreal.Vector(-200, 600, 90)),
|
||||
])
|
||||
shots.set_editor_property("shot_names", ["overview", "character", "scale-cubes"])
|
||||
shots.set_editor_property("shot_names",
|
||||
["overview", "character", "scale-cubes", "first-person"])
|
||||
shots.set_editor_property("quit_when_done", True)
|
||||
log("staged: ShotSequencer with 3 cameras")
|
||||
log("staged: ShotSequencer with 4 cameras")
|
||||
except Exception as exc: # noqa
|
||||
log(f"staging failed: {exc!r}")
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# Creates the Enhanced Input assets the first-person character loads:
|
||||
# /Game/Input/IA_Move Axis2D, WASD + left stick
|
||||
# /Game/Input/IA_Look Axis2D, mouse XY + right stick
|
||||
# /Game/Input/IA_Jump axis1d, Space / gamepad face button
|
||||
# /Game/Input/IMC_Default InputMappingContext holding the three mappings
|
||||
#
|
||||
# Run headless (imports/dirty packages are saved before exit):
|
||||
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
# -script=<repo>/Tools/ue_create_input_assets.py -nullrhi -unattended -nopause \
|
||||
# -stdout -FullStdOutLogOutput
|
||||
#
|
||||
# The result goes to /tmp/gigline/input-assets.txt because 5.8's pythonscript
|
||||
# commandlet swallows print() output with -stdout.
|
||||
#
|
||||
# Mapping choices:
|
||||
# - Look stays unit-free (no scalar modifier): the character applies its own
|
||||
# degrees-per-second, and a modifier here would double-scale.
|
||||
# - Move is a 2D axis: W = +Y, S = -Y, D = +X, A = -X via Negate on the Y axis.
|
||||
# For keyboard keys the Negate modifier's Y axis maps to the axis's Y.
|
||||
|
||||
import unreal
|
||||
|
||||
ASSET_DIR = "/Game/Input"
|
||||
_KEEP_ALIVE = []
|
||||
|
||||
|
||||
def negate_y_modifier():
|
||||
"""A negate-by-Y modifier, built with its Y flag set and X/Z cleared.
|
||||
|
||||
Returns the object plus its falsy reference; constructing the default
|
||||
modifier gives all axes true, so each flag is set explicitly. The caller owns
|
||||
keep-alive.
|
||||
"""
|
||||
neg = unreal.InputModifierNegate()
|
||||
neg.set_editor_property("x", False)
|
||||
neg.set_editor_property("y", True)
|
||||
neg.set_editor_property("z", False)
|
||||
return neg
|
||||
RESULT = "/tmp/gigline/input-assets.txt"
|
||||
lines = []
|
||||
|
||||
|
||||
def log(msg):
|
||||
lines.append(str(msg))
|
||||
unreal.log(str(msg))
|
||||
|
||||
|
||||
def fresh(name, cls, factory):
|
||||
"""Load the asset if it exists, otherwise create it.
|
||||
|
||||
Deleting then recreating in the same commandlet run fails: create_asset
|
||||
returns None for a path deleted earlier in the process. Overwriting in place
|
||||
is also what keeps the assets' identities stable across reruns.
|
||||
"""
|
||||
path = f"{ASSET_DIR}/{name}"
|
||||
existing = unreal.EditorAssetLibrary.load_asset(path)
|
||||
if existing is not None:
|
||||
return existing
|
||||
return unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
name, ASSET_DIR, cls, factory())
|
||||
|
||||
|
||||
def save(asset):
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(asset, only_if_is_dirty=False)
|
||||
|
||||
|
||||
def make_key(name):
|
||||
"""Build an FKey by name.
|
||||
|
||||
`unreal.InputCoreTypes` is not exposed to Python in 5.8 and `unreal.Key` does
|
||||
not accept a constructor argument, but the struct's `import_text` parses the
|
||||
key's export text, which is just its name.
|
||||
"""
|
||||
key = unreal.Key()
|
||||
key.import_text(name)
|
||||
return key
|
||||
|
||||
|
||||
def build():
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(ASSET_DIR):
|
||||
unreal.EditorAssetLibrary.make_directory(ASSET_DIR)
|
||||
|
||||
# --- actions ---------------------------------------------------------
|
||||
# Move keys are one-dimensional: W/S carry forward/back on the value's single
|
||||
# component and D/A carry strafe, combined by the character. A 2D value would
|
||||
# need per-key axis modifiers, and modifiers do not survive the save path here.
|
||||
ia_move = fresh("IA_Move", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_move.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
|
||||
save(ia_move)
|
||||
|
||||
ia_look = fresh("IA_Look", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_look.set_editor_property("value_type", unreal.InputActionValueType.AXIS2D)
|
||||
save(ia_look)
|
||||
|
||||
ia_jump = fresh("IA_Jump", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_jump.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
save(ia_jump)
|
||||
|
||||
# --- mapping context -------------------------------------------------
|
||||
imc = fresh("IMC_Default", unreal.InputMappingContext, unreal.InputMappingContext_Factory)
|
||||
|
||||
# Nine entries, one per key. No modifiers: the character negates S and A in
|
||||
# C++ instead, because a modifier object attached to a mapping struct
|
||||
# serialises as None on save in this build (the struct's object array does not
|
||||
# keep the modifier alive across the save path). One less moving part, and the
|
||||
# negation is visible where it is used.
|
||||
# Two actions feed the one-dimensional move: IA_Move for forward/back, and a
|
||||
# second action for strafe, both reporting on the value's single component.
|
||||
ia_strafe = fresh("IA_Strafe", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_strafe.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
|
||||
save(ia_strafe)
|
||||
|
||||
ia_back = fresh("IA_MoveBack", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_back.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
|
||||
save(ia_back)
|
||||
|
||||
ia_left = fresh("IA_StrafeLeft", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_left.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
|
||||
save(ia_left)
|
||||
|
||||
mapping_table = [
|
||||
(ia_move, "W"), (ia_back, "S"), (ia_strafe, "D"), (ia_left, "A"),
|
||||
(ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"),
|
||||
(ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"),
|
||||
(ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"),
|
||||
]
|
||||
|
||||
mappings = []
|
||||
for action, key_name in mapping_table:
|
||||
try:
|
||||
entry = unreal.EnhancedActionKeyMapping()
|
||||
entry.set_editor_property("action", action)
|
||||
entry.set_editor_property("key", make_key(key_name))
|
||||
mappings.append(entry)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log(f"skip {action.get_name()}/{key_name}: {exc}")
|
||||
|
||||
# Array property: rebuild it rather than mutating in place.
|
||||
imc.set_editor_property("mappings", mappings)
|
||||
save(imc)
|
||||
|
||||
log(f"IA_Move={ia_move.get_path_name()} IA_Look={ia_look.get_path_name()} "
|
||||
f"IA_Jump={ia_jump.get_path_name()}")
|
||||
log(f"IMC_Default={imc.get_path_name()} mappings={len(mappings)}")
|
||||
log("assets=" + ",".join(unreal.EditorAssetLibrary.list_assets(ASSET_DIR)))
|
||||
|
||||
|
||||
try:
|
||||
build()
|
||||
except Exception as exc: # noqa: BLE001 - surface the failure in the result file
|
||||
import traceback
|
||||
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||
log(traceback.format_exc())
|
||||
|
||||
with open(RESULT, "w") as handle:
|
||||
handle.write("\n".join(lines))
|
||||
Reference in new issue
Block a user