Files

192 lines
5.4 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "InteractionComponent.h"
#include "DrawDebugHelpers.h"
#include "Engine/World.h"
#include "GameFramework/Controller.h"
#include "GameFramework/Pawn.h"
#include "Interactable.h"
UInteractionComponent::UInteractionComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// On espace les traces : c'est une requete physique, pas gratuite, et
// l'affichage du prompt n'a pas besoin d'etre rafraichi chaque frame.
TimeSinceLastTrace += DeltaTime;
if (TimeSinceLastTrace < TraceInterval)
{
return;
}
TimeSinceLastTrace = 0.f;
UpdateFocus();
}
bool UInteractionComponent::GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const
{
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
if (!OwnerPawn)
{
return false;
}
// On passe par le controller plutot que d'aller chercher la camera nous-memes :
// GetPlayerViewPoint renvoie exactement ce que le joueur voit a l'ecran, y
// compris les effets de camera. Le trace part donc pile du centre du viseur.
if (const AController* OwnerController = OwnerPawn->GetController())
{
OwnerController->GetPlayerViewPoint(OutLocation, OutRotation);
return true;
}
return false;
}
void UInteractionComponent::UpdateFocus()
{
FVector ViewLocation;
FRotator ViewRotation;
if (!GetViewPoint(ViewLocation, ViewRotation))
{
SetFocusedActor(nullptr);
RefreshPrompt();
return;
}
const FVector TraceEnd = ViewLocation + ViewRotation.Vector() * InteractionRange;
FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionTrace), /*bTraceComplex=*/false, GetOwner());
// Sphere balayee plutot que rayon pur : quelques cm de tolerance suffisent
// pour que viser un petit objet cesse d'etre un exercice de precision.
FHitResult Hit;
const bool bHit = GetWorld()->SweepSingleByChannel(
Hit,
ViewLocation,
TraceEnd,
FQuat::Identity,
TraceChannel,
FCollisionShape::MakeSphere(TraceRadius),
Params);
AActor* Candidate = bHit ? Hit.GetActor() : nullptr;
if (!IsValidInteractable(Candidate))
{
Candidate = nullptr;
}
SetFocusedActor(Candidate);
// Apres SetFocusedActor, jamais avant : l'ordre garantit que l'UI recoit
// d'abord "la cible a change", puis le texte correspondant.
RefreshPrompt();
#if ENABLE_DRAW_DEBUG
if (bDrawDebug)
{
const FColor Color = Candidate ? FColor::Green : FColor::Red;
DrawDebugLine(GetWorld(), ViewLocation, TraceEnd, Color, false, TraceInterval, 0, 1.f);
if (bHit)
{
DrawDebugSphere(GetWorld(), Hit.ImpactPoint, TraceRadius, 12, Color, false, TraceInterval);
}
}
#endif
}
bool UInteractionComponent::IsValidInteractable(AActor* Actor) const
{
if (!IsValid(Actor) || !Actor->Implements<UInteractable>())
{
return false;
}
// L'objet a le dernier mot : une porte verrouillee peut refuser d'etre visee.
return IInteractable::Execute_CanInteract(Actor, GetOwner());
}
void UInteractionComponent::SetFocusedActor(AActor* NewFocus)
{
AActor* OldFocus = FocusedActor.Get();
const bool bWantsFocus = (NewFocus != nullptr);
// On compare les pointeurs ET l'etat booleen. Sans le booleen, le cas
// "la cible s'est detruite toute seule" passerait inapercu : FocusedActor
// renvoie deja nullptr, donc nullptr == nullptr, et on sortirait sans
// prevenir l'UI -- le prompt resterait affiche indefiniment.
if (OldFocus == NewFocus && bHasFocusedActor == bWantsFocus)
{
return;
}
// IsValid() et pas seulement != nullptr : l'ancienne cible a pu etre detruite
// entre deux traces, typiquement un objet qu'on vient de ramasser.
if (IsValid(OldFocus) && OldFocus->Implements<UInteractable>())
{
IInteractable::Execute_OnEndFocus(OldFocus);
}
FocusedActor = NewFocus;
bHasFocusedActor = bWantsFocus;
if (NewFocus)
{
IInteractable::Execute_OnBeginFocus(NewFocus);
}
OnFocusChanged.Broadcast(NewFocus, OldFocus);
}
void UInteractionComponent::TryInteract()
{
// On retrace avant d'agir : jusqu'a TraceInterval secondes ont pu s'ecouler
// depuis le dernier trace, et interagir avec un objet qu'on ne vise plus
// serait un bug tres visible.
UpdateFocus();
AActor* Target = FocusedActor.Get();
if (!Target)
{
return;
}
IInteractable::Execute_Interact(Target, GetOwner());
// L'interaction vient peut-etre de detruire la cible, ou de la rendre
// non-interactive (porte maintenant ouverte). On reevalue tout de suite
// plutot que d'attendre le prochain trace periodique.
UpdateFocus();
}
void UInteractionComponent::RefreshPrompt()
{
AActor* Target = FocusedActor.Get();
// Un seul FText::Format par trace, soit 20 fois par seconde et seulement
// quand on vise quelque chose. Recalculer ici plutot que dans le getter
// evite qu'un widget qui interroge le prompt plusieurs fois par frame
// paie le format autant de fois.
const FText NewPrompt = IsValid(Target)
? IInteractable::Execute_GetInteractionPrompt(Target, GetOwner())
: FText::GetEmpty();
// EqualTo et non IdenticalTo : on compare ce qui s'affiche, pas l'instance.
// Deux FText::Format successifs produisent toujours deux instances
// differentes, IdenticalTo redeclencherait donc l'UI a chaque trace.
if (CachedPrompt.EqualTo(NewPrompt))
{
return;
}
CachedPrompt = NewPrompt;
OnPromptChanged.Broadcast(CachedPrompt);
}