Planting interaction blocker implemented, ui showing planting mode

This commit is contained in:
2026-09-14 09:39:31 +02:00
parent f9384cc3a0
commit ca6f0debe9
10 changed files with 161 additions and 57 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -21,6 +21,7 @@ void UInteractionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
void UInteractionSubsystem::Deinitialize()
{
InteractableActorList.Empty();
InteractionUIBlockers.Empty();
if (UInteractionSubsystem* IS = GetWorld()->GetSubsystem<UInteractionSubsystem>())
{
@@ -104,19 +105,23 @@ AActor* UInteractionSubsystem::GetClosestInteractable(const UObject* WorldContex
Closest = Actor.Get();
}
if (IS->CurrentlyInteractableObject != Closest)
// UI-only gate: blocking never changes what this function returns to gameplay code,
// it only decides which actor is allowed to show the interaction UI.
AActor* UIActor = IS->HasInteractionUIBlocks() ? nullptr : Closest;
if (IS->CurrentlyInteractableObject.Get() != UIActor)
{
if (IS->CurrentlyInteractableObject.IsValid())
{
// UE_LOG(LogTemp, Warning, TEXT("Turning off last interactable: %s"), *IS->CurrentlyInteractableObject.Get()->GetActorNameOrLabel())
IInteractableActorInterface::Execute_ToggleInteractableUI(IS->CurrentlyInteractableObject.Get(), false);
}
if (Closest)
if (UIActor)
{
// UE_LOG(LogTemp, Warning, TEXT("Turning ON interactable: %s"), *Closest->GetActorNameOrLabel())
IInteractableActorInterface::Execute_ToggleInteractableUI(Closest, true);
// UE_LOG(LogTemp, Warning, TEXT("Turning ON interactable: %s"), *UIActor->GetActorNameOrLabel())
IInteractableActorInterface::Execute_ToggleInteractableUI(UIActor, true);
}
IS->CurrentlyInteractableObject = Closest;
IS->CurrentlyInteractableObject = UIActor;
}
return Closest;
@@ -131,9 +136,104 @@ void UInteractionSubsystem::UpdateInteractableText(AActor* Interactable)
if (InteractionSubsystem->CurrentlyInteractableObject != Interactable) return;
// The interaction UI is being held hidden by a blocker - leave it alone.
if (InteractionSubsystem->HasInteractionUIBlocks()) return;
IInteractableActorInterface::Execute_ToggleInteractableUI(InteractionSubsystem->CurrentlyInteractableObject.Get(), true);
}
void UInteractionSubsystem::AddInteractionUIBlock(const UObject* WorldContextObject, UObject* Blocker)
{
if (!IsValid(WorldContextObject) || !IsValid(WorldContextObject->GetWorld()) || !IsValid(Blocker))
{
return;
}
UInteractionSubsystem* IS = WorldContextObject->GetWorld()->GetSubsystem<UInteractionSubsystem>();
if (!IS)
{
return;
}
// Drop dead entries before touching the array so a recycled weak pointer can never be "revived".
IS->PruneInteractionUIBlockers();
const bool bAlreadyBlocking = IS->InteractionUIBlockers.ContainsByPredicate(
[Blocker](const TWeakObjectPtr<UObject>& Entry) { return Entry.Get() == Blocker; });
if (bAlreadyBlocking)
{
return;
}
const TWeakObjectPtr<UObject> BlockerRef(Blocker);
IS->InteractionUIBlockers.Add(BlockerRef);
UE_LOG(LogTemp, Verbose, TEXT("Interaction UI blocked by %s (%d blocker(s) active)"),
*Blocker->GetName(), IS->InteractionUIBlockers.Num());
// A widget might be on screen right now, take it down instead of waiting for the next tick.
IS->HideCurrentInteractableUI();
}
void UInteractionSubsystem::RemoveInteractionUIBlock(const UObject* WorldContextObject, UObject* Blocker)
{
if (!IsValid(WorldContextObject) || !IsValid(WorldContextObject->GetWorld()) || !IsValid(Blocker))
{
return;
}
UInteractionSubsystem* IS = WorldContextObject->GetWorld()->GetSubsystem<UInteractionSubsystem>();
if (!IS)
{
return;
}
IS->InteractionUIBlockers.RemoveAll(
[Blocker](const TWeakObjectPtr<UObject>& Entry) { return Entry.Get() == Blocker; });
// Nothing else to do - the next Tick re-evaluates the closest interactable and brings the UI back.
}
bool UInteractionSubsystem::IsInteractionUIBlocked(const UObject* WorldContextObject)
{
if (!IsValid(WorldContextObject) || !IsValid(WorldContextObject->GetWorld()))
{
return false;
}
UInteractionSubsystem* IS = WorldContextObject->GetWorld()->GetSubsystem<UInteractionSubsystem>();
return IS ? IS->HasInteractionUIBlocks() : false;
}
bool UInteractionSubsystem::HasInteractionUIBlocks() const
{
for (const TWeakObjectPtr<UObject>& Blocker : InteractionUIBlockers)
{
if (Blocker.IsValid())
{
return true;
}
}
return false;
}
void UInteractionSubsystem::PruneInteractionUIBlockers()
{
InteractionUIBlockers.RemoveAll([](const TWeakObjectPtr<UObject>& Entry) { return !Entry.IsValid(); });
}
void UInteractionSubsystem::HideCurrentInteractableUI()
{
if (CurrentlyInteractableObject.IsValid())
{
IInteractableActorInterface::Execute_ToggleInteractableUI(CurrentlyInteractableObject.Get(), false);
}
CurrentlyInteractableObject = nullptr;
}
void UInteractionSubsystem::RescanWorld()
{
UWorld* World = GetWorld();

View File

@@ -32,6 +32,22 @@ public:
UFUNCTION(BlueprintCallable, Category = "Interactable")
static void UpdateInteractableText(AActor* Interactable);
/**
* Keeps the interaction UI hidden until the same Blocker calls RemoveInteractionUIBlock.
* Only the UI is affected - GetClosestInteractable() and Execute_Interact() keep working normally,
* so gameplay interaction is never disabled by this.
* Any number of blockers can be registered at the same time; the UI only comes back once the
* last one has released it.
*/
UFUNCTION(BlueprintCallable, Category = "Interactable|UI", meta = (WorldContext = "WorldContextObject"))
static void AddInteractionUIBlock(const UObject* WorldContextObject, UObject* Blocker);
UFUNCTION(BlueprintCallable, Category = "Interactable|UI", meta = (WorldContext = "WorldContextObject"))
static void RemoveInteractionUIBlock(const UObject* WorldContextObject, UObject* Blocker);
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interactable|UI", meta = (WorldContext = "WorldContextObject"))
static bool IsInteractionUIBlocked(const UObject* WorldContextObject);
protected:
UFUNCTION()
@@ -40,9 +56,26 @@ protected:
void RegisterInteractableActor(AActor* InteractableActor);
void UnregisterInteractableActor(AActor* InteractableActor);
/** True while at least one still-alive blocker is registered */
bool HasInteractionUIBlocks() const;
/** Drops blockers that have been destroyed, so a stale entry can never keep the UI hidden forever */
void PruneInteractionUIBlockers();
/** Immediately takes down whatever interaction UI is currently shown */
void HideCurrentInteractableUI();
UPROPERTY()
TArray<TWeakObjectPtr<AActor>> InteractableActorList;
UPROPERTY()
TWeakObjectPtr<AActor> CurrentlyInteractableObject;
/**
* Objects currently holding the interaction UI hidden.
* Weak on purpose: if a blocker is destroyed without releasing, the entry stops counting by itself.
* Deliberately not a UPROPERTY - TWeakObjectPtr needs no GC reflection and this array must never
* keep a blocker alive.
*/
TArray<TWeakObjectPtr<UObject>> InteractionUIBlockers;
};

View File

@@ -153,13 +153,6 @@ void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
Super::EndPlay(EndPlayReason);
}
void ASeedBedActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
RefreshAllSlotVisuals();
}
void ASeedBedActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

View File

@@ -213,7 +213,6 @@ public:
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void OnConstruction(const FTransform& Transform) override;
/** Creates (or rebuilds) the components of a single slot and adds it to the slot map */
FSeedBedSeedSlot& CreateSlot(FIntVector2 SlotCoordinate, bool bRebuildIfExisting);

View File

@@ -190,11 +190,6 @@ void AEleriPlayerController::SetupInputComponent()
PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed);
}
if (PlantSeedAction)
{
PlayerEnhancedInputComponent->BindAction(PlantSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnPlantSeedAction);
}
if (UiBackAction)
{
PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction);
@@ -1132,7 +1127,10 @@ void AEleriPlayerController::OnInteract(const FInputActionValue &Value)
return;
if (PlayerCharacter->GetIsPlanting())
{
TryPlantSelectedSeed();
return;
}
// Ask the InteractionSystem plugin's subsystem for the closest valid interactable
// and fire the interaction on it if we have one.
@@ -1149,7 +1147,11 @@ void AEleriPlayerController::OnInteractExit(const FInputActionValue &Value)
if (!IsValid(PlayerCharacter))
return;
// if (PlayerCharacter->GetIsPlanting()) PlayerCharacter->PlantSeed();
if (PlayerCharacter->GetIsPlanting())
{
ClearSeedToPlant();
return;
}
PlayerCharacter->OnInteractExit.Broadcast();
OnChangeInputContext.Broadcast(1);
@@ -1200,22 +1202,6 @@ void AEleriPlayerController::SetPlantingMode(bool bActive)
return;
PlayerCharacter->SetPlanting(bActive);
// Planting gets its own context so it can take a key over without touching the other contexts
if (IsValid(PlantingContext))
{
if (UEnhancedInputLocalPlayerSubsystem *Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
if (bActive)
{
Subsystem->AddMappingContext(PlantingContext, 6);
}
else
{
Subsystem->RemoveMappingContext(PlantingContext);
}
}
}
}
void AEleriPlayerController::TryPlantSelectedSeed()
@@ -1260,14 +1246,6 @@ void AEleriPlayerController::TryPlantSelectedSeed()
}
}
void AEleriPlayerController::OnPlantSeedAction(const FInputActionValue &Value)
{
if (!IsValid(PlayerCharacter))
return;
TryPlantSelectedSeed();
}
void AEleriPlayerController::StartMinigame(EMinigameType MinigameType, const TSoftObjectPtr<UBaseMinigameDataAsset> &DataAsset)
{
if (!IsValid(PlayerCharacter) ||

View File

@@ -18,6 +18,7 @@
#include "Components/StatForgeComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "ProjectEleri/Items/SeedBedActor.h"
#include "Subsystem/InteractionSubsystem.h"
#if WITH_EDITOR
@@ -339,6 +340,10 @@ void AMyCharacter::SetPlanting(bool bActive)
if (bIsPlanting)
{
// Planting takes over the interact input, so keep the plugin's interaction UI out of the way.
// The plugin knows nothing about planting - it just holds/releases a generic UI block.
UInteractionSubsystem::AddInteractionUIBlock(this, this);
SeedBedActors.Empty();
TArray<AActor*> OutActors;
UGameplayStatics::GetAllActorsOfClass(this, ASeedBedActor::StaticClass(), OutActors);
@@ -349,12 +354,16 @@ void AMyCharacter::SetPlanting(bool bActive)
}
else
{
UInteractionSubsystem::RemoveInteractionUIBlock(this, this);
if (LastClosestSeedBedActor)
{
LastClosestSeedBedActor->StopHighlightingSlot();
LastClosestSeedBedActor = nullptr;
}
}
PlayerController->GetMainGameWidget()->SwitchKeybindsDisplay(bActive ? 6 : 1);
}
void AMyCharacter::BeginWatering()

View File

@@ -225,14 +225,6 @@ public:
UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputAction* WaterSeedAction;
/** Action used to plant the selected seed into the closest seed bed slot */
UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputAction* PlantSeedAction;
/** Mapping context pushed while planting, it may only contain actions that planting needs */
UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputMappingContext* PlantingContext;
UPROPERTY(EditDefaultsOnly, Category = "Book Input")
UInputAction* BookNextPageAction;
UPROPERTY(EditDefaultsOnly, Category = "Book Input")