// 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(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(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()) { 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()) { 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); }