247 lines
7.3 KiB
C++
247 lines
7.3 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;
|
|
|
|
// Indispensable pour que Server_Interact soit route : un composant non
|
|
// replique voit ses RPC serveur silencieusement jetes par le moteur. Aucune
|
|
// propriete n'est repliquee pour autant, seul le routage nous interesse.
|
|
SetIsReplicatedByDefault(true);
|
|
}
|
|
|
|
void UInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
|
{
|
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
|
|
|
// Le trace ne sert qu'a afficher un prompt, donc uniquement a celui qui
|
|
// regarde. Sans ce filtre, l'hote lancerait un trace physique pour CHACUN
|
|
// des quatre pawns a 20 Hz, dont trois dont il n'affichera jamais l'UI.
|
|
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
|
|
if (!OwnerPawn || !OwnerPawn->IsLocallyControlled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// L'hote agit directement, le client demande. Le test porte sur l'autorite
|
|
// et non sur "suis-je un client" : sur un serveur d'ecoute, le joueur qui
|
|
// heberge passe par la premiere branche et ne paie aucune latence.
|
|
if (GetOwner()->HasAuthority())
|
|
{
|
|
PerformInteract(Target);
|
|
}
|
|
else
|
|
{
|
|
Server_Interact(Target);
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// Cote client la cible est encore la a cet instant -- le serveur n'a pas
|
|
// encore repondu. Ce n'est pas grave : la destruction repliquee videra
|
|
// FocusedActor toute seule, et bHasFocusedActor fera diffuser la transition.
|
|
UpdateFocus();
|
|
}
|
|
|
|
void UInteractionComponent::Server_Interact_Implementation(AActor* Target)
|
|
{
|
|
PerformInteract(Target);
|
|
}
|
|
|
|
void UInteractionComponent::PerformInteract(AActor* Target)
|
|
{
|
|
AActor* Owner = GetOwner();
|
|
if (!Owner || !Owner->HasAuthority() || !IsValidInteractable(Target))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Le client a vise il y a un aller-retour : entre-temps un autre joueur a pu
|
|
// ramasser l'objet, ou s'en eloigner. On revalide donc la distance ici, avec
|
|
// la meme portee que le trace mais elargie par ServerRangeTolerance.
|
|
const float MaxDistance = InteractionRange * ServerRangeTolerance;
|
|
if (FVector::DistSquared(Owner->GetActorLocation(), Target->GetActorLocation()) > FMath::Square(MaxDistance))
|
|
{
|
|
UE_LOG(LogTemp, Verbose, TEXT("UInteractionComponent : %s a demande %s, hors de portee cote serveur."),
|
|
*GetNameSafe(Owner), *GetNameSafe(Target));
|
|
return;
|
|
}
|
|
|
|
IInteractable::Execute_Interact(Target, Owner);
|
|
}
|
|
|
|
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);
|
|
}
|