Contextual interaction system #6
@@ -3,7 +3,8 @@
|
||||
"allow": [
|
||||
"mcp__unreal-mcp__describe_toolset",
|
||||
"mcp__unreal-mcp__call_tool",
|
||||
"Bash(python *)"
|
||||
"Bash(python *)",
|
||||
"mcp__unreal-mcp__list_toolsets"
|
||||
]
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
|
||||
@@ -73,6 +73,65 @@ AppliedTargetedHardwareClass=Desktop
|
||||
DefaultGraphicsPerformance=Maximum
|
||||
AppliedDefaultGraphicsPerformance=Maximum
|
||||
|
||||
[/Script/Engine.CollisionProfile]
|
||||
-Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision",bCanModify=False)
|
||||
-Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||
-Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||
-Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||
-Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||
-Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.",bCanModify=False)
|
||||
-Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ",bCanModify=False)
|
||||
-Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ",bCanModify=False)
|
||||
-Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic",Response=ECR_Block),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.",bCanModify=False)
|
||||
-Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.",bCanModify=False)
|
||||
-Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors",bCanModify=False)
|
||||
-Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors",bCanModify=False)
|
||||
-Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.",bCanModify=False)
|
||||
-Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.",bCanModify=False)
|
||||
-Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.",bCanModify=False)
|
||||
-Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.",bCanModify=False)
|
||||
-Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.",bCanModify=False)
|
||||
-Profiles=(Name="UI",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Block),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False)
|
||||
-Profiles=(Name="WaterBodyCollision",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="",CustomResponses=((Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="Default Water Collision Profile (Created by Water Plugin)")
|
||||
+Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision")
|
||||
+Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ")
|
||||
+Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ")
|
||||
+Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ")
|
||||
+Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ")
|
||||
+Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.")
|
||||
+Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ")
|
||||
+Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ")
|
||||
+Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic"),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.")
|
||||
+Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.")
|
||||
+Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors")
|
||||
+Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors")
|
||||
+Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.")
|
||||
+Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.")
|
||||
+Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.")
|
||||
+Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.")
|
||||
+Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.")
|
||||
+Profiles=(Name="UI",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility"),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ")
|
||||
+Profiles=(Name="WaterBodyCollision",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="",CustomResponses=((Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="Default Water Collision Profile (Created by Water Plugin)")
|
||||
+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Block,bTraceType=True,bStaticObject=False,Name="Interactable")
|
||||
-ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall")
|
||||
-ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn")
|
||||
-ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic")
|
||||
-ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor")
|
||||
-ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic")
|
||||
+ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall")
|
||||
+ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn")
|
||||
+ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic")
|
||||
+ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor")
|
||||
+ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic")
|
||||
-CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic")
|
||||
-CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic")
|
||||
-CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle")
|
||||
-CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn")
|
||||
+CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic")
|
||||
+CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic")
|
||||
+CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle")
|
||||
+CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn")
|
||||
|
||||
[/Script/Engine.Engine]
|
||||
+ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
|
||||
+ActiveGameNameRedirects=(OldGameName="/Script/TP_ThirdPerson",NewGameName="/Script/KingshearthLegacy")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[/Script/EngineSettings.GeneralProjectSettings]
|
||||
ProjectID=CE954E444D2A872317AAC38182169D03
|
||||
ProjectName=Third Person Game Template
|
||||
ProjectName=Kingsharth Legacy
|
||||
|
||||
[ConsoleVariables]
|
||||
CommonUI.CheckKeyboardFocusAndParentage=1
|
||||
CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=1
|
||||
CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
// 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);
|
||||
|
||||
/** Friendly object name for prompts, e.g. "Berry Bush", "Wooden Chest", "Workbench". Optional - an empty default just drops the "with X" part of the prompt. */
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
|
||||
FText GetInteractableDisplayName();
|
||||
|
||||
/** 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,258 @@
|
||||
// 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();
|
||||
UpdateHoldProgress();
|
||||
}
|
||||
|
||||
bool UInteractionComponent::PerformInteractionTrace(FHitResult& OutHit) const
|
||||
{
|
||||
AActor* Owner = GetOwner();
|
||||
UWorld* World = GetWorld();
|
||||
if (!Owner || !World)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Owner->Implements<UInteractionLookupProvider>())
|
||||
{
|
||||
// Authoritative once implemented - a miss here means "nothing found", full stop.
|
||||
// Don't silently fall through to the default trace below just because this one
|
||||
// came back empty; that would use a completely different (un-offset) trace and
|
||||
// could contradict what the override deliberately decided.
|
||||
return IInteractionLookupProvider::Execute_InteractionLookupLoop(Owner, OutHit);
|
||||
}
|
||||
|
||||
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);
|
||||
const bool bImplementsInteractable = bHit && Hit.GetActor() && Hit.GetActor()->Implements<UInteractable>();
|
||||
AActor* NewFocusedActor = bImplementsInteractable ? Hit.GetActor() : nullptr;
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bShowDebugTraces)
|
||||
{
|
||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Tick: bHit=%s hitActor='%s' implementsInteractable=%s currentFocused='%s'"),
|
||||
bHit ? TEXT("true") : TEXT("false"),
|
||||
*GetNameSafe(bHit ? Hit.GetActor() : nullptr),
|
||||
bImplementsInteractable ? TEXT("true") : TEXT("false"),
|
||||
*GetNameSafe(FocusedActor.Get()));
|
||||
}
|
||||
#endif
|
||||
|
||||
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::UpdateHoldProgress()
|
||||
{
|
||||
const bool bTimerActive = GetWorld()->GetTimerManager().IsTimerActive(HoldTimerHandle);
|
||||
const float Progress = bTimerActive && HoldThreshold > 0.0f
|
||||
? FMath::Clamp(GetWorld()->GetTimerManager().GetTimerElapsed(HoldTimerHandle) / HoldThreshold, 0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
|
||||
if (FMath::IsNearlyEqual(Progress, LastBroadcastHoldProgress, KINDA_SMALL_NUMBER))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LastBroadcastHoldProgress = Progress;
|
||||
OnInteractHoldProgressChanged.Broadcast(Progress);
|
||||
}
|
||||
|
||||
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);
|
||||
RefreshFocusedOptions();
|
||||
CloseContextMenu();
|
||||
}
|
||||
else if (!bHoldConsumed && CurrentOptions.Num() >= 1)
|
||||
{
|
||||
ExecuteOption(0);
|
||||
RefreshFocusedOptions();
|
||||
}
|
||||
|
||||
ClearCachedTarget();
|
||||
}
|
||||
|
||||
void UInteractionComponent::CycleContextMenuOptions(float Direction)
|
||||
{
|
||||
if (!bContextMenuOpen || CurrentOptions.Num() == 0 || FMath::IsNearlyZero(Direction))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Scrolling up (positive Direction) moves the highlight toward the top of the list (index 0),
|
||||
// matching how the mouse wheel scrolls content elsewhere - hence the flipped sign here.
|
||||
const int32 Step = Direction > 0.0f ? -1 : 1;
|
||||
const int32 NewHighlightedIndex = FMath::Clamp(HighlightedIndex + Step, 0, CurrentOptions.Num() - 1);
|
||||
|
||||
if (NewHighlightedIndex == HighlightedIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HighlightedIndex = NewHighlightedIndex;
|
||||
OnContextMenuHighlightChanged.Broadcast(HighlightedIndex);
|
||||
}
|
||||
|
||||
void UInteractionComponent::HandleHoldThresholdReached()
|
||||
{
|
||||
bHoldConsumed = true;
|
||||
|
||||
if (CurrentOptions.Num() == 2)
|
||||
{
|
||||
ExecuteOption(1);
|
||||
RefreshFocusedOptions();
|
||||
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::RefreshFocusedOptions()
|
||||
{
|
||||
AActor* Actor = FocusedActor.Get();
|
||||
if (!Actor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FocusedOptions = IInteractable::Execute_GetInteractionOptions(Actor, GetOwner());
|
||||
OnFocusedInteractableChanged.Broadcast(Actor, FocusedOptions);
|
||||
}
|
||||
|
||||
void UInteractionComponent::ClearCachedTarget()
|
||||
{
|
||||
CurrentTarget = nullptr;
|
||||
CurrentOptions.Reset();
|
||||
HighlightedIndex = 0;
|
||||
bHoldConsumed = false;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// 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);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteractHoldProgressChanged, float, Progress);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Fired every tick while the hold timer is running, with the 0-1 fraction of
|
||||
* HoldThreshold elapsed so far - drives a fill bar for the "hold E" prompt.
|
||||
* Fires once more with 0 the instant the hold ends, however it ends (release,
|
||||
* secondary fired, Context Menu opened), so a bound bar always snaps back
|
||||
* empty. Never fires at all for a 1-option target, since no hold timer is
|
||||
* started for it.
|
||||
*/
|
||||
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||
FOnInteractHoldProgressChanged OnInteractHoldProgressChanged;
|
||||
|
||||
/** 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();
|
||||
|
||||
/** Re-derives hold progress from HoldTimerHandle every tick and broadcasts OnInteractHoldProgressChanged when it changes. */
|
||||
void UpdateHoldProgress();
|
||||
|
||||
/** 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);
|
||||
|
||||
/**
|
||||
* Re-pulls GetInteractionOptions() for the still-focused actor and re-broadcasts
|
||||
* OnFocusedInteractableChanged. Call right after ExecuteOption - the interaction just run
|
||||
* may have changed what that same actor now offers (e.g. a chest that's now empty), and
|
||||
* UpdateFocus alone would never notice since it only re-pulls options when the focused
|
||||
* actor's identity changes, not when its offered options do. No-op if nothing is focused.
|
||||
*/
|
||||
void RefreshFocusedOptions();
|
||||
|
||||
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;
|
||||
|
||||
/** Last value passed to OnInteractHoldProgressChanged, so UpdateHoldProgress only broadcasts on actual change. */
|
||||
float LastBroadcastHoldProgress = 0.0f;
|
||||
};
|
||||
@@ -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,58 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InteractionTypes.generated.h"
|
||||
|
||||
class UTexture2D;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/** Optional - shown by UInteractionOptionEntryWidget's Icon image when set, hidden otherwise. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||
TObjectPtr<UTexture2D> Icon = nullptr;
|
||||
|
||||
/**
|
||||
* Optional - when this is the primary (index 0) option and this is set, the ever-present
|
||||
* prompt's first line uses this verbatim instead of building "{DisplayName} with
|
||||
* {ObjectName}" - e.g. a chest can set this to "Open Chest" instead of ending up as
|
||||
* "Open with Chest". The "Press E" / "Hold E for ..." lines are unaffected.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||
FText PromptOverride;
|
||||
|
||||
/**
|
||||
* Optional per-option override for the Context Menu row's text color (both normal and
|
||||
* highlighted state use this same color, so the indication doesn't disappear on hover) -
|
||||
* e.g. red for an action you currently can't perform, gold for a special one. Overrides
|
||||
* UInteractionOptionEntryWidget's NormalTextColor/HighlightedTextColor for this option
|
||||
* only. Alpha 0 (the default) means "use the entry widget's own colors".
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||
FLinearColor TextColorOverride = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
/**
|
||||
* Optional per-option override for the Context Menu row's highlight tint - overrides
|
||||
* UInteractionOptionEntryWidget's HighlightColor for this option only. Alpha 0 (the
|
||||
* default) means "use the entry widget's own color".
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||
FLinearColor HighlightColorOverride = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
};
|
||||
@@ -19,6 +19,7 @@ public class KingshearthLegacy : ModuleRules
|
||||
"GameplayStateTreeModule",
|
||||
"UMG",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"GameplayAbilities",
|
||||
"GameplayTags",
|
||||
"GameplayTasks"
|
||||
@@ -30,6 +31,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,119 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InteractionContextMenuWidget.h"
|
||||
#include "InteractionOptionListItem.h"
|
||||
#include "InteractionPromptWidget.h"
|
||||
#include "Interaction/InteractionComponent.h"
|
||||
#include "Components/ListView.h"
|
||||
#include "KingshearthLegacy.h"
|
||||
|
||||
void UInteractionContextMenuWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
if (OptionsList)
|
||||
{
|
||||
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::InitializeInteraction(UInteractionComponent* InInteractionComponent)
|
||||
{
|
||||
if (InteractionComponent)
|
||||
{
|
||||
InteractionComponent->OnContextMenuOpened.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
|
||||
InteractionComponent->OnContextMenuHighlightChanged.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
|
||||
InteractionComponent->OnContextMenuClosed.RemoveDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
|
||||
}
|
||||
|
||||
InteractionComponent = InInteractionComponent;
|
||||
|
||||
if (InteractionComponent)
|
||||
{
|
||||
InteractionComponent->OnContextMenuOpened.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuOpened);
|
||||
InteractionComponent->OnContextMenuHighlightChanged.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuHighlightChanged);
|
||||
InteractionComponent->OnContextMenuClosed.AddDynamic(this, &UInteractionContextMenuWidget::HandleContextMenuClosed);
|
||||
}
|
||||
|
||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] ContextMenu widget InitializeInteraction: OptionsList bound: %s, WBP_InteractionQuick bound: %s"),
|
||||
OptionsList ? TEXT("yes") : TEXT("NO"),
|
||||
WBP_InteractionQuick ? TEXT("yes") : TEXT("NO - check its Name in the Designer tree matches the native property, and that it's reparented to UInteractionPromptWidget"));
|
||||
|
||||
if (WBP_InteractionQuick)
|
||||
{
|
||||
WBP_InteractionQuick->InitializeInteraction(InInteractionComponent);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::PopulateOptionsList(const TArray<FInteractionOption>& Options)
|
||||
{
|
||||
if (!OptionsList)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OptionsList->ClearListItems();
|
||||
ListItems.Reset(Options.Num());
|
||||
|
||||
TArray<UObject*> ItemObjects;
|
||||
ItemObjects.Reserve(Options.Num());
|
||||
|
||||
for (const FInteractionOption& OptionEntry : Options)
|
||||
{
|
||||
UInteractionOptionListItem* Item = NewObject<UInteractionOptionListItem>(this);
|
||||
Item->Option = OptionEntry;
|
||||
ListItems.Add(Item);
|
||||
ItemObjects.Add(Item);
|
||||
}
|
||||
|
||||
OptionsList->SetListItems(ItemObjects);
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::SelectHighlighted(int32 Index)
|
||||
{
|
||||
if (!OptionsList)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ListItems.IsValidIndex(Index))
|
||||
{
|
||||
OptionsList->SetSelectedItem(ListItems[Index]);
|
||||
}
|
||||
else
|
||||
{
|
||||
OptionsList->ClearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex)
|
||||
{
|
||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Widget received OnContextMenuOpened: %d option(s), OptionsList bound: %s"),
|
||||
Options.Num(), OptionsList ? TEXT("yes") : TEXT("NO - add a ListView named exactly \"OptionsList\" in this widget's Designer tree"));
|
||||
|
||||
CachedOptions = Options;
|
||||
PopulateOptionsList(CachedOptions);
|
||||
SelectHighlighted(InitialHighlightedIndex);
|
||||
|
||||
if (OptionsList)
|
||||
{
|
||||
OptionsList->SetVisibility(ESlateVisibility::Visible);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::HandleContextMenuHighlightChanged(int32 NewHighlightedIndex)
|
||||
{
|
||||
SelectHighlighted(NewHighlightedIndex);
|
||||
}
|
||||
|
||||
void UInteractionContextMenuWidget::HandleContextMenuClosed()
|
||||
{
|
||||
CachedOptions.Reset();
|
||||
ListItems.Reset();
|
||||
|
||||
if (OptionsList)
|
||||
{
|
||||
OptionsList->ClearListItems();
|
||||
OptionsList->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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 UInteractionOptionListItem;
|
||||
class UInteractionPromptWidget;
|
||||
class UListView;
|
||||
|
||||
/**
|
||||
* The Context Menu shown while holding Interact on a 3+-option IInteractable
|
||||
* (OptionsList, driven by OnContextMenuOpened/HighlightChanged/Closed). The
|
||||
* ever-present "you're looking at something" prompt lives in a separate
|
||||
* nested UInteractionPromptWidget instance (e.g. WBP_InteractionQuick) -
|
||||
* BindWidget can only see direct nodes of this widget's own tree, not reach
|
||||
* inside another UserWidget's tree, so that prompt/slider logic can't live
|
||||
* here; InitializeInteraction forwards to it instead, so the HUD only needs
|
||||
* to call this one entry point.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class UInteractionContextMenuWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Call once, right after creation (see UPlayerHUDWidget::InitializeHUD), to bind to InInteractionComponent's delegates, including forwarding to the nested WBP_InteractionQuick prompt widget. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void InitializeInteraction(UInteractionComponent* InInteractionComponent);
|
||||
|
||||
protected:
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
/** Rebuilds OptionsList's items from Options; ListItems is kept alive here so the ListView's UObjects don't get GC'd. */
|
||||
void PopulateOptionsList(const TArray<FInteractionOption>& Options);
|
||||
|
||||
/** Selects ListItems[Index] on OptionsList, which drives each row's IUserObjectListEntry::NativeOnItemSelectionChanged. */
|
||||
void SelectHighlighted(int32 Index);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleContextMenuHighlightChanged(int32 NewHighlightedIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleContextMenuClosed();
|
||||
|
||||
/**
|
||||
* The Context Menu's option list. Optional, place it in the WBP subclass's Designer
|
||||
* tree as a ListView, single-selection, with its Entry Widget Class set to a WBP
|
||||
* subclass of UInteractionOptionEntryWidget - that class receives each row's data
|
||||
* and highlighted state natively, no BP event graph logic needed.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UListView> OptionsList;
|
||||
|
||||
/**
|
||||
* The ever-present prompt + hold-progress bar, nested as a WBP_InteractionQuick
|
||||
* instance (a UInteractionPromptWidget subclass) named exactly "WBP_InteractionQuick"
|
||||
* in the Designer tree. Optional - InitializeInteraction just skips forwarding to it
|
||||
* when unbound.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UInteractionPromptWidget> WBP_InteractionQuick;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FInteractionOption> CachedOptions;
|
||||
|
||||
/** UObject wrappers backing OptionsList - one per CachedOptions entry, rebuilt in PopulateOptionsList. */
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UInteractionOptionListItem>> ListItems;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InteractionOptionEntryWidget.h"
|
||||
#include "InteractionOptionListItem.h"
|
||||
#include "Components/Border.h"
|
||||
#include "Components/Image.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Components/Widget.h"
|
||||
|
||||
void UInteractionOptionEntryWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
// Capture Background's Designer-authored color before ApplyOptionVisuals ever runs, so
|
||||
// "no override" can fall back to it later instead of an arbitrary hardcoded default.
|
||||
if (UImage* BackgroundImage = Cast<UImage>(Background.Get()))
|
||||
{
|
||||
DefaultBackgroundColor = BackgroundImage->GetColorAndOpacity();
|
||||
}
|
||||
else if (UBorder* BackgroundBorder = Cast<UBorder>(Background.Get()))
|
||||
{
|
||||
DefaultBackgroundColor = BackgroundBorder->GetBrushColor();
|
||||
}
|
||||
|
||||
// The ListView only calls NativeOnItemSelectionChanged when a row's selection state
|
||||
// actually changes, so a freshly-spawned row that has never been (de)selected would
|
||||
// otherwise keep whatever visibility Highlight has in the Designer - force it into
|
||||
// the correct "not highlighted" state right away instead.
|
||||
ApplyHighlightVisuals();
|
||||
}
|
||||
|
||||
void UInteractionOptionEntryWidget::NativeOnListItemObjectSet(UObject* ListItemObject)
|
||||
{
|
||||
if (const UInteractionOptionListItem* ListItem = Cast<UInteractionOptionListItem>(ListItemObject))
|
||||
{
|
||||
Option = ListItem->Option;
|
||||
ApplyOptionVisuals();
|
||||
|
||||
// SListView recycles row widgets between items; a row reassigned from a previously-
|
||||
// selected item to a new one doesn't necessarily get a fresh NativeOnItemSelectionChanged
|
||||
// call in the same pass, so re-derive the real selection state here rather than trusting
|
||||
// whatever bHighlighted this (possibly recycled) widget already had.
|
||||
bHighlighted = IsListItemSelected();
|
||||
ApplyHighlightVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionOptionEntryWidget::NativeOnItemSelectionChanged(bool bIsSelected)
|
||||
{
|
||||
bHighlighted = bIsSelected;
|
||||
ApplyHighlightVisuals();
|
||||
}
|
||||
|
||||
void UInteractionOptionEntryWidget::ApplyOptionVisuals()
|
||||
{
|
||||
if (InteractionText)
|
||||
{
|
||||
InteractionText->SetText(Option.DisplayName);
|
||||
}
|
||||
|
||||
if (Icon)
|
||||
{
|
||||
if (Option.Icon)
|
||||
{
|
||||
Icon->SetBrushFromTexture(Option.Icon);
|
||||
Icon->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
}
|
||||
else
|
||||
{
|
||||
Icon->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
if (Background)
|
||||
{
|
||||
const FLinearColor ResolvedBackgroundColor = Option.HighlightColorOverride.A > 0.0f ? Option.HighlightColorOverride : DefaultBackgroundColor;
|
||||
|
||||
if (UImage* BackgroundImage = Cast<UImage>(Background.Get()))
|
||||
{
|
||||
BackgroundImage->SetColorAndOpacity(ResolvedBackgroundColor);
|
||||
}
|
||||
else if (UBorder* BackgroundBorder = Cast<UBorder>(Background.Get()))
|
||||
{
|
||||
BackgroundBorder->SetBrushColor(ResolvedBackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionOptionEntryWidget::ApplyHighlightVisuals()
|
||||
{
|
||||
// Alpha 0 (the struct default) means the option didn't request an override - fall back
|
||||
// to this widget's own Style colors.
|
||||
const FLinearColor ResolvedHighlightColor = Option.HighlightColorOverride.A > 0.0f ? Option.HighlightColorOverride : HighlightColor;
|
||||
const FLinearColor ResolvedTextColor = Option.TextColorOverride.A > 0.0f
|
||||
? Option.TextColorOverride
|
||||
: (bHighlighted ? HighlightedTextColor : NormalTextColor);
|
||||
|
||||
if (Highlight)
|
||||
{
|
||||
Highlight->SetVisibility(bHighlighted ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden);
|
||||
|
||||
if (UImage* HighlightImage = Cast<UImage>(Highlight.Get()))
|
||||
{
|
||||
HighlightImage->SetColorAndOpacity(ResolvedHighlightColor);
|
||||
}
|
||||
else if (UBorder* HighlightBorder = Cast<UBorder>(Highlight.Get()))
|
||||
{
|
||||
HighlightBorder->SetBrushColor(ResolvedHighlightColor);
|
||||
}
|
||||
}
|
||||
|
||||
if (InteractionText)
|
||||
{
|
||||
InteractionText->SetColorAndOpacity(ResolvedTextColor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Blueprint/IUserObjectListEntry.h"
|
||||
#include "Interaction/InteractionTypes.h"
|
||||
#include "InteractionOptionEntryWidget.generated.h"
|
||||
|
||||
class UImage;
|
||||
class UTextBlock;
|
||||
class UWidget;
|
||||
|
||||
/**
|
||||
* One row of the Context Menu's OptionsList (a UListView). Assign a WBP
|
||||
* subclass of this as that ListView's Entry Widget Class in the Designer -
|
||||
* name your widgets exactly "InteractionText" (Text), "Icon" (Image) and
|
||||
* "Highlight" (a Border, or an Image - either works, tinted with
|
||||
* HighlightColor) and all population/highlight styling is handled here in
|
||||
* C++, driven by the ListView itself via IUserObjectListEntry; no BP event
|
||||
* graph logic needed.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class KINGSHEARTHLEGACY_API UInteractionOptionEntryWidget : public UUserWidget, public IUserObjectListEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||
const FInteractionOption& GetOption() const { return Option; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||
bool IsHighlighted() const { return bHighlighted; }
|
||||
|
||||
protected:
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
/** IUserObjectListEntry - called by the owning ListView when this row is assigned an UInteractionOptionListItem. */
|
||||
virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override;
|
||||
|
||||
/** IUserObjectListEntry - called by the owning ListView when this row's selected state changes (drives the Context Menu highlight). */
|
||||
virtual void NativeOnItemSelectionChanged(bool bIsSelected) override;
|
||||
|
||||
void ApplyOptionVisuals();
|
||||
|
||||
/** Applies bHighlighted to Highlight's visibility/tint and InteractionText's color. Called on construct (so freshly-spawned rows never default to "selected") and on every selection change. */
|
||||
void ApplyHighlightVisuals();
|
||||
|
||||
/** Row label - bind a TextBlock named exactly "InteractionText" in the Designer. */
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UTextBlock> InteractionText;
|
||||
|
||||
/** Option icon - bind an Image named exactly "Icon" in the Designer. Hidden automatically when the option has no Icon set. */
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UImage> Icon;
|
||||
|
||||
/** Highlight background - bind a Border or an Image named exactly "Highlight" in the Designer. Shown and tinted HighlightColor only while this row is selected/highlighted. */
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UWidget> Highlight;
|
||||
|
||||
/**
|
||||
* Optional persistent row background - bind a Border or an Image named exactly
|
||||
* "Background" in the Designer. Unlike Highlight, this isn't gated on selection - it's
|
||||
* tinted with the option's HighlightColorOverride (from FInteractionOption) whenever one
|
||||
* is set, so a "can't do this" or "special" row stays visually marked regardless of
|
||||
* whether it's currently highlighted. Left at its Designer-authored appearance when the
|
||||
* option has no override.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UWidget> Background;
|
||||
|
||||
/** Tint applied to Highlight while this row is highlighted. */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
|
||||
FLinearColor HighlightColor = FLinearColor(1.0f, 0.85f, 0.3f, 1.0f);
|
||||
|
||||
/** InteractionText color while this row is NOT highlighted. */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
|
||||
FLinearColor NormalTextColor = FLinearColor::White;
|
||||
|
||||
/** InteractionText color while this row IS highlighted. */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|Style")
|
||||
FLinearColor HighlightedTextColor = FLinearColor::Black;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||
FInteractionOption Option;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||
bool bHighlighted = false;
|
||||
|
||||
private:
|
||||
|
||||
/**
|
||||
* Background's Designer-authored color, captured once in NativeConstruct before any
|
||||
* option is ever assigned. Rows are recycled by the ListView, so a row previously tinted
|
||||
* by one option's HighlightColorOverride needs an explicit color to fall back to when
|
||||
* reassigned to a different, unconfigured option - rather than just leaving whatever
|
||||
* tint the recycled widget last had.
|
||||
*/
|
||||
FLinearColor DefaultBackgroundColor = FLinearColor::White;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "Interaction/InteractionTypes.h"
|
||||
#include "InteractionOptionListItem.generated.h"
|
||||
|
||||
/**
|
||||
* Plain UObject wrapper around one FInteractionOption - UListView items must be
|
||||
* UObjects, not structs, so UInteractionContextMenuWidget wraps each option in
|
||||
* one of these before calling OptionsList->SetListItems.
|
||||
*/
|
||||
UCLASS()
|
||||
class KINGSHEARTHLEGACY_API UInteractionOptionListItem : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||
FInteractionOption Option;
|
||||
};
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InteractionPromptWidget.h"
|
||||
#include "Interaction/InteractionComponent.h"
|
||||
#include "Interaction/Interactable.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Components/Widget.h"
|
||||
#include "SegmentedBarWidget.h"
|
||||
#include "KingshearthLegacy.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/** Toggles TextContainer when bound (so a background living alongside PromptText hides with it), else PromptText itself. */
|
||||
void SetPromptVisible(UWidget* TextContainer, UTextBlock* PromptText, bool bVisible)
|
||||
{
|
||||
const ESlateVisibility Visibility = bVisible ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed;
|
||||
|
||||
if (TextContainer)
|
||||
{
|
||||
TextContainer->SetVisibility(Visibility);
|
||||
}
|
||||
else if (PromptText)
|
||||
{
|
||||
PromptText->SetVisibility(Visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
SetPromptVisible(TextContainer, PromptText, false);
|
||||
if (InfoText)
|
||||
{
|
||||
InfoText->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
if (WBP_InteractionsSlider)
|
||||
{
|
||||
WBP_InteractionsSlider->SetVisibility(ESlateVisibility::Collapsed);
|
||||
// USegmentedBarWidget defaults to full (it's shared with the health/stamina bars), but
|
||||
// the hold bar should start empty and only fill as OnInteractHoldProgressChanged reports.
|
||||
WBP_InteractionsSlider->SetSegments(0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::InitializeInteraction(UInteractionComponent* InInteractionComponent)
|
||||
{
|
||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Prompt widget InitializeInteraction: component=%s, PromptText bound: %s, InfoText bound: %s, TextContainer bound: %s, Slider bound: %s"),
|
||||
*GetNameSafe(InInteractionComponent),
|
||||
PromptText ? TEXT("yes") : TEXT("NO"),
|
||||
InfoText ? TEXT("yes") : TEXT("NO"),
|
||||
TextContainer ? TEXT("yes") : TEXT("NO"),
|
||||
WBP_InteractionsSlider ? TEXT("yes") : TEXT("NO"));
|
||||
|
||||
if (InteractionComponent)
|
||||
{
|
||||
InteractionComponent->OnFocusedInteractableChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandleFocusedInteractableChanged);
|
||||
InteractionComponent->OnContextMenuOpened.RemoveDynamic(this, &UInteractionPromptWidget::HandleContextMenuOpened);
|
||||
InteractionComponent->OnContextMenuClosed.RemoveDynamic(this, &UInteractionPromptWidget::HandleContextMenuClosed);
|
||||
InteractionComponent->OnInteractHoldProgressChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandleHoldProgressChanged);
|
||||
}
|
||||
|
||||
InteractionComponent = InInteractionComponent;
|
||||
|
||||
if (InteractionComponent)
|
||||
{
|
||||
InteractionComponent->OnFocusedInteractableChanged.AddDynamic(this, &UInteractionPromptWidget::HandleFocusedInteractableChanged);
|
||||
InteractionComponent->OnContextMenuOpened.AddDynamic(this, &UInteractionPromptWidget::HandleContextMenuOpened);
|
||||
InteractionComponent->OnContextMenuClosed.AddDynamic(this, &UInteractionPromptWidget::HandleContextMenuClosed);
|
||||
InteractionComponent->OnInteractHoldProgressChanged.AddDynamic(this, &UInteractionPromptWidget::HandleHoldProgressChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleHoldProgressChanged(float Progress)
|
||||
{
|
||||
if (WBP_InteractionsSlider)
|
||||
{
|
||||
WBP_InteractionsSlider->SetSegments(Progress, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options)
|
||||
{
|
||||
UE_LOG(LogKingshearthLegacy, Log, TEXT("[Interaction] Prompt widget HandleFocusedInteractableChanged: actor='%s' options=%d"),
|
||||
*GetNameSafe(FocusedActor), Options.Num());
|
||||
|
||||
LastFocusedActor = FocusedActor;
|
||||
LastFocusedOptions = Options;
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex)
|
||||
{
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleContextMenuClosed()
|
||||
{
|
||||
UpdatePromptVisibility();
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::UpdatePromptVisibility()
|
||||
{
|
||||
if (!PromptText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto HideAll = [this]()
|
||||
{
|
||||
SetPromptVisible(TextContainer, PromptText, false);
|
||||
if (InfoText)
|
||||
{
|
||||
InfoText->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
if (WBP_InteractionsSlider)
|
||||
{
|
||||
WBP_InteractionsSlider->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
// The Context Menu owns the screen while it's open - keep the ever-present prompt
|
||||
// out of its way until OnContextMenuClosed brings us back here.
|
||||
if (InteractionComponent && InteractionComponent->IsContextMenuOpen())
|
||||
{
|
||||
HideAll();
|
||||
return;
|
||||
}
|
||||
|
||||
AActor* FocusedActor = LastFocusedActor.Get();
|
||||
if (!FocusedActor || LastFocusedOptions.Num() == 0)
|
||||
{
|
||||
HideAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Primary line: "{Verb} with {ObjectName}" (or PromptOverride verbatim, if the primary
|
||||
// option set one). Hint line: "Press E" always, plus a hint for the hold behaviour only
|
||||
// when there is one (2 options = secondary, 3+ = Context Menu).
|
||||
const FInteractionOption& PrimaryOption = LastFocusedOptions[0];
|
||||
FString Prompt;
|
||||
if (!PrimaryOption.PromptOverride.IsEmpty())
|
||||
{
|
||||
Prompt = PrimaryOption.PromptOverride.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
const FText ObjectName = IInteractable::Execute_GetInteractableDisplayName(FocusedActor);
|
||||
Prompt = ObjectName.IsEmpty()
|
||||
? PrimaryOption.DisplayName.ToString()
|
||||
: FString::Printf(TEXT("%s with %s"), *PrimaryOption.DisplayName.ToString(), *ObjectName.ToString());
|
||||
}
|
||||
|
||||
FString Hint = TEXT("Press E");
|
||||
if (LastFocusedOptions.Num() == 2)
|
||||
{
|
||||
Hint += FString::Printf(TEXT("\nHold E for %s"), *LastFocusedOptions[1].DisplayName.ToString());
|
||||
}
|
||||
else if (LastFocusedOptions.Num() >= 3)
|
||||
{
|
||||
Hint += TEXT("\nHold E for Context Menu");
|
||||
}
|
||||
|
||||
PromptText->SetText(FText::FromString(Prompt));
|
||||
SetPromptVisible(TextContainer, PromptText, true);
|
||||
|
||||
if (InfoText)
|
||||
{
|
||||
InfoText->SetText(FText::FromString(Hint));
|
||||
InfoText->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
}
|
||||
|
||||
if (WBP_InteractionsSlider)
|
||||
{
|
||||
WBP_InteractionsSlider->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
WBP_InteractionsSlider->SetSegments(0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Interaction/InteractionTypes.h"
|
||||
#include "InteractionPromptWidget.generated.h"
|
||||
|
||||
class UInteractionComponent;
|
||||
class USegmentedBarWidget;
|
||||
class UTextBlock;
|
||||
class UWidget;
|
||||
|
||||
/**
|
||||
* The 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) plus an optional hold-to-interact fill bar. Kept as its own
|
||||
* UserWidget (e.g. WBP_InteractionQuick) rather than folded into
|
||||
* UInteractionContextMenuWidget so it can be laid out and nested independently
|
||||
* of the Context Menu's option list - see UInteractionContextMenuWidget's
|
||||
* WBP_InteractionQuick binding, which forwards InitializeInteraction here.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class UInteractionPromptWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Call once, right after creation, to bind to InInteractionComponent's delegates. Safe to call again to rebind to a different component. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void InitializeInteraction(UInteractionComponent* InInteractionComponent);
|
||||
|
||||
protected:
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
UFUNCTION()
|
||||
void HandleFocusedInteractableChanged(AActor* FocusedActor, const TArray<FInteractionOption>& Options);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleContextMenuOpened(const TArray<FInteractionOption>& Options, int32 InitialHighlightedIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleContextMenuClosed();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleHoldProgressChanged(float Progress);
|
||||
|
||||
/** Ever-present prompt's primary line - "{Verb} with {ObjectName}" (or PromptOverride verbatim). Named exactly "PromptText" in the Designer. */
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UTextBlock> PromptText;
|
||||
|
||||
/**
|
||||
* Optional wrapper around PromptText (e.g. a panel/border providing its background, or the
|
||||
* Overlay it shares with WBP_InteractionsSlider) named exactly "TextContainer" in the
|
||||
* Designer. When bound, its visibility is toggled instead of PromptText's own - so a
|
||||
* background/slider living alongside PromptText, not inside it, actually hides too. Falls
|
||||
* back to toggling PromptText directly when this isn't bound.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UWidget> TextContainer;
|
||||
|
||||
/**
|
||||
* Optional second line - "Press E" plus a hint for the hold behaviour (2 options =
|
||||
* secondary, 3+ = Context Menu). Named exactly "InfoText" in the Designer. Kept separate
|
||||
* from PromptText so it can sit outside TextContainer (e.g. below the slider bar rather
|
||||
* than layered on top of it). Toggles visibility on its own, in lockstep with PromptText.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<UTextBlock> InfoText;
|
||||
|
||||
/**
|
||||
* Optional hold-to-interact fill bar - drop a WBP_SegmentedBar instance into the
|
||||
* Designer tree named exactly "WBP_InteractionsSlider" and it fills 0 to 1 while
|
||||
* holding E on a 2+-option target, driven by InteractionComponent's
|
||||
* OnInteractHoldProgressChanged. Left unbound, holding just does nothing visually.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
|
||||
TObjectPtr<USegmentedBarWidget> WBP_InteractionsSlider;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
||||
|
||||
private:
|
||||
|
||||
/** Rebuilds PromptText from LastFocused*, collapsing it while the Context Menu is open. */
|
||||
void UpdatePromptVisibility();
|
||||
|
||||
TWeakObjectPtr<AActor> LastFocusedActor;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FInteractionOption> LastFocusedOptions;
|
||||
};
|
||||
@@ -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