(Feat) Player name tag

This commit is contained in:
2026-08-03 11:44:01 +02:00
parent a4cc59a411
commit 7595df6862
4 changed files with 120 additions and 64 deletions
@@ -9,7 +9,6 @@
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "PlayerNameplateWidget.h"
#include "TimerManager.h"
UPlayerNameplateComponent::UPlayerNameplateComponent()
{
@@ -31,10 +30,11 @@ UPlayerNameplateComponent::UPlayerNameplateComponent()
// du personnage pouvant changer.
SetRelativeLocation(FVector(0.f, 0.f, 100.f));
// Cachee au depart : la premiere passe de Refresh decidera. Sans ca, une
// etiquette sans nom clignote pendant la frame qui precede l'arrivee du
// PlayerState.
// Cachee au depart : la premiere passe decidera. Sans ca, une etiquette
// sans nom clignote pendant la frame qui precede l'arrivee du PlayerState.
SetVisibility(false);
PrimaryComponentTick.bCanEverTick = true;
}
void UPlayerNameplateComponent::BeginPlay()
@@ -46,40 +46,20 @@ void UPlayerNameplateComponent::BeginPlay()
// notre serveur d'ecoute, qui n'est pas dans ce mode.
if (GetNetMode() == NM_DedicatedServer)
{
SetComponentTickEnabled(false);
return;
}
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
// Son propre pseudo : on eteint definitivement, sans timer. Ce n'est pas un
// cas a reevaluer -- un pawn ne change pas de proprietaire en cours de vie,
// et apres un respawn c'est un NOUVEL acteur qui repassera par ici.
// Son propre pseudo : on eteint definitivement, tick compris. Ce n'est pas
// un cas a reevaluer -- un pawn ne change pas de proprietaire en cours de
// vie, et apres un respawn c'est un NOUVEL acteur qui repassera par ici.
if (bHideOnOwningPlayer && OwnerPawn && OwnerPawn->IsLocallyControlled())
{
SetVisibility(false);
return;
SetComponentTickEnabled(false);
}
GetWorld()->GetTimerManager().SetTimer(
RefreshTimer, this, &UPlayerNameplateComponent::Refresh,
FMath::Max(RefreshInterval, 0.05f), /*bLoop=*/true);
// Une premiere passe tout de suite : attendre RefreshInterval laisserait
// l'etiquette absente un quart de seconde a l'apparition d'un coequipier.
Refresh();
}
void UPlayerNameplateComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// Un timer laisse en vie apres la destruction du composant rappellerait une
// methode sur un objet mort -- typiquement a la mort d'un joueur, ou son
// pawn est detruit.
if (UWorld* World = GetWorld())
{
World->GetTimerManager().ClearTimer(RefreshTimer);
}
Super::EndPlay(EndPlayReason);
}
APlayerState* UPlayerNameplateComponent::GetOwnerPlayerState() const
@@ -88,8 +68,33 @@ APlayerState* UPlayerNameplateComponent::GetOwnerPlayerState() const
return OwnerPawn ? OwnerPawn->GetPlayerState() : nullptr;
}
void UPlayerNameplateComponent::Refresh()
void UPlayerNameplateComponent::RefreshName()
{
// Le PlayerState arrive APRES le pawn chez un client : la premiere passe
// trouve donc un nom vide, et il faut continuer a regarder.
const APlayerState* State = GetOwnerPlayerState();
if (!State)
{
return;
}
const FString CurrentName = State->GetPlayerName();
if (CurrentName.IsEmpty() || CurrentName == LastShownName)
{
return;
}
if (UPlayerNameplateWidget* Nameplate = Cast<UPlayerNameplateWidget>(GetUserWidgetObject()))
{
Nameplate->SetPlayerName(FText::FromString(CurrentName));
LastShownName = CurrentName;
}
}
void UPlayerNameplateComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Le point de vue du joueur LOCAL, et non celui du pawn porteur : c'est de
// notre ecran qu'on parle. GetFirstLocalPlayerController et non
// GetFirstPlayerController -- en PIE a deux fenetres, le second rendrait le
@@ -101,41 +106,53 @@ void UPlayerNameplateComponent::Refresh()
return;
}
const float DistanceSquared = FVector::DistSquared(
const float Distance = FVector::Dist(
Viewer->PlayerCameraManager->GetCameraLocation(),
GetComponentLocation());
if (DistanceSquared > FMath::Square(MaxDrawDistance))
// Au-dela de la portee on eteint le composant lui-meme, pas seulement son
// opacite : un widget transparent continue d'etre projete et dessine.
if (Distance > MaxDrawDistance)
{
SetVisibility(false);
return;
}
// Le PlayerState arrive apres le pawn chez un client : tant qu'il manque,
// on reste invisible plutot que d'afficher une etiquette vide. La passe
// suivante le trouvera.
const APlayerState* State = GetOwnerPlayerState();
if (!State)
RefreshName();
// Sans nom a montrer, rien a montrer du tout.
if (LastShownName.IsEmpty())
{
SetVisibility(false);
return;
}
const FString CurrentName = State->GetPlayerName();
if (CurrentName.IsEmpty())
UPlayerNameplateWidget* Nameplate = Cast<UPlayerNameplateWidget>(GetUserWidgetObject());
if (!Nameplate)
{
SetVisibility(false);
return;
}
if (CurrentName != LastShownName)
// Echelle en 1/d : c'est la loi de la perspective, et c'est ce que l'oeil
// attend. A ReferenceDistance l'etiquette vaut exactement ce qui est dessine
// dans le WBP ; deux fois plus pres elle double, deux fois plus loin elle
// est de moitie -- borne des deux cotes.
const float SafeDistance = FMath::Max(Distance, 1.f);
const float Scale = FMath::Clamp(ReferenceDistance / SafeDistance, MinScale, MaxScale);
Nameplate->SetRenderScale(FVector2D(Scale, Scale));
// Fondu sur la derniere bande avant la portee maximale. FadeLength est
// borne a MaxDrawDistance : un fondu plus long que la portee elle-meme
// donnerait une etiquette jamais totalement opaque.
const float EffectiveFade = FMath::Clamp(FadeLength, 0.f, MaxDrawDistance);
float Opacity = 1.f;
if (EffectiveFade > KINDA_SMALL_NUMBER)
{
if (UPlayerNameplateWidget* Nameplate = Cast<UPlayerNameplateWidget>(GetUserWidgetObject()))
{
Nameplate->SetPlayerName(FText::FromString(CurrentName));
LastShownName = CurrentName;
}
const float FadeStart = MaxDrawDistance - EffectiveFade;
Opacity = FMath::Clamp(1.f - (Distance - FadeStart) / EffectiveFade, 0.f, 1.f);
}
Nameplate->SetRenderOpacity(Opacity);
SetVisibility(true);
}