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:
@@ -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/AttributeSets",
|
||||
"KingshearthLegacy/GameplayEffects",
|
||||
"KingshearthLegacy/Interaction",
|
||||
"KingshearthLegacy/UI/Menu",
|
||||
"KingshearthLegacy/Variant_Platforming",
|
||||
"KingshearthLegacy/Variant_Platforming/Animation",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "GE_HealthDebuff.h"
|
||||
#include "GE_StaminaFatiguePenalty.h"
|
||||
#include "KingshearthLegacyGameplayTags.h"
|
||||
#include "Interaction/InteractionComponent.h"
|
||||
|
||||
AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
|
||||
{
|
||||
@@ -64,6 +65,8 @@ AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
|
||||
|
||||
StaminaAttributeSet = CreateDefaultSubobject<UStaminaAttributeSet>(TEXT("StaminaAttributeSet"));
|
||||
AbilitySystemComponent->AddAttributeSetSubobject(StaminaAttributeSet.Get());
|
||||
|
||||
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
|
||||
}
|
||||
|
||||
void AKingshearthLegacyCharacter::BeginPlay()
|
||||
@@ -276,6 +279,11 @@ void AKingshearthLegacyCharacter::SetupPlayerInputComponent(UInputComponent* Pla
|
||||
// Sprinting
|
||||
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AKingshearthLegacyCharacter::StartSprint);
|
||||
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
|
||||
{
|
||||
@@ -343,6 +351,35 @@ void AKingshearthLegacyCharacter::DoJumpEnd()
|
||||
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()
|
||||
{
|
||||
const bool bCouldJump = CanJump();
|
||||
|
||||
@@ -15,6 +15,7 @@ class UInputAction;
|
||||
class UAbilitySystemComponent;
|
||||
class UHealthAttributeSet;
|
||||
class UStaminaAttributeSet;
|
||||
class UInteractionComponent;
|
||||
struct FInputActionValue;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
|
||||
@@ -58,6 +59,14 @@ protected:
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
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 */
|
||||
UPROPERTY(EditAnywhere, Category="Sprint")
|
||||
float SprintSpeedMultiplier = 1.6f;
|
||||
@@ -120,6 +129,9 @@ protected:
|
||||
/** Called when the sprint input is released */
|
||||
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. */
|
||||
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. */
|
||||
FActiveGameplayEffectHandle HealthDebuffEffectHandle;
|
||||
|
||||
/** Drives the contextual "hold E" interaction system - see UInteractionComponent. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Interaction", meta = (AllowPrivateAccess = "true"))
|
||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
||||
|
||||
public:
|
||||
|
||||
//~ Begin IAbilitySystemInterface
|
||||
@@ -219,6 +235,21 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
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:
|
||||
|
||||
/** Returns CameraBoom subobject **/
|
||||
|
||||
@@ -102,8 +102,9 @@ void AKingshearthLegacyPlayerController::OnPossess(APawn* InPawn)
|
||||
const AKingshearthLegacyCharacter* PossessedCharacter = Cast<AKingshearthLegacyCharacter>(InPawn);
|
||||
UHealthAttributeSet* PossessedHealthAttributeSet = PossessedCharacter ? PossessedCharacter->GetHealthAttributeSet() : 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 "UI/HealthBarWidget.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)
|
||||
{
|
||||
@@ -13,4 +15,11 @@ void UPlayerHUDWidget::InitializeHUD(UHealthAttributeSet* InHealthAttributeSet,
|
||||
{
|
||||
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 UHealthAttributeSet;
|
||||
class UStaminaAttributeSet;
|
||||
class UInteractionComponent;
|
||||
class UInteractionContextMenuWidget;
|
||||
|
||||
/**
|
||||
* Top-level player HUD. Owns a health bar and a stamina bar and forwards
|
||||
@@ -21,7 +23,7 @@ class UPlayerHUDWidget : public UUserWidget
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "HUD")
|
||||
void InitializeHUD(UHealthAttributeSet* InHealthAttributeSet, UStaminaAttributeSet* InStaminaAttributeSet);
|
||||
void InitializeHUD(UHealthAttributeSet* InHealthAttributeSet, UStaminaAttributeSet* InStaminaAttributeSet, UInteractionComponent* InInteractionComponent);
|
||||
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
@@ -29,4 +31,7 @@ protected:
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UStaminaBarWidget> StaminaBar;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UInteractionContextMenuWidget> ContextMenuWidget;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user