Implement sprinting
This commit is contained in:
@@ -371,7 +371,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
||||
- [x] Decide first-person, third-person, or hybrid camera. Decision: hybrid camera with third person as default and optional first-person toggle.
|
||||
- [x] Implement first/third-person camera toggle.
|
||||
- [x] Implement movement.
|
||||
- [ ] Implement sprinting.
|
||||
- [x] Implement sprinting.
|
||||
- [ ] Define real-world baseline walking speed.
|
||||
- [ ] Define real-world baseline running speed.
|
||||
- [ ] Connect movement speed to age, condition, strength, endurance, hunger, thirst, injury, carried weight, and terrain.
|
||||
@@ -1465,4 +1465,4 @@ Earliest incomplete foundation items:
|
||||
|
||||
Immediate next item:
|
||||
|
||||
- [ ] Implement sprinting.
|
||||
- [ ] Define real-world baseline walking speed.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
import unreal
|
||||
|
||||
|
||||
def load(path):
|
||||
asset = unreal.EditorAssetLibrary.load_asset(path)
|
||||
if not asset:
|
||||
raise RuntimeError(f"Could not load {path}")
|
||||
return asset
|
||||
|
||||
|
||||
def create_input_action(path):
|
||||
existing = unreal.EditorAssetLibrary.load_asset(path)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
template_path = "/Game/Input/Actions/IA_Interact"
|
||||
template = unreal.EditorAssetLibrary.load_asset(template_path)
|
||||
if not template:
|
||||
template_path = "/Game/Input/Actions/IA_Jump"
|
||||
template = unreal.EditorAssetLibrary.load_asset(template_path)
|
||||
if not template:
|
||||
raise RuntimeError("Could not load an input action template")
|
||||
|
||||
action = unreal.EditorAssetLibrary.duplicate_asset(template_path, path)
|
||||
if not action:
|
||||
raise RuntimeError(f"Could not create {path}")
|
||||
return action
|
||||
|
||||
|
||||
def set_boolean_value_type(action):
|
||||
action.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
try:
|
||||
action.set_editor_property("triggers", [])
|
||||
except Exception as exc:
|
||||
unreal.log_warning(f"Could not clear sprint triggers; keeping template defaults: {exc}")
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(action)
|
||||
|
||||
|
||||
def mapping_exists(context, action, key_name):
|
||||
mapping_data = context.get_editor_property("default_key_mappings")
|
||||
for mapping in list(mapping_data.get_editor_property("mappings")):
|
||||
mapping_key = mapping.get_editor_property("key")
|
||||
if (
|
||||
mapping.get_editor_property("action") == action
|
||||
and str(mapping_key.get_editor_property("key_name")) == key_name
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def map_key(context, action, key_name):
|
||||
if mapping_exists(context, action, key_name):
|
||||
unreal.log(f"Mapping already exists: {action.get_name()} -> {key_name}")
|
||||
return
|
||||
|
||||
key = unreal.Key()
|
||||
key.set_editor_property("key_name", key_name)
|
||||
|
||||
mapping_data = context.get_editor_property("default_key_mappings")
|
||||
mappings = list(mapping_data.get_editor_property("mappings"))
|
||||
new_mapping = unreal.EnhancedActionKeyMapping()
|
||||
new_mapping.set_editor_property("action", action)
|
||||
new_mapping.set_editor_property("key", key)
|
||||
mappings.append(new_mapping)
|
||||
mapping_data.set_editor_property("mappings", mappings)
|
||||
context.set_editor_property("default_key_mappings", mapping_data)
|
||||
|
||||
unreal.log(f"Added mapping: {action.get_name()} -> {key_name}")
|
||||
|
||||
|
||||
def main():
|
||||
sprint_action = create_input_action("/Game/Input/Actions/IA_Sprint")
|
||||
set_boolean_value_type(sprint_action)
|
||||
|
||||
context = load("/Game/Input/IMC_Default")
|
||||
map_key(context, sprint_action, "LeftShift")
|
||||
map_key(context, sprint_action, "Gamepad_LeftThumbstick")
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(context)
|
||||
|
||||
character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter")
|
||||
character_cdo = unreal.get_default_object(character_bp.generated_class())
|
||||
character_cdo.set_editor_property("SprintAction", sprint_action)
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(character_bp)
|
||||
|
||||
unreal.log("Agrarian sprint input setup complete.")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,44 @@
|
||||
import unreal
|
||||
|
||||
|
||||
def load(path):
|
||||
asset = unreal.EditorAssetLibrary.load_asset(path)
|
||||
if not asset:
|
||||
raise RuntimeError(f"Could not load {path}")
|
||||
return asset
|
||||
|
||||
|
||||
def mapping_found(context, action, key_name):
|
||||
mapping_data = context.get_editor_property("default_key_mappings")
|
||||
for mapping in list(mapping_data.get_editor_property("mappings")):
|
||||
mapping_key = mapping.get_editor_property("key")
|
||||
if (
|
||||
mapping.get_editor_property("action") == action
|
||||
and str(mapping_key.get_editor_property("key_name")) == key_name
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
action = load("/Game/Input/Actions/IA_Sprint")
|
||||
context = load("/Game/Input/IMC_Default")
|
||||
character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter")
|
||||
character_cdo = unreal.get_default_object(character_bp.generated_class())
|
||||
|
||||
missing = []
|
||||
for key_name in ["LeftShift", "Gamepad_LeftThumbstick"]:
|
||||
if not mapping_found(context, action, key_name):
|
||||
missing.append(f"missing mapping {key_name}")
|
||||
|
||||
assigned_action = character_cdo.get_editor_property("SprintAction")
|
||||
if assigned_action != action:
|
||||
missing.append("BP_ThirdPersonCharacter SprintAction is not IA_Sprint")
|
||||
|
||||
if missing:
|
||||
raise RuntimeError("Sprint input verification failed: " + "; ".join(missing))
|
||||
|
||||
unreal.log("Agrarian sprint input verification complete.")
|
||||
|
||||
|
||||
main()
|
||||
@@ -16,9 +16,12 @@
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "AgrarianGame.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
AAgrarianGameCharacter::AAgrarianGameCharacter()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// Set size for collision capsule
|
||||
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
|
||||
|
||||
@@ -35,7 +38,7 @@ AAgrarianGameCharacter::AAgrarianGameCharacter()
|
||||
// instead of recompiling to adjust them
|
||||
GetCharacterMovement()->JumpZVelocity = 500.f;
|
||||
GetCharacterMovement()->AirControl = 0.35f;
|
||||
GetCharacterMovement()->MaxWalkSpeed = 500.f;
|
||||
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
|
||||
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
|
||||
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
|
||||
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
|
||||
@@ -60,6 +63,37 @@ AAgrarianGameCharacter::AAgrarianGameCharacter()
|
||||
// are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++)
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::Tick(float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
|
||||
if (!HasAuthority() || !bWantsToSprint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanSprint())
|
||||
{
|
||||
SetWantsToSprint(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (GetVelocity().SizeSquared2D() > KINDA_SMALL_NUMBER && SurvivalComponent)
|
||||
{
|
||||
SurvivalComponent->SpendStamina(SprintStaminaCostPerSecond * DeltaSeconds);
|
||||
if (!CanSprint())
|
||||
{
|
||||
SetWantsToSprint(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
DOREPLIFETIME(AAgrarianGameCharacter, bWantsToSprint);
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
{
|
||||
// Set up action bindings
|
||||
@@ -81,6 +115,13 @@ void AAgrarianGameCharacter::SetupPlayerInputComponent(UInputComponent* PlayerIn
|
||||
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::Interact);
|
||||
}
|
||||
|
||||
if (SprintAction)
|
||||
{
|
||||
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::StartSprint);
|
||||
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AAgrarianGameCharacter::StopSprint);
|
||||
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Canceled, this, &AAgrarianGameCharacter::StopSprint);
|
||||
}
|
||||
|
||||
if (ToggleCameraAction)
|
||||
{
|
||||
EnhancedInputComponent->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::ToggleCameraPerspective);
|
||||
@@ -115,6 +156,16 @@ void AAgrarianGameCharacter::Interact()
|
||||
TryInteract();
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::StartSprint()
|
||||
{
|
||||
SetWantsToSprint(true);
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::StopSprint()
|
||||
{
|
||||
SetWantsToSprint(false);
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::ToggleCameraPerspective()
|
||||
{
|
||||
SetFirstPersonCamera(!bFirstPersonCamera);
|
||||
@@ -155,6 +206,48 @@ void AAgrarianGameCharacter::SetFirstPersonCamera(bool bEnableFirstPerson)
|
||||
}
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::SetWantsToSprint(bool bNewWantsToSprint)
|
||||
{
|
||||
const bool bAllowedSprintIntent = bNewWantsToSprint && CanSprint();
|
||||
if (bWantsToSprint == bAllowedSprintIntent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bWantsToSprint = bAllowedSprintIntent;
|
||||
ApplyMovementSpeed();
|
||||
|
||||
if (!HasAuthority())
|
||||
{
|
||||
ServerSetWantsToSprint(bAllowedSprintIntent);
|
||||
}
|
||||
}
|
||||
|
||||
bool AAgrarianGameCharacter::CanSprint() const
|
||||
{
|
||||
return SurvivalComponent
|
||||
&& SurvivalComponent->IsAlive()
|
||||
&& SurvivalComponent->Survival.Stamina > MinSprintStamina;
|
||||
}
|
||||
|
||||
bool AAgrarianGameCharacter::IsSprinting() const
|
||||
{
|
||||
return bWantsToSprint && CanSprint();
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::ApplyMovementSpeed()
|
||||
{
|
||||
if (UCharacterMovementComponent* MovementComponent = GetCharacterMovement())
|
||||
{
|
||||
MovementComponent->MaxWalkSpeed = IsSprinting() ? SprintSpeed : WalkSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::OnRep_SprintState()
|
||||
{
|
||||
ApplyMovementSpeed();
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::DoMove(float Right, float Forward)
|
||||
{
|
||||
if (GetController() != nullptr)
|
||||
@@ -240,6 +333,11 @@ void AAgrarianGameCharacter::TryInteract()
|
||||
}
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::ServerSetWantsToSprint_Implementation(bool bNewWantsToSprint)
|
||||
{
|
||||
SetWantsToSprint(bNewWantsToSprint);
|
||||
}
|
||||
|
||||
void AAgrarianGameCharacter::ServerInteract_Implementation(AActor* TargetActor)
|
||||
{
|
||||
if (!TargetActor || !TargetActor->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()))
|
||||
|
||||
@@ -73,6 +73,10 @@ protected:
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* InteractAction;
|
||||
|
||||
/** Hold to sprint while stamina allows it. */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* SprintAction;
|
||||
|
||||
/** Toggle between third-person and first-person camera views. */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* ToggleCameraAction;
|
||||
@@ -81,6 +85,22 @@ protected:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Interaction", meta = (ClampMin = "100"))
|
||||
float InteractionDistance = 450.0f;
|
||||
|
||||
/** Baseline movement speed before sprint, skill, injury, load, and terrain modifiers. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0"))
|
||||
float WalkSpeed = 500.0f;
|
||||
|
||||
/** Short-burst movement speed used by the first sprinting pass. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0"))
|
||||
float SprintSpeed = 750.0f;
|
||||
|
||||
/** Stamina spent each second while sprinting and moving. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0"))
|
||||
float SprintStaminaCostPerSecond = 18.0f;
|
||||
|
||||
/** Minimum stamina required to start or continue sprinting. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0", ClampMax = "100"))
|
||||
float MinSprintStamina = 5.0f;
|
||||
|
||||
/** Third-person spring arm distance used when returning from first person. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Camera", meta = (ClampMin = "0"))
|
||||
float ThirdPersonCameraDistance = 400.0f;
|
||||
@@ -93,6 +113,10 @@ protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Agrarian|Camera", meta = (AllowPrivateAccess = "true"))
|
||||
bool bFirstPersonCamera = false;
|
||||
|
||||
/** Replicated player intent to sprint; actual sprinting also depends on stamina and alive state. */
|
||||
UPROPERTY(ReplicatedUsing = OnRep_SprintState, VisibleAnywhere, BlueprintReadOnly, Category="Agrarian|Movement", meta = (AllowPrivateAccess = "true"))
|
||||
bool bWantsToSprint = false;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
@@ -100,6 +124,9 @@ public:
|
||||
|
||||
protected:
|
||||
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
/** Initialize input action bindings */
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
@@ -114,12 +141,30 @@ protected:
|
||||
/** Called for interaction input */
|
||||
void Interact();
|
||||
|
||||
/** Called when sprint input is pressed. */
|
||||
void StartSprint();
|
||||
|
||||
/** Called when sprint input is released. */
|
||||
void StopSprint();
|
||||
|
||||
/** Called for camera perspective toggle input */
|
||||
void ToggleCameraPerspective();
|
||||
|
||||
/** Applies local camera presentation state. */
|
||||
void SetFirstPersonCamera(bool bEnableFirstPerson);
|
||||
|
||||
/** Applies the requested sprint intent locally and, when needed, on the server. */
|
||||
void SetWantsToSprint(bool bNewWantsToSprint);
|
||||
|
||||
/** Returns true when the character has enough survival state to sprint. */
|
||||
bool CanSprint() const;
|
||||
|
||||
/** Applies current walk or sprint speed to character movement. */
|
||||
void ApplyMovementSpeed();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_SprintState();
|
||||
|
||||
public:
|
||||
|
||||
/** Handles move inputs from either controls or UI interfaces */
|
||||
@@ -146,10 +191,18 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category="Agrarian|Camera")
|
||||
bool IsFirstPersonCamera() const { return bFirstPersonCamera; }
|
||||
|
||||
/** Returns true when sprint intent and current stamina allow sprinting. */
|
||||
UFUNCTION(BlueprintPure, Category="Agrarian|Movement")
|
||||
bool IsSprinting() const;
|
||||
|
||||
/** Server-authoritative interaction entry point. */
|
||||
UFUNCTION(Server, Reliable)
|
||||
void ServerInteract(AActor* TargetActor);
|
||||
|
||||
/** Server-authoritative sprint intent update. */
|
||||
UFUNCTION(Server, Reliable)
|
||||
void ServerSetWantsToSprint(bool bNewWantsToSprint);
|
||||
|
||||
public:
|
||||
|
||||
/** Returns CameraBoom subobject **/
|
||||
|
||||
Reference in New Issue
Block a user