diff --git a/Content/Game/Player/BP_FpsPlayer.uasset b/Content/Game/Player/BP_FpsPlayer.uasset index 6bc6a9f..6c9bd21 100644 --- a/Content/Game/Player/BP_FpsPlayer.uasset +++ b/Content/Game/Player/BP_FpsPlayer.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55afc9d979b9d6c147744e4d721e808f0811b120bfb4a95040da9c7a5cb203c5 -size 28321 +oid sha256:d71bb277d0f226b071bbd5d2bdb1c3802225d9b233ecb2250c438ec6a97bf8c7 +size 29950 diff --git a/Content/Game/UI/Player/WBP_PlayerNameplateWBP_PlayerNameplate.uasset b/Content/Game/UI/Player/WBP_PlayerNameplateWBP_PlayerNameplate.uasset new file mode 100644 index 0000000..95fcce0 --- /dev/null +++ b/Content/Game/UI/Player/WBP_PlayerNameplateWBP_PlayerNameplate.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b62be648f7d255ed1dee8ee5194a200c668d8f0c5bb305216dca23f706596847 +size 25383 diff --git a/Source/Survival_projet/Private/PlayerNameplateComponent.cpp b/Source/Survival_projet/Private/PlayerNameplateComponent.cpp index a462f7c..b3bf8d2 100644 --- a/Source/Survival_projet/Private/PlayerNameplateComponent.cpp +++ b/Source/Survival_projet/Private/PlayerNameplateComponent.cpp @@ -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(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(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(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(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); } diff --git a/Source/Survival_projet/Public/PlayerNameplateComponent.h b/Source/Survival_projet/Public/PlayerNameplateComponent.h index 21a6bf8..5d5a3aa 100644 --- a/Source/Survival_projet/Public/PlayerNameplateComponent.h +++ b/Source/Survival_projet/Public/PlayerNameplateComponent.h @@ -17,10 +17,16 @@ class APlayerState; * reorienter soi-meme a chaque image, et elle serait occultee par le decor -- * un pseudo qui disparait derriere un caillou n'aide personne. * - * Contrepartie assumee de l'espace ecran : la taille ne diminue pas avec la - * distance. C'est le comportement des jeux coop du genre, et c'est justement - * pourquoi une distance de disparition est indispensable -- sans elle, quatre - * pseudos resteraient lisibles a l'autre bout de la carte. + * L'espace ecran n'applique AUCUNE echelle de distance -- verifie dans + * SWorldWidgetScreenLayer::Tick, qui ne fait que projeter la position. Le + * pseudo garderait donc une taille en pixels constante pendant que le + * 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 * est replique par le moteur. Ajouter une propriete ici enverrait deux fois la @@ -35,7 +41,20 @@ public: UPlayerNameplateComponent(); 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: /** @@ -47,15 +66,34 @@ protected: float MaxDrawDistance = 2500.f; /** - * Periode de reevaluation, en secondes. - * - * Un timer et pas un Tick : la distance a la camera n'a aucune raison - * 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. + * 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 + * elle retrecit -- en 1/d, comme une vraie perspective. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate", meta = (ClampMin = "0.05", Units = "s")) - float RefreshInterval = 0.25f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate|Echelle", meta = (ClampMin = "50", Units = "cm")) + 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. @@ -68,13 +106,11 @@ protected: bool bHideOnOwningPlayer = true; 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. */ 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.