Rework interaction system
This commit is contained in:
@@ -141,6 +141,6 @@ bSupportsTouch=False
|
||||
bSupportsGamepad=True
|
||||
DefaultGamepadName=Generic
|
||||
bCanChangeGamepadType=True
|
||||
+ControllerData=/Game/Input/GamepadControllerData.GamepadControllerData_C
|
||||
+ControllerData=/Game/Input/KeyboardControllerData.KeyboardControllerData_C
|
||||
+ControllerData=/Game/Input/CommonUI/KeyboardControllerData.KeyboardControllerData_C
|
||||
+ControllerData=/Game/Input/CommonUI/GamepadControllerData.GamepadControllerData_C
|
||||
|
||||
|
||||
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.
BIN
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,7 @@
|
||||
#include "ClothingItemInstance.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NakedDesire/Interactables/ItemPickup.h"
|
||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
|
||||
#include "NakedDesire/SaveGame/ItemSaveRecord.h"
|
||||
@@ -108,6 +109,24 @@ USkeletalMeshComponent* UClothingManager::GetMeshComponent(const EClothingSlotTy
|
||||
}
|
||||
}
|
||||
|
||||
void UClothingManager::SpawnClothingPickup(UClothingItemInstance* ItemInstance)
|
||||
{
|
||||
if (!ItemPickupActor)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("UClothingManager::SpawnClothingPickup ItemPickupActor is not set"));
|
||||
return;
|
||||
}
|
||||
|
||||
AItemPickup* NewItemPickup = GetWorld()->SpawnActor<AItemPickup>(ItemPickupActor, GetOwner()->GetActorTransform());
|
||||
if (!NewItemPickup)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("UClothingManager::SpawnClothingPickup NewItemPickup == nullptr"));
|
||||
return;
|
||||
}
|
||||
|
||||
NewItemPickup->SetItem(ItemInstance);
|
||||
}
|
||||
|
||||
void UClothingManager::PutOnClothing(UClothingItemInstance* ClothingItemInstance)
|
||||
{
|
||||
if (!ClothingItemInstance)
|
||||
@@ -220,6 +239,8 @@ void UClothingManager::DropClothing(const EClothingSlotType ClothingType)
|
||||
FItemSaveRecord ItemSaveRecord;
|
||||
ItemSaveRecord.Init(ClothingItemInstance);
|
||||
SaveSubsystem->GetCurrentSave()->DroppedItems.Push(ItemSaveRecord);
|
||||
|
||||
SpawnClothingPickup(ClothingItemInstance);
|
||||
|
||||
OnClothingDropped.Broadcast(ClothingItemInstance);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "ClothingManager.generated.h"
|
||||
|
||||
class AItemPickup;
|
||||
class UGlobalSaveGameData;
|
||||
class AClothingPickup;
|
||||
class UClothingItemInstance;
|
||||
@@ -53,7 +54,11 @@ public:
|
||||
|
||||
private:
|
||||
USkeletalMeshComponent* GetMeshComponent(EClothingSlotType SlotType) const;
|
||||
void SpawnClothingPickup(UClothingItemInstance* ItemInstance);
|
||||
|
||||
UPROPERTY()
|
||||
TMap<EClothingSlotType, TObjectPtr<UClothingItemInstance>> EquippedClothing;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Clothing")
|
||||
TSubclassOf<AItemPickup> ItemPickupActor;
|
||||
};
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
|
||||
#include "InteractableBase.h"
|
||||
#include "Components/WidgetComponent.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NakedDesire/InteractionSystem/InteractionManager.h"
|
||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||
|
||||
|
||||
AInteractableBase::AInteractableBase()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PrimaryActorTick.TickInterval = 0.25f;
|
||||
|
||||
RootSceneComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
RootComponent = RootSceneComponent;
|
||||
|
||||
WidgetAnchor = CreateDefaultSubobject<USceneComponent>(TEXT("Widget Anchor"));
|
||||
WidgetAnchor->SetupAttachment(RootComponent);
|
||||
|
||||
InteractionTrigger = CreateDefaultSubobject<UWidgetComponent>(TEXT("Interaction Trigger"));
|
||||
InteractionTrigger->SetupAttachment(WidgetAnchor);
|
||||
|
||||
InteractionTrigger->SetDrawSize(FVector2D(35, 35));
|
||||
InteractionTrigger->SetWidgetSpace(EWidgetSpace::Screen);
|
||||
InteractionTrigger->SetWindowFocusable(false);
|
||||
}
|
||||
|
||||
void AInteractableBase::Tick(float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
|
||||
if (!Player)
|
||||
{
|
||||
if (ACharacter* PlayerCharacter = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0))
|
||||
{
|
||||
Player = Cast<ANakedDesireCharacter>(PlayerCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
if (Player)
|
||||
{
|
||||
const TScriptInterface<IInteractionTarget> NearestInteractionTarget = Player->InteractionManager->GetNearestInteractionTarget();
|
||||
if (NearestInteractionTarget.GetObject() == this)
|
||||
{
|
||||
InteractionTrigger->SetVisibility(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
InteractionTrigger->SetVisibility(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "NakedDesire/InteractionSystem/InteractionTarget.h"
|
||||
#include "InteractableBase.generated.h"
|
||||
|
||||
class ANakedDesireCharacter;
|
||||
class UWidgetComponent;
|
||||
|
||||
UCLASS()
|
||||
class NAKEDDESIRE_API AInteractableBase : public AActor, public IInteractionTarget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
USceneComponent* RootSceneComponent = nullptr;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
USceneComponent* WidgetAnchor = nullptr;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
UWidgetComponent* InteractionTrigger = nullptr;
|
||||
|
||||
UPROPERTY()
|
||||
ANakedDesireCharacter* Player = nullptr;
|
||||
|
||||
public:
|
||||
AInteractableBase();
|
||||
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
|
||||
#include "ItemPickup.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "Components/WidgetComponent.h"
|
||||
#include "NakedDesire/Clothing/ClothingItem.h"
|
||||
#include "NakedDesire/Clothing/ClothingItemInstance.h"
|
||||
#include "NakedDesire/Clothing/ClothingManager.h"
|
||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||
|
||||
AItemPickup::AItemPickup()
|
||||
{
|
||||
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
SetRootComponent(Mesh);
|
||||
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
|
||||
Collider = CreateDefaultSubobject<UBoxComponent>(TEXT("Collider"));
|
||||
Collider->SetupAttachment(RootComponent);
|
||||
Collider->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
|
||||
InteractionHint = CreateDefaultSubobject<UWidgetComponent>(TEXT("Interaction Hint"));
|
||||
InteractionHint->SetupAttachment(RootComponent);
|
||||
}
|
||||
|
||||
void AItemPickup::Interact_Implementation(ANakedDesireCharacter* Player)
|
||||
{
|
||||
if (ClothingItemInstance)
|
||||
{
|
||||
Player->ClothingManager->TakeClothing(ClothingItemInstance);
|
||||
Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
bool AItemPickup::CanInteract_Implementation(ANakedDesireCharacter* Player) const
|
||||
{
|
||||
return ClothingItemInstance != nullptr;
|
||||
}
|
||||
|
||||
void AItemPickup::HideInteractionHint_Implementation()
|
||||
{
|
||||
ApplyOutline(Mesh, false, 0);
|
||||
InteractionHint->SetVisibility(false);
|
||||
}
|
||||
|
||||
void AItemPickup::ShowInteractionFocusHint_Implementation()
|
||||
{
|
||||
ApplyOutline(Mesh, true, 2);
|
||||
InteractionHint->SetVisibility(true);
|
||||
}
|
||||
|
||||
void AItemPickup::ShowInteractionProximityHint_Implementation()
|
||||
{
|
||||
ApplyOutline(Mesh, true, 1);
|
||||
InteractionHint->SetVisibility(false);
|
||||
}
|
||||
|
||||
void AItemPickup::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
InteractionHint->SetVisibility(false);
|
||||
}
|
||||
|
||||
void AItemPickup::SetItem(UClothingItemInstance* InItem)
|
||||
{
|
||||
ClothingItemInstance = InItem;
|
||||
|
||||
Mesh->SetStaticMesh(InItem->GetClothingItem()->StaticMesh);
|
||||
}
|
||||
|
||||
void AItemPickup::ApplyOutline(UStaticMeshComponent* InMesh, bool bEnabled, int32 StencilValue)
|
||||
{
|
||||
InMesh->SetRenderCustomDepth(bEnabled);
|
||||
InMesh->SetCustomDepthStencilValue(StencilValue);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "NakedDesire/Interaction/Interactable.h"
|
||||
#include "ItemPickup.generated.h"
|
||||
|
||||
class UWidgetComponent;
|
||||
class UBoxComponent;
|
||||
class UClothingItemInstance;
|
||||
|
||||
UCLASS(Abstract)
|
||||
class NAKEDDESIRE_API AItemPickup : public AActor, public IInteractable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
AItemPickup();
|
||||
|
||||
public:
|
||||
virtual void Interact_Implementation(ANakedDesireCharacter* Player) override;
|
||||
virtual bool CanInteract_Implementation(ANakedDesireCharacter* Player) const override;
|
||||
virtual void HideInteractionHint_Implementation() override;
|
||||
virtual void ShowInteractionFocusHint_Implementation() override;
|
||||
virtual void ShowInteractionProximityHint_Implementation() override;
|
||||
|
||||
|
||||
void SetItem(UClothingItemInstance* InItem);
|
||||
UClothingItemInstance* GetItem() const { return ClothingItemInstance; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UClothingItemInstance> ClothingItemInstance;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
TObjectPtr<UStaticMeshComponent> Mesh;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
TObjectPtr<UBoxComponent> Collider;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
TObjectPtr<UWidgetComponent> InteractionHint;
|
||||
|
||||
void ApplyOutline(UStaticMeshComponent* InMesh, bool bEnabled, int32 StencilValue);
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
#include "Wardrobe.h"
|
||||
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
|
||||
#include "NakedDesire/SaveGame/ItemSaveRecord.h"
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InteractableBase.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Wardrobe.generated.h"
|
||||
|
||||
class UClothingItemInstance;
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class NAKEDDESIRE_API AWardrobe : public AInteractableBase
|
||||
class NAKEDDESIRE_API AWardrobe : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
@@ -21,5 +20,6 @@ public:
|
||||
void AddItem(UClothingItemInstance* ClothingItemInstance);
|
||||
void RemoveItem(UClothingItemInstance* ClothingItemInstance) const;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "Interactable.generated.h"
|
||||
|
||||
class ANakedDesireCharacter;
|
||||
|
||||
UINTERFACE(MinimalAPI, BlueprintType)
|
||||
class UInteractable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class NAKEDDESIRE_API IInteractable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Returns true if this actor can currently be interacted with by the given player
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
bool CanInteract(ANakedDesireCharacter* Player) const;
|
||||
|
||||
// Executes the interaction (called server-side)
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
void Interact(ANakedDesireCharacter* Player);
|
||||
|
||||
// Text shown in the interaction prompt UI
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
FText GetInteractionPrompt() const;
|
||||
|
||||
// Called (client-side) when this actor enters the player's proximity radius
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
void ShowInteractionProximityHint();
|
||||
|
||||
// Called (client-side) when this actor becomes the focused interaction target
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
void ShowInteractionFocusHint();
|
||||
|
||||
// Called (client-side) when this actor loses proximity or focus
|
||||
UFUNCTION(BlueprintNativeEvent)
|
||||
void HideInteractionHint();
|
||||
};
|
||||
@@ -0,0 +1,208 @@
|
||||
#include "InteractionComponent.h"
|
||||
#include "Interactable.h"
|
||||
#include "Engine/OverlapResult.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||
|
||||
UInteractionComponent::UInteractionComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void UInteractionComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Only run on the locally controlled pawn — outlines and focus are per-client visuals
|
||||
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
|
||||
if (!OwnerPawn)
|
||||
return;
|
||||
|
||||
GetWorld()->GetTimerManager().SetTimer(
|
||||
InteractionTimerHandle,
|
||||
this,
|
||||
&UInteractionComponent::UpdateInteraction,
|
||||
0.1f,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
void UInteractionComponent::UpdateInteraction()
|
||||
{
|
||||
UpdateNearbyInteractables();
|
||||
UpdateFocusedInteractable();
|
||||
}
|
||||
|
||||
void UInteractionComponent::Interact()
|
||||
{
|
||||
AActor* Target = FocusedInteractable.Get();
|
||||
if (!Target)
|
||||
return;
|
||||
|
||||
if (!IInteractable::Execute_CanInteract(Target, Cast<ANakedDesireCharacter>(GetOwner())))
|
||||
return;
|
||||
IInteractable::Execute_Interact(Target, Cast<ANakedDesireCharacter>(GetOwner()));
|
||||
}
|
||||
|
||||
void UInteractionComponent::UpdateNearbyInteractables()
|
||||
{
|
||||
AActor* Owner = GetOwner();
|
||||
if (!Owner)
|
||||
return;
|
||||
|
||||
// Sphere overlap to find all nearby actors
|
||||
TArray<FOverlapResult> Overlaps;
|
||||
FCollisionObjectQueryParams ObjectParams;
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic);
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_WorldStatic);
|
||||
|
||||
GetWorld()->OverlapMultiByObjectType(
|
||||
Overlaps,
|
||||
Owner->GetActorLocation(),
|
||||
FQuat::Identity,
|
||||
ObjectParams,
|
||||
FCollisionShape::MakeSphere(ProximityRadius)
|
||||
);
|
||||
|
||||
// Build the new set from overlap results, filtering to IInteractable actors
|
||||
TSet<TWeakObjectPtr<AActor>> NewNearby;
|
||||
for (const FOverlapResult& Overlap : Overlaps)
|
||||
{
|
||||
AActor* Actor = Overlap.GetActor();
|
||||
if (Actor && Actor != Owner && Actor->Implements<UInteractable>())
|
||||
{
|
||||
NewNearby.Add(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
// Actors that left proximity: hide their hints
|
||||
for (const TWeakObjectPtr<AActor>& OldActor : NearbyInteractables)
|
||||
{
|
||||
if (OldActor.IsValid() && !NewNearby.Contains(OldActor))
|
||||
{
|
||||
if (FocusedInteractable == OldActor)
|
||||
{
|
||||
FocusedInteractable = nullptr;
|
||||
OnFocusedInteractableChanged.Broadcast(nullptr);
|
||||
}
|
||||
IInteractable::Execute_HideInteractionHint(OldActor.Get());
|
||||
}
|
||||
}
|
||||
|
||||
NearbyInteractables = MoveTemp(NewNearby);
|
||||
|
||||
// Recompute the proximity hint for every nearby actor based on current LOS.
|
||||
// Runs every tick so actors that enter/leave line-of-sight mid-session update
|
||||
// immediately — e.g. a wall sliding into place hides the hint without the
|
||||
// actor ever leaving the proximity radius.
|
||||
// The focused actor is skipped: UpdateFocusedInteractable owns its hint state.
|
||||
for (const TWeakObjectPtr<AActor>& WeakActor : NearbyInteractables)
|
||||
{
|
||||
AActor* Actor = WeakActor.Get();
|
||||
if (!Actor || FocusedInteractable == Actor)
|
||||
continue;
|
||||
|
||||
if (HasLineOfSightFromPawn(Actor))
|
||||
IInteractable::Execute_ShowInteractionProximityHint(Actor);
|
||||
else
|
||||
IInteractable::Execute_HideInteractionHint(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionComponent::UpdateFocusedInteractable()
|
||||
{
|
||||
FVector ViewLocation;
|
||||
FRotator ViewRotation;
|
||||
if (!GetOwnerViewPoint(ViewLocation, ViewRotation))
|
||||
return;
|
||||
|
||||
const FVector ViewForward = ViewRotation.Vector();
|
||||
ANakedDesireCharacter* Player = Cast<ANakedDesireCharacter>(GetOwner());
|
||||
|
||||
// Among nearby interactables, find the one most centered in the player's view
|
||||
// that is also within interaction distance and passes CanInteract
|
||||
AActor* BestActor = nullptr;
|
||||
float BestDot = -1.f;
|
||||
|
||||
for (const TWeakObjectPtr<AActor>& WeakActor : NearbyInteractables)
|
||||
{
|
||||
AActor* Actor = WeakActor.Get();
|
||||
if (!Actor)
|
||||
continue;
|
||||
|
||||
if (FVector::DistSquared(Player->GetActorLocation(), Actor->GetActorLocation()) > FMath::Square(InteractionDistance))
|
||||
continue;
|
||||
|
||||
if (!IInteractable::Execute_CanInteract(Actor, Player))
|
||||
continue;
|
||||
|
||||
// Look-cone check — cheap filter before the LOS trace
|
||||
const FVector ToActor = (Actor->GetActorLocation() - ViewLocation).GetSafeNormal();
|
||||
const float Dot = FVector::DotProduct(ViewForward, ToActor);
|
||||
if (Dot < LookDotThreshold || Dot <= BestDot)
|
||||
continue;
|
||||
|
||||
// LOS from the pawn's eye position — not the camera, so looking over a
|
||||
// wall with the third-person camera does not grant interaction range
|
||||
if (!HasLineOfSightFromPawn(Actor))
|
||||
continue;
|
||||
|
||||
BestDot = Dot;
|
||||
BestActor = Actor;
|
||||
}
|
||||
|
||||
if (FocusedInteractable.Get() == BestActor)
|
||||
return;
|
||||
|
||||
// Restore the previously focused actor — only show a proximity hint if it is
|
||||
// still nearby AND in line-of-sight; hide it otherwise
|
||||
if (AActor* OldFocused = FocusedInteractable.Get())
|
||||
{
|
||||
if (NearbyInteractables.Contains(OldFocused) && HasLineOfSightFromPawn(OldFocused))
|
||||
IInteractable::Execute_ShowInteractionProximityHint(OldFocused);
|
||||
else
|
||||
IInteractable::Execute_HideInteractionHint(OldFocused);
|
||||
}
|
||||
|
||||
FocusedInteractable = BestActor;
|
||||
|
||||
if (BestActor)
|
||||
IInteractable::Execute_ShowInteractionFocusHint(BestActor);
|
||||
|
||||
OnFocusedInteractableChanged.Broadcast(BestActor);
|
||||
}
|
||||
|
||||
bool UInteractionComponent::GetOwnerViewPoint(FVector& OutLocation, FRotator& OutRotation) const
|
||||
{
|
||||
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
|
||||
if (!OwnerPawn)
|
||||
return false;
|
||||
|
||||
AController* Controller = OwnerPawn->GetController();
|
||||
if (!Controller)
|
||||
return false;
|
||||
|
||||
Controller->GetPlayerViewPoint(OutLocation, OutRotation);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInteractionComponent::HasLineOfSightFromPawn(AActor* Target) const
|
||||
{
|
||||
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
|
||||
if (!OwnerPawn || !Target)
|
||||
return false;
|
||||
|
||||
// Start from the pawn's own eye position, NOT the spring-arm camera.
|
||||
// This means a player who tilts the camera above a wall still cannot
|
||||
// reach objects on the other side — the trace originates from the character mesh.
|
||||
const FVector Start = OwnerPawn->GetPawnViewLocation();
|
||||
const FVector End = Target->GetActorLocation();
|
||||
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionLOS), false);
|
||||
Params.AddIgnoredActor(GetOwner());
|
||||
Params.AddIgnoredActor(Target);
|
||||
|
||||
FHitResult Hit;
|
||||
const bool bBlocked = GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params);
|
||||
return !bBlocked;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "InteractionComponent.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFocusedInteractableChanged, AActor*, NewFocused);
|
||||
|
||||
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
|
||||
class NAKEDDESIRE_API UInteractionComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UInteractionComponent();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
// Call from interact input — sends server RPC if not authority
|
||||
void Interact();
|
||||
|
||||
UFUNCTION(BlueprintPure)
|
||||
AActor* GetFocusedInteractable() const { return FocusedInteractable.Get(); }
|
||||
|
||||
// Broadcasts whenever the focused (interactable) actor changes; nullptr = no target
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnFocusedInteractableChanged OnFocusedInteractableChanged;
|
||||
|
||||
protected:
|
||||
|
||||
// Actors within this radius receive a proximity outline
|
||||
UPROPERTY(EditDefaultsOnly, Category="Interaction")
|
||||
float ProximityRadius = 350.f;
|
||||
|
||||
// Player must be within this distance to focus an interactable (must be <= ProximityRadius)
|
||||
UPROPERTY(EditDefaultsOnly, Category="Interaction")
|
||||
float InteractionDistance = 150.f;
|
||||
|
||||
// Minimum dot product between look direction and direction to target.
|
||||
// cos(20°) ≈ 0.94 — lower values allow a wider look cone.
|
||||
UPROPERTY(EditDefaultsOnly, Category="Interaction")
|
||||
float LookDotThreshold = 0.9f;
|
||||
|
||||
private:
|
||||
FTimerHandle InteractionTimerHandle;
|
||||
|
||||
void UpdateInteraction();
|
||||
|
||||
TSet<TWeakObjectPtr<AActor>> NearbyInteractables;
|
||||
TWeakObjectPtr<AActor> FocusedInteractable;
|
||||
|
||||
// Physics scan → update NearbyInteractables set → manage proximity outlines
|
||||
void UpdateNearbyInteractables();
|
||||
|
||||
// Among nearby actors, find the one best matching look + distance conditions
|
||||
void UpdateFocusedInteractable();
|
||||
|
||||
bool GetOwnerViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
|
||||
|
||||
// Traces from the pawn's eye position (NOT the camera) to the target.
|
||||
// Returns true only if nothing blocks the line — prevents exploiting the
|
||||
// third-person camera to interact through walls by looking over them.
|
||||
bool HasLineOfSightFromPawn(AActor* Target) const;
|
||||
};
|
||||
@@ -13,16 +13,16 @@ void ULocationRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
|
||||
{
|
||||
Super::Init(PlayerCharacter);
|
||||
|
||||
AreaEnterHandle = PlayerCharacter->OnAreaEnter.AddUObject(this, &ULocationRestriction::OnAreaEnter);
|
||||
AreaExitHandle = PlayerCharacter->OnAreaExit.AddUObject(this, &ULocationRestriction::OnAreaExit);
|
||||
// AreaEnterHandle = PlayerCharacter->OnAreaEnter.AddUObject(this, &ULocationRestriction::OnAreaEnter);
|
||||
// AreaExitHandle = PlayerCharacter->OnAreaExit.AddUObject(this, &ULocationRestriction::OnAreaExit);
|
||||
}
|
||||
|
||||
void ULocationRestriction::Stop()
|
||||
{
|
||||
Super::Stop();
|
||||
|
||||
Player->OnAreaEnter.Remove(AreaEnterHandle);
|
||||
Player->OnAreaExit.Remove(AreaExitHandle);
|
||||
// Player->OnAreaEnter.Remove(AreaEnterHandle);
|
||||
// Player->OnAreaExit.Remove(AreaExitHandle);
|
||||
}
|
||||
|
||||
FText ULocationRestriction::GetDescription() const
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
#include "NakedDesireCharacter.h"
|
||||
#include "NakedDesire/Locations/LocationTrigger.h"
|
||||
#include "NakedDesire/Clothing/ClothingManager.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "NakedDesire/InteractionSystem/InteractionManager.h"
|
||||
#include "NakedDesire/InteractionSystem/InteractionTarget.h"
|
||||
#include "NakedDesire/MissionBuilder/MissionsManager.h"
|
||||
#include "NakedDesire/Stats/StatsManager.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
@@ -15,22 +11,17 @@
|
||||
#include "Internationalization/Text.h"
|
||||
#include "NakedDesire/Clothing/ClothingItem.h"
|
||||
#include "NakedDesire/Clothing/ClothingItemInstance.h"
|
||||
#include "NakedDesire/Clothing/ClothingSlotsData.h"
|
||||
#include "NakedDesire/Global/Constants.h"
|
||||
#include "NakedDesire/Global/NakedDesireHUD.h"
|
||||
#include "NakedDesire/Global/NakedDesireUserSettings.h"
|
||||
#include "NakedDesire/Interaction/InteractionComponent.h"
|
||||
#include "NakedDesire/UI/GameLayoutWidget.h"
|
||||
#include "NakedDesire/UI/HUDWidget.h"
|
||||
#include "NakedDesire/UI/RadialMenu/RadialMenuController.h"
|
||||
#include "Perception/AIPerceptionStimuliSourceComponent.h"
|
||||
#include "Perception/AISense_Sight.h"
|
||||
|
||||
ANakedDesireCharacter::ANakedDesireCharacter()
|
||||
{
|
||||
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
|
||||
|
||||
GetCapsuleComponent()->OnComponentBeginOverlap.AddUniqueDynamic(this, &ANakedDesireCharacter::OnBeginOverlap);
|
||||
GetCapsuleComponent()->OnComponentEndOverlap.AddUniqueDynamic(this, &ANakedDesireCharacter::OnEndOverlap);
|
||||
|
||||
bUseControllerRotationYaw = false;
|
||||
|
||||
@@ -40,8 +31,6 @@ ANakedDesireCharacter::ANakedDesireCharacter()
|
||||
ClothingManager = CreateDefaultSubobject<UClothingManager>("Clothing Manager");
|
||||
StatsManager = CreateDefaultSubobject<UStatsManager>("Stats Manager");
|
||||
MissionsManager = CreateDefaultSubobject<UMissionsManager>("Missions Manager");
|
||||
InteractionManager = CreateDefaultSubobject<UInteractionManager>("Interaction Manager");
|
||||
RadialMenuController = CreateDefaultSubobject<URadialMenuController>(TEXT("Radial Menu Controller"));
|
||||
|
||||
NipplesMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>("Nipples");
|
||||
NipplesMeshComponent->SetupAttachment(GetMesh());
|
||||
@@ -90,6 +79,7 @@ ANakedDesireCharacter::ANakedDesireCharacter()
|
||||
AnalCensorship->SetupAttachment(GetMesh(), FName(TEXT("pelvis")));
|
||||
|
||||
StimuliSourceComponent = CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("Stimuli Source Component"));
|
||||
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("Interaction Component"));
|
||||
}
|
||||
|
||||
EGait ANakedDesireCharacter::GetGait() const
|
||||
@@ -97,11 +87,6 @@ EGait ANakedDesireCharacter::GetGait() const
|
||||
return Gait;
|
||||
}
|
||||
|
||||
EStance ANakedDesireCharacter::GetStance() const
|
||||
{
|
||||
return Stance;
|
||||
}
|
||||
|
||||
void ANakedDesireCharacter::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
@@ -138,6 +123,7 @@ void ANakedDesireCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInp
|
||||
EnhancedInputComponent->BindAction(RunAction, ETriggerEvent::Completed, this, &ANakedDesireCharacter::OnRunRelease);
|
||||
EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Completed, this, &ANakedDesireCharacter::OnCrouchToggle);
|
||||
EnhancedInputComponent->BindAction(EquipmentAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnEquipmentPress);
|
||||
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnInteractPress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,36 +219,6 @@ bool ANakedDesireCharacter::CheckSight(const FVector& StartLocation, const FVect
|
||||
return bHit;
|
||||
}
|
||||
|
||||
void ANakedDesireCharacter::OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
if (OtherActor->Implements<UInteractionTarget>())
|
||||
{
|
||||
InteractionManager->OnTargetEnter(OtherActor);
|
||||
}
|
||||
|
||||
if (const ALocationTrigger* AreaTrigger = Cast<ALocationTrigger>(OtherActor))
|
||||
{
|
||||
CurrentArea = AreaTrigger->GetLocationData();
|
||||
OnAreaEnter.Broadcast(CurrentArea);
|
||||
}
|
||||
}
|
||||
|
||||
void ANakedDesireCharacter::OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
|
||||
{
|
||||
if (OtherActor->Implements<UInteractionTarget>())
|
||||
{
|
||||
InteractionManager->OnTargetExit(OtherActor);
|
||||
}
|
||||
|
||||
if (const ALocationTrigger* AreaTrigger = Cast<ALocationTrigger>(OtherActor))
|
||||
{
|
||||
CurrentArea = nullptr;
|
||||
OnAreaExit.Broadcast(AreaTrigger->GetLocationData());
|
||||
}
|
||||
}
|
||||
|
||||
void ANakedDesireCharacter::OnClothingEquip(UClothingItemInstance* ClothingItemInstance)
|
||||
{
|
||||
if (ClothingItemInstance->GetClothingItem()->HiddenBodyParts.Contains(EBodyPart::Ass))
|
||||
@@ -374,28 +330,9 @@ void ANakedDesireCharacter::OnEquipmentPress(const FInputActionValue& Value)
|
||||
HUD->GetGameLayoutWidget()->OpenInventory();
|
||||
}
|
||||
|
||||
void ANakedDesireCharacter::BuildRadialMenuEntries()
|
||||
void ANakedDesireCharacter::OnInteractPress(const FInputActionValue& Value)
|
||||
{
|
||||
if (!SlotsData)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("ANakedDesireCharacter::BuildRadialMenuEntries SlotsData not defined"));
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FRadialMenuEntry> Entries;
|
||||
|
||||
for (const auto& [Key, Value] : SlotsData->Slots)
|
||||
{
|
||||
FRadialMenuEntry Entry;
|
||||
const UClothingItemInstance* EquippedItem = ClothingManager->GetSlotClothing(Key);
|
||||
Entry.bEnabled = true;
|
||||
Entry.DisplayName = Value.Name;
|
||||
Entry.Icon = EquippedItem ? EquippedItem->GetClothingItem()->Icon : Value.Icon;
|
||||
Entry.ItemId = FName(Value.Name.ToString());
|
||||
Entries.Push(Entry);
|
||||
}
|
||||
|
||||
RadialMenuController->Entries = Entries;
|
||||
InteractionComponent->Interact();
|
||||
}
|
||||
|
||||
void ANakedDesireCharacter::NotifyNoticed(ANPCAIController* NPCController)
|
||||
|
||||
@@ -9,17 +9,15 @@
|
||||
#include "NakedDesire/Global/Gait.h"
|
||||
#include "NakedDesire/Global/NakedDesireUserSettings.h"
|
||||
#include "NakedDesire/Global/Stance.h"
|
||||
#include "NakedDesire/UI/RadialMenu/RadialMenuController.h"
|
||||
#include "Perception/AISightTargetInterface.h"
|
||||
#include "NakedDesireCharacter.generated.h"
|
||||
|
||||
class UInteractionComponent;
|
||||
class ANakedDesireHUD;
|
||||
class UClothingItem;
|
||||
class UClothingItemInstance;
|
||||
class UClothingSlotsData;
|
||||
class URadialMenuController;
|
||||
class UAIPerceptionStimuliSourceComponent;
|
||||
class UInteractionManager;
|
||||
class UClothingManager;
|
||||
class UStatsManager;
|
||||
class UMissionsManager;
|
||||
@@ -27,7 +25,6 @@ class ANPCAIController;
|
||||
class ULocationData;
|
||||
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(FOnNoticedSignature, ANPCAIController*);
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(FOnAreaChangeSignature, ULocationData*);
|
||||
|
||||
UCLASS(config=Game)
|
||||
class ANakedDesireCharacter : public ACharacter, public IAISightTargetInterface
|
||||
@@ -55,6 +52,9 @@ public:
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Input")
|
||||
UInputAction* EquipmentAction;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Input")
|
||||
UInputAction* InteractAction;
|
||||
|
||||
// Clothing
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Clothing")
|
||||
@@ -136,14 +136,11 @@ public:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||
UMissionsManager* MissionsManager;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||
UInteractionManager* InteractionManager;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||
UAIPerceptionStimuliSourceComponent* StimuliSourceComponent;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
TObjectPtr<URadialMenuController> RadialMenuController;
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Interaction")
|
||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
||||
|
||||
// Variables
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||
@@ -159,18 +156,12 @@ public:
|
||||
|
||||
FOnNoticedSignature OnNoticed;
|
||||
|
||||
FOnAreaChangeSignature OnAreaEnter;
|
||||
FOnAreaChangeSignature OnAreaExit;
|
||||
|
||||
void NotifyNoticed(ANPCAIController* NPCController);
|
||||
void SetIsRunning(bool Value);
|
||||
|
||||
UFUNCTION(BlueprintPure)
|
||||
EGait GetGait() const;
|
||||
|
||||
UFUNCTION(BlueprintPure)
|
||||
EStance GetStance() const;
|
||||
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
|
||||
virtual void NotifyControllerChanged() override;
|
||||
@@ -180,14 +171,6 @@ public:
|
||||
private:
|
||||
EGait Gait = EGait::Walk;
|
||||
EStance Stance = EStance::Stand;
|
||||
|
||||
UFUNCTION()
|
||||
void OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,
|
||||
int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
|
||||
|
||||
UFUNCTION()
|
||||
void OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,
|
||||
int32 OtherBodyIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void OnClothingEquip(UClothingItemInstance* ClothingItemInstance);
|
||||
@@ -204,8 +187,7 @@ private:
|
||||
void OnRunRelease(const FInputActionValue& Value);
|
||||
void OnCrouchToggle(const FInputActionValue& Value);
|
||||
void OnEquipmentPress(const FInputActionValue& Value);
|
||||
|
||||
void BuildRadialMenuEntries();
|
||||
void OnInteractPress(const FInputActionValue& Value);
|
||||
|
||||
bool CheckSight(const FVector& StartLocation, const FVector& EndLocation, FHitResult& HitResult, const AActor* IgnoreActor);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user