Planting interaction blocker implemented, ui showing planting mode
This commit is contained in:
BIN
Content/ST_CommonWords.uasset
LFS
BIN
Content/ST_CommonWords.uasset
LFS
Binary file not shown.
Binary file not shown.
BIN
Content/UI/MainUI.uasset
LFS
BIN
Content/UI/MainUI.uasset
LFS
Binary file not shown.
@@ -21,6 +21,7 @@ void UInteractionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|||||||
void UInteractionSubsystem::Deinitialize()
|
void UInteractionSubsystem::Deinitialize()
|
||||||
{
|
{
|
||||||
InteractableActorList.Empty();
|
InteractableActorList.Empty();
|
||||||
|
InteractionUIBlockers.Empty();
|
||||||
|
|
||||||
if (UInteractionSubsystem* IS = GetWorld()->GetSubsystem<UInteractionSubsystem>())
|
if (UInteractionSubsystem* IS = GetWorld()->GetSubsystem<UInteractionSubsystem>())
|
||||||
{
|
{
|
||||||
@@ -104,19 +105,23 @@ AActor* UInteractionSubsystem::GetClosestInteractable(const UObject* WorldContex
|
|||||||
Closest = Actor.Get();
|
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())
|
if (IS->CurrentlyInteractableObject.IsValid())
|
||||||
{
|
{
|
||||||
// UE_LOG(LogTemp, Warning, TEXT("Turning off last interactable: %s"), *IS->CurrentlyInteractableObject.Get()->GetActorNameOrLabel())
|
// UE_LOG(LogTemp, Warning, TEXT("Turning off last interactable: %s"), *IS->CurrentlyInteractableObject.Get()->GetActorNameOrLabel())
|
||||||
IInteractableActorInterface::Execute_ToggleInteractableUI(IS->CurrentlyInteractableObject.Get(), false);
|
IInteractableActorInterface::Execute_ToggleInteractableUI(IS->CurrentlyInteractableObject.Get(), false);
|
||||||
}
|
}
|
||||||
if (Closest)
|
if (UIActor)
|
||||||
{
|
{
|
||||||
// UE_LOG(LogTemp, Warning, TEXT("Turning ON interactable: %s"), *Closest->GetActorNameOrLabel())
|
// UE_LOG(LogTemp, Warning, TEXT("Turning ON interactable: %s"), *UIActor->GetActorNameOrLabel())
|
||||||
IInteractableActorInterface::Execute_ToggleInteractableUI(Closest, true);
|
IInteractableActorInterface::Execute_ToggleInteractableUI(UIActor, true);
|
||||||
}
|
}
|
||||||
IS->CurrentlyInteractableObject = Closest;
|
IS->CurrentlyInteractableObject = UIActor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Closest;
|
return Closest;
|
||||||
@@ -131,9 +136,104 @@ void UInteractionSubsystem::UpdateInteractableText(AActor* Interactable)
|
|||||||
|
|
||||||
if (InteractionSubsystem->CurrentlyInteractableObject != Interactable) return;
|
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);
|
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()
|
void UInteractionSubsystem::RescanWorld()
|
||||||
{
|
{
|
||||||
UWorld* World = GetWorld();
|
UWorld* World = GetWorld();
|
||||||
|
|||||||
@@ -32,6 +32,22 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Interactable")
|
UFUNCTION(BlueprintCallable, Category = "Interactable")
|
||||||
static void UpdateInteractableText(AActor* 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:
|
protected:
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
@@ -40,9 +56,26 @@ protected:
|
|||||||
void RegisterInteractableActor(AActor* InteractableActor);
|
void RegisterInteractableActor(AActor* InteractableActor);
|
||||||
void UnregisterInteractableActor(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()
|
UPROPERTY()
|
||||||
TArray<TWeakObjectPtr<AActor>> InteractableActorList;
|
TArray<TWeakObjectPtr<AActor>> InteractableActorList;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TWeakObjectPtr<AActor> CurrentlyInteractableObject;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,13 +153,6 @@ void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|||||||
Super::EndPlay(EndPlayReason);
|
Super::EndPlay(EndPlayReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ASeedBedActor::OnConstruction(const FTransform& Transform)
|
|
||||||
{
|
|
||||||
Super::OnConstruction(Transform);
|
|
||||||
|
|
||||||
RefreshAllSlotVisuals();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ASeedBedActor::Tick(float DeltaTime)
|
void ASeedBedActor::Tick(float DeltaTime)
|
||||||
{
|
{
|
||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
|
|||||||
@@ -213,7 +213,6 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) 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 */
|
/** Creates (or rebuilds) the components of a single slot and adds it to the slot map */
|
||||||
FSeedBedSeedSlot& CreateSlot(FIntVector2 SlotCoordinate, bool bRebuildIfExisting);
|
FSeedBedSeedSlot& CreateSlot(FIntVector2 SlotCoordinate, bool bRebuildIfExisting);
|
||||||
|
|||||||
@@ -190,11 +190,6 @@ void AEleriPlayerController::SetupInputComponent()
|
|||||||
PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed);
|
PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PlantSeedAction)
|
|
||||||
{
|
|
||||||
PlayerEnhancedInputComponent->BindAction(PlantSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnPlantSeedAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UiBackAction)
|
if (UiBackAction)
|
||||||
{
|
{
|
||||||
PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction);
|
PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction);
|
||||||
@@ -1132,7 +1127,10 @@ void AEleriPlayerController::OnInteract(const FInputActionValue &Value)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (PlayerCharacter->GetIsPlanting())
|
if (PlayerCharacter->GetIsPlanting())
|
||||||
|
{
|
||||||
TryPlantSelectedSeed();
|
TryPlantSelectedSeed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ask the InteractionSystem plugin's subsystem for the closest valid interactable
|
// Ask the InteractionSystem plugin's subsystem for the closest valid interactable
|
||||||
// and fire the interaction on it if we have one.
|
// and fire the interaction on it if we have one.
|
||||||
@@ -1149,7 +1147,11 @@ void AEleriPlayerController::OnInteractExit(const FInputActionValue &Value)
|
|||||||
if (!IsValid(PlayerCharacter))
|
if (!IsValid(PlayerCharacter))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// if (PlayerCharacter->GetIsPlanting()) PlayerCharacter->PlantSeed();
|
if (PlayerCharacter->GetIsPlanting())
|
||||||
|
{
|
||||||
|
ClearSeedToPlant();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
PlayerCharacter->OnInteractExit.Broadcast();
|
PlayerCharacter->OnInteractExit.Broadcast();
|
||||||
OnChangeInputContext.Broadcast(1);
|
OnChangeInputContext.Broadcast(1);
|
||||||
@@ -1200,22 +1202,6 @@ void AEleriPlayerController::SetPlantingMode(bool bActive)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
PlayerCharacter->SetPlanting(bActive);
|
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()
|
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)
|
void AEleriPlayerController::StartMinigame(EMinigameType MinigameType, const TSoftObjectPtr<UBaseMinigameDataAsset> &DataAsset)
|
||||||
{
|
{
|
||||||
if (!IsValid(PlayerCharacter) ||
|
if (!IsValid(PlayerCharacter) ||
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "Components/StatForgeComponent.h"
|
#include "Components/StatForgeComponent.h"
|
||||||
#include "GameFramework/SpringArmComponent.h"
|
#include "GameFramework/SpringArmComponent.h"
|
||||||
#include "ProjectEleri/Items/SeedBedActor.h"
|
#include "ProjectEleri/Items/SeedBedActor.h"
|
||||||
|
#include "Subsystem/InteractionSubsystem.h"
|
||||||
|
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
@@ -339,6 +340,10 @@ void AMyCharacter::SetPlanting(bool bActive)
|
|||||||
|
|
||||||
if (bIsPlanting)
|
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();
|
SeedBedActors.Empty();
|
||||||
TArray<AActor*> OutActors;
|
TArray<AActor*> OutActors;
|
||||||
UGameplayStatics::GetAllActorsOfClass(this, ASeedBedActor::StaticClass(), OutActors);
|
UGameplayStatics::GetAllActorsOfClass(this, ASeedBedActor::StaticClass(), OutActors);
|
||||||
@@ -349,12 +354,16 @@ void AMyCharacter::SetPlanting(bool bActive)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
UInteractionSubsystem::RemoveInteractionUIBlock(this, this);
|
||||||
|
|
||||||
if (LastClosestSeedBedActor)
|
if (LastClosestSeedBedActor)
|
||||||
{
|
{
|
||||||
LastClosestSeedBedActor->StopHighlightingSlot();
|
LastClosestSeedBedActor->StopHighlightingSlot();
|
||||||
LastClosestSeedBedActor = nullptr;
|
LastClosestSeedBedActor = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlayerController->GetMainGameWidget()->SwitchKeybindsDisplay(bActive ? 6 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AMyCharacter::BeginWatering()
|
void AMyCharacter::BeginWatering()
|
||||||
|
|||||||
@@ -225,14 +225,6 @@ public:
|
|||||||
UPROPERTY(EditDefaultsOnly, Category = "Action Input")
|
UPROPERTY(EditDefaultsOnly, Category = "Action Input")
|
||||||
UInputAction* WaterSeedAction;
|
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")
|
UPROPERTY(EditDefaultsOnly, Category = "Book Input")
|
||||||
UInputAction* BookNextPageAction;
|
UInputAction* BookNextPageAction;
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "Book Input")
|
UPROPERTY(EditDefaultsOnly, Category = "Book Input")
|
||||||
|
|||||||
Reference in New Issue
Block a user