Sprint- und Ausdauermechanik hinzugefügt

Die Sprint-Funktionalität wurde in `AKingshearthLegacyCharacter` implementiert, einschließlich neuer Methoden (`StartSprint`, `StopSprint`, `Tick`) und Eigenschaften (`SprintSpeedMultiplier`, `SprintStaminaDrainRate`, etc.). Die Eingabeverarbeitung wurde entsprechend erweitert.

Eine neue `UStaminaComponent` wurde hinzugefügt, die Ausdauer, Fatigue und Regeneration verwaltet. Ereignisse wie `OnStaminaChanged` und `OnExhausted` ermöglichen die Überwachung von Änderungen.

Die Benutzeroberfläche wurde angepasst: `UPlayerHUDWidget` und `UStaminaBarWidget` unterstützen nun die Anzeige der Ausdauer. Alte, fest codierte Werte wurden entfernt, und die Widgets sind nun dynamisch an die `StaminaComponent` gebunden.

Zusätzlich wurden Änderungen an `.uasset`-Dateien vorgenommen, um die neuen Mechaniken in Unreal Engine zu unterstützen.
This commit is contained in:
2026-08-27 23:43:11 +02:00
parent 78730ce939
commit 83cc8f592e
12 changed files with 365 additions and 31 deletions
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,7 @@
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "KingshearthLegacy.h"
#include "StaminaComponent.h"
AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
{
@@ -50,11 +51,41 @@ AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
// are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++)
}
void AKingshearthLegacyCharacter::BeginPlay()
{
Super::BeginPlay();
BaseWalkSpeed = GetCharacterMovement()->MaxWalkSpeed;
StaminaComponent = FindComponentByClass<UStaminaComponent>();
}
void AKingshearthLegacyCharacter::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (!StaminaComponent)
{
return;
}
const bool bIsMoving = GetVelocity().SizeSquared() > FMath::Square(SprintMovementThreshold);
if (bWantsToSprint && bIsMoving && StaminaComponent->GetStamina() > 0.0f)
{
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed * SprintSpeedMultiplier;
StaminaComponent->RemoveStamina(SprintStaminaDrainRate * DeltaSeconds);
}
else
{
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
}
}
void AKingshearthLegacyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
@@ -65,6 +96,10 @@ void AKingshearthLegacyCharacter::SetupPlayerInputComponent(UInputComponent* Pla
// Looking
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AKingshearthLegacyCharacter::Look);
// Sprinting
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AKingshearthLegacyCharacter::StartSprint);
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AKingshearthLegacyCharacter::StopSprint);
}
else
{
@@ -131,3 +166,25 @@ void AKingshearthLegacyCharacter::DoJumpEnd()
// signal the character to stop jumping
StopJumping();
}
void AKingshearthLegacyCharacter::Jump()
{
const bool bCouldJump = CanJump();
Super::Jump();
if (bCouldJump && StaminaComponent)
{
StaminaComponent->RemoveStamina(JumpStaminaCost);
}
}
void AKingshearthLegacyCharacter::StartSprint()
{
bWantsToSprint = true;
}
void AKingshearthLegacyCharacter::StopSprint()
{
bWantsToSprint = false;
}
@@ -10,6 +10,7 @@
class USpringArmComponent;
class UCameraComponent;
class UInputAction;
class UStaminaComponent;
struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
@@ -49,16 +50,42 @@ protected:
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* MouseLookAction;
/** Sprint Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* SprintAction;
/** Walk speed is multiplied by this while sprinting */
UPROPERTY(EditAnywhere, Category="Sprint")
float SprintSpeedMultiplier = 1.6f;
/** Stamina drained per second while sprinting */
UPROPERTY(EditAnywhere, Category="Sprint")
float SprintStaminaDrainRate = 25.0f;
/** Minimum speed (cm/s) for the character to be considered "moving" for sprint purposes. */
UPROPERTY(EditAnywhere, Category="Sprint")
float SprintMovementThreshold = 10.0f;
/** Stamina consumed by each successful jump. */
UPROPERTY(EditAnywhere, Category="Stamina")
float JumpStaminaCost = 15.0f;
public:
/** Constructor */
AKingshearthLegacyCharacter();
AKingshearthLegacyCharacter();
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
/** Initialize input action bindings */
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
/** Overridden to charge JumpStaminaCost whenever a jump actually starts. */
virtual void Jump() override;
protected:
/** Called for movement input */
@@ -67,6 +94,20 @@ protected:
/** Called for looking input */
void Look(const FInputActionValue& Value);
/** Called when the sprint input is pressed */
void StartSprint();
/** Called when the sprint input is released */
void StopSprint();
/** Cached from the CharacterMovementComponent's MaxWalkSpeed in BeginPlay, restored whenever not sprinting. */
float BaseWalkSpeed = 0.0f;
bool bWantsToSprint = false;
UPROPERTY()
TObjectPtr<UStaminaComponent> StaminaComponent;
public:
/** Handles move inputs from either controls or UI interfaces */
@@ -10,6 +10,7 @@
#include "Widgets/Input/SVirtualJoystick.h"
#include "UI/PlayerHUDWidget.h"
#include "HealthComponent.h"
#include "StaminaComponent.h"
void AKingshearthLegacyPlayerController::BeginPlay()
{
@@ -87,7 +88,7 @@ void AKingshearthLegacyPlayerController::OnPossess(APawn* InPawn)
if (PlayerHUD)
{
PlayerHUD->InitializeHUD(InPawn->FindComponentByClass<UHealthComponent>());
PlayerHUD->InitializeHUD(InPawn->FindComponentByClass<UHealthComponent>(), InPawn->FindComponentByClass<UStaminaComponent>());
}
}
@@ -0,0 +1,115 @@
#include "StaminaComponent.h"
UStaminaComponent::UStaminaComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UStaminaComponent::BeginPlay()
{
Super::BeginPlay();
CurrentStamina = MaxStamina;
}
void UStaminaComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
TimeSinceLastStaminaSpend += DeltaTime;
const float TimeSinceRegenStarted = TimeSinceLastStaminaSpend - StaminaRegenDelay;
if (TimeSinceRegenStarted > 0.0f && CurrentStamina < GetEffectiveMaxStamina())
{
const float RampedRegenRate = StaminaRegenRampTime > 0.0f
? FMath::Min(TimeSinceRegenStarted / StaminaRegenRampTime, 1.0f) * StaminaRegenRate
: StaminaRegenRate;
SetStamina(CurrentStamina + RampedRegenRate * DeltaTime);
// Fatigue only builds while Stamina is actively regaining - never while draining, never
// while idle. It never recovers on its own; only an explicit SetFatigue call does.
SetFatigue(Fatigue + FatigueGainRateWhileRegaining * DeltaTime);
}
}
float UStaminaComponent::GetStamina() const
{
return CurrentStamina;
}
float UStaminaComponent::GetMaxStamina() const
{
return MaxStamina;
}
float UStaminaComponent::GetFatigue() const
{
return Fatigue;
}
float UStaminaComponent::GetEffectiveMaxStamina() const
{
return MaxStamina * (1.0f - Fatigue);
}
void UStaminaComponent::SetMaxStamina(float NewMaxStamina)
{
MaxStamina = FMath::Max(NewMaxStamina, 0.0f);
CurrentStamina = FMath::Clamp(CurrentStamina, 0.0f, GetEffectiveMaxStamina());
}
void UStaminaComponent::SetStamina(float NewStamina)
{
const float PreviousStamina = CurrentStamina;
CurrentStamina = FMath::Clamp(NewStamina, 0.0f, GetEffectiveMaxStamina());
if (CurrentStamina < PreviousStamina)
{
TimeSinceLastStaminaSpend = 0.0f;
}
OnStaminaChanged.Broadcast(CurrentStamina);
// Only trigger the exhaustion signal when crossing from not-empty to empty.
if (PreviousStamina > 0.0f && CurrentStamina <= 0.0f)
{
OnExhausted.Broadcast();
}
}
void UStaminaComponent::AddStamina(float Amount)
{
if (Amount <= 0.0f)
{
return;
}
SetStamina(CurrentStamina + Amount);
}
void UStaminaComponent::RemoveStamina(float Amount)
{
if (Amount <= 0.0f)
{
return;
}
SetStamina(CurrentStamina - Amount);
}
void UStaminaComponent::SetFatigue(float NewFatigue)
{
Fatigue = FMath::Clamp(NewFatigue, 0.0f, 1.0f);
const float ClampedStamina = FMath::Clamp(CurrentStamina, 0.0f, GetEffectiveMaxStamina());
if (!FMath::IsNearlyEqual(ClampedStamina, CurrentStamina))
{
CurrentStamina = ClampedStamina;
OnStaminaChanged.Broadcast(CurrentStamina);
}
OnFatigueChanged.Broadcast(Fatigue);
}
@@ -0,0 +1,93 @@
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "StaminaComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStaminaChanged, float, NewStamina);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFatigueChanged, float, NewFatigue);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnExhausted);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class KINGSHEARTHLEGACY_API UStaminaComponent : public UActorComponent
{
GENERATED_BODY()
public:
UStaminaComponent();
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
public:
// Stamina
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetStamina() const;
/** Nominal cap, unaffected by Fatigue. Feeds the stamina bar's total width. */
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetMaxStamina() const;
/** 0-1 fraction of MaxStamina currently locked/unusable due to over-extension. */
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetFatigue() const;
UFUNCTION(BlueprintCallable, Category = "Stamina")
void SetMaxStamina(float NewMaxStamina);
UFUNCTION(BlueprintCallable, Category = "Stamina")
void SetStamina(float NewStamina);
UFUNCTION(BlueprintCallable, Category = "Stamina")
void AddStamina(float Amount);
UFUNCTION(BlueprintCallable, Category = "Stamina")
void RemoveStamina(float Amount);
UFUNCTION(BlueprintCallable, Category = "Fatigue")
void SetFatigue(float NewFatigue);
protected:
/** MaxStamina minus whatever Fatigue currently locks away - what Stamina is actually clamped against. */
float GetEffectiveMaxStamina() const;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
float MaxStamina = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stamina")
float CurrentStamina = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Fatigue")
float Fatigue = 0.0f;
/** Regen rate once fully ramped up, in stamina/sec. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
float StaminaRegenRate = 20.0f;
/** Seconds after the last stamina spend before regen starts at all. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
float StaminaRegenDelay = 1.0f;
/** Seconds for the regen rate to ramp from 0 up to StaminaRegenRate once regen starts, instead of kicking in at full speed immediately. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
float StaminaRegenRampTime = 1.5f;
/** Seconds since Stamina was last reduced by a spend (RemoveStamina/SetStamina decrease). Resets the regen delay/ramp whenever stamina is spent. */
float TimeSinceLastStaminaSpend = 0.0f;
/** Fatigue gained per second while Stamina is actively regaining (ramping back up). Never recovers on its own otherwise - only an explicit SetFatigue call (e.g. a debug key) reduces it. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fatigue")
float FatigueGainRateWhileRegaining = 0.1f;
public:
UPROPERTY(BlueprintAssignable, Category = "Stamina")
FOnStaminaChanged OnStaminaChanged;
UPROPERTY(BlueprintAssignable, Category = "Fatigue")
FOnFatigueChanged OnFatigueChanged;
/** Broadcast when Stamina is driven from a positive value to empty. */
UPROPERTY(BlueprintAssignable, Category = "Stamina")
FOnExhausted OnExhausted;
};
@@ -2,12 +2,15 @@
#include "UI/HealthBarWidget.h"
#include "UI/StaminaBarWidget.h"
void UPlayerHUDWidget::InitializeHUD(UHealthComponent* InHealthComponent)
void UPlayerHUDWidget::InitializeHUD(UHealthComponent* InHealthComponent, UStaminaComponent* InStaminaComponent)
{
if (HealthBar)
{
HealthBar->InitializeHealthBar(InHealthComponent);
}
// StaminaBar self-initializes from hardcoded defaults in its own NativeConstruct.
if (StaminaBar)
{
StaminaBar->InitializeStaminaBar(InStaminaComponent);
}
}
@@ -7,6 +7,7 @@
class UHealthBarWidget;
class UStaminaBarWidget;
class UHealthComponent;
class UStaminaComponent;
/**
* Top-level player HUD. Owns a health bar and a stamina bar and forwards
@@ -20,7 +21,7 @@ class UPlayerHUDWidget : public UUserWidget
public:
UFUNCTION(BlueprintCallable, Category = "HUD")
void InitializeHUD(UHealthComponent* InHealthComponent);
void InitializeHUD(UHealthComponent* InHealthComponent, UStaminaComponent* InStaminaComponent);
protected:
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
@@ -1,11 +1,39 @@
#include "StaminaBarWidget.h"
#include "StaminaComponent.h"
void UStaminaBarWidget::NativeConstruct()
void UStaminaBarWidget::InitializeStaminaBar(UStaminaComponent* InStaminaComponent)
{
Super::NativeConstruct();
if (!InStaminaComponent)
{
return;
}
SetStatValue(DefaultCurrentStamina, DefaultMaxStamina);
SetFatigue(DefaultFatigue);
if (StaminaComponent)
{
StaminaComponent->OnStaminaChanged.RemoveDynamic(this, &UStaminaBarWidget::HandleStaminaChanged);
StaminaComponent->OnFatigueChanged.RemoveDynamic(this, &UStaminaBarWidget::HandleFatigueChanged);
}
StaminaComponent = InStaminaComponent;
StaminaComponent->OnStaminaChanged.AddDynamic(this, &UStaminaBarWidget::HandleStaminaChanged);
StaminaComponent->OnFatigueChanged.AddDynamic(this, &UStaminaBarWidget::HandleFatigueChanged);
SetStatValue(StaminaComponent->GetStamina(), StaminaComponent->GetMaxStamina());
SetFatigue(StaminaComponent->GetFatigue());
}
void UStaminaBarWidget::HandleStaminaChanged(float NewStamina)
{
if (StaminaComponent)
{
SetStatValue(NewStamina, StaminaComponent->GetMaxStamina());
}
}
void UStaminaBarWidget::HandleFatigueChanged(float NewFatigue)
{
SetFatigue(NewFatigue);
}
void UStaminaBarWidget::SetFatigue(float NewFatigue)
+16 -21
View File
@@ -4,16 +4,12 @@
#include "UI/StatBarWidget.h"
#include "StaminaBarWidget.generated.h"
class UStaminaComponent;
/**
* Stat bar for stamina. Currently self-initializes from hardcoded defaults,
* since there is no stamina/fatigue gameplay component (DataModel) yet.
*
* TODO: once a UStaminaComponent exists (mirroring UHealthComponent), replace
* the hardcoded NativeConstruct initialization with an InitializeStaminaBar(
* UStaminaComponent* InStaminaComponent) call bound to that component's own
* OnStaminaChanged/OnFatigueChanged delegates, following the exact pattern
* used by UHealthBarWidget::InitializeHealthBar. SetFatigue/OnFatigueChanged
* below are already shaped for that hookup.
* Stat bar bound to a live UStaminaComponent. Call InitializeStaminaBar once the
* component is known (typically from the owning PlayerController on possession),
* mirroring UHealthBarWidget::InitializeHealthBar.
*/
UCLASS(abstract)
class UStaminaBarWidget : public UStatBarWidget
@@ -21,6 +17,9 @@ class UStaminaBarWidget : public UStatBarWidget
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Stamina")
void InitializeStaminaBar(UStaminaComponent* InStaminaComponent);
UFUNCTION(BlueprintCallable, Category = "Fatigue")
void SetFatigue(float NewFatigue);
@@ -28,22 +27,18 @@ public:
float GetFatigue() const { return Fatigue; }
protected:
virtual void NativeConstruct() override;
UFUNCTION()
void HandleStaminaChanged(float NewStamina);
/** Hook for Blueprint-side fatigue visuals (e.g. a darkened "locked" portion of the bar). Unused until a real fatigue value is fed in. */
UFUNCTION()
void HandleFatigueChanged(float NewFatigue);
/** Hook for Blueprint-side fatigue visuals (e.g. a darkened "locked" portion of the bar). */
UFUNCTION(BlueprintImplementableEvent, Category = "Fatigue")
void OnFatigueChanged(float NewFatigue);
/** Placeholder starting values until a real UStaminaComponent DataModel exists. */
UPROPERTY(EditDefaultsOnly, Category = "Stamina")
float DefaultCurrentStamina = 75.0f;
UPROPERTY(EditDefaultsOnly, Category = "Stamina")
float DefaultMaxStamina = 100.0f;
/** 0-1 starting fatigue applied in NativeConstruct, e.g. 0.1 = 10% of the bar locked/unusable. */
UPROPERTY(EditDefaultsOnly, Category = "Stamina")
float DefaultFatigue = 0.1f;
UPROPERTY(BlueprintReadOnly, Category = "Stamina")
TObjectPtr<UStaminaComponent> StaminaComponent;
/** 0-1 exhaustion amount. Drives the bar's penalty segment via SetPenaltyPercent. */
UPROPERTY(BlueprintReadOnly, Category = "Fatigue")