(Feat) Player name tag
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,6 @@
|
|||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "GameFramework/PlayerState.h"
|
#include "GameFramework/PlayerState.h"
|
||||||
#include "PlayerNameplateWidget.h"
|
#include "PlayerNameplateWidget.h"
|
||||||
#include "TimerManager.h"
|
|
||||||
|
|
||||||
UPlayerNameplateComponent::UPlayerNameplateComponent()
|
UPlayerNameplateComponent::UPlayerNameplateComponent()
|
||||||
{
|
{
|
||||||
@@ -31,10 +30,11 @@ UPlayerNameplateComponent::UPlayerNameplateComponent()
|
|||||||
// du personnage pouvant changer.
|
// du personnage pouvant changer.
|
||||||
SetRelativeLocation(FVector(0.f, 0.f, 100.f));
|
SetRelativeLocation(FVector(0.f, 0.f, 100.f));
|
||||||
|
|
||||||
// Cachee au depart : la premiere passe de Refresh decidera. Sans ca, une
|
// Cachee au depart : la premiere passe decidera. Sans ca, une etiquette
|
||||||
// etiquette sans nom clignote pendant la frame qui precede l'arrivee du
|
// sans nom clignote pendant la frame qui precede l'arrivee du PlayerState.
|
||||||
// PlayerState.
|
|
||||||
SetVisibility(false);
|
SetVisibility(false);
|
||||||
|
|
||||||
|
PrimaryComponentTick.bCanEverTick = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPlayerNameplateComponent::BeginPlay()
|
void UPlayerNameplateComponent::BeginPlay()
|
||||||
@@ -46,40 +46,20 @@ void UPlayerNameplateComponent::BeginPlay()
|
|||||||
// notre serveur d'ecoute, qui n'est pas dans ce mode.
|
// notre serveur d'ecoute, qui n'est pas dans ce mode.
|
||||||
if (GetNetMode() == NM_DedicatedServer)
|
if (GetNetMode() == NM_DedicatedServer)
|
||||||
{
|
{
|
||||||
|
SetComponentTickEnabled(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
|
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
|
||||||
|
|
||||||
// Son propre pseudo : on eteint definitivement, sans timer. Ce n'est pas un
|
// Son propre pseudo : on eteint definitivement, tick compris. Ce n'est pas
|
||||||
// cas a reevaluer -- un pawn ne change pas de proprietaire en cours de vie,
|
// un cas a reevaluer -- un pawn ne change pas de proprietaire en cours de
|
||||||
// et apres un respawn c'est un NOUVEL acteur qui repassera par ici.
|
// vie, et apres un respawn c'est un NOUVEL acteur qui repassera par ici.
|
||||||
if (bHideOnOwningPlayer && OwnerPawn && OwnerPawn->IsLocallyControlled())
|
if (bHideOnOwningPlayer && OwnerPawn && OwnerPawn->IsLocallyControlled())
|
||||||
{
|
{
|
||||||
SetVisibility(false);
|
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
|
APlayerState* UPlayerNameplateComponent::GetOwnerPlayerState() const
|
||||||
@@ -88,8 +68,33 @@ APlayerState* UPlayerNameplateComponent::GetOwnerPlayerState() const
|
|||||||
return OwnerPawn ? OwnerPawn->GetPlayerState() : nullptr;
|
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
|
// Le point de vue du joueur LOCAL, et non celui du pawn porteur : c'est de
|
||||||
// notre ecran qu'on parle. GetFirstLocalPlayerController et non
|
// notre ecran qu'on parle. GetFirstLocalPlayerController et non
|
||||||
// GetFirstPlayerController -- en PIE a deux fenetres, le second rendrait le
|
// GetFirstPlayerController -- en PIE a deux fenetres, le second rendrait le
|
||||||
@@ -101,41 +106,53 @@ void UPlayerNameplateComponent::Refresh()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float DistanceSquared = FVector::DistSquared(
|
const float Distance = FVector::Dist(
|
||||||
Viewer->PlayerCameraManager->GetCameraLocation(),
|
Viewer->PlayerCameraManager->GetCameraLocation(),
|
||||||
GetComponentLocation());
|
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);
|
SetVisibility(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le PlayerState arrive apres le pawn chez un client : tant qu'il manque,
|
RefreshName();
|
||||||
// on reste invisible plutot que d'afficher une etiquette vide. La passe
|
|
||||||
// suivante le trouvera.
|
// Sans nom a montrer, rien a montrer du tout.
|
||||||
const APlayerState* State = GetOwnerPlayerState();
|
if (LastShownName.IsEmpty())
|
||||||
if (!State)
|
|
||||||
{
|
{
|
||||||
SetVisibility(false);
|
SetVisibility(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FString CurrentName = State->GetPlayerName();
|
UPlayerNameplateWidget* Nameplate = Cast<UPlayerNameplateWidget>(GetUserWidgetObject());
|
||||||
if (CurrentName.IsEmpty())
|
if (!Nameplate)
|
||||||
{
|
{
|
||||||
SetVisibility(false);
|
SetVisibility(false);
|
||||||
return;
|
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()))
|
const float FadeStart = MaxDrawDistance - EffectiveFade;
|
||||||
{
|
Opacity = FMath::Clamp(1.f - (Distance - FadeStart) / EffectiveFade, 0.f, 1.f);
|
||||||
Nameplate->SetPlayerName(FText::FromString(CurrentName));
|
|
||||||
LastShownName = CurrentName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Nameplate->SetRenderOpacity(Opacity);
|
||||||
|
|
||||||
SetVisibility(true);
|
SetVisibility(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,16 @@ class APlayerState;
|
|||||||
* reorienter soi-meme a chaque image, et elle serait occultee par le decor --
|
* reorienter soi-meme a chaque image, et elle serait occultee par le decor --
|
||||||
* un pseudo qui disparait derriere un caillou n'aide personne.
|
* un pseudo qui disparait derriere un caillou n'aide personne.
|
||||||
*
|
*
|
||||||
* Contrepartie assumee de l'espace ecran : la taille ne diminue pas avec la
|
* L'espace ecran n'applique AUCUNE echelle de distance -- verifie dans
|
||||||
* distance. C'est le comportement des jeux coop du genre, et c'est justement
|
* SWorldWidgetScreenLayer::Tick, qui ne fait que projeter la position. Le
|
||||||
* pourquoi une distance de disparition est indispensable -- sans elle, quatre
|
* pseudo garderait donc une taille en pixels constante pendant que le
|
||||||
* pseudos resteraient lisibles a l'autre bout de la carte.
|
* personnage, lui, retrecit : l'oeil lit ca comme une etiquette qui GROSSIT en
|
||||||
|
* s'eloignant. C'est pourquoi l'echelle est calculee ici, a la main, en 1/d
|
||||||
|
* comme le ferait une vraie perspective.
|
||||||
|
*
|
||||||
|
* L'opacite suit le meme chemin : une etiquette qui s'eteint d'un coup a la
|
||||||
|
* distance limite se remarque bien plus qu'un fondu, alors qu'elle est censee
|
||||||
|
* ne pas se faire remarquer du tout.
|
||||||
*
|
*
|
||||||
* Le composant ne replique RIEN : le pseudo vit deja sur le PlayerState, qui
|
* Le composant ne replique RIEN : le pseudo vit deja sur le PlayerState, qui
|
||||||
* est replique par le moteur. Ajouter une propriete ici enverrait deux fois la
|
* est replique par le moteur. Ajouter une propriete ici enverrait deux fois la
|
||||||
@@ -35,7 +41,20 @@ public:
|
|||||||
UPlayerNameplateComponent();
|
UPlayerNameplateComponent();
|
||||||
|
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
|
||||||
|
/**
|
||||||
|
* Un Tick ici, contrairement au reste du projet.
|
||||||
|
*
|
||||||
|
* La regle "jamais de Tick pour lire un etat" vise les widgets qui
|
||||||
|
* interrogent une valeur au lieu de s'abonner a un delegue. Ce n'est pas le
|
||||||
|
* cas : on interpole en continu deux grandeurs geometriques -- echelle et
|
||||||
|
* opacite -- qui changent a chaque pas du joueur. Sur un timer a 0,25 s
|
||||||
|
* l'etiquette grossirait par paliers visibles.
|
||||||
|
*
|
||||||
|
* Le cout est nul en pratique : UWidgetComponent tick deja en espace ecran
|
||||||
|
* pour se projeter, on ajoute une distance et deux setters.
|
||||||
|
*/
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/**
|
/**
|
||||||
@@ -47,15 +66,34 @@ protected:
|
|||||||
float MaxDrawDistance = 2500.f;
|
float MaxDrawDistance = 2500.f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Periode de reevaluation, en secondes.
|
* Distance a laquelle l'etiquette est a sa taille de reference, c'est-a-dire
|
||||||
*
|
* exactement celle dessinee dans le WBP. Plus pres elle grossit, plus loin
|
||||||
* Un timer et pas un Tick : la distance a la camera n'a aucune raison
|
* elle retrecit -- en 1/d, comme une vraie perspective.
|
||||||
* d'etre recalculee 120 fois par seconde pour une etiquette qui apparait ou
|
|
||||||
* disparait une fois toutes les dix secondes. Quatre joueurs a 120 ips
|
|
||||||
* feraient 480 calculs de distance par seconde pour rien.
|
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate", meta = (ClampMin = "0.05", Units = "s"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate|Echelle", meta = (ClampMin = "50", Units = "cm"))
|
||||||
float RefreshInterval = 0.25f;
|
float ReferenceDistance = 600.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bornes de l'echelle. Le minimum evite un pseudo reduit a une bouillie de
|
||||||
|
* deux pixels ; le maximum evite qu'un coequipier colle a la camera ne
|
||||||
|
* remplisse l'ecran de son nom.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate|Echelle", meta = (ClampMin = "0.05"))
|
||||||
|
float MinScale = 0.4f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate|Echelle", meta = (ClampMin = "0.1"))
|
||||||
|
float MaxScale = 1.4f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Longueur du fondu, comptee EN DECA de MaxDrawDistance : l'opacite passe de
|
||||||
|
* 1 a 0 sur cette bande, puis l'etiquette est carrement eteinte.
|
||||||
|
*
|
||||||
|
* Exprimee comme une longueur et non comme une distance de depart : une
|
||||||
|
* distance de depart superieure a MaxDrawDistance serait un reglage invalide
|
||||||
|
* silencieux, alors qu'une longueur se borne toute seule.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate|Fondu", meta = (ClampMin = "0", Units = "cm"))
|
||||||
|
float FadeLength = 700.f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cacher l'etiquette de son PROPRE personnage.
|
* Cacher l'etiquette de son PROPRE personnage.
|
||||||
@@ -68,13 +106,11 @@ protected:
|
|||||||
bool bHideOnOwningPlayer = true;
|
bool bHideOnOwningPlayer = true;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/** Distance a la camera locale, puis mise a jour du texte si besoin. */
|
|
||||||
void Refresh();
|
|
||||||
|
|
||||||
/** Le PlayerState du pawn porteur, ou nullptr tant qu'il n'est pas arrive. */
|
/** Le PlayerState du pawn porteur, ou nullptr tant qu'il n'est pas arrive. */
|
||||||
APlayerState* GetOwnerPlayerState() const;
|
APlayerState* GetOwnerPlayerState() const;
|
||||||
|
|
||||||
FTimerHandle RefreshTimer;
|
/** Recopie le pseudo dans le widget, uniquement s'il a change. */
|
||||||
|
void RefreshName();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dernier pseudo reellement ecrit dans le widget.
|
* Dernier pseudo reellement ecrit dans le widget.
|
||||||
|
|||||||
Reference in New Issue
Block a user