Implement an interact hold input action and a new callback for it, seed bed collect seed action as well, change lavender seed mesh to sprite

This commit is contained in:
2026-10-01 22:01:54 +02:00
parent b5304a74aa
commit f2115c537b
10 changed files with 302 additions and 9 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -84,14 +84,24 @@ AActor* UInteractionSubsystem::GetClosestInteractable(const UObject* WorldContex
if (!Actor.IsValid()) if (!Actor.IsValid())
continue; continue;
const float Dist = FVector::DistSquared(Actor->GetActorLocation(), PlayerCharacter->GetActorLocation()); // Actors whose real interaction target is not their own origin (a bloomed plant inside a seed bed
// grid, for example) report the point everything should be measured against. Distance, the on-screen
// test and the "closest wins" pick then all reason about that point instead of the actor's center.
// Zero means the actor has no preference, so its own origin is used.
FVector FocusLocation = IInteractableActorInterface::Execute_GetInteractionFocusLocation(Actor.Get());
if (FocusLocation.IsZero())
{
FocusLocation = Actor->GetActorLocation();
}
const float Dist = FVector::DistSquared(FocusLocation, PlayerCharacter->GetActorLocation());
const float MaxInteractionDistance = IInteractableActorInterface::Execute_GetInteractionDistance(Actor.Get()); const float MaxInteractionDistance = IInteractableActorInterface::Execute_GetInteractionDistance(Actor.Get());
if (FMath::Pow(MaxInteractionDistance, 2) < Dist || Dist > ClosestDist) if (FMath::Pow(MaxInteractionDistance, 2) < Dist || Dist > ClosestDist)
continue; continue;
FVector2D ScreenLoc; FVector2D ScreenLoc;
if (!PC->ProjectWorldLocationToScreen(Actor->GetActorLocation(), ScreenLoc)) if (!PC->ProjectWorldLocationToScreen(FocusLocation, ScreenLoc))
{ {
continue; continue;
} }

View File

@@ -3,6 +3,7 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "UObject/Interface.h" #include "UObject/Interface.h"
#include "InteractionSettings.h" #include "InteractionSettings.h"
#include "InteractableActorInterface.generated.h" #include "InteractableActorInterface.generated.h"
@@ -44,4 +45,18 @@ public:
{ {
return GetDefault<UInteractionSettings>()->InteractableMaxDistance; return GetDefault<UInteractionSettings>()->InteractableMaxDistance;
} }
/**
* Point the interaction system should reason about for this actor: the distance check, the on-screen
* test and the "closest wins" pick all measure towards it. Zero means "no preference" and the actor's
* own origin is used, which is right for a single-object actor. An actor whose real interaction target
* sits somewhere else (a bloomed plant inside a seed bed grid, for example) overrides this so the prompt
* lands where the player looks.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
FVector GetInteractionFocusLocation() const;
virtual FVector GetInteractionFocusLocation_Implementation() const
{
return FVector::ZeroVector;
}
}; };

View File

@@ -19,6 +19,8 @@
#include "Kismet/KismetMaterialLibrary.h" #include "Kismet/KismetMaterialLibrary.h"
#include "Materials/MaterialInstanceDynamic.h" #include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h" #include "Materials/MaterialInterface.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerController.h"
#include "ProjectEleri/Components/EleriPaperSpriteComponent.h" #include "ProjectEleri/Components/EleriPaperSpriteComponent.h"
#include "ProjectEleri/Settings/EleriGameSettings.h" #include "ProjectEleri/Settings/EleriGameSettings.h"
#include "NiagaraComponent.h" #include "NiagaraComponent.h"
@@ -134,6 +136,10 @@ ASeedBedActor::ASeedBedActor()
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root")); USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
Root->SetMobility(EComponentMobility::Movable); Root->SetMobility(EComponentMobility::Movable);
SetRootComponent(Root); SetRootComponent(Root);
// The prompt is attached to the bed so a slot location can be expressed relative to it and follows it
WidgetComponent = CreateDefaultSubobject<UInteractionWidgetComponent>(TEXT("WidgetComponent"));
WidgetComponent->SetupAttachment(Root);
} }
void ASeedBedActor::BeginPlay() void ASeedBedActor::BeginPlay()
@@ -240,7 +246,165 @@ void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
void ASeedBedActor::Tick(float DeltaTime) void ASeedBedActor::Tick(float DeltaTime)
{ {
Super::Tick(DeltaTime); Super::Tick(DeltaTime);
// The interaction plugin picks its target every tick, so the slot the prompt points at has to be fresh too
UpdateBloomedSlotTarget();
// Keep the prompt glued to the bloomed plant while the player walks around the bed
if (bInteractionUIActive && bHasBloomedSlotInRange)
{
PlaceInteractionWidgetOverSlot(ClosestBloomedSlot);
}
} }
//------------------------------------------------------------------------------------------------
// INTERACTION
//------------------------------------------------------------------------------------------------
void ASeedBedActor::UpdateBloomedSlotTarget()
{
bHasBloomedSlotInRange = false;
ClosestBloomedSlot = FIntVector2::NoneValue;
const UWorld* World = GetWorld();
const APlayerController* PlayerController = World ? World->GetFirstPlayerController() : nullptr;
const APawn* PlayerPawn = PlayerController ? PlayerController->GetPawn() : nullptr;
if (!IsValid(PlayerPawn))
{
return;
}
FIntVector2 FoundSlot;
if (FindClosestBloomedSlot(PlayerPawn->GetActorLocation(), FoundSlot))
{
ClosestBloomedSlot = FoundSlot;
bHasBloomedSlotInRange = true;
}
}
bool ASeedBedActor::FindClosestBloomedSlot(const FVector& FromWorldLocation, FIntVector2& OutSlotCoordinate) const
{
// The reach is measured from the slot and not from the center of the bed, so an outer plant is as easy
// to reach as a middle one even though the actor origin is far away from it
const float ReachSquared = FMath::Square(BloomInteractDistance);
float ClosestDistanceSquared = ReachSquared;
bool bFound = false;
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
// Only a living plant that has reached its bloom can be collected off the bed
if (!SlotHasLivingPlant(SlotPair.Value) || !SlotPair.Value.SlotData.bBloomed)
{
continue;
}
FVector SlotWorldLocation;
if (!GetSlotWorldLocation(SlotPair.Key, SlotWorldLocation))
{
continue;
}
const float DistanceSquared = FVector::DistSquared(FromWorldLocation, SlotWorldLocation);
if (DistanceSquared > ClosestDistanceSquared)
{
continue;
}
ClosestDistanceSquared = DistanceSquared;
OutSlotCoordinate = SlotPair.Key;
bFound = true;
}
return bFound;
}
void ASeedBedActor::PlaceInteractionWidgetOverSlot(FIntVector2 SlotCoordinate)
{
if (!WidgetComponent)
{
return;
}
// The widget is attached to the root, so a slot location is expressed relative to the bed and follows it
WidgetComponent->SetRelativeLocation(GetSlotRelativeLocation(SlotCoordinate) + FVector(0.f, 0.f, InteractionPromptHeight));
}
bool ASeedBedActor::CollectBloomedSlot(FIntVector2 SlotCoordinate)
{
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot || !SlotHasLivingPlant(*Slot) || !Slot->SlotData.bBloomed)
{
return false;
}
// Harvesting empties the slot and takes the plant mesh with it, so the next seed can be planted right away
ClearSeedFromSlot(SlotCoordinate, true);
// The collected plant was the prompt's target, the next tick re-evaluates the bed
ClosestBloomedSlot = FIntVector2::NoneValue;
bHasBloomedSlotInRange = false;
return true;
}
bool ASeedBedActor::IsInteractable_Implementation() const
{
return bHasBloomedSlotInRange;
}
FText ASeedBedActor::GetInteractionText_Implementation() const
{
return BloomedPlantInteractionText;
}
void ASeedBedActor::Interact_Implementation(AActor* InteractionInstigator)
{
if (ClosestBloomedSlot != FIntVector2::NoneValue)
{
CollectBloomedSlot(ClosestBloomedSlot);
}
}
void ASeedBedActor::ToggleInteractableUI_Implementation(bool bActive)
{
if (bActive && !IInteractableActorInterface::Execute_IsInteractable(this))
{
return;
}
IInteractableActorInterface::ToggleInteractableUI_Implementation(bActive);
bInteractionUIActive = bActive;
// Move the prompt over the bloomed plant before it is shown, so it never flashes at the actor origin
if (bActive)
{
PlaceInteractionWidgetOverSlot(ClosestBloomedSlot);
}
if (WidgetComponent)
{
WidgetComponent->ToggleWidget(bActive);
}
}
float ASeedBedActor::GetInteractionDistance_Implementation() const
{
return BloomInteractDistance;
}
FVector ASeedBedActor::GetInteractionFocusLocation_Implementation() const
{
FVector SlotWorldLocation;
if (ClosestBloomedSlot != FIntVector2::NoneValue && GetSlotWorldLocation(ClosestBloomedSlot, SlotWorldLocation))
{
return SlotWorldLocation;
}
return GetActorLocation();
}
//------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------
// TIME // TIME
//------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------
@@ -677,6 +841,13 @@ void ASeedBedActor::ApplySlotVisuals(FIntVector2 SlotCoordinate, FSeedBedSeedSlo
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.MoistureHoursRemaining > 0 ? 1.f : 0.f); Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.MoistureHoursRemaining > 0 ? 1.f : 0.f);
} }
} }
// VFX
if (IsValid(Slot.NiagaraComponent) && !Slot.SlotData.bBloomed && Slot.NiagaraComponent->IsVisible())
{
Slot.NiagaraComponent->Deactivate();
Slot.NiagaraComponent->SetVisibility(false);
}
// PLANT ---------------------------------------------------------------------------------------- // PLANT ----------------------------------------------------------------------------------------
if (!IsValid(Slot.PlantSprite)) if (!IsValid(Slot.PlantSprite))

View File

@@ -4,6 +4,8 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "Components/InteractionWidgetComponent.h"
#include "Interface/InteractableActorInterface.h"
#include "ProjectEleri/System/TimeOfDayStruct.h" #include "ProjectEleri/System/TimeOfDayStruct.h"
#include "SeedBedActor.generated.h" #include "SeedBedActor.generated.h"
@@ -114,7 +116,7 @@ struct FSeedBedSaveData
}; };
UCLASS() UCLASS()
class PROJECTELERI_API ASeedBedActor : public AActor class PROJECTELERI_API ASeedBedActor : public AActor, public IInteractableActorInterface
{ {
GENERATED_BODY() GENERATED_BODY()
@@ -122,6 +124,32 @@ public:
ASeedBedActor(); ASeedBedActor();
virtual void Tick(float DeltaTime) override; virtual void Tick(float DeltaTime) override;
//------------------------------------------------------------------------------------------------
// INTERACTION
//------------------------------------------------------------------------------------------------
/** True while a living, bloomed plant is close enough to the player to be collected */
virtual bool IsInteractable_Implementation() const override;
/** Text shown on the prompt floating above a bloomed plant */
virtual FText GetInteractionText_Implementation() const override;
/** Harvests the bloomed plant the prompt currently points at */
virtual void Interact_Implementation(AActor* InteractionInstigator) override;
/** Shows / hides the interaction prompt and moves it over the bloomed plant */
virtual void ToggleInteractableUI_Implementation(bool bActive) override;
virtual float GetInteractionDistance_Implementation() const override;
/** The plugin targets this point instead of the actor origin, so range and on-screen checks are measured
* to the bloomed plant rather than to the center of the bed */
virtual FVector GetInteractionFocusLocation_Implementation() const override;
/** Collects the bloomed plant of a slot, returns false when the slot holds nothing harvestable */
UFUNCTION(BlueprintCallable, Category = "Seed Bed|Interaction")
bool CollectBloomedSlot(FIntVector2 SlotCoordinate);
//------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------
// DATA // DATA
//------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------
@@ -319,6 +347,45 @@ protected:
UFUNCTION() UFUNCTION()
void HandleHourPassed(); void HandleHourPassed();
//------------------------------------------------------------------------------------------------
// INTERACTION
//------------------------------------------------------------------------------------------------
/** Re-evaluates which bloomed slot (if any) is within reach of the local player */
void UpdateBloomedSlotTarget();
/** Finds the living, bloomed slot closest to the given world location and within BloomInteractDistance */
bool FindClosestBloomedSlot(const FVector& FromWorldLocation, FIntVector2& OutSlotCoordinate) const;
/** Moves the interaction widget above the given slot */
void PlaceInteractionWidgetOverSlot(FIntVector2 SlotCoordinate);
/** Prompt floating above a bloomed plant, created once and moved to whichever slot is in reach */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Seed Bed|Interaction")
UInteractionWidgetComponent* WidgetComponent = nullptr;
/** How close the player has to be to a bloomed plant for it to become interactable */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Interaction")
float BloomInteractDistance = 250.f;
/** How far above a slot the interaction prompt floats */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Interaction")
float InteractionPromptHeight = 120.f;
/** Text shown on the prompt floating above a bloomed plant */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Interaction")
FText BloomedPlantInteractionText = NSLOCTEXT("SeedBedInteraction", "CollectBloomedPlant", "Collect");
/** Slot the interaction prompt currently points at, FIntVector2::NoneValue when there is none */
UPROPERTY(Transient)
FIntVector2 ClosestBloomedSlot = FIntVector2::NoneValue;
/** True while a living, bloomed plant is within BloomInteractDistance of the local player */
bool bHasBloomedSlotInRange = false;
/** True while the interaction plugin is showing this bed's prompt widget */
bool bInteractionUIActive = false;
/** Whether the highlight decals of the bed are currently shown */ /** Whether the highlight decals of the bed are currently shown */
bool bPlantingHighlightActive = false; bool bPlantingHighlightActive = false;

View File

@@ -210,6 +210,11 @@ void AEleriPlayerController::SetupInputComponent()
{ {
PlayerEnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnInteract); PlayerEnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnInteract);
} }
if (InteractHoldAction)
{
PlayerEnhancedInputComponent->BindAction(InteractHoldAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnInteractHoldStart);
PlayerEnhancedInputComponent->BindAction(InteractHoldAction, ETriggerEvent::Triggered, this, &AEleriPlayerController::OnInteractHold);
}
if (InteractExitAction) if (InteractExitAction)
{ {
PlayerEnhancedInputComponent->BindAction(InteractExitAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnInteractExit); PlayerEnhancedInputComponent->BindAction(InteractExitAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnInteractExit);
@@ -1177,6 +1182,20 @@ void AEleriPlayerController::OnInteract(const FInputActionValue &Value)
PlayerCharacter->OnInteract.Broadcast(); PlayerCharacter->OnInteract.Broadcast();
} }
void AEleriPlayerController::OnInteractHoldStart(const FInputActionValue& Value)
{
OnButtonHeld.Broadcast(true, InteractHoldAction);
}
void AEleriPlayerController::OnInteractHold(const FInputActionValue& Value)
{
if (!IsValid(PlayerCharacter))
return;
UE_LOG(LogTemp, Warning, TEXT("INTERACT HOLD"));
OnButtonHeld.Broadcast(false, InteractHoldAction);
}
void AEleriPlayerController::OnInteractExit(const FInputActionValue &Value) void AEleriPlayerController::OnInteractExit(const FInputActionValue &Value)
{ {
if (!IsValid(PlayerCharacter)) if (!IsValid(PlayerCharacter))

View File

@@ -46,6 +46,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWatering, const bool, Started);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnChangeInputContext, const int32, Index); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnChangeInputContext, const int32, Index);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRemovalModeToggle, const bool, Active); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRemovalModeToggle, const bool, Active);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerToggleFlying, const bool, Active); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerToggleFlying, const bool, Active);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnButtonHeld, const bool, StartHolding, const UInputAction*, InputAction);
DECLARE_DELEGATE_OneParam(FOnMinigameComplete, int32 /*Score*/); DECLARE_DELEGATE_OneParam(FOnMinigameComplete, int32 /*Score*/);
@@ -129,6 +130,9 @@ public:
UPROPERTY(BlueprintAssignable) UPROPERTY(BlueprintAssignable)
FOnPlayerToggleFlying OnPlayerToggleFlying; FOnPlayerToggleFlying OnPlayerToggleFlying;
UPROPERTY(BlueprintAssignable)
FOnButtonHeld OnButtonHeld;
UPROPERTY(BlueprintReadOnly) UPROPERTY(BlueprintReadOnly)
AMyCharacter* PlayerCharacter; AMyCharacter* PlayerCharacter;
@@ -224,6 +228,8 @@ public:
UPROPERTY(EditDefaultsOnly, Category = "Action Input") UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputAction* InteractAction; UInputAction* InteractAction;
UPROPERTY(EditDefaultsOnly, Category = "Action Input") UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputAction* InteractHoldAction;
UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputAction* InteractExitAction; UInputAction* InteractExitAction;
UPROPERTY(EditDefaultsOnly, Category = "Action Input") UPROPERTY(EditDefaultsOnly, Category = "Action Input")
@@ -372,6 +378,8 @@ public:
void ToggleBazarInput(bool bActive); void ToggleBazarInput(bool bActive);
void OnInteract(const FInputActionValue& Value); void OnInteract(const FInputActionValue& Value);
void OnInteractHoldStart(const FInputActionValue& Value);
void OnInteractHold(const FInputActionValue& Value);
void OnInteractExit(const FInputActionValue& Value); void OnInteractExit(const FInputActionValue& Value);
void OnHarvestSeed(const FInputActionValue& Value); void OnHarvestSeed(const FInputActionValue& Value);