Neues Interaktionssystem implementiert

Einführung eines modularen Interaktionssystems:
- Hinzufügen der `IInteractable`- und `IInteractionLookupProvider`-Schnittstellen.
- Implementierung der `UInteractionComponent` zur Steuerung von Interaktionen (Tippen, Halten, Kontextmenü).
- Integration der `InteractionComponent` in `KingshearthLegacyCharacter`.
- Hinzufügen des `InteractionContextMenuWidget` für UI-Interaktionen.
- Änderungen an `PlayerHUDWidget`, um Interaktions-Widgets zu unterstützen.
- Neue Assets und Konfigurationsänderungen für Kollisionen und Redirects.
- Verbesserte Modularität und Benutzerfreundlichkeit durch klare Trennung von Logik und Darstellung.
This commit is contained in:
2026-09-05 03:04:12 +02:00
parent befed4c2fa
commit 8099d69a23
22 changed files with 722 additions and 4 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
"allow": [ "allow": [
"mcp__unreal-mcp__describe_toolset", "mcp__unreal-mcp__describe_toolset",
"mcp__unreal-mcp__call_tool", "mcp__unreal-mcp__call_tool",
"Bash(python *)" "Bash(python *)",
"mcp__unreal-mcp__list_toolsets"
] ]
}, },
"enabledMcpjsonServers": [ "enabledMcpjsonServers": [
+3
View File
@@ -73,6 +73,9 @@ AppliedTargetedHardwareClass=Desktop
DefaultGraphicsPerformance=Maximum DefaultGraphicsPerformance=Maximum
AppliedDefaultGraphicsPerformance=Maximum AppliedDefaultGraphicsPerformance=Maximum
[/Script/Engine.CollisionProfile]
+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,Name="Interactable",DefaultResponse=ECR_Block,bTraceType=True,bStaticObject=False)
[/Script/Engine.Engine] [/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy") +ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy") +ActiveGameNameRedirects=(OldGameName="/Script/TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
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,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "InteractionTypes.h"
#include "Interactable.generated.h"
/**
* Generic interaction interface. Blueprintable so pure-Blueprint actors
* (BP_DoorFrame, BP_JumpPad, ...) can implement it without any C++.
*/
UINTERFACE(MinimalAPI, Blueprintable)
class UInteractable : public UInterface
{
GENERATED_BODY()
};
/**
* Implement this and return interaction data - the InteractionComponent decides
* on its own whether that means a direct tap, a hold, or a Context Menu.
*/
class IInteractable
{
GENERATED_BODY()
public:
/** Returns the interactions currently available to Interactor, in priority order (index 0 = primary/tap). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
TArray<FInteractionOption> GetInteractionOptions(AActor* Interactor);
/** Runs the interaction identified by ActionID (one of the ActionIDs previously returned by GetInteractionOptions). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void ExecuteInteraction(AActor* Interactor, FName ActionID);
};
@@ -0,0 +1,205 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "InteractionComponent.h"
#include "Interactable.h"
#include "InteractionLookupProvider.h"
#include "Engine/World.h"
#include "TimerManager.h"
#include "GameFramework/Actor.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/Controller.h"
#include "DrawDebugHelpers.h"
#include "KingshearthLegacy.h"
UInteractionComponent::UInteractionComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UInteractionComponent::BeginPlay()
{
Super::BeginPlay();
UpdateFocus();
}
void UInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
UpdateFocus();
}
bool UInteractionComponent::PerformInteractionTrace(FHitResult& OutHit) const
{
AActor* Owner = GetOwner();
UWorld* World = GetWorld();
if (!Owner || !World)
{
return false;
}
if (Owner->Implements<UInteractionLookupProvider>())
{
if (IInteractionLookupProvider::Execute_InteractionLookupLoop(Owner, OutHit))
{
return true;
}
}
FVector TraceStart = Owner->GetActorLocation();
FRotator TraceRotation = Owner->GetActorRotation();
if (const APawn* OwnerPawn = Cast<APawn>(Owner))
{
if (const AController* Controller = OwnerPawn->GetController())
{
Controller->GetPlayerViewPoint(TraceStart, TraceRotation);
}
else
{
TraceStart = OwnerPawn->GetPawnViewLocation();
TraceRotation = OwnerPawn->GetViewRotation();
}
}
const FVector TraceEnd = TraceStart + TraceRotation.Vector() * InteractionRange;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(Owner);
const bool bHit = World->LineTraceSingleByChannel(OutHit, TraceStart, TraceEnd, InteractionTraceChannel, QueryParams);
#if ENABLE_DRAW_DEBUG
if (bShowDebugTraces)
{
const bool bHitInteractable = bHit && OutHit.GetActor() && OutHit.GetActor()->Implements<UInteractable>();
DrawDebugLine(World, TraceStart, bHit ? OutHit.Location : TraceEnd, bHitInteractable ? FColor::Green : FColor::Red, false, 0.0f, 0, 1.5f);
if (bHit)
{
DrawDebugSphere(World, OutHit.Location, 8.0f, 12, bHitInteractable ? FColor::Green : FColor::Red, false, 0.0f);
}
}
#endif
return bHit;
}
void UInteractionComponent::UpdateFocus()
{
FHitResult Hit;
const bool bHit = PerformInteractionTrace(Hit);
AActor* NewFocusedActor = (bHit && Hit.GetActor() && Hit.GetActor()->Implements<UInteractable>()) ? Hit.GetActor() : nullptr;
if (NewFocusedActor == FocusedActor.Get())
{
return;
}
FocusedActor = NewFocusedActor;
FocusedOptions = NewFocusedActor ? IInteractable::Execute_GetInteractionOptions(NewFocusedActor, GetOwner()) : TArray<FInteractionOption>();
#if ENABLE_DRAW_DEBUG
if (bShowDebugTraces)
{
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Focus changed to '%s' (%d option(s))"), *GetNameSafe(NewFocusedActor), FocusedOptions.Num());
}
#endif
OnFocusedInteractableChanged.Broadcast(NewFocusedActor, FocusedOptions);
}
void UInteractionComponent::TryBeginInteract()
{
UpdateFocus();
if (!FocusedActor.IsValid() || FocusedOptions.Num() == 0)
{
return;
}
CurrentTarget = FocusedActor;
CurrentOptions = FocusedOptions;
HighlightedIndex = 0;
bHoldConsumed = false;
if (CurrentOptions.Num() >= 2)
{
GetWorld()->GetTimerManager().SetTimer(HoldTimerHandle, this, &UInteractionComponent::HandleHoldThresholdReached, HoldThreshold, false);
}
}
void UInteractionComponent::TryEndInteract()
{
GetWorld()->GetTimerManager().ClearTimer(HoldTimerHandle);
if (bContextMenuOpen)
{
ExecuteOption(HighlightedIndex);
CloseContextMenu();
}
else if (!bHoldConsumed && CurrentOptions.Num() >= 1)
{
ExecuteOption(0);
}
ClearCachedTarget();
}
void UInteractionComponent::CycleContextMenuOptions(float Direction)
{
if (!bContextMenuOpen || CurrentOptions.Num() == 0 || FMath::IsNearlyZero(Direction))
{
return;
}
const int32 Step = Direction > 0.0f ? 1 : -1;
HighlightedIndex = (HighlightedIndex + Step + CurrentOptions.Num()) % CurrentOptions.Num();
OnContextMenuHighlightChanged.Broadcast(HighlightedIndex);
}
void UInteractionComponent::HandleHoldThresholdReached()
{
bHoldConsumed = true;
if (CurrentOptions.Num() == 2)
{
ExecuteOption(1);
ClearCachedTarget();
}
else if (CurrentOptions.Num() >= 3)
{
OpenContextMenu();
}
}
void UInteractionComponent::OpenContextMenu()
{
bContextMenuOpen = true;
HighlightedIndex = 0;
OnContextMenuOpened.Broadcast(CurrentOptions, HighlightedIndex);
}
void UInteractionComponent::CloseContextMenu()
{
bContextMenuOpen = false;
OnContextMenuClosed.Broadcast();
}
void UInteractionComponent::ExecuteOption(int32 Index)
{
AActor* Target = CurrentTarget.Get();
if (!Target || !CurrentOptions.IsValidIndex(Index))
{
return;
}
IInteractable::Execute_ExecuteInteraction(Target, GetOwner(), CurrentOptions[Index].ActionID);
}
void UInteractionComponent::ClearCachedTarget()
{
CurrentTarget = nullptr;
CurrentOptions.Reset();
HighlightedIndex = 0;
bHoldConsumed = false;
}
@@ -0,0 +1,144 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionTypes.h"
#include "InteractionComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnFocusedInteractableChanged, AActor*, FocusedActor, const TArray<FInteractionOption>&, FocusedOptions);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContextMenuOpened, const TArray<FInteractionOption>&, Options, int32, InitialHighlightedIndex);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContextMenuHighlightChanged, int32, NewHighlightedIndex);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnContextMenuClosed);
/**
* Drives the whole "hold E" contextual interaction system for its owning Actor:
* every tick it look-traces for an IInteractable, distinguishes tap/hold on the
* currently-focused one, and either executes an interaction directly (1-2 options)
* or drives a Context Menu (3+ options). Interactable objects need to know nothing
* about any of this - they just implement IInteractable and return their own
* FInteractionOption data.
*
* Detection is a single line trace on the "Interactable" trace channel (see
* DefaultEngine.ini - ECC_GameTraceChannel1, default response Block), not a
* proximity sweep. That channel defaults to blocking everything, exactly like
* Visibility, so ordinary geometry occludes it correctly out of the box; to make
* only part of an actor count as its "interactable hit box", set that specific
* component's Interactable trace response and turn the rest of the mesh's off
* (Ignore) in its Collision settings - no C++ changes needed per-actor.
*/
UCLASS(ClassGroup = (Interaction), Blueprintable, meta = (BlueprintSpawnableComponent))
class KINGSHEARTHLEGACY_API UInteractionComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInteractionComponent();
/** Fired every time the look-traced focus changes, including to/from nothing - drives an ever-present "you can interact" prompt. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnFocusedInteractableChanged OnFocusedInteractableChanged;
/** Fired when 3+ options are available and the hold threshold is reached. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnContextMenuOpened OnContextMenuOpened;
/** Fired whenever CycleContextMenuOptions moves the highlighted entry. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnContextMenuHighlightChanged OnContextMenuHighlightChanged;
/** Fired when the Context Menu closes, whether by confirming a choice or losing its target. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnContextMenuClosed OnContextMenuClosed;
/** Call from the Interact input's Started/pressed event. */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void TryBeginInteract();
/** Call from the Interact input's Completed/released event. */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void TryEndInteract();
/** Call while the Context Menu is open (e.g. from a mouse wheel axis) to move the highlighted entry. No-op otherwise. */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void CycleContextMenuOptions(float Direction);
UFUNCTION(BlueprintPure, Category = "Interaction")
bool IsContextMenuOpen() const { return bContextMenuOpen; }
/** The actor currently under the look-trace, or nullptr if none. */
UFUNCTION(BlueprintPure, Category = "Interaction")
AActor* GetFocusedActor() const { return FocusedActor.Get(); }
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/** Max distance of the look trace. */
UPROPERTY(EditAnywhere, Category = "Interaction")
float InteractionRange = 300.0f;
/** Trace channel the look trace queries - see the "Interactable" channel set up in DefaultEngine.ini. */
UPROPERTY(EditAnywhere, Category = "Interaction")
TEnumAsByte<ECollisionChannel> InteractionTraceChannel = ECC_GameTraceChannel1;
/** Seconds the Interact input must be held before it counts as a hold rather than a tap. */
UPROPERTY(EditAnywhere, Category = "Interaction")
float HoldThreshold = 0.4f;
/** Draws the look trace every tick (green = hit an IInteractable, red = blocked/missed) and logs focus changes. */
UPROPERTY(EditAnywhere, Category = "Interaction|Debug")
bool bShowDebugTraces = false;
/**
* Finds the look-trace target. If the owning Actor implements IInteractionLookupProvider,
* defers to its InteractionLookupLoop first - implement that interface directly on your
* Character/Actor to fully replace how a target is found (VR hand ray, a cone check, ...)
* without touching the tap/hold/menu logic below. Falls back to a line trace from the
* controller's view point along its view direction, InteractionRange long, on
* InteractionTraceChannel, whenever the interface isn't implemented or returns false.
*/
bool PerformInteractionTrace(FHitResult& OutHit) const;
/** Re-runs PerformInteractionTrace, updates FocusedActor/FocusedOptions, and broadcasts OnFocusedInteractableChanged if either changed. */
void UpdateFocus();
/** Bound to the hold timer; only ever runs when CurrentOptions.Num() >= 2. */
void HandleHoldThresholdReached();
void OpenContextMenu();
void CloseContextMenu();
/** Executes CurrentOptions[Index] against CurrentTarget, if both are still valid. */
void ExecuteOption(int32 Index);
void ClearCachedTarget();
/** Actor currently under the look trace and implementing IInteractable, updated every tick by UpdateFocus. */
UPROPERTY()
TWeakObjectPtr<AActor> FocusedActor;
UPROPERTY()
TArray<FInteractionOption> FocusedOptions;
/** Snapshot of FocusedActor/FocusedOptions taken when Interact was pressed - what TryEndInteract actually acts on. */
UPROPERTY()
TWeakObjectPtr<AActor> CurrentTarget;
UPROPERTY()
TArray<FInteractionOption> CurrentOptions;
UPROPERTY()
int32 HighlightedIndex = 0;
UPROPERTY()
bool bContextMenuOpen = false;
/** Set the moment the hold timer fires, so TryEndInteract knows not to also run the tap path. */
bool bHoldConsumed = false;
FTimerHandle HoldTimerHandle;
};
@@ -0,0 +1,33 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "InteractionLookupProvider.generated.h"
UINTERFACE(MinimalAPI, Blueprintable)
class UInteractionLookupProvider : public UInterface
{
GENERATED_BODY()
};
/**
* Optional interface for the Actor a UInteractionComponent is attached to (e.g.
* BP_ThirdPersonCharacter). Implement it there - Class Settings > Implement Interface,
* just like IInteractable on an interactable object - and override InteractionLookupLoop
* in that Actor's own Event Graph to replace how its "look at" target is found (a
* different origin, a cone check, a VR hand-ray, ...). No separate component subclass
* or class-swapping needed. If not implemented, or the implementation returns false,
* UInteractionComponent falls back to its own default camera-forward line trace.
*/
class IInteractionLookupProvider
{
GENERATED_BODY()
public:
/** Return true and fill OutHit with what was found; return false to fall back to the component's default trace. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
bool InteractionLookupLoop(FHitResult& OutHit);
};
@@ -0,0 +1,25 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "InteractionTypes.generated.h"
/**
* A single interaction an IInteractable currently offers. Order matters:
* index 0 is always the primary (tap) action, index 1 (if present) is the
* secondary (hold) action, and 3+ entries populate the Context Menu in order.
*/
USTRUCT(BlueprintType)
struct FInteractionOption
{
GENERATED_BODY()
/** Shown to the player (Context Menu entry, interaction prompt, ...) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FText DisplayName;
/** Opaque id passed back to IInteractable::ExecuteInteraction - the object interprets it itself */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FName ActionID;
};
@@ -30,6 +30,7 @@ public class KingshearthLegacy : ModuleRules
"KingshearthLegacy", "KingshearthLegacy",
"KingshearthLegacy/AttributeSets", "KingshearthLegacy/AttributeSets",
"KingshearthLegacy/GameplayEffects", "KingshearthLegacy/GameplayEffects",
"KingshearthLegacy/Interaction",
"KingshearthLegacy/UI/Menu", "KingshearthLegacy/UI/Menu",
"KingshearthLegacy/Variant_Platforming", "KingshearthLegacy/Variant_Platforming",
"KingshearthLegacy/Variant_Platforming/Animation", "KingshearthLegacy/Variant_Platforming/Animation",
@@ -19,6 +19,7 @@
#include "GE_HealthDebuff.h" #include "GE_HealthDebuff.h"
#include "GE_StaminaFatiguePenalty.h" #include "GE_StaminaFatiguePenalty.h"
#include "KingshearthLegacyGameplayTags.h" #include "KingshearthLegacyGameplayTags.h"
#include "Interaction/InteractionComponent.h"
AKingshearthLegacyCharacter::AKingshearthLegacyCharacter() AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
{ {
@@ -64,6 +65,8 @@ AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
StaminaAttributeSet = CreateDefaultSubobject<UStaminaAttributeSet>(TEXT("StaminaAttributeSet")); StaminaAttributeSet = CreateDefaultSubobject<UStaminaAttributeSet>(TEXT("StaminaAttributeSet"));
AbilitySystemComponent->AddAttributeSetSubobject(StaminaAttributeSet.Get()); AbilitySystemComponent->AddAttributeSetSubobject(StaminaAttributeSet.Get());
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
} }
void AKingshearthLegacyCharacter::BeginPlay() void AKingshearthLegacyCharacter::BeginPlay()
@@ -276,6 +279,11 @@ void AKingshearthLegacyCharacter::SetupPlayerInputComponent(UInputComponent* Pla
// Sprinting // Sprinting
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AKingshearthLegacyCharacter::StartSprint); EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AKingshearthLegacyCharacter::StartSprint);
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AKingshearthLegacyCharacter::StopSprint); EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AKingshearthLegacyCharacter::StopSprint);
// Interacting
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AKingshearthLegacyCharacter::DoInteractStart);
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Completed, this, &AKingshearthLegacyCharacter::DoInteractEnd);
EnhancedInputComponent->BindAction(InteractCycleAction, ETriggerEvent::Triggered, this, &AKingshearthLegacyCharacter::InteractCycle);
} }
else else
{ {
@@ -343,6 +351,35 @@ void AKingshearthLegacyCharacter::DoJumpEnd()
StopJumping(); StopJumping();
} }
void AKingshearthLegacyCharacter::DoInteractStart()
{
if (InteractionComponent)
{
InteractionComponent->TryBeginInteract();
}
}
void AKingshearthLegacyCharacter::DoInteractEnd()
{
if (InteractionComponent)
{
InteractionComponent->TryEndInteract();
}
}
void AKingshearthLegacyCharacter::DoInteractCycle(float Direction)
{
if (InteractionComponent)
{
InteractionComponent->CycleContextMenuOptions(Direction);
}
}
void AKingshearthLegacyCharacter::InteractCycle(const FInputActionValue& Value)
{
DoInteractCycle(Value.Get<float>());
}
void AKingshearthLegacyCharacter::Jump() void AKingshearthLegacyCharacter::Jump()
{ {
const bool bCouldJump = CanJump(); const bool bCouldJump = CanJump();
@@ -15,6 +15,7 @@ class UInputAction;
class UAbilitySystemComponent; class UAbilitySystemComponent;
class UHealthAttributeSet; class UHealthAttributeSet;
class UStaminaAttributeSet; class UStaminaAttributeSet;
class UInteractionComponent;
struct FInputActionValue; struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All); DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
@@ -58,6 +59,14 @@ protected:
UPROPERTY(EditAnywhere, Category="Input") UPROPERTY(EditAnywhere, Category="Input")
UInputAction* SprintAction; UInputAction* SprintAction;
/** Interact Input Action - Started begins a tap/hold, Completed resolves it */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* InteractAction;
/** Cycles the Context Menu's highlighted option while it is open (e.g. mouse wheel axis) */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* InteractCycleAction;
/** Walk speed is multiplied by this while sprinting */ /** Walk speed is multiplied by this while sprinting */
UPROPERTY(EditAnywhere, Category="Sprint") UPROPERTY(EditAnywhere, Category="Sprint")
float SprintSpeedMultiplier = 1.6f; float SprintSpeedMultiplier = 1.6f;
@@ -120,6 +129,9 @@ protected:
/** Called when the sprint input is released */ /** Called when the sprint input is released */
void StopSprint(); void StopSprint();
/** Called for Context Menu cycling input (e.g. mouse wheel axis) */
void InteractCycle(const FInputActionValue& Value);
/** Advances the regen delay/ramp curve and applies Stamina/Fatigue regen for this frame. */ /** Advances the regen delay/ramp curve and applies Stamina/Fatigue regen for this frame. */
void TickStaminaRegen(float DeltaSeconds); void TickStaminaRegen(float DeltaSeconds);
@@ -144,6 +156,10 @@ protected:
/** Currently-applied GE_HealthDebuff instance, if any - reapplied/removed wholesale by SetHealthDebuff so it behaves like the old "set to X" API instead of stacking. */ /** Currently-applied GE_HealthDebuff instance, if any - reapplied/removed wholesale by SetHealthDebuff so it behaves like the old "set to X" API instead of stacking. */
FActiveGameplayEffectHandle HealthDebuffEffectHandle; FActiveGameplayEffectHandle HealthDebuffEffectHandle;
/** Drives the contextual "hold E" interaction system - see UInteractionComponent. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Interaction", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInteractionComponent> InteractionComponent;
public: public:
//~ Begin IAbilitySystemInterface //~ Begin IAbilitySystemInterface
@@ -219,6 +235,21 @@ public:
UFUNCTION(BlueprintCallable, Category="Input") UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoJumpEnd(); virtual void DoJumpEnd();
/** Handles interact pressed inputs from either controls or UI interfaces - begins a tap/hold */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoInteractStart();
/** Handles interact released inputs from either controls or UI interfaces - resolves the tap/hold */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoInteractEnd();
/** Handles Context Menu cycling inputs (e.g. mouse wheel) from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoInteractCycle(float Direction);
/** Returns InteractionComponent subobject **/
FORCEINLINE UInteractionComponent* GetInteractionComponent() const { return InteractionComponent; }
public: public:
/** Returns CameraBoom subobject **/ /** Returns CameraBoom subobject **/
@@ -102,8 +102,9 @@ void AKingshearthLegacyPlayerController::OnPossess(APawn* InPawn)
const AKingshearthLegacyCharacter* PossessedCharacter = Cast<AKingshearthLegacyCharacter>(InPawn); const AKingshearthLegacyCharacter* PossessedCharacter = Cast<AKingshearthLegacyCharacter>(InPawn);
UHealthAttributeSet* PossessedHealthAttributeSet = PossessedCharacter ? PossessedCharacter->GetHealthAttributeSet() : nullptr; UHealthAttributeSet* PossessedHealthAttributeSet = PossessedCharacter ? PossessedCharacter->GetHealthAttributeSet() : nullptr;
UStaminaAttributeSet* PossessedStaminaAttributeSet = PossessedCharacter ? PossessedCharacter->GetStaminaAttributeSet() : nullptr; UStaminaAttributeSet* PossessedStaminaAttributeSet = PossessedCharacter ? PossessedCharacter->GetStaminaAttributeSet() : nullptr;
UInteractionComponent* PossessedInteractionComponent = PossessedCharacter ? PossessedCharacter->GetInteractionComponent() : nullptr;
PlayerHUD->InitializeHUD(PossessedHealthAttributeSet, PossessedStaminaAttributeSet); PlayerHUD->InitializeHUD(PossessedHealthAttributeSet, PossessedStaminaAttributeSet, PossessedInteractionComponent);
} }
} }
@@ -0,0 +1,110 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "InteractionContextMenuWidget.h"
#include "Interaction/InteractionComponent.h"
#include "Components/VerticalBox.h"
#include "Components/TextBlock.h"
#include "Blueprint/WidgetTree.h"
#include "KingshearthLegacy.h"
void UInteractionContextMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
if (PromptText)
{
PromptText->SetVisibility(ESlateVisibility::Collapsed);
}
if (OptionsBox)
{
OptionsBox->SetVisibility(ESlateVisibility::Collapsed);
}
}
void UInteractionContextMenuWidget::InitializeInteraction(UInteractionComponent* InInteractionComponent)
{
if (InteractionComponent)
{
InteractionComponent->OnFocusedInteractableChanged.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleFocusedInteractableChanged);
InteractionComponent->OnContextMenuOpened.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
InteractionComponent->OnContextMenuHighlightChanged.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
InteractionComponent->OnContextMenuClosed.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
}
InteractionComponent = InInteractionComponent;
if (InteractionComponent)
{
InteractionComponent->OnFocusedInteractableChanged.AddDynamic(this, &UInteractionContextMenuWidget::HandleFocusedInteractableChanged);
InteractionComponent->OnContextMenuOpened.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
InteractionComponent->OnContextMenuHighlightChanged.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
InteractionComponent->OnContextMenuClosed.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
}
}
void UInteractionContextMenuWidget::HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options)
{
if (!PromptText)
{
return;
}
if (FocusedActor && Options.Num() > 0)
{
PromptText->SetText(Options[0].DisplayName);
PromptText->SetVisibility(ESlateVisibility::HitTestInvisible);
}
else
{
PromptText->SetVisibility(ESlateVisibility::Collapsed);
}
}
void UInteractionContextMenuWidget::RefreshOptions(const TArray<FInteractionOption>& Options, int32 HighlightedIndex)
{
if (!OptionsBox)
{
return;
}
OptionsBox->ClearChildren();
for (int32 Index = 0; Index < Options.Num(); ++Index)
{
UTextBlock* Row = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass());
Row->SetText(Options[Index].DisplayName);
Row->SetColorAndOpacity(FSlateColor(Index == HighlightedIndex ? HighlightedColor : NormalColor));
OptionsBox->AddChildToVerticalBox(Row);
}
}
void UInteractionContextMenuWidget::HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex)
{
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Widget received OnContextMenuOpened: %d option(s), OptionsBox bound: %s"),
Options.Num(), OptionsBox ? TEXT("yes") : TEXT("NO - add a VerticalBox named exactly \"OptionsBox\" in this widget's Designer tree"));
CachedOptions = Options;
RefreshOptions(CachedOptions, InitialHighlightedIndex);
if (OptionsBox)
{
OptionsBox->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
void UInteractionContextMenuWidget::HandleContextMenuHighlightChanged(int32 NewHighlightedIndex)
{
RefreshOptions(CachedOptions, NewHighlightedIndex);
}
void UInteractionContextMenuWidget::HandleContextMenuClosed()
{
CachedOptions.Reset();
if (OptionsBox)
{
OptionsBox->ClearChildren();
OptionsBox->SetVisibility(ESlateVisibility::Collapsed);
}
}
@@ -0,0 +1,76 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Interaction/InteractionTypes.h"
#include "InteractionContextMenuWidget.generated.h"
class UInteractionComponent;
class UVerticalBox;
class UTextBlock;
/**
* One widget covering both interaction UI needs so you only wire up a single thing
* in WBP_PlayerHUD: an ever-present "you're looking at something" prompt (PromptText,
* driven by OnFocusedInteractableChanged - shows any time the look trace is on an
* IInteractable, regardless of option count or whether Interact is even pressed) and
* the Context Menu shown while holding Interact on a 3+-option IInteractable
* (OptionsBox, driven by OnContextMenuOpened/HighlightChanged/Closed). Both bound
* widgets are optional and toggle their own visibility independently - add whichever
* you want in the Designer, named exactly as below; all population/highlight/show-hide
* logic lives here in C++.
*/
UCLASS(abstract)
class UInteractionContextMenuWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** Call once, right after creation (see UPlayerHUDWidget::InitializeHUD), to bind to InInteractionComponent's delegates. */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void InitializeInteraction(UInteractionComponent* InInteractionComponent);
protected:
virtual void NativeConstruct() override;
/** Rebuilds OptionsBox's rows from Options, styling HighlightedIndex differently from the rest. */
void RefreshOptions(const TArray<FInteractionOption>& Options, int32 HighlightedIndex);
UFUNCTION()
void HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options);
UFUNCTION()
void HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex);
UFUNCTION()
void HandleContextMenuHighlightChanged(int32 NewHighlightedIndex);
UFUNCTION()
void HandleContextMenuClosed();
/** Text color used for the highlighted row. */
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
FLinearColor HighlightedColor = FLinearColor(1.0f, 0.85f, 0.3f, 1.0f);
/** Text color used for every other row. */
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
FLinearColor NormalColor = FLinearColor::White;
/** Ever-present prompt - shows the primary option's DisplayName any time the look trace is on an IInteractable. Optional, place it in the WBP subclass's Designer tree. */
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UTextBlock> PromptText;
/** Container the Context Menu's option rows are built into. Optional, place it in the WBP subclass's Designer tree. */
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UVerticalBox> OptionsBox;
UPROPERTY()
TArray<FInteractionOption> CachedOptions;
UPROPERTY()
TObjectPtr<UInteractionComponent> InteractionComponent;
};
@@ -1,8 +1,10 @@
#include "PlayerHUDWidget.h" #include "PlayerHUDWidget.h"
#include "UI/HealthBarWidget.h" #include "UI/HealthBarWidget.h"
#include "UI/StaminaBarWidget.h" #include "UI/StaminaBarWidget.h"
#include "UI/InteractionContextMenuWidget.h"
#include "KingshearthLegacy.h"
void UPlayerHUDWidget::InitializeHUD(UHealthAttributeSet* InHealthAttributeSet, UStaminaAttributeSet* InStaminaAttributeSet) void UPlayerHUDWidget::InitializeHUD(UHealthAttributeSet* InHealthAttributeSet, UStaminaAttributeSet* InStaminaAttributeSet, UInteractionComponent* InInteractionComponent)
{ {
if (HealthBar) if (HealthBar)
{ {
@@ -13,4 +15,11 @@ void UPlayerHUDWidget::InitializeHUD(UHealthAttributeSet* InHealthAttributeSet,
{ {
StaminaBar->InitializeStaminaBar(InStaminaAttributeSet); StaminaBar->InitializeStaminaBar(InStaminaAttributeSet);
} }
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] PlayerHUD ContextMenuWidget bound: %s"), ContextMenuWidget ? TEXT("yes") : TEXT("NO - check the widget in ContextMenuWidget is named exactly \"ContextMenuWidget\" in WBP_PlayerHUD's Designer tree"));
if (ContextMenuWidget)
{
ContextMenuWidget->InitializeInteraction(InInteractionComponent);
}
} }
@@ -8,6 +8,8 @@ class UHealthBarWidget;
class UStaminaBarWidget; class UStaminaBarWidget;
class UHealthAttributeSet; class UHealthAttributeSet;
class UStaminaAttributeSet; class UStaminaAttributeSet;
class UInteractionComponent;
class UInteractionContextMenuWidget;
/** /**
* Top-level player HUD. Owns a health bar and a stamina bar and forwards * Top-level player HUD. Owns a health bar and a stamina bar and forwards
@@ -21,7 +23,7 @@ class UPlayerHUDWidget : public UUserWidget
public: public:
UFUNCTION(BlueprintCallable, Category = "HUD") UFUNCTION(BlueprintCallable, Category = "HUD")
void InitializeHUD(UHealthAttributeSet* InHealthAttributeSet, UStaminaAttributeSet* InStaminaAttributeSet); void InitializeHUD(UHealthAttributeSet* InHealthAttributeSet, UStaminaAttributeSet* InStaminaAttributeSet, UInteractionComponent* InInteractionComponent);
protected: protected:
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
@@ -29,4 +31,7 @@ protected:
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UStaminaBarWidget> StaminaBar; TObjectPtr<UStaminaBarWidget> StaminaBar;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
TObjectPtr<UInteractionContextMenuWidget> ContextMenuWidget;
}; };