(Feat) Add mulitplayer
This commit is contained in:
@@ -367,6 +367,8 @@ private:
|
||||
* foyer. Se base sur le point de vue reel du pawn, donc sur ce qu'il voit a
|
||||
* l'ecran, et non sur la position de son corps.
|
||||
*/
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
int32 ResolveAimedSlot(const AActor* Interactor) const;
|
||||
|
||||
/** Decide ce que ferait un appui sur Interagir, sans rien modifier. */
|
||||
@@ -455,8 +457,17 @@ private:
|
||||
/** Inventaire du pawn qui interagit, ou nullptr. */
|
||||
static UInventoryComponent* GetInventoryOf(AActor* Interactor);
|
||||
|
||||
/** Etat courant de chaque emplacement de cuisson. Toujours dimensionne a MaxCookingSlots. */
|
||||
UPROPERTY(Transient)
|
||||
/**
|
||||
* Etat courant de chaque emplacement de cuisson. Toujours dimensionne a
|
||||
* MaxCookingSlots.
|
||||
*
|
||||
* Replique : c'est ce qui fait changer le mesh de la nourriture sous les
|
||||
* yeux de tous. Le FTimerHandle que porte chaque slot n'est pas une
|
||||
* UPROPERTY et ne part donc pas sur le reseau -- c'est voulu, la cuisson se
|
||||
* compte uniquement sur le serveur et les clients n'en voient que le
|
||||
* resultat.
|
||||
*/
|
||||
UPROPERTY(Transient, ReplicatedUsing = OnRep_Slots)
|
||||
TArray<FCookingSlotState> Slots;
|
||||
|
||||
/**
|
||||
@@ -464,7 +475,12 @@ private:
|
||||
* qui brule ; quand il est consomme, tout le monde avance d'un cran et la
|
||||
* derniere buche disparait du foyer.
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
/**
|
||||
* Replique : le nombre de buches visibles EST la jauge de carburant, c'est
|
||||
* la seule facon pour un joueur de savoir combien il reste de feu. Les
|
||||
* secondes restantes partent avec, mais seul le serveur les decompte.
|
||||
*/
|
||||
UPROPERTY(Transient, ReplicatedUsing = OnRep_Fuel)
|
||||
TArray<FFuelSlotState> Fuel;
|
||||
|
||||
FTimerHandle FuelTimer;
|
||||
@@ -477,5 +493,16 @@ private:
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UStaticMeshComponent>> FuelIndicators;
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_IsLit)
|
||||
bool bIsLit = false;
|
||||
|
||||
/** Le contenu du grill a change cote serveur : on repose tous les meshes. */
|
||||
UFUNCTION()
|
||||
void OnRep_Slots();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_Fuel();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_IsLit();
|
||||
};
|
||||
|
||||
@@ -117,6 +117,14 @@ public:
|
||||
*
|
||||
* @return true si la fabrication a eu lieu.
|
||||
*/
|
||||
/**
|
||||
* Fabrique la recette. Chez un client, envoie la demande au serveur et rend
|
||||
* true : le resultat redescendra par la replication de l'inventaire.
|
||||
*
|
||||
* Le serveur refait TOUJOURS CanCraft de son cote, avec sa propre liste de
|
||||
* postes accessibles -- celle que le controller tient a jour par
|
||||
* Client_OpenCraftingAtStation et Server_ReleaseStation.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
bool Craft(UCraftingRecipeDataAsset* Recipe);
|
||||
|
||||
@@ -167,6 +175,10 @@ protected:
|
||||
TObjectPtr<UInventoryComponent> Inventory;
|
||||
|
||||
private:
|
||||
/** La fabrication demandee par un client, arbitree par le serveur. */
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_Craft(UCraftingRecipeDataAsset* Recipe);
|
||||
|
||||
/**
|
||||
* Combien de postes de chaque type sont a portee, et non un simple
|
||||
* "present / absent" -- voir AddAvailableStation.
|
||||
|
||||
@@ -76,6 +76,8 @@ public:
|
||||
// Called every frame
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
// Called to bind functionality to input
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
@@ -487,6 +489,47 @@ protected:
|
||||
void UseSelectedItem();
|
||||
|
||||
private:
|
||||
// ------------------------------------------------------------------
|
||||
// Réseau
|
||||
//
|
||||
// Jeter et utiliser engendrent ou détruisent des choses dans le monde :
|
||||
// ce sont des actions d'autorité. Le routage vit dans les fonctions
|
||||
// publiques elles-mêmes, pour que ni l'UI ni l'input n'aient à savoir
|
||||
// s'ils tournent chez l'hôte ou chez un client.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_DropFromInventory(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity);
|
||||
|
||||
/**
|
||||
* Le slot est passé explicitement : SelectedHotbarIndex n'est PAS répliqué,
|
||||
* c'est une intention purement locale. Le serveur ne peut donc pas deviner
|
||||
* ce que le joueur avait en main.
|
||||
*/
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_UseItem(int32 SlotIndex);
|
||||
|
||||
/** L'utilisation réelle, côté autorité. */
|
||||
void PerformUseItem(int32 SlotIndex);
|
||||
|
||||
/**
|
||||
* Point d'entrée unique du sprint : applique en local ET prévient le serveur.
|
||||
*
|
||||
* Sans le second, le serveur croirait le client à `WalkSpeed` et corrigerait
|
||||
* sa position à chaque pas — c'est le rubber-banding classique, et il ne se
|
||||
* voit que sur une machine distante, jamais chez l'hôte.
|
||||
*/
|
||||
void SetSprinting(bool bNewSprinting);
|
||||
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_SetSprinting(bool bNewSprinting);
|
||||
|
||||
/** La vitesse suit l'état, y compris sur les pawns des autres joueurs. */
|
||||
UFUNCTION()
|
||||
void OnRep_IsSprinting();
|
||||
|
||||
void ApplySprintSpeed();
|
||||
|
||||
/** Fait avancer toutes les animations de caméra (intégration uniquement). */
|
||||
void UpdateCameraEffects(float DeltaTime);
|
||||
|
||||
@@ -542,6 +585,8 @@ private:
|
||||
/** Vitesse verticale mémorisée pendant la chute, pour calibrer l'atterrissage. */
|
||||
float LastFallSpeed = 0.f;
|
||||
|
||||
/** Répliqué pour que les autres joueurs voient la bonne vitesse et la bonne anim. */
|
||||
UPROPERTY(ReplicatedUsing = OnRep_IsSprinting)
|
||||
bool bIsSprinting = false;
|
||||
|
||||
/** Évite de spammer le log si IA_Look est mal configurée. */
|
||||
|
||||
@@ -103,6 +103,22 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "UI|Reglages")
|
||||
void OpenSettings();
|
||||
|
||||
/**
|
||||
* Ouvre le panneau d'invitation de l'overlay Steam.
|
||||
*
|
||||
* Passe par le controller et non directement du widget au sous-systeme :
|
||||
* inviter est une action du JOUEUR LOCAL, et le controller est justement
|
||||
* l'objet qui le represente. Un widget qui irait chercher un
|
||||
* GameInstanceSubsystem tout seul se brancherait sur le mauvais joueur le
|
||||
* jour ou l'ecran partage existera.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Coop")
|
||||
void ShowInviteFriends();
|
||||
|
||||
/** Faux quand Steam est absent ou qu'aucune session n'a pu etre creee. */
|
||||
UFUNCTION(BlueprintPure, Category = "Coop")
|
||||
bool CanInviteFriends() const;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
@@ -389,9 +405,68 @@ private:
|
||||
UFUNCTION()
|
||||
void HandlePawnDied();
|
||||
|
||||
/** Detruit le cadavre et demande un nouveau pawn au GameMode. */
|
||||
/**
|
||||
* Detruit le cadavre et demande un nouveau pawn au GameMode.
|
||||
*
|
||||
* Le GameMode n'existe QUE sur le serveur : chez un client
|
||||
* GetAuthGameMode() rend nullptr, toujours. Un client route donc par
|
||||
* Server_RequestRespawn, et ne garde en local que son fondu de retour --
|
||||
* c'est de l'affichage, ca lui appartient.
|
||||
*/
|
||||
void RespawnPlayer();
|
||||
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_RequestRespawn();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Ouverture d'ecran demandee par un acteur du monde
|
||||
//
|
||||
// Interact_Implementation s'execute sur le SERVEUR depuis le passage en
|
||||
// coop. Or ces acteurs ouvrent une interface, qui n'existe que sur la
|
||||
// machine du joueur : l'appel doit donc repartir vers le client par une
|
||||
// RPC Client_. Sans elles, le serveur cree des widgets dans le vide et
|
||||
// l'ecran du joueur ne s'ouvre jamais.
|
||||
//
|
||||
// L'etat "devant quel poste je suis" est tenu des DEUX cotes : le client en
|
||||
// a besoin pour filtrer sa liste de recettes, le serveur pour valider le
|
||||
// craft. Ne le tenir que d'un cote laisserait soit une liste vide, soit un
|
||||
// etabli utilisable depuis la foret.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UFUNCTION(Client, Reliable)
|
||||
void Client_OpenCraftingAtStation(ECraftingStation Station);
|
||||
|
||||
UFUNCTION(Client, Reliable)
|
||||
void Client_OpenStorage(AStorageContainer* Container);
|
||||
|
||||
/** Le joueur a referme son ecran : le serveur reprend le poste accorde. */
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_ReleaseStation(ECraftingStation Station);
|
||||
|
||||
/** Le joueur a referme le coffre : le serveur rabat le couvercle. */
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_ReleaseStorage(AStorageContainer* Container);
|
||||
|
||||
/** Accorde le poste sur le composant du pawn et note lequel. */
|
||||
void GrantStation(ECraftingStation Station);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Pseudo
|
||||
//
|
||||
// Le serveur ne peut PAS deviner le pseudo Steam d'un client : l'API
|
||||
// d'identite ne connait que l'utilisateur local de chaque machine. C'est
|
||||
// donc au client de l'annoncer. Sans ca, le moteur laisse le nom par
|
||||
// defaut du fichier de configuration -- on lit "Player" au-dessus des
|
||||
// quatre joueurs, ce qui se remarque tout de suite.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Le pseudo de l'utilisateur Steam local, ou une chaine vide si indisponible. */
|
||||
FString ResolveLocalPlayerNickname() const;
|
||||
|
||||
/** Ecrit le pseudo sur le PlayerState, qui le replique a tout le monde. */
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_SetPlayerNickname(const FString& Nickname);
|
||||
|
||||
FTimerHandle RespawnTimerHandle;
|
||||
|
||||
/** Surveillance de l'eloignement, actif seulement pendant qu'un coffre est ouvert. */
|
||||
|
||||
@@ -40,7 +40,14 @@ public:
|
||||
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
/** Declenche l'interaction sur l'objet vise, s'il y en a un. */
|
||||
/**
|
||||
* Declenche l'interaction sur l'objet vise, s'il y en a un.
|
||||
*
|
||||
* Le trace, lui, reste PUREMENT LOCAL : chacun vise chez soi, a sa propre
|
||||
* frequence, sans le moindre aller-retour reseau -- le prompt doit s'afficher
|
||||
* a l'instant ou l'on vise, pas 60 ms plus tard. Seule l'action part au
|
||||
* serveur, qui est le seul a pouvoir modifier le monde.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void TryInteract();
|
||||
|
||||
@@ -93,7 +100,36 @@ protected:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||
bool bDrawDebug = false;
|
||||
|
||||
/**
|
||||
* Marge accordee par le serveur quand il verifie la distance annoncee par un
|
||||
* client. 1.0 refuserait des interactions parfaitement legitimes : entre le
|
||||
* moment ou le client vise et celui ou son message arrive, son personnage a
|
||||
* bouge, et la position que le serveur connait de lui a jusqu'a un aller-retour
|
||||
* de retard.
|
||||
*
|
||||
* Ce n'est PAS de l'anti-triche -- coop entre amis, on ne paie pas ce prix --
|
||||
* mais un garde-fou contre un client desynchronise qui ouvrirait un coffre a
|
||||
* l'autre bout de la carte.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Reseau", meta = (ClampMin = "1.0"))
|
||||
float ServerRangeTolerance = 2.f;
|
||||
|
||||
private:
|
||||
/**
|
||||
* L'interaction demandee par un client, executee par le serveur.
|
||||
*
|
||||
* Reliable : rater un ramassage parce qu'un paquet s'est perdu serait
|
||||
* incomprehensible pour le joueur. Le debit est nul de toute facon, une
|
||||
* interaction est un evenement rare a l'echelle du reseau.
|
||||
*/
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_Interact(AActor* Target);
|
||||
|
||||
/**
|
||||
* L'execution reelle, cote autorite. Revalide la cible : le client peut
|
||||
* l'avoir vue valide alors qu'un autre joueur venait de la ramasser.
|
||||
*/
|
||||
void PerformInteract(AActor* Target);
|
||||
/** Relance le trace et met a jour FocusedActor. */
|
||||
void UpdateFocus();
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ public:
|
||||
|
||||
virtual void InitializeComponent() override;
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
/**
|
||||
* Ajoute des objets, en remplissant d'abord les piles entamees.
|
||||
* @param RemainingUses charges de l'exemplaire ajoute ; -1 signifie "plein".
|
||||
@@ -274,11 +276,89 @@ protected:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire|Barre rapide", meta = (ClampMin = "0"))
|
||||
int32 HotbarSlotCount = 10;
|
||||
|
||||
/** Toujours dimensionne a GetTotalSlotCount(). */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
|
||||
/**
|
||||
* Toujours dimensionne a GetTotalSlotCount().
|
||||
*
|
||||
* Replique a TOUT LE MONDE et non au seul proprietaire, pour deux raisons.
|
||||
*
|
||||
* Technique d'abord : les conditions de GetLifetimeReplicatedProps sont
|
||||
* mises en cache par CLASSE, pas par instance. Un COND_OwnerOnly viserait
|
||||
* donc aussi le composant d'un coffre -- or un coffre n'appartient a aucune
|
||||
* connexion, et sa grille n'arriverait chez personne. Le meme composant sert
|
||||
* le sac et le contenant, c'est une decision d'architecture qu'on ne veut pas
|
||||
* defaire pour un drapeau de replication.
|
||||
*
|
||||
* De fond ensuite : les coffres sont a acces simultane, donc leur contenu
|
||||
* DOIT arriver a tous ceux qui l'ouvrent. Et voir le sac d'un coequipier n'a
|
||||
* rien d'un probleme dans une coop entre amis sans anti-triche -- c'est meme
|
||||
* ce qui rendra un futur echange d'objets gratuit.
|
||||
*
|
||||
* Cout reel : 4 joueurs x 30 slots de trois champs, envoyes uniquement quand
|
||||
* ils changent, et delta-compresses. Negligeable.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Slots, Category = "Inventaire")
|
||||
TArray<FInventorySlot> Slots;
|
||||
|
||||
/**
|
||||
* Le contenu vient d'arriver du serveur : on rediffuse le delegue habituel.
|
||||
*
|
||||
* C'est LA ligne qui recable toute l'interface d'un coup. Grille, barre
|
||||
* rapide, panneau de detail et coffres sont deja abonnes a
|
||||
* OnInventoryChanged et ne font aucun Tick -- ils n'ont donc pas une seule
|
||||
* ligne a changer pour devenir reseau.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void OnRep_Slots();
|
||||
|
||||
private:
|
||||
// ------------------------------------------------------------------
|
||||
// Reseau
|
||||
//
|
||||
// Le routage vit DANS les fonctions publiques et non chez leurs appelants :
|
||||
// une case d'inventaire appelle MoveItem() sans savoir si elle tourne chez
|
||||
// l'hote ou chez un client, et c'est tres bien ainsi. Ajouter le test
|
||||
// d'autorite dans chaque widget serait dix occasions de l'oublier.
|
||||
//
|
||||
// Pas de prediction locale : le client envoie et attend OnRep_Slots. Sur un
|
||||
// relais Steam l'aller-retour est de quelques dizaines de millisecondes,
|
||||
// invisible pour un glisser-deposer -- alors qu'une prediction fausse
|
||||
// donnerait une pile qui saute, symptome bien plus desagreable.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Ce composant peut-il modifier son contenu ici et maintenant. */
|
||||
bool HasInventoryAuthority() const;
|
||||
|
||||
/**
|
||||
* Par quel composant faire transiter une RPC serveur.
|
||||
*
|
||||
* Renvoie celui des deux qui appartient au pawn du joueur LOCAL : un coffre
|
||||
* n'appartient a aucune connexion, et une RPC emise depuis lui serait
|
||||
* silencieusement jetee par le moteur. C'est le piege numero un des
|
||||
* transferts vers un conteneur.
|
||||
*/
|
||||
static UInventoryComponent* FindNetProxy(UInventoryComponent* A, UInventoryComponent* B);
|
||||
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_TransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity);
|
||||
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_QuickTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To);
|
||||
|
||||
/**
|
||||
* From est passe explicitement et n'est PAS "this" cote serveur : la RPC
|
||||
* transite par l'inventaire du joueur local, qui peut tres bien etre la
|
||||
* DESTINATION -- c'est le cas du bouton "Tout prendre" d'un coffre. Deduire
|
||||
* la source du recepteur viderait le sac du joueur dans lui-meme.
|
||||
*/
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_TransferAllTo(UInventoryComponent* From, UInventoryComponent* To, int32 FirstIndex);
|
||||
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_ConsumeUse(int32 SlotIndex);
|
||||
|
||||
/** Les regles de deplacement, une fois l'autorite acquise. */
|
||||
static bool ApplyTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity);
|
||||
|
||||
/** Redimensionne le tableau si la taille cible a change. */
|
||||
void EnsureSlotCount();
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ class USettingsMenuWidget;
|
||||
enum class EMenuPendingAction : uint8
|
||||
{
|
||||
None,
|
||||
|
||||
/**
|
||||
* Le fondu part AVANT la creation de la session, qui est asynchrone. La map
|
||||
* n'est donc chargee qu'a la reponse du service en ligne -- ou tout de suite
|
||||
* si celui-ci refuse, puisqu'un echec reseau ne doit pas empecher de jouer.
|
||||
*/
|
||||
StartGame
|
||||
};
|
||||
|
||||
@@ -38,9 +44,21 @@ class SURVIVAL_PROJET_API AMainMenuPlayerController : public APlayerController
|
||||
public:
|
||||
AMainMenuPlayerController();
|
||||
|
||||
/** Fondu au noir, puis chargement de la map de jeu. */
|
||||
/**
|
||||
* Fondu au noir, creation de la session, puis ouverture de la map en
|
||||
* serveur d'ecoute.
|
||||
*
|
||||
* Il n'existe qu'UN seul bouton pour lancer une partie : toute partie est
|
||||
* une partie en ligne, meme jouee seul. Distinguer "solo" et "heberger"
|
||||
* obligerait le joueur a decider avant de savoir si un ami se connectera,
|
||||
* et surtout a relancer sa partie pour changer d'avis.
|
||||
*
|
||||
* On ne rejoint jamais depuis ce menu : l'arrivee se fait uniquement par
|
||||
* invitation Steam, geree par USessionSubsystem quelle que soit la map
|
||||
* affichee a cet instant.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Menu")
|
||||
void StartSoloGame();
|
||||
void StartGame();
|
||||
|
||||
/** Ouvre la demande de confirmation. Ne quitte rien par elle-meme. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Menu")
|
||||
@@ -125,6 +143,25 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleSettingsCloseRequested();
|
||||
|
||||
/**
|
||||
* La session n'a pas pu etre creee -- Steam eteint, hors ligne, service en
|
||||
* panne. On charge la map QUAND MEME.
|
||||
*
|
||||
* C'est un choix delibere : un joueur qui clique Jouer veut jouer. Le rendre
|
||||
* a son menu avec un message d'erreur reseau alors que rien ne l'empeche de
|
||||
* jouer seul serait une punition pour une panne qui n'est pas la sienne. Il
|
||||
* perd seulement la possibilite d'inviter, et le bouton Inviter le dira.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void HandleHostFailed(FText Reason);
|
||||
|
||||
/**
|
||||
* Echec de connexion sur invitation : on remonte le fondu et on rend le menu.
|
||||
* Ici pas de repli possible, la partie visee est celle d'un ami.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void HandleJoinFailed(FText Reason);
|
||||
|
||||
/** Ferme le jeu sans rien demander. */
|
||||
void CloseGame();
|
||||
|
||||
|
||||
@@ -28,7 +28,14 @@ public:
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
protected:
|
||||
/** Lance la partie solo. Nomme exactement "PlayButton" dans le WBP. */
|
||||
/**
|
||||
* Lance la partie. Nomme exactement "PlayButton" dans le WBP.
|
||||
*
|
||||
* Un seul bouton, et toute partie est en ligne : il n'y a ni "Heberger" ni
|
||||
* "Rejoindre". On arrive chez un ami par invitation Steam et par rien
|
||||
* d'autre, ce qui evite au joueur de choisir un mode avant de savoir si
|
||||
* quelqu'un le rejoindra.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> PlayButton;
|
||||
|
||||
@@ -39,17 +46,6 @@ protected:
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UButton> SettingsButton;
|
||||
|
||||
/**
|
||||
* Coop a venir. BindWidgetOptional et non BindWidget : tu peux dessiner ton
|
||||
* WBP_MainMenu sans eux aujourd'hui, le Blueprint compilera quand meme.
|
||||
* S'ils existent, ils sont grises automatiquement.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UButton> HostButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UButton> JoinButton;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandlePlayClicked();
|
||||
|
||||
@@ -45,6 +45,18 @@ protected:
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UButton> SettingsButton;
|
||||
|
||||
/**
|
||||
* Ouvre le panneau d'invitation de l'overlay Steam. C'est la SEULE facon de
|
||||
* faire venir un ami : il n'existe ni navigateur de parties, ni bouton
|
||||
* Rejoindre dans le menu principal.
|
||||
*
|
||||
* Grise tout seul quand l'invitation est impossible -- Steam absent, ou
|
||||
* partie lancee alors qu'il l'etait. Un bouton qui ne repond pas au clic
|
||||
* passe pour un bug ; un bouton grise se comprend tout seul.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UButton> InviteButton;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleResumeClicked();
|
||||
@@ -54,4 +66,7 @@ private:
|
||||
|
||||
UFUNCTION()
|
||||
void HandleQuitToMenuClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleInviteClicked();
|
||||
};
|
||||
|
||||
@@ -27,10 +27,15 @@ class SURVIVAL_PROJET_API APickupItem : public AActor, public IInteractable
|
||||
public:
|
||||
APickupItem();
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
/**
|
||||
* Configure un pickup engendre a l'execution (objet jete au sol).
|
||||
* A appeler entre SpawnActorDeferred() et FinishSpawning() pour que
|
||||
* OnConstruction ait deja les bonnes donnees et pose le bon mesh.
|
||||
*
|
||||
* A n'appeler que sur le SERVEUR : c'est lui qui engendre les acteurs du
|
||||
* monde, les clients recoivent l'acteur deja configure par replication.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Item")
|
||||
void InitPickup(UItemDataAsset* InItemData, int32 InQuantity, int32 InRemainingUses = -1);
|
||||
@@ -62,21 +67,44 @@ protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item")
|
||||
TObjectPtr<USphereComponent> InteractionSphere;
|
||||
|
||||
/** Quel objet ce pickup represente. A assigner sur chaque instance. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
|
||||
/**
|
||||
* Quel objet ce pickup represente. A assigner sur chaque instance.
|
||||
*
|
||||
* ReplicatedUsing et non simplement Replicated : chez un client, l'acteur
|
||||
* est construit AVANT que ses proprietes arrivent. OnConstruction s'execute
|
||||
* donc avec ItemData a nullptr et ne pose aucun mesh -- l'objet resterait
|
||||
* invisible pour tout le monde sauf l'hote. C'est OnRep qui repose le visuel
|
||||
* au moment ou la donnee arrive vraiment.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_ItemData, Category = "Item")
|
||||
TObjectPtr<UItemDataAsset> ItemData;
|
||||
|
||||
/** Combien d'exemplaires ce tas contient. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item", meta = (ClampMin = "1"))
|
||||
/**
|
||||
* Combien d'exemplaires ce tas contient.
|
||||
* Replique parce que le prompt l'affiche ("Ramasser Branche x3") et qu'un
|
||||
* ramassage partiel, inventaire plein, le fait diminuer sous les yeux de tous.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Item")
|
||||
int32 Quantity = 1;
|
||||
|
||||
/**
|
||||
* Charges restantes pour un objet a usages multiples.
|
||||
* -1 = plein, ce qui est le cas de tout objet pose a la main dans le niveau.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Item")
|
||||
int32 RemainingUses = -1;
|
||||
|
||||
/** Repose le mesh quand la donnee de l'objet arrive chez un client. */
|
||||
UFUNCTION()
|
||||
void OnRep_ItemData();
|
||||
|
||||
/**
|
||||
* Applique le visuel decrit par l'ItemData. Partage par OnConstruction et
|
||||
* OnRep_ItemData : deux copies finiraient par diverger, et le symptome
|
||||
* serait un objet correct dans l'editeur mais nu en jeu.
|
||||
*/
|
||||
void ApplyItemVisuals();
|
||||
|
||||
/**
|
||||
* Reprend automatiquement le WorldMesh de l'ItemData dans l'editeur.
|
||||
* Decoche pour imposer un mesh different sur une instance precise.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/WidgetComponent.h"
|
||||
#include "PlayerNameplateComponent.generated.h"
|
||||
|
||||
class APlayerState;
|
||||
|
||||
/**
|
||||
* L'etiquette de pseudo posee au-dessus d'un joueur.
|
||||
*
|
||||
* Herite de UWidgetComponent et le configure en espace ECRAN : c'est ce qui la
|
||||
* fait toujours faire face a la camera, gratuitement et exactement, sans le
|
||||
* moindre calcul d'orientation par frame. En espace Monde il faudrait la
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* meme chaine sur le reseau.
|
||||
*/
|
||||
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
|
||||
class SURVIVAL_PROJET_API UPlayerNameplateComponent : public UWidgetComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPlayerNameplateComponent();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Au-dela de cette distance, l'etiquette disparait. 2500 cm = 25 m, soit un
|
||||
* peu plus que la portee a laquelle on distingue un coequipier dans un
|
||||
* decor dense.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate", meta = (ClampMin = "100", Units = "cm"))
|
||||
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.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate", meta = (ClampMin = "0.05", Units = "s"))
|
||||
float RefreshInterval = 0.25f;
|
||||
|
||||
/**
|
||||
* Cacher l'etiquette de son PROPRE personnage.
|
||||
*
|
||||
* Vrai par defaut : en vue premiere personne on ne voit jamais son propre
|
||||
* corps, mais une etiquette en espace ecran, elle, se projetterait en plein
|
||||
* milieu du viseur.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Nameplate")
|
||||
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;
|
||||
|
||||
/**
|
||||
* Dernier pseudo reellement ecrit dans le widget.
|
||||
*
|
||||
* Le PlayerState arrive APRES le pawn chez un client : la premiere passe
|
||||
* trouve donc un nom vide, et il faut continuer a regarder. Comparer plutot
|
||||
* que reecrire evite de reconstruire la mise en page du Text Block quatre
|
||||
* fois par seconde pour une chaine identique.
|
||||
*/
|
||||
FString LastShownName;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "PlayerNameplateWidget.generated.h"
|
||||
|
||||
class UTextBlock;
|
||||
|
||||
/**
|
||||
* L'etiquette flottante au-dessus d'un joueur : son pseudo Steam.
|
||||
*
|
||||
* Volontairement passive -- elle ne va chercher aucune donnee et ne s'abonne a
|
||||
* rien. C'est UPlayerNameplateComponent qui la nourrit, parce que lui seul sait
|
||||
* DE QUEL joueur il s'agit : un widget pose sur le pawn d'autrui n'a aucun moyen
|
||||
* fiable de remonter jusqu'a son PlayerState sans supposer une hierarchie.
|
||||
*
|
||||
* Classe Abstract comme les autres widgets du projet : on cree un
|
||||
* WBP_PlayerNameplate qui en herite, le C++ porte la logique et le Blueprint la
|
||||
* mise en page.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UPlayerNameplateWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Ecrit le pseudo affiche. Sans effet si le Text Block n'existe pas. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Nameplate")
|
||||
void SetPlayerName(const FText& InName);
|
||||
|
||||
protected:
|
||||
/** A nommer exactement "PlayerNameText" dans le WBP. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UTextBlock> PlayerNameText;
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Interfaces/OnlineSessionInterface.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "SessionSubsystem.generated.h"
|
||||
|
||||
/**
|
||||
* Raison d'un echec, deja redigee pour etre affichee telle quelle.
|
||||
*
|
||||
* Il n'existe volontairement PAS de delegue de succes symetrique : quand
|
||||
* l'hebergement ou la connexion reussit, la map change et tout ce qui ecoutait
|
||||
* est detruit dans la seconde. Un delegue de succes serait donc soit jamais lu,
|
||||
* soit lu par un widget qui n'existe deja plus.
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionError, FText, Reason);
|
||||
|
||||
/** Fin d'une recherche. SessionCount vaut 0 si personne n'heberge. */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSessionSearchFinished, bool, bWasSuccessful, int32, SessionCount);
|
||||
|
||||
/**
|
||||
* Hebergement et connexion : creer une partie, la trouver, la rejoindre.
|
||||
*
|
||||
* GameInstanceSubsystem et pas un composant : une session doit survivre au
|
||||
* changement de map, puisque c'est justement elle qui le provoque. Le critere
|
||||
* habituel du projet -- "si le pawn meurt, est-ce que ca doit survivre ?" --
|
||||
* la place au meme endroit que les reglages.
|
||||
*
|
||||
* Le C++ ici ne parle JAMAIS de Steam. Tout passe par IOnlineSubsystem, dont
|
||||
* l'implementation est choisie par DefaultEngine.ini. Consequence directe : le
|
||||
* meme code marche en LAN via OnlineSubsystemNULL, ce qui permet de tester a
|
||||
* deux instances sur la meme machine sans client Steam.
|
||||
*/
|
||||
UCLASS()
|
||||
class SURVIVAL_PROJET_API USessionSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Coop a quatre : la valeur de reference du projet. */
|
||||
static constexpr int32 DefaultMaxPlayers = 4;
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/**
|
||||
* Cree la session puis ouvre la map en serveur d'ecoute.
|
||||
*
|
||||
* C'est le sous-systeme qui charge la map et non l'appelant, parce que la
|
||||
* creation est ASYNCHRONE : entre le clic et la reponse de Steam il peut
|
||||
* s'ecouler une seconde, et ouvrir la map avant d'avoir la session donnerait
|
||||
* une partie que personne ne peut rejoindre.
|
||||
*
|
||||
* En cas d'echec, rien ne bouge et OnHostFailed est diffuse.
|
||||
*
|
||||
* MaxPlayers n'a volontairement PAS de valeur par defaut : UHT ne sait pas
|
||||
* resoudre une constante qualifiee dans une signature UFUNCTION, et ecrire
|
||||
* "= 4" ici recreerait le second nombre a tenir en phase avec le GameMode.
|
||||
* Les appelants passent USessionSubsystem::DefaultMaxPlayers.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Session")
|
||||
void HostSession(const TSoftObjectPtr<UWorld>& Level, int32 MaxPlayers);
|
||||
|
||||
/** Cherche les parties joignables. Le resultat arrive par OnSearchFinished. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Session")
|
||||
void FindSessions(int32 MaxResults = 20);
|
||||
|
||||
/**
|
||||
* Rejoint un resultat de la derniere recherche, puis voyage vers l'hote.
|
||||
* Un index hors bornes diffuse OnJoinFailed plutot que de ne rien faire.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Session")
|
||||
void JoinFoundSession(int32 ResultIndex = 0);
|
||||
|
||||
/**
|
||||
* Cherche puis rejoint la premiere partie trouvee, en une commande console.
|
||||
*
|
||||
* Aucun bouton ne mene ici : on ne rejoint une partie QUE par invitation
|
||||
* Steam. Cette commande n'existe que pour pouvoir tester a deux instances
|
||||
* sur une seule machine, ou aucune invitation n'est possible. La taper dans
|
||||
* la console (touche ~) sur la seconde fenetre.
|
||||
*/
|
||||
UFUNCTION(Exec, Category = "Session")
|
||||
void EmberJoin();
|
||||
|
||||
/**
|
||||
* Ouvre le panneau d'invitation de l'overlay Steam.
|
||||
*
|
||||
* @return false si l'overlay est indisponible -- pas de service en ligne,
|
||||
* pas de session, ou overlay desactive cote client Steam.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Session")
|
||||
bool ShowInviteUI();
|
||||
|
||||
/**
|
||||
* Peut-on inviter maintenant. Sert a griser le bouton plutot qu'a le laisser
|
||||
* cliquer dans le vide : une partie lancee alors que Steam etait absent
|
||||
* tourne tres bien, mais n'a personne a qui s'annoncer.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Session")
|
||||
bool CanInvite() const;
|
||||
|
||||
/**
|
||||
* Quitte la session courante. A appeler en revenant au menu principal :
|
||||
* une session laissee derriere soi empeche d'en creer une nouvelle, et
|
||||
* laisse surtout ta partie annoncee a tes amis alors qu'elle n'existe plus.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Session")
|
||||
void LeaveSession();
|
||||
|
||||
/** Vrai si une session est ouverte, qu'on l'heberge ou qu'on l'ait rejointe. */
|
||||
UFUNCTION(BlueprintPure, Category = "Session")
|
||||
bool IsSessionActive() const;
|
||||
|
||||
/**
|
||||
* Nom du service en ligne reellement charge : "Steam", ou "NULL" en LAN.
|
||||
* Sert a l'UI et surtout au diagnostic -- voir "NULL" alors qu'on attendait
|
||||
* Steam est le symptome le plus frequent, et il se lit en une seconde.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Session")
|
||||
FString GetOnlineServiceName() const;
|
||||
|
||||
/** Combien de resultats la derniere recherche a rapportes. */
|
||||
UFUNCTION(BlueprintPure, Category = "Session")
|
||||
int32 GetFoundSessionCount() const;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Session")
|
||||
FOnSessionError OnHostFailed;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Session")
|
||||
FOnSessionError OnJoinFailed;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Session")
|
||||
FOnSessionSearchFinished OnSearchFinished;
|
||||
|
||||
private:
|
||||
void HandleCreateSessionComplete(FName SessionName, bool bWasSuccessful);
|
||||
void HandleFindSessionsComplete(bool bWasSuccessful);
|
||||
void HandleJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
|
||||
void HandleDestroySessionComplete(FName SessionName, bool bWasSuccessful);
|
||||
|
||||
/**
|
||||
* L'ami a clique "Rejoindre la partie" dans l'overlay Steam.
|
||||
*
|
||||
* Ecoute des l'Initialize et jamais coupee : une invitation peut tomber a
|
||||
* n'importe quel instant, y compris en pleine partie ou alors qu'aucun menu
|
||||
* n'est affiche. Le sous-systeme est le seul objet dont on soit certain
|
||||
* qu'il soit vivant a ce moment-la.
|
||||
*/
|
||||
void HandleInviteAccepted(const bool bWasSuccessful, const int32 ControllerId, FUniqueNetIdPtr UserId, const FOnlineSessionSearchResult& InviteResult);
|
||||
|
||||
/**
|
||||
* Second temps de la commande EmberJoin : la recherche a rendu, on rejoint.
|
||||
* UFUNCTION obligatoire, c'est ce qui permet le AddDynamic sur OnSearchFinished.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void HandleExecJoinSearchFinished(bool bWasSuccessful, int32 SessionCount);
|
||||
|
||||
/** La creation proprement dite, une fois le terrain degage. */
|
||||
void CreateSessionNow();
|
||||
|
||||
void JoinSearchResult(const FOnlineSessionSearchResult& Result);
|
||||
|
||||
/**
|
||||
* Vrai quand le service charge est OnlineSubsystemNULL, c'est-a-dire qu'on
|
||||
* n'a pas Steam. Toutes les sessions basculent alors en LAN : c'est ce qui
|
||||
* rend le test a deux instances possible sur une seule machine.
|
||||
*/
|
||||
bool IsLanMode() const;
|
||||
|
||||
/** Coupe proprement un delegue encore branche. Idempotent. */
|
||||
void ClearPendingDelegates();
|
||||
|
||||
IOnlineSessionPtr SessionInterface;
|
||||
|
||||
TSharedPtr<FOnlineSessionSearch> SearchSettings;
|
||||
|
||||
FDelegateHandle CreateSessionCompleteHandle;
|
||||
FDelegateHandle FindSessionsCompleteHandle;
|
||||
FDelegateHandle JoinSessionCompleteHandle;
|
||||
FDelegateHandle DestroySessionCompleteHandle;
|
||||
FDelegateHandle InviteAcceptedHandle;
|
||||
|
||||
/** Map a ouvrir une fois la session creee. */
|
||||
TSoftObjectPtr<UWorld> PendingHostLevel;
|
||||
|
||||
int32 PendingMaxPlayers = DefaultMaxPlayers;
|
||||
|
||||
/**
|
||||
* Une session trainait encore : on la detruit d'abord et la creation reprend
|
||||
* dans le callback de destruction. Sans ce drapeau, quitter une partie puis
|
||||
* en relancer une echoue silencieusement -- le moteur refuse deux sessions
|
||||
* du meme nom, et l'erreur ne remonte nulle part.
|
||||
*/
|
||||
bool bCreateAfterDestroy = false;
|
||||
};
|
||||
@@ -64,6 +64,19 @@ public:
|
||||
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
/** Le couvercle vient de changer d'etat cote serveur : on joue l'animation. */
|
||||
UFUNCTION()
|
||||
void OnRep_Open();
|
||||
|
||||
/**
|
||||
* Son et relance du tick d'animation. Partage par SetOpen (serveur) et
|
||||
* OnRep_Open (clients) : deux copies finiraient par diverger, et le
|
||||
* symptome serait un couvercle muet chez les invites.
|
||||
*/
|
||||
void ApplyOpenState();
|
||||
|
||||
// --- IInteractable ---
|
||||
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
|
||||
virtual void Interact_Implementation(AActor* Interactor) override;
|
||||
@@ -87,6 +100,11 @@ public:
|
||||
* parce qu'il s'est trop eloigne. Un coffre qui deciderait seul finirait
|
||||
* ouvert dans la moitie de ces cas.
|
||||
*/
|
||||
/**
|
||||
* Leve ou referme le couvercle. **Serveur uniquement** : le couvercle est
|
||||
* un etat du MONDE, les quatre joueurs doivent le voir bouger. Un client
|
||||
* qui l'appellerait verrait son coffre s'ouvrir tout seul chez lui.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Storage")
|
||||
void SetOpen(bool bInOpen);
|
||||
|
||||
@@ -232,6 +250,11 @@ private:
|
||||
/** Cale la boite sur le volume combine des deux meshes, couvercle ferme. */
|
||||
void FitInteractionBox();
|
||||
|
||||
/**
|
||||
* Repliqué à tout le monde : voir le couvercle d'un coffre se lever est le
|
||||
* seul signe qu'un coéquipier est en train de fouiller dedans.
|
||||
*/
|
||||
UPROPERTY(ReplicatedUsing = OnRep_Open)
|
||||
bool bOpen = false;
|
||||
|
||||
/** 0 = ferme, 1 = ouvert. Interpole, jamais pose directement. */
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "SurvivalGameMode.generated.h"
|
||||
|
||||
/**
|
||||
* L'arbitre de la partie. N'existe QUE sur le serveur -- sur un client,
|
||||
* GetGameMode() rend nullptr, toujours. Ce n'est pas un bug mais le design du
|
||||
* framework : le GameMode decide, le GameState montre.
|
||||
*
|
||||
* Corollaire a garder en tete pour tout le portage coop : le moindre appel a
|
||||
* GetGameMode() ecrit du cote client doit etre remplace par un RPC serveur ou
|
||||
* par une lecture du GameState. C'est le cas du respawn, aujourd'hui declenche
|
||||
* depuis AFpsPlayerController.
|
||||
*
|
||||
* Y mettra plus tard : les regles de mort, le cycle jour/nuit, la sauvegarde
|
||||
* du monde. Pour l'instant il ne fait que borner le nombre de joueurs et
|
||||
* cabler GameState et PlayerState.
|
||||
*/
|
||||
UCLASS()
|
||||
class SURVIVAL_PROJET_API ASurvivalGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ASurvivalGameMode();
|
||||
|
||||
virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override;
|
||||
virtual void PostLogin(APlayerController* NewPlayer) override;
|
||||
virtual void Logout(AController* Exiting) override;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Plafond de joueurs, applique a la AGameSession.
|
||||
*
|
||||
* Regle ici et pas seulement dans les reglages de session : la session
|
||||
* annonce des places libres, mais c'est le GameMode qui refuse reellement la
|
||||
* connexion. Sans ce plafond, un cinquieme joueur qui arrive par une
|
||||
* invitation directe entre quand meme.
|
||||
*
|
||||
* Sa valeur par defaut est posee dans le constructeur depuis
|
||||
* USessionSubsystem::DefaultMaxPlayers, pour qu'il n'existe qu'UN seul 4
|
||||
* dans le projet : deux nombres qui doivent rester egaux finissent toujours
|
||||
* par diverger, et le symptome serait une place annoncee mais refusee.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Coop", meta = (ClampMin = "1", ClampMax = "16"))
|
||||
int32 MaxCoopPlayers;
|
||||
};
|
||||
@@ -34,6 +34,8 @@ public:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
/** Rend de la vie, de la faim et de la soif. Les valeurs sont bornees au max. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Survie")
|
||||
void ApplyRestore(float HealthAmount, float HungerAmount, float ThirstAmount);
|
||||
@@ -106,11 +108,38 @@ private:
|
||||
/** Appelee par le timer : fait descendre les jauges et applique les degats. */
|
||||
void UpdateStats();
|
||||
|
||||
/**
|
||||
* Les jauges viennent d'arriver du serveur : on rediffuse le delegue
|
||||
* habituel, et WBP_SurvivalStats se met a jour sans une ligne de changement.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void OnRep_Stats();
|
||||
|
||||
FTimerHandle UpdateTimerHandle;
|
||||
|
||||
// La simulation tourne UNIQUEMENT sur le serveur : quatre clients qui
|
||||
// feraient descendre leur faim chacun de leur cote divergeraient en
|
||||
// quelques minutes, et le premier a mourir ne serait pas le meme partout.
|
||||
// Les clients ne font que recevoir.
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_Stats)
|
||||
float Health = 0.f;
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_Stats)
|
||||
float Hunger = 0.f;
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_Stats)
|
||||
float Thirst = 0.f;
|
||||
|
||||
/**
|
||||
* ReplicatedUsing et non simplement Replicated : OnDied n'est diffuse que
|
||||
* sur le serveur, or c'est ce delegue qui declenche l'ecran de mort et le
|
||||
* respawn cote joueur. Sans OnRep, un client mourrait sans jamais le savoir
|
||||
* et continuerait a jouer un personnage que le serveur considere mort.
|
||||
*/
|
||||
UPROPERTY(ReplicatedUsing = OnRep_Dead)
|
||||
bool bDead = false;
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_Dead();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user