(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:
@@ -154,6 +154,16 @@ public:
|
||||
UFUNCTION(Exec, Category = "Apparence")
|
||||
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. */
|
||||
UFUNCTION(BlueprintPure, Category = "Input")
|
||||
const UInputAction* GetInteractAction() const { return InteractAction; }
|
||||
@@ -230,10 +240,25 @@ public:
|
||||
*/
|
||||
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:
|
||||
// Called when the game starts or when spawned
|
||||
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;
|
||||
|
||||
// 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
|
||||
// complet pour en stocker une valeur par defaut. Meme raison pour
|
||||
// ECraftingStation, stocke en membre.
|
||||
#include "CharacterAppearanceTypes.h"
|
||||
#include "Components/SlateWrapperTypes.h"
|
||||
#include "CraftingRecipeDataAsset.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
@@ -115,6 +116,19 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Coop")
|
||||
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. */
|
||||
UFUNCTION(BlueprintPure, Category = "Coop")
|
||||
bool CanInviteFriends() const;
|
||||
@@ -572,6 +586,20 @@ private:
|
||||
UFUNCTION(Server, Reliable)
|
||||
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;
|
||||
|
||||
/** Le fondu d'arrivée attend le pawn. */
|
||||
|
||||
@@ -4,20 +4,70 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerState.h"
|
||||
#include "CharacterAppearanceTypes.h"
|
||||
#include "SurvivalPlayerState.generated.h"
|
||||
|
||||
/**
|
||||
* Ce que les AUTRES joueurs savent de toi.
|
||||
*
|
||||
* 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.
|
||||
* Ce que les AUTRES joueurs savent de toi : le pseudo, porte par APlayerState
|
||||
* lui-meme, et l'apparence du personnage.
|
||||
*
|
||||
* 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,
|
||||
* 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()
|
||||
class SURVIVAL_PROJET_API ASurvivalPlayerState : public APlayerState
|
||||
{
|
||||
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 "GameFramework/GameUserSettings.h"
|
||||
#include "CharacterAppearanceTypes.h"
|
||||
#include "SurvivalUserSettings.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalSettingsApplied);
|
||||
@@ -286,7 +287,39 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||
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:
|
||||
/**
|
||||
* 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. */
|
||||
UPROPERTY(config)
|
||||
float LookSensitivity;
|
||||
|
||||
Reference in New Issue
Block a user