(Feat) Customisation -- replication et persistance

Lot 3 : l'apparence survit desormais au respawn, au changement de map et au
relancement du jeu, et les trois autres joueurs la voient.

Elle vit sur ASurvivalPlayerState et pas sur le pawn : c'est le JOUEUR
qu'elle decrit, pas le corps. Le pawn meurt et reapparait, le PlayerState
non -- reapparaitre avec le visage de quelqu'un d'autre n'aurait aucun sens,
alors que perdre son inventaire au sol est la regle du jeu.

SubmitCharacterAppearance() sur AFpsPlayerController est le point d'entree
UNIQUE : il persiste ET annonce au serveur, jamais l'un sans l'autre.
Persister sans annoncer laisserait le joueur seul a se voir change, annoncer
sans persister le ferait repartir au visage par defaut au lancement suivant.
C'est par la que passent les commandes console, et par la que passera
l'ecran du lot 4.

La course pawn / PlayerState est resolue des DEUX cotes. Chez un client ils
arrivent par deux chemins de replication independants et rien ne dit lequel
gagne : OnRep_CharacterAppearance monte le personnage si le pawn est deja la,
AFpsPlayer::OnRep_PlayerState va chercher l'apparence si c'est l'inverse. Les
deux appellent la meme fonction idempotente. Chez l'hote aucun OnRep ne se
declenche, d'ou l'appel local dans SetCharacterAppearance -- sans lui, l'hote
serait le seul a ne pas voir son propre personnage.

Rien n'est valide cote PlayerState, deliberement : borner un index demande le
catalogue, que seul UCharacterAppearanceComponent connait, et celui-ci passe
deja tout ce qu'il monte par Catalog->Sanitize() sur chaque machine.

Piege de test note dans CLAUDE.md : en PIE a deux joueurs, les deux fenetres
lisent le meme .ini, donc les deux personnages demarrent identiques.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-08 19:45:01 +02:00
parent 368f27a417
commit 5970db0512
9 changed files with 337 additions and 10 deletions
+24 -5
View File
@@ -685,12 +685,31 @@ couleurs sur des MID). `DA_CharacterParts` couvre Body A ; **Body B reste à rem
`ABP_PlayerBody_B` et sans barbes ni moustaches — le pack n'en livre pas, et une liste vide se `ABP_PlayerBody_B` et sans barbes ni moustaches — le pack n'en livre pas, et une liste vide se
comporte déjà comme `NoPart`. comporte déjà comme `NoPart`.
Restent les lots 3 et 4 : **Le lot 3 est fait** : l'apparence vit sur `ASurvivalPlayerState` en `ReplicatedUsing` et se
persiste dans `USurvivalUserSettings`. Elle survit donc au respawn — le pawn est un nouvel acteur,
le PlayerState non — et au relancement du jeu.
- **Lot 3** — réplication par `ASurvivalPlayerState` en `ReplicatedUsing`, persistance dans - **`SubmitCharacterAppearance()` sur `AFpsPlayerController` est le point d'entrée UNIQUE.** Il
`USurvivalUserSettings` persiste *et* annonce au serveur, jamais l'un sans l'autre : persister sans annoncer laisserait
- **Lot 4** — l'écran : `ACharacterPreviewActor` dans `MenuScene`, caméra à cadrages interpolés, le joueur seul à se voir changé, annoncer sans persister le ferait repartir au visage par défaut
UI à gauche au lancement suivant. C'est par là que passent les commandes console, et par là que passera
l'écran du lot 4.
- **La course pawn / PlayerState se résout des deux côtés.** Chez un client ils arrivent par deux
chemins de réplication indépendants et rien ne dit lequel gagne : `OnRep_CharacterAppearance`
monte le personnage si le pawn est déjà là, `AFpsPlayer::OnRep_PlayerState` va chercher
l'apparence si c'est le pawn qui est arrivé le premier. Les deux appellent la même fonction
idempotente. Chez l'hôte, aucun `OnRep` ne se déclenche — c'est le `BeginPlay` du pawn qui monte,
et `SetCharacterAppearance` qui applique en local juste après avoir écrit la propriété.
- **Rien n'est validé côté PlayerState, et c'est délibéré** : borner un index demande le catalogue,
que seul `UCharacterAppearanceComponent` connaît — or il passe déjà tout ce qu'il monte par
`Catalog->Sanitize()`, sur chaque machine. Dupliquer la validation demanderait de donner le
catalogue au PlayerState pour un résultat identique.
- Piège de test : **en PIE à deux joueurs, les deux fenêtres lisent le même `.ini`**, donc les deux
personnages démarrent identiques. Il faut changer l'un des deux à la console pour vérifier que la
réplication marche.
Reste le **lot 4** — l'écran : `ACharacterPreviewActor` dans `MenuScene`, caméra à cadrages
interpolés, UI à gauche. Et le remplissage de **Body B** dans `DA_CharacterParts`.
#### Trois pièges du lot 2, aucun visible depuis le code #### Trois pièges du lot 2, aucun visible depuis le code
@@ -17,6 +17,7 @@
#include "InputActionValue.h" #include "InputActionValue.h"
#include "CharacterAppearanceComponent.h" #include "CharacterAppearanceComponent.h"
#include "CraftingComponent.h" #include "CraftingComponent.h"
#include "FpsPlayerController.h"
#include "HeadBobComponent.h" #include "HeadBobComponent.h"
#include "InteractionComponent.h" #include "InteractionComponent.h"
#include "InventoryComponent.h" #include "InventoryComponent.h"
@@ -24,6 +25,7 @@
#include "Net/UnrealNetwork.h" #include "Net/UnrealNetwork.h"
#include "PickupItem.h" #include "PickupItem.h"
#include "SurvivalGameInstance.h" #include "SurvivalGameInstance.h"
#include "SurvivalPlayerState.h"
#include "SurvivalStatsComponent.h" #include "SurvivalStatsComponent.h"
#include "SurvivalUserSettings.h" #include "SurvivalUserSettings.h"
@@ -134,6 +136,12 @@ void AFpsPlayer::BeginPlay()
SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath); SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath);
// Chez l'hote et en solo, le PlayerState est deja la a cet instant :
// OnRep_PlayerState ne s'y declenchant jamais, c'est ce seul appel qui monte
// le personnage. Chez un client il rend souvent la main sans rien faire,
// l'OnRep prenant le relais -- les deux chemins sont idempotents.
RefreshAppearanceFromPlayerState();
// Sans cet abonnement, un objet fabrique alors que l'inventaire est plein // Sans cet abonnement, un objet fabrique alors que l'inventaire est plein
// serait purement et simplement perdu. // serait purement et simplement perdu.
CraftingComponent->OnCraftOverflow.AddDynamic(this, &AFpsPlayer::HandleCraftOverflow); CraftingComponent->OnCraftOverflow.AddDynamic(this, &AFpsPlayer::HandleCraftOverflow);
@@ -362,6 +370,28 @@ void AFpsPlayer::AddCrouchTilt(float Direction)
CrouchTiltSpring.AddImpulse(Direction * UE_EULERS_NUMBER * Omega); CrouchTiltSpring.AddImpulse(Direction * UE_EULERS_NUMBER * Omega);
} }
void AFpsPlayer::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
RefreshAppearanceFromPlayerState();
}
void AFpsPlayer::RefreshAppearanceFromPlayerState()
{
if (!AppearanceComponent)
{
return;
}
// Absent n'est pas une erreur : c'est le cas normal quand le pawn gagne la
// course. L'apparence viendra par OnRep_CharacterAppearance.
if (const ASurvivalPlayerState* State = GetPlayerState<ASurvivalPlayerState>())
{
AppearanceComponent->ApplyAppearance(State->GetCharacterAppearance());
}
}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Commandes console d'apparence -- de simples delegations. Voir le commentaire // Commandes console d'apparence -- de simples delegations. Voir le commentaire
// de FpsPlayer.h : les UFUNCTION(Exec) d'un composant ne sont jamais routees, // de FpsPlayer.h : les UFUNCTION(Exec) d'un composant ne sont jamais routees,
@@ -373,6 +403,7 @@ void AFpsPlayer::EmberPart(const FString& Part, int32 Index)
if (AppearanceComponent) if (AppearanceComponent)
{ {
AppearanceComponent->ApplyPartByName(Part, Index); AppearanceComponent->ApplyPartByName(Part, Index);
SubmitCurrentAppearance();
} }
} }
@@ -381,6 +412,19 @@ void AFpsPlayer::EmberColor(const FString& Part, int32 ColorIndex)
if (AppearanceComponent) if (AppearanceComponent)
{ {
AppearanceComponent->ApplyColorByName(Part, ColorIndex); AppearanceComponent->ApplyColorByName(Part, ColorIndex);
SubmitCurrentAppearance();
}
}
void AFpsPlayer::SubmitCurrentAppearance()
{
// Applique en local D'ABORD (c'est deja fait par l'appelant), soumis
// ensuite : le contraire ferait attendre l'aller-retour reseau pour voir
// son propre changement, ce qui donnerait un reglage qui repond en retard.
// C'est le meme ordre que Server_SetSprinting.
if (AFpsPlayerController* FpsController = Cast<AFpsPlayerController>(GetController()))
{
FpsController->SubmitCharacterAppearance(AppearanceComponent->GetAppearance());
} }
} }
@@ -9,7 +9,9 @@
#include "FpsPlayer.h" #include "FpsPlayer.h"
#include "GameFramework/GameModeBase.h" #include "GameFramework/GameModeBase.h"
#include "SurvivalGameInstance.h" #include "SurvivalGameInstance.h"
#include "SurvivalPlayerState.h"
#include "SurvivalStatsComponent.h" #include "SurvivalStatsComponent.h"
#include "SurvivalUserSettings.h"
#include "HotbarWidget.h" #include "HotbarWidget.h"
#include "InputActionValue.h" #include "InputActionValue.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
@@ -111,6 +113,12 @@ void AFpsPlayerController::BeginPlay()
Server_SetPlayerNickname(Nickname); Server_SetPlayerNickname(Nickname);
} }
} }
// Dans la foulee du pseudo : les deux repondent au meme constat, le
// serveur ne peut connaitre ni l'un ni l'autre. Ils partent donc
// ensemble, au seul moment ou l'on est certain que le PlayerState local
// existe.
AnnounceCharacterAppearance();
} }
// Le delegue se declenche a la fin de CHAQUE fondu : c'est // Le delegue se declenche a la fin de CHAQUE fondu : c'est
@@ -1094,6 +1102,47 @@ void AFpsPlayerController::Server_SetPlayerNickname_Implementation(const FString
UE_LOG(LogTemp, Log, TEXT("AFpsPlayerController : pseudo [%s]."), *State->GetPlayerName()); UE_LOG(LogTemp, Log, TEXT("AFpsPlayerController : pseudo [%s]."), *State->GetPlayerName());
} }
void AFpsPlayerController::SubmitCharacterAppearance(const FCharacterAppearance& InAppearance)
{
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->SetCharacterAppearance(InAppearance);
// Ecrit tout de suite, contrairement aux reglages de l'ecran d'options
// qui attendent sa fermeture : un changement d'apparence n'a pas
// d'ecran a fermer quand il vient de la console, et le perdre a la
// sortie du jeu serait le plus sur moyen de croire que rien ne marche.
Settings->SaveSettings();
}
// Meme raccourci que pour le pseudo : l'hote est deja l'autorite, passer
// par la RPC marcherait mais ferait un aller-retour pour rien.
if (HasAuthority())
{
Server_SetCharacterAppearance_Implementation(InAppearance);
}
else
{
Server_SetCharacterAppearance(InAppearance);
}
}
void AFpsPlayerController::AnnounceCharacterAppearance()
{
if (const USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
SubmitCharacterAppearance(Settings->GetCharacterAppearance());
}
}
void AFpsPlayerController::Server_SetCharacterAppearance_Implementation(const FCharacterAppearance& InAppearance)
{
if (ASurvivalPlayerState* State = GetPlayerState<ASurvivalPlayerState>())
{
State->SetCharacterAppearance(InAppearance);
}
}
void AFpsPlayerController::ShowInviteFriends() void AFpsPlayerController::ShowInviteFriends()
{ {
if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr) if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
@@ -0,0 +1,74 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "SurvivalPlayerState.h"
#include "CharacterAppearanceComponent.h"
#include "FpsPlayer.h"
#include "Net/UnrealNetwork.h"
void ASurvivalPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Aucune condition : COND_OwnerOnly serait exactement le contraire de ce
// qu'on veut. L'interet d'une apparence, c'est que les trois autres la
// voient ; celle du proprietaire est la seule dont il pourrait se passer,
// puisqu'il ne voit pas son propre corps en vue premiere personne.
DOREPLIFETIME(ASurvivalPlayerState, CharacterAppearance);
}
void ASurvivalPlayerState::SetCharacterAppearance(const FCharacterAppearance& InAppearance)
{
if (!HasAuthority())
{
UE_LOG(LogTemp, Warning, TEXT("ASurvivalPlayerState : SetCharacterAppearance appelee sans autorite, ignoree."));
return;
}
// L'apparence n'est PAS validee ici, et c'est deliberé : borner un index
// demande le catalogue, que seul UCharacterAppearanceComponent connait. Or
// celui-ci passe deja tout ce qu'il monte par Catalog->Sanitize(), donc un
// index aberrant venu du reseau est corrige au montage, sur chaque machine.
// Dupliquer la validation ici demanderait de donner le catalogue au
// PlayerState pour un resultat identique -- et cette coop entre amis n'a de
// toute facon aucune anti-triche a faire respecter.
CharacterAppearance = InAppearance;
// Un OnRep ne se declenche jamais sur la machine qui ecrit la propriete.
// Chez l'hote, qui est serveur ET client local, sans cette ligne son propre
// personnage resterait au visage par defaut pendant que les autres le
// verraient correctement.
ApplyAppearanceToPawn();
}
void ASurvivalPlayerState::OnRep_CharacterAppearance()
{
ApplyAppearanceToPawn();
}
void ASurvivalPlayerState::ApplyAppearanceToPawn()
{
// Le pawn peut manquer, et ce n'est pas une erreur : chez un client, le
// PlayerState et le pawn arrivent par deux chemins de replication
// independants, dans un ordre qu'on ne controle pas. Quand c'est le
// PlayerState qui gagne, on ne fait rien -- c'est le BeginPlay du pawn qui
// viendra chercher l'apparence a son tour.
if (AFpsPlayer* Player = Cast<AFpsPlayer>(GetPawn()))
{
if (UCharacterAppearanceComponent* Appearance = Player->GetAppearanceComponent())
{
Appearance->ApplyAppearance(CharacterAppearance);
}
}
}
void ASurvivalPlayerState::CopyProperties(APlayerState* PlayerState)
{
Super::CopyProperties(PlayerState);
if (ASurvivalPlayerState* SurvivalState = Cast<ASurvivalPlayerState>(PlayerState))
{
SurvivalState->CharacterAppearance = CharacterAppearance;
}
}
@@ -344,3 +344,8 @@ void USurvivalUserSettings::SetPlayInBackground(bool bValue)
{ {
bPlayInBackground = bValue; bPlayInBackground = bValue;
} }
void USurvivalUserSettings::SetCharacterAppearance(const FCharacterAppearance& InAppearance)
{
CharacterAppearance = InAppearance;
}
+25
View File
@@ -154,6 +154,16 @@ public:
UFUNCTION(Exec, Category = "Apparence") UFUNCTION(Exec, Category = "Apparence")
void EmberAppearanceDump(); void EmberAppearanceDump();
private:
/**
* Persiste l'apparence du composant et l'annonce au serveur, via le
* controller. Appelee apres chaque commande console, pour que le
* changement se voie chez les autres et survive au relancement.
*/
void SubmitCurrentAppearance();
public:
/** Sert a l'UI pour afficher la vraie touche mappee, plutot qu'un "E" ecrit en dur. */ /** Sert a l'UI pour afficher la vraie touche mappee, plutot qu'un "E" ecrit en dur. */
UFUNCTION(BlueprintPure, Category = "Input") UFUNCTION(BlueprintPure, Category = "Input")
const UInputAction* GetInteractAction() const { return InteractAction; } const UInputAction* GetInteractAction() const { return InteractAction; }
@@ -230,10 +240,25 @@ public:
*/ */
virtual void CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult) override; virtual void CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult) override;
/**
* L'autre cote de la course avec le PlayerState.
*
* Chez un client, le pawn et le PlayerState arrivent par deux chemins de
* replication independants : tantot l'apparence est la avant le corps --
* ASurvivalPlayerState::OnRep_CharacterAppearance la pose alors lui-meme --
* tantot c'est l'inverse, et c'est ici qu'on va la chercher. Ni l'un ni
* l'autre ne peut supposer avoir gagne, donc les deux appellent la meme
* fonction idempotente.
*/
virtual void OnRep_PlayerState() override;
protected: protected:
// Called when the game starts or when spawned // Called when the game starts or when spawned
virtual void BeginPlay() override; virtual void BeginPlay() override;
/** Lit l'apparence sur le PlayerState et la monte, s'il est deja arrive. */
void RefreshAppearanceFromPlayerState();
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
// Appelés par le CharacterMovement quand la capsule change de taille (accroupi) // Appelés par le CharacterMovement quand la capsule change de taille (accroupi)
@@ -6,6 +6,7 @@
// ESlateVisibility n'est que declare en avant par le moteur : il faut le type // ESlateVisibility n'est que declare en avant par le moteur : il faut le type
// complet pour en stocker une valeur par defaut. Meme raison pour // complet pour en stocker une valeur par defaut. Meme raison pour
// ECraftingStation, stocke en membre. // ECraftingStation, stocke en membre.
#include "CharacterAppearanceTypes.h"
#include "Components/SlateWrapperTypes.h" #include "Components/SlateWrapperTypes.h"
#include "CraftingRecipeDataAsset.h" #include "CraftingRecipeDataAsset.h"
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
@@ -115,6 +116,19 @@ public:
UFUNCTION(BlueprintCallable, Category = "Coop") UFUNCTION(BlueprintCallable, Category = "Coop")
void ShowInviteFriends(); void ShowInviteFriends();
/**
* Le point d'entree unique d'un changement d'apparence : persiste dans les
* reglages du joueur ET l'annonce au serveur.
*
* Les deux ensemble et jamais l'un sans l'autre -- persister sans annoncer
* laisserait le joueur seul a se voir change, annoncer sans persister le
* ferait repartir au visage par defaut au prochain lancement. C'est par ici
* que passent les commandes console, et par ici que passera l'ecran de
* customisation.
*/
UFUNCTION(BlueprintCallable, Category = "Apparence")
void SubmitCharacterAppearance(const FCharacterAppearance& InAppearance);
/** Faux quand Steam est absent ou qu'aucune session n'a pu etre creee. */ /** Faux quand Steam est absent ou qu'aucune session n'a pu etre creee. */
UFUNCTION(BlueprintPure, Category = "Coop") UFUNCTION(BlueprintPure, Category = "Coop")
bool CanInviteFriends() const; bool CanInviteFriends() const;
@@ -572,6 +586,20 @@ private:
UFUNCTION(Server, Reliable) UFUNCTION(Server, Reliable)
void Server_SetPlayerNickname(const FString& Nickname); void Server_SetPlayerNickname(const FString& Nickname);
/**
* Meme chemin que le pseudo, et pour la meme raison : le serveur ne peut
* pas deviner l'apparence choisie par un client, elle vit dans le fichier
* de reglages de SA machine. C'est donc lui qui l'annonce.
*
* Treize octets envoyes une fois a la connexion -- l'apparence ne change
* pas en cours de partie, l'ecran de customisation vit dans le menu.
*/
UFUNCTION(Server, Reliable)
void Server_SetCharacterAppearance(const FCharacterAppearance& InAppearance);
/** Lit l'apparence persistee et l'envoie au serveur. Appelee au BeginPlay. */
void AnnounceCharacterAppearance();
FTimerHandle RespawnTimerHandle; FTimerHandle RespawnTimerHandle;
/** Le fondu d'arrivée attend le pawn. */ /** Le fondu d'arrivée attend le pawn. */
@@ -4,20 +4,70 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/PlayerState.h" #include "GameFramework/PlayerState.h"
#include "CharacterAppearanceTypes.h"
#include "SurvivalPlayerState.generated.h" #include "SurvivalPlayerState.generated.h"
/** /**
* Ce que les AUTRES joueurs savent de toi. * Ce que les AUTRES joueurs savent de toi : le pseudo, porte par APlayerState
* * lui-meme, et l'apparence du personnage.
* Vide pour l'instant. Y mettra : pseudo, couleur, temps de survie, nombre de
* morts -- tout ce qu'un coequipier doit pouvoir lire sur ton compte.
* *
* La regle de partage avec le pawn est simple : le PlayerState survit a la mort * La regle de partage avec le pawn est simple : le PlayerState survit a la mort
* du pawn, le pawn non. L'inventaire et les jauges restent donc sur le pawn, * du pawn, le pawn non. L'inventaire et les jauges restent donc sur le pawn,
* puisque la regle du jeu veut qu'on perde tout au sol en mourant. * puisque la regle du jeu veut qu'on perde tout au sol en mourant -- alors que
* reapparaitre avec le visage de quelqu'un d'autre n'aurait aucun sens.
*
* C'est exactement pour cette raison que l'apparence est ici et non sur
* AFpsPlayer : elle appartient au JOUEUR, pas au corps.
*/ */
UCLASS() UCLASS()
class SURVIVAL_PROJET_API ASurvivalPlayerState : public APlayerState class SURVIVAL_PROJET_API ASurvivalPlayerState : public APlayerState
{ {
GENERATED_BODY() GENERATED_BODY()
public:
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
UFUNCTION(BlueprintPure, Category = "Apparence")
const FCharacterAppearance& GetCharacterAppearance() const { return CharacterAppearance; }
/**
* A n'appeler que sur le SERVEUR : c'est lui qui fait autorite sur ce que
* les autres voient.
*
* Applique aussi en local dans la foulee, parce qu'un OnRep ne se declenche
* jamais sur la machine qui ecrit. Sans ce second appel, l'hote serait le
* seul a ne pas voir sa propre apparence -- le genre de bug qui ne se
* remarque qu'en testant a deux, et du mauvais cote.
*/
void SetCharacterAppearance(const FCharacterAppearance& InAppearance);
/**
* Pousse l'apparence sur le pawn possede, s'il existe deja.
*
* Idempotente et appelee des DEUX cotes de la course : par l'OnRep quand
* l'apparence arrive avant le pawn, et par le pawn lui-meme a son BeginPlay
* quand c'est l'inverse. On ne sait pas lequel des deux gagne -- chez un
* client le pawn et le PlayerState arrivent par deux chemins de replication
* independants -- donc les deux doivent savoir se debrouiller seuls.
*/
void ApplyAppearanceToPawn();
protected:
/**
* Repliquee a TOUT LE MONDE et pas au seul proprietaire : c'est justement
* ce que les autres doivent voir. Treize octets par joueur, envoyes une
* fois -- l'apparence ne change pas en cours de partie.
*/
UPROPERTY(ReplicatedUsing = OnRep_CharacterAppearance)
FCharacterAppearance CharacterAppearance;
UFUNCTION()
void OnRep_CharacterAppearance();
/**
* Le moteur engendre un NOUVEAU PlayerState a chaque voyage de map, et
* recopie l'ancien par ici. Sans cette surcharge, changer de carte remettrait
* tout le monde au visage par defaut.
*/
virtual void CopyProperties(APlayerState* PlayerState) override;
}; };
@@ -4,6 +4,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/GameUserSettings.h" #include "GameFramework/GameUserSettings.h"
#include "CharacterAppearanceTypes.h"
#include "SurvivalUserSettings.generated.h" #include "SurvivalUserSettings.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalSettingsApplied); DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalSettingsApplied);
@@ -286,7 +287,39 @@ public:
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio") UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
void SetPlayInBackground(bool bValue); void SetPlayInBackground(bool bValue);
// ------------------------------------------------------------------
// Apparence du personnage
//
// Ici et pas dans une sauvegarde de partie : l'apparence appartient au
// JOUEUR et pas a une partie donnee, elle doit le suivre d'une session a
// l'autre. C'est aussi ce qui donne un visage a qui arrive par invitation
// Steam sans passer par l'ecran de customisation.
// ------------------------------------------------------------------
UFUNCTION(BlueprintPure, Category = "Reglages|Apparence")
const FCharacterAppearance& GetCharacterAppearance() const { return CharacterAppearance; }
/**
* N'ecrit rien sur le disque : la sauvegarde a lieu a la fermeture de
* l'ecran, comme pour tous les autres reglages. Ne diffuse pas non plus
* OnSurvivalSettingsApplied -- le pawn ne relit pas son apparence dans les
* reglages, il la tient du PlayerState.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages|Apparence")
void SetCharacterAppearance(const FCharacterAppearance& InAppearance);
private: private:
/**
* Serialisee telle quelle dans le .ini, sous la forme
* `CharacterAppearance=(BodyType=BodyA,HeadIndex=0,...)`.
*
* C'est le dividende de la struct d'index : une apparence faite de
* TSoftObjectPtr aurait donne une douzaine de chemins d'assets a ecrire, et
* une seule renommee d'asset aurait suffi a la casser silencieusement.
*/
UPROPERTY(config)
FCharacterAppearance CharacterAppearance;
/** Echelle lisible (5 a 100), convertie a la lecture. Jamais brute. */ /** Echelle lisible (5 a 100), convertie a la lecture. Jamais brute. */
UPROPERTY(config) UPROPERTY(config)
float LookSensitivity; float LookSensitivity;