(Feat) Add Animation Fps Charater

This commit is contained in:
2026-08-13 22:07:08 +02:00
parent 176beab9fc
commit cca54a5e0d
157 changed files with 850 additions and 148 deletions
@@ -0,0 +1,54 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "AnimNotify_ItemAction.generated.h"
/**
* Declenche l'effet de l'objet en main a l'instant precis ou le geste frappe.
*
* ------------------------------------------------------------------
* Pourquoi un notify plutot que le clic
* ------------------------------------------------------------------
*
* Le clic gauche et le COUP sont deux choses distinctes, separees par le temps
* que met le bras a descendre. Faire partir le trace de recolte sur l'appui
* donne une hache qui abat l'arbre avant de l'avoir touche : le joueur voit le
* resultat, puis l'animation qui aurait du le produire. C'est le meme
* decalage qu'un bruit de pas joue au debut de la foulee.
*
* L'instant de l'impact n'est pas une valeur qu'on puisse ecrire en C++ : il
* appartient a l'ANIMATION, il change d'un montage a l'autre, et il rebouge a
* chaque fois qu'on retouche la courbe. Un delai en secondes sur le DataAsset
* serait a re-regler a la main a chaque nouvelle animation livree -- et faux
* des qu'on toucherait au Play Rate. Le marqueur, lui, suit le montage.
*
* ------------------------------------------------------------------
* Reseau : ce notify ne tombe QUE chez le joueur local
* ------------------------------------------------------------------
*
* Le montage d'usage est joue sur les BRAS, qui sont en OnlyOwnerSee et
* n'existent visuellement que chez le proprietaire du pawn
* (UHeldItemComponent::PlayLocalArmsMontage). Le notify part donc sur une
* seule machine, exactement comme le clic qu'il remplace, et le routage vers
* le serveur reste celui d'avant : AFpsPlayer::TriggerHeldItemAction envoie la
* RPC, le serveur fait autorite.
*
* Corollaire a ne pas oublier le jour ou un montage d'usage sera joue aussi sur
* le CORPS (visible des autres joueurs) : ce montage-la tournerait sur les
* quatre machines, et sans la garde IsLocallyControlled() de
* TriggerHeldItemAction chaque coup de hache partirait en quatre exemplaires.
*/
UCLASS(const, hidecategories = Object, collapsecategories, meta = (DisplayName = "Item Action"))
class EMBERWILD_API UAnimNotify_ItemAction : public UAnimNotify
{
GENERATED_BODY()
public:
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference) override;
virtual FString GetNotifyName_Implementation() const override;
};
@@ -0,0 +1,174 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CharacterAppearanceTypes.h"
#include "CharacterAppearanceComponent.generated.h"
class UCharacterPartsDataAsset;
class USkeletalMeshComponent;
struct FCharacterBodySet;
/** Diffuse a chaque apparence appliquee, pour que l'UI se rafraichisse. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAppearanceApplied);
/**
* Monte un personnage a partir d'une FCharacterAppearance : assigne les meshes,
* les met en leader pose, et pose les couleurs sur des materiaux dynamiques.
*
* Un systeme = un composant, comme le reste du projet. Mais celui-ci a une
* particularite qui vaut d'etre tenue : il ne suppose PAS vivre sur le pawn. Il
* cherche le mesh d'un ACharacter s'il en trouve un, accepte qu'on lui en
* designe un autre, et fonctionne donc a l'identique sur le mannequin de l'ecran
* de customisation. C'est ce qui garantit que le personnage montre au joueur et
* celui que ses amis verront ne peuvent pas diverger : c'est le meme code.
*
* Il ne replique rien -- l'apparence vivra sur ASurvivalPlayerState, comme le
* pseudo, et pour la meme raison : elle doit survivre a la mort du pawn.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UCharacterAppearanceComponent : public UActorComponent
{
GENERATED_BODY()
public:
UCharacterAppearanceComponent();
virtual void BeginPlay() override;
UPROPERTY(BlueprintAssignable, Category = "Appearance")
FOnAppearanceApplied OnAppearanceApplied;
/** Ton DA_CharacterParts. Sans lui le composant ne monte rien et le dit. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Appearance")
TObjectPtr<UCharacterPartsDataAsset> Catalog;
/**
* L'apparence posee au BeginPlay, tant que rien ne l'a remplacee.
*
* Elle sert au mannequin de l'ecran, aux tests en PIE, et de repli si les
* reglages du joueur sont vides -- c'est-a-dire au tout premier lancement.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Appearance")
FCharacterAppearance DefaultAppearance;
/**
* Designe le mesh porteur. A appeler AVANT la premiere application.
*
* Inutile sur un ACharacter, dont le mesh est trouve tout seul. Necessaire
* sur un acteur quelconque -- le mannequin de l'ecran de customisation.
*/
UFUNCTION(BlueprintCallable, Category = "Appearance")
void SetTargetMesh(USkeletalMeshComponent* InTargetMesh);
/**
* Monte l'apparence demandee. Idempotente, et bon marche a la repetition :
* les meshes ne sont retouches que si une PIECE a change, jamais pour une
* simple couleur. C'est ce qui permet a un curseur d'appeler cette fonction
* a chaque image sans engendrer un materiau par frame.
*/
UFUNCTION(BlueprintCallable, Category = "Appearance")
void ApplyAppearance(const FCharacterAppearance& InAppearance);
UFUNCTION(BlueprintPure, Category = "Appearance")
const FCharacterAppearance& GetAppearance() const { return Appearance; }
// ------------------------------------------------------------------
// Reglage par nom -- ce qui permet de tester le montage avant qu'il
// existe la moindre interface.
//
// Ces fonctions ne portent PAS le specificateur Exec, et ce n'est pas un
// oubli : la chaine de routage des commandes console (voir Player.cpp)
// interroge le monde, le PlayerInput, le controller, le pawn, le HUD, le
// GameMode, le CheatManager, le GameState et le camera manager -- jamais
// les composants, AActor ne surchargeant pas ProcessConsoleExec pour les
// parcourir. Une UFUNCTION(Exec) posee ici serait silencieusement ignoree.
// Les commandes vivent donc sur AFpsPlayer, qui delegue ici.
// ------------------------------------------------------------------
/**
* Cherche, parmi les slots de materiau du mesh de tete, celui dont le nom
* contient SlotHint. INDEX_NONE si aucun -- l'appelant ne pose alors rien.
*/
static int32 FindEyeMaterialSlot(const USkeletalMeshComponent* HeadComponent, FName SlotHint);
/** Categories : body, head, hair, eyebrows, beard, mustache, eyes. */
UFUNCTION(BlueprintCallable, Category = "Appearance|Debug")
void ApplyPartByName(const FString& Part, int32 Index);
/** Categories : skin, hair, eye, underwear. */
UFUNCTION(BlueprintCallable, Category = "Appearance|Debug")
void ApplyColorByName(const FString& Part, int32 ColorIndex);
/** Ecrit l'apparence courante et le nombre d'options de chaque categorie. */
UFUNCTION(BlueprintCallable, Category = "Appearance|Debug")
void LogAppearance() const;
protected:
virtual void OnUnregister() override;
private:
/**
* Trouve le mesh porteur : celui de l'ACharacter en priorite, sinon le
* premier SkeletalMesh de l'acteur.
*
* L'ordre compte et n'est pas anodin : AFpsPlayer porte AUSSI FirstPersonArms,
* et prendre « le premier trouve » y monterait la tete du personnage sur les
* bras vus a l'ecran.
*/
USkeletalMeshComponent* ResolveTargetMesh();
/** Cree le composant d'une categorie s'il manque, et le rend. */
USkeletalMeshComponent* EnsurePartComponent(ECharacterPartCategory Category);
/** Cree un composant enfant deja attache, leader-pose et configure. */
USkeletalMeshComponent* CreateFollowerComponent(FName ComponentName);
/** Assigne les meshes de corps, l'AnimBP, les pieces et le materiau d'iris. */
void RebuildMeshes(const FCharacterBodySet& BodySet);
/** Pose les quatre couleurs sur les materiaux dynamiques. */
void ApplyColors(const FCharacterBodySet& BodySet);
/** Pose un parametre vectoriel sur TOUS les elements d'un mesh. */
/**
* Assigne un mesh a un composant en vidant d'abord ses materiaux surcharges.
*
* Indispensable : OverrideMaterials appartient au COMPOSANT, pas au mesh, et
* survit donc a un changement d'asset. Sans ce vidage, le corps B garderait
* la peau du corps A.
*/
static void SetPartMesh(USkeletalMeshComponent* MeshComponent, USkeletalMesh* NewMesh);
static void TintAllElements(USkeletalMeshComponent* MeshComponent, FName Parameter, const FLinearColor& Color);
/** Le mesh porteur, leader de tous les autres. */
UPROPERTY(Transient)
TObjectPtr<USkeletalMeshComponent> TargetMesh;
/**
* Un composant par categorie, indexe par ECharacterPartCategory.
*
* Un tableau plutot qu'un membre nomme par piece : le montage tient alors
* dans une boucle, et ajouter une categorie ne demande qu'une valeur
* d'enum. Une case peut rester nulle -- une silhouette sans barbe n'engendre
* jamais le composant correspondant.
*/
UPROPERTY(Transient)
TArray<TObjectPtr<USkeletalMeshComponent>> PartComponents;
/**
* Les morceaux de corps au-dela du porteur. Vide aujourd'hui : le catalogue
* n'a qu'un mesh par silhouette. C'est ici qu'atterriront les segments le
* jour des armures, sans que la logique change.
*/
UPROPERTY(Transient)
TArray<TObjectPtr<USkeletalMeshComponent>> ExtraBodyComponents;
FCharacterAppearance Appearance;
/** Faux tant qu'aucun montage n'a eu lieu : force la premiere reconstruction. */
bool bBuilt = false;
};
@@ -0,0 +1,143 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "CharacterAppearanceTypes.generated.h"
class USkeletalMesh;
class UTexture2D;
/** Les deux silhouettes livrees par le pack. */
UENUM(BlueprintType)
enum class ECharacterBodyType : uint8
{
BodyA UMETA(DisplayName = "Body A"),
BodyB UMETA(DisplayName = "Body B"),
/** Sentinelle, comme ECraftingStation::Count. Jamais une valeur valide. */
Count UMETA(Hidden)
};
/**
* Les emplacements de pieces montees sur le corps.
*
* Sert d'index dans le tableau de composants du monteur : une seule fonction de
* montage plutot qu'un membre nomme par piece, et ajouter une categorie ne
* demande qu'une entree ici.
*/
UENUM(BlueprintType)
enum class ECharacterPartCategory : uint8
{
Head,
Hair,
Eyebrows,
Beard,
Mustache,
Count UMETA(Hidden)
};
/**
* Une entree du catalogue : la piece que le joueur choisit dans une categorie.
*/
USTRUCT(BlueprintType)
struct FCharacterPartEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Part")
TObjectPtr<USkeletalMesh> Mesh;
/**
* La vignette du bouton, dans la grille de l'ecran de customisation.
* Meme type que l'icone d'UItemDataAsset, pour que les deux grilles du jeu
* se dessinent de la meme facon.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Part")
TObjectPtr<UTexture2D> Icon;
};
/**
* L'apparence d'un personnage, en 11 octets.
*
* Des INDEX et jamais des chemins d'assets, pour deux raisons qui vont dans le
* meme sens : un chemin coute une chaine de caracteres a chaque replication,
* alors qu'un index tient dans un octet ; et un index se VALIDE contre le
* catalogue, quand un chemin envoye par un client pourrait designer n'importe
* quel asset du jeu.
*
* Les couleurs suivent la meme logique : un index dans la palette, pas un
* FLinearColor. C'est ce qui permet a la struct entiere de tenir dans une
* poignee d'octets, et de se sauvegarder telle quelle dans le .ini de
* USurvivalUserSettings.
*/
USTRUCT(BlueprintType)
struct FCharacterAppearance
{
GENERATED_BODY()
/** Index qui signifie « aucune piece » -- barbe rasee, crane nu. */
static constexpr uint8 NoPart = 255;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
ECharacterBodyType BodyType = ECharacterBodyType::BodyA;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
uint8 HeadIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
uint8 HairIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
uint8 EyebrowsIndex = 0;
/** NoPart par defaut : un visage glabre est le point de depart naturel. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
uint8 BeardIndex = NoPart;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
uint8 MustacheIndex = NoPart;
/**
* Les yeux ne sont pas un mesh mais un MATERIAU pose sur un slot du mesh de
* tete -- le pack ne livre aucun `SKM_Eyes`. D'ou une categorie a part,
* absente d'ECharacterPartCategory qui ne parle que de composants.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
uint8 EyesIndex = 0;
// --------------------------------------------------------------
// Couleurs : un index dans la palette correspondante du catalogue.
// Un bouton de l'ecran = un index, sans reglage intermediaire.
// --------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance|Colors")
uint8 SkinColorIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance|Colors")
uint8 HairColorIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance|Colors")
uint8 EyeColorIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance|Colors")
uint8 UnderwearColorIndex = 0;
bool operator==(const FCharacterAppearance& Other) const
{
return BodyType == Other.BodyType
&& HeadIndex == Other.HeadIndex
&& HairIndex == Other.HairIndex
&& EyebrowsIndex == Other.EyebrowsIndex
&& BeardIndex == Other.BeardIndex
&& MustacheIndex == Other.MustacheIndex
&& EyesIndex == Other.EyesIndex
&& SkinColorIndex == Other.SkinColorIndex
&& HairColorIndex == Other.HairColorIndex
&& EyeColorIndex == Other.EyeColorIndex
&& UnderwearColorIndex == Other.UnderwearColorIndex;
}
bool operator!=(const FCharacterAppearance& Other) const { return !(*this == Other); }
};
@@ -0,0 +1,193 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "CharacterAppearanceTypes.h"
#include "CharacterPreviewActor.h"
#include "CharacterCustomizationWidget.generated.h"
class ACharacterPreviewActor;
class UButton;
class UCharacterPartsDataAsset;
class UCustomizationCategoryWidget;
class UCustomizationOptionWidget;
class UPanelWidget;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCustomizationConfirmed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCustomizationCancelled);
/**
* Ce que le joueur peut regler, dans l'ordre d'affichage de la colonne.
*
* Pieces et couleurs sont dans le MEME enum : ce sont onze rangees de la meme
* colonne, et les distinguer par deux listes obligerait a dessiner deux fois la
* meme chose. La difference ne vit que dans BuildOptions, qui remplit la grille
* de vignettes ou de pastilles selon le cas.
*/
UENUM(BlueprintType)
enum class ECustomizationCategory : uint8
{
Body,
Head,
Hair,
Eyebrows,
Beard,
Mustache,
Eyes,
SkinColor,
HairColor,
EyeColor,
UnderwearColor,
Count UMETA(Hidden)
};
/**
* L'ecran de customisation : la colonne de reglages a gauche, le personnage a
* droite.
*
* Le widget ne monte rien lui-meme -- il pousse une FCharacterAppearance sur
* ACharacterPreviewActor et lit le catalogue pour savoir quoi proposer. C'est
* le meme partage des roles que partout ailleurs dans le projet : l'interface
* affiche et demande, le systeme decide.
*
* Classe Abstract : on cree un WBP_CharacterCustomization qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API UCharacterCustomizationWidget : public UUserWidget
{
GENERATED_BODY()
public:
UCharacterCustomizationWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeOnInitialized() override;
virtual void NativeConstruct() override;
/**
* Echap = Retour, comme le bouton.
*
* Obligatoire ici et pas seulement confortable : dans la map du menu, le
* controller ne binde AUCUNE action d'input. Tout Echap qui fonctionne passe
* donc par le NativeOnKeyDown d'un widget -- il n'y a pas d'autre chemin.
*/
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
/** Le joueur valide : l'ecran a fini, l'apparence est dans GetAppearance(). */
UPROPERTY(BlueprintAssignable, Category = "Customization")
FOnCustomizationConfirmed OnConfirmed;
/** Le joueur renonce : rien n'a ete valide, on rend la main au menu. */
UPROPERTY(BlueprintAssignable, Category = "Customization")
FOnCustomizationCancelled OnCancelled;
/**
* Branche l'ecran sur son mannequin et son point de depart.
*
* A appeler AVANT d'ajouter le widget au viewport : NativeConstruct
* reconstruit les listes, et il les batirait vides sans catalogue.
*
* Surtout PAS nommee Initialize : UUserWidget en a deja une, virtuelle, et
* une surcharge de signature differente la masquerait au lieu de la
* surcharger (C4263/C4264). Meme famille de piege que les membres nommes
* Player ou Slot dans une classe derivee du moteur.
*/
UFUNCTION(BlueprintCallable, Category = "Customization")
void SetupCustomization(ACharacterPreviewActor* InPreview, const FCharacterAppearance& InAppearance);
UFUNCTION(BlueprintPure, Category = "Customization")
const FCharacterAppearance& GetAppearance() const { return Appearance; }
protected:
/**
* Le catalogue. Assigne-lui le meme DA_CharacterParts que BP_FpsPlayer.
*
* Sur le widget et pas pris sur le mannequin : l'ecran doit pouvoir batir
* ses listes meme si le mannequin manque encore, et un catalogue different
* des deux cotes se verrait immediatement a l'ecran.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Customization")
TObjectPtr<UCharacterPartsDataAsset> Catalog;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Customization")
TSubclassOf<UCustomizationCategoryWidget> CategoryWidgetClass;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Customization")
TSubclassOf<UCustomizationOptionWidget> OptionWidgetClass;
/** La colonne des categories. Un Vertical Box nomme exactement ainsi. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UPanelWidget> CategoryList;
/** La grille d'options. Un Wrap Box va tres bien. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UPanelWidget> OptionGrid;
/** Valide et lance la partie. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> PlayButton;
/** Retour au menu principal, sans rien valider. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> BackButton;
// ------------------------------------------------------------------
// Rotation du personnage au clic-glisser
// ------------------------------------------------------------------
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual FReply NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
private:
/** Reconstruit la colonne de gauche. Une seule fois, les categories sont fixes. */
void BuildCategories();
/** Reconstruit la grille selon la categorie active. */
void BuildOptions();
/** Repose l'apparence sur le mannequin et rafraichit la selection affichee. */
void RefreshPreview();
/** Encadre la case qui porte le choix courant, efface les autres. */
void RefreshSelection();
/** L'index courant dans la categorie active, pour l'encadrement. */
int32 GetCurrentIndexForCategory(ECustomizationCategory Category) const;
/** Ecrit un choix dans l'apparence. Le seul endroit qui la modifie. */
void SetIndexForCategory(ECustomizationCategory Category, int32 NewIndex);
/** Combien d'options a proposer, catalogue en main. */
int32 GetOptionCountForCategory(ECustomizationCategory Category) const;
UFUNCTION()
void HandleCategoryClicked(int32 CategoryIndex);
UFUNCTION()
void HandleOptionClicked(int32 OptionIndex);
UFUNCTION()
void HandlePlayClicked();
UFUNCTION()
void HandleBackClicked();
UPROPERTY(Transient)
TObjectPtr<ACharacterPreviewActor> Preview;
UPROPERTY(Transient)
TArray<TObjectPtr<UCustomizationCategoryWidget>> CategoryWidgets;
UPROPERTY(Transient)
TArray<TObjectPtr<UCustomizationOptionWidget>> OptionWidgets;
FCharacterAppearance Appearance;
ECustomizationCategory ActiveCategory = ECustomizationCategory::Body;
/** Vrai entre l'appui et le relachement, pendant qu'on fait tourner. */
bool bDragging = false;
};
@@ -0,0 +1,194 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CharacterAppearanceTypes.h"
#include "CharacterPartsDataAsset.generated.h"
class UAnimInstance;
class UMaterialInterface;
class USkeletalMesh;
class UTexture2D;
/**
* Tout ce qui compose une silhouette : ses meshes de corps, son AnimBP, et les
* pieces disponibles pour chaque categorie.
*/
USTRUCT(BlueprintType)
struct FCharacterBodySet
{
GENERATED_BODY()
/** La vignette du bouton de silhouette, meme role que celle d'une piece. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Body")
TObjectPtr<UTexture2D> Icon;
/**
* Les morceaux du corps, dans l'ordre. Le PREMIER est le porteur : c'est lui
* qui va sur le composant Mesh du Character et qui joue l'animation, les
* suivants deviennent des composants enfants en leader pose.
*
* Une LISTE alors qu'il n'y a qu'une case aujourd'hui, et ce n'est pas de la
* sur-conception : le pack livre exprès `SKM_BodyA_torso`, `_arms`,
* `_legs_01/02/03`, `_hands`, `_feet`, precisement pour qu'une armure puisse
* eteindre les segments qu'elle recouvre. Le jour venu, la case 0 deviendra
* `SKM_BodyA_empty` -- invisible, porteur d'animation -- et les huit segments
* suivront derriere, SANS que ce code change. Un TObjectPtr unique, lui,
* imposerait de tout reprendre au premier plastron.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Body")
TArray<TObjectPtr<USkeletalMesh>> BodyMeshes;
/** L'AnimBP du porteur. Il change avec la silhouette, d'ou sa place ici. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Body")
TSubclassOf<UAnimInstance> AnimClass;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Parts")
TArray<FCharacterPartEntry> Heads;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Parts")
TArray<FCharacterPartEntry> Hairstyles;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Parts")
TArray<FCharacterPartEntry> Eyebrows;
/** Vide pour une silhouette qui n'en porte pas -- le pack n'en donne qu'a A. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Parts")
TArray<FCharacterPartEntry> Beards;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Parts")
TArray<FCharacterPartEntry> Mustaches;
/**
* Fragment cherche dans le NOM des slots de materiau du mesh de tete pour y
* trouver celui des yeux.
*
* Les yeux ne sont pas un mesh : le pack les livre comme un element du mesh
* de tete. Il ne suffit donc pas de poser un parametre de couleur, il faut
* REMPLACER le materiau de ce slot -- encore faut-il savoir lequel.
*
* Un fragment de nom et pas un index, parce qu'un index est faux deux fois :
* il oblige a le verifier a la main dans l'editeur, et il suppose surtout
* que les six tetes rangent leurs slots dans le MEME ordre. Les slots du
* pack s'appellent `M_Eye_A_01`, `M_Eye`, `M_Eyes_02`... tous contiennent
* « Eye », aucun slot de peau ne le contient.
*
* Si le fragment ne trouve rien, le materiau n'est PAS pose et un
* avertissement part dans le log. C'est volontaire : poser l'oeil sur un
* slot au hasard repeindrait le visage entier, un symptome qu'on met bien
* plus longtemps a diagnostiquer qu'une ligne de log.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Parts")
FName EyeMaterialSlotHint = TEXT("Eye");
};
/**
* Le catalogue : la table de correspondance entre les index d'une
* FCharacterAppearance et les assets reels.
*
* Un seul asset a remplir, et le SEUL endroit a toucher pour ajouter une
* coiffure. C'est aussi lui qui borne ce qu'un client peut demander : une
* apparence arrivee du reseau est passee a Sanitize(), qui ramene tout index
* hors bornes a une valeur sure.
*/
UCLASS(BlueprintType)
class EMBERWILD_API UCharacterPartsDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/**
* Une entree par valeur d'ECharacterBodyType, dans l'ordre de l'enum.
* L'ordre EST le contrat : c'est lui qui donne son sens a BodyType.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog")
TArray<FCharacterBodySet> BodySets;
// ------------------------------------------------------------------
// Palettes : une couleur = un bouton a l'ecran, sans reglage entre les
// deux. L'ordre EST l'ordre d'affichage, et l'index qui part sur le reseau.
//
// Partagees par les deux silhouettes : deux nuanciers de peau a tenir a
// jour divergeraient au premier ajout.
// ------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Palettes")
TArray<FLinearColor> SkinColors;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Palettes")
TArray<FLinearColor> HairColors;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Palettes")
TArray<FLinearColor> EyeColors;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Palettes")
TArray<FLinearColor> UnderwearColors;
/** Les materiaux d'iris, un par forme d'oeil proposee. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Palettes")
TArray<TObjectPtr<UMaterialInterface>> EyeMaterials;
// ------------------------------------------------------------------
// Noms des parametres des materiaux du pack.
//
// Ici et pas en dur dans le C++ : ils appartiennent aux assets, pas au
// code. Changer de pack de personnages ne doit demander qu'un nouveau
// catalogue -- et ces noms contiennent des ESPACES, ce qu'on n'invente
// pas de memoire.
// ------------------------------------------------------------------
/** `M_Body_Base`. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Material Parameters")
FName SkinColorParameter = TEXT("Skin color");
/**
* `M_Head_Base` -- le meme role que ci-dessus, mais l'autre master, et
* surtout un NOM DIFFERENT : `Skin` cote tete, `Skin color` cote corps.
*
* Le master contient bien la chaine « Skin Base color », mais ce n'est pas
* le nom du parametre expose -- verifie dans `MI_HeadA_01_a`, ou le chercher
* ne rend aucun resultat alors que `Skin` apparait dans le groupe
* « Base colors ». Morale : un nom de parametre se lit dans l'editeur du
* Material Instance, jamais dans les chaines du binaire du master.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Material Parameters")
FName HeadSkinColorParameter = TEXT("Skin");
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Material Parameters")
FName UnderwearColorParameter = TEXT("Underware color");
/** `M_Hairstyle_Base`, parent des cheveux, barbes, moustaches ET sourcils. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Material Parameters")
FName HairColorParameter = TEXT("Color");
/** `M_Eyes_Base`. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Catalog|Material Parameters")
FName EyeColorParameter = TEXT("Eye color");
// ------------------------------------------------------------------
// Lecture
// ------------------------------------------------------------------
/** Le jeu de pieces d'une silhouette, ou nullptr si l'index est hors bornes. */
const FCharacterBodySet* GetBodySet(ECharacterBodyType BodyType) const;
/** La liste d'une categorie pour une silhouette, vide si rien n'est defini. */
const TArray<FCharacterPartEntry>& GetParts(ECharacterBodyType BodyType, ECharacterPartCategory Category) const;
/** Nombre d'options d'une categorie. Sert a borner l'UI comme la console. */
UFUNCTION(BlueprintPure, Category = "Catalog")
int32 GetPartCount(ECharacterBodyType BodyType, ECharacterPartCategory Category) const;
/**
* Ramene une apparence dans les bornes du catalogue.
*
* Appelee sur le SERVEUR a la reception d'une apparence client, et au
* chargement des reglages -- un catalogue ampute entre deux versions du jeu
* laisserait sinon des index qui ne designent plus rien. Les pieces
* facultatives (barbe, moustache) retombent sur NoPart, les obligatoires
* sur 0 : mieux vaut la mauvaise tete que pas de tete.
*/
void Sanitize(FCharacterAppearance& InOut) const;
};
@@ -0,0 +1,146 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CharacterAppearanceTypes.h"
#include "CharacterPreviewActor.generated.h"
class UCameraComponent;
class UCharacterAppearanceComponent;
class USkeletalMeshComponent;
/**
* Les deux cadrages de l'ecran de customisation.
*
* Deux et pas un par categorie : ce qu'on veut, c'est voir le visage quand on
* choisit une barbe et la silhouette entiere quand on choisit un corps. Un
* cadrage par categorie donnerait onze reglages a poser a la main pour onze
* fois le meme resultat.
*/
UENUM(BlueprintType)
enum class ECharacterPreviewFraming : uint8
{
FullBody,
Face,
Count UMETA(Hidden)
};
/**
* Le mannequin de l'ecran de customisation : un personnage pose dans la map de
* menu, qu'on fait tourner a la souris et que la camera cadre selon ce que le
* joueur est en train de regler.
*
* Il porte le MEME UCharacterAppearanceComponent que le pawn de jeu, et c'est
* tout l'interet : le personnage qu'on regarde ici est monte par exactement le
* code qui montera celui que les autres joueurs verront. Deux chemins de
* montage finiraient par diverger, et le joueur s'en apercevrait au pire
* moment -- une fois en partie.
*/
UCLASS()
class EMBERWILD_API ACharacterPreviewActor : public AActor
{
GENERATED_BODY()
public:
ACharacterPreviewActor();
virtual void Tick(float DeltaSeconds) override;
/** Monte l'apparence sur le mannequin. */
UFUNCTION(BlueprintCallable, Category = "Preview")
void ApplyAppearance(const FCharacterAppearance& InAppearance);
/** La camera glisse vers le cadrage demande. Sans effet s'il y est deja. */
UFUNCTION(BlueprintCallable, Category = "Preview")
void SetFraming(ECharacterPreviewFraming NewFraming);
/** Fait pivoter le personnage. Appelee pendant un clic-glisser. */
UFUNCTION(BlueprintCallable, Category = "Preview")
void AddPreviewYaw(float DeltaDegrees);
/** Remet le personnage face a la camera. */
UFUNCTION(BlueprintCallable, Category = "Preview")
void ResetPreviewYaw();
UFUNCTION(BlueprintPure, Category = "Preview")
UCameraComponent* GetPreviewCamera() const { return PreviewCamera; }
UFUNCTION(BlueprintPure, Category = "Preview")
UCharacterAppearanceComponent* GetAppearanceComponent() const { return AppearanceComponent; }
/** Le reglage vit ici et pas sur le widget : c'est une sensibilite de camera. */
UFUNCTION(BlueprintPure, Category = "Preview")
float GetYawPerPixel() const { return YawPerPixel; }
protected:
virtual void BeginPlay() override;
/** Le porteur d'animation et de pose ; les pieces s'y accrochent. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview")
TObjectPtr<USkeletalMeshComponent> BodyMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview")
TObjectPtr<UCharacterAppearanceComponent> AppearanceComponent;
/**
* Pivot de rotation, entre la racine et le mesh.
*
* Le personnage tourne en faisant pivoter CE composant et jamais l'acteur :
* la camera est fille de l'acteur, la faire tourner avec lui donnerait un
* decor qui defile pendant qu'on regarde un personnage immobile.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview")
TObjectPtr<USceneComponent> TurntablePivot;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview")
TObjectPtr<UCameraComponent> PreviewCamera;
/**
* Les deux positions de camera, a placer A LA SOURIS dans le viewport du
* Blueprint plutot qu'a taper en coordonnees.
*
* Ce sont des reperes vides : la camera n'y est pas accrochee, elle glisse
* vers eux. Sans quoi changer de categorie la ferait sauter d'un cadrage a
* l'autre, ce qui se lit comme une coupure de montage.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview|Framing")
TObjectPtr<USceneComponent> FullBodyViewSpot;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview|Framing")
TObjectPtr<USceneComponent> FaceViewSpot;
/**
* Vitesse de glissement de la camera entre deux cadrages.
*
* Une vitesse d'interpolation et pas une duree : le mouvement freine en
* arrivant au lieu de s'arreter net, et deux changements de categorie
* coup sur coup s'enchainent sans a-coup -- la camera repart d'ou elle est.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Preview|Framing", meta = (ClampMin = "0.5"))
float FramingInterpSpeed = 6.f;
/** Degres de rotation par pixel de deplacement horizontal de la souris. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Preview|Turntable")
float YawPerPixel = 0.35f;
private:
/** Le repere vise par la camera, selon le cadrage courant. */
USceneComponent* GetSpotForFraming(ECharacterPreviewFraming InFraming) const;
/**
* Coupe le Tick quand la camera est arrivee.
*
* Meme principe que le couvercle d'AStorageContainer : cet acteur ne tick
* que pendant qu'il bouge. Un mannequin immobile dans un menu n'a aucune
* raison de couter une frame.
*/
void UpdateFramingInterpolation(float DeltaSeconds);
ECharacterPreviewFraming CurrentFraming = ECharacterPreviewFraming::FullBody;
/** Rotation accumulee par le clic-glisser, en degres. */
float TurntableYaw = 0.f;
};
@@ -0,0 +1,123 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Containers/Ticker.h"
#include "ConfirmDialogWidget.generated.h"
class UButton;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnConfirmDialogConfirmed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnConfirmDialogCancelled);
/**
* Boite de dialogue "es-tu sur ?", volontairement generique.
*
* Elle ne sait pas ce qu'elle confirme : elle pose la question et previent via
* ses delegues. C'est l'appelant qui decide de l'action. Ainsi la meme boite
* servira pour quitter, effacer une sauvegarde ou abandonner une partie.
*
* Classe Abstract : on cree un WBP_ConfirmDialog qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API UConfirmDialogWidget : public UUserWidget
{
GENERATED_BODY()
public:
UConfirmDialogWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
UPROPERTY(BlueprintAssignable, Category = "Dialogue")
FOnConfirmDialogConfirmed OnConfirmed;
UPROPERTY(BlueprintAssignable, Category = "Dialogue")
FOnConfirmDialogCancelled OnCancelled;
/**
* Affiche la boite et lui donne le focus clavier.
*
* Passe par ici et jamais par SetVisibility directement : sans le focus,
* la boite ne recevrait aucune touche et Echap resterait sans effet.
*/
UFUNCTION(BlueprintCallable, Category = "Dialogue")
void Open(const FText& InMessage);
/**
* Meme chose, mais avec un retour automatique passe le delai.
*
* @param InMessageFormat doit contenir {0}, remplace par les secondes
* restantes. Exemple : "Garder ce mode ?\nRetour
* automatique dans {0} s."
*
* A l'expiration, OnCancelled est diffuse -- comme si le joueur avait
* refuse. C'est le seul filet quand la question porte sur un changement qui
* peut rendre l'ecran illisible.
*/
UFUNCTION(BlueprintCallable, Category = "Dialogue")
void OpenWithCountdown(const FText& InMessageFormat, float Seconds);
/** Referme la boite. Ne previent personne : l'appelant sait ce qu'il fait. */
UFUNCTION(BlueprintCallable, Category = "Dialogue")
void Close();
protected:
/** Echap equivaut a cliquer Annuler. Decoche pour une question incontournable. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dialogue")
bool bCancelOnEscape = true;
/**
* Entree equivaut a cliquer Confirmer.
*
* Desactive par defaut, volontairement : sur une question destructrice, un
* joueur qui martele Entree validerait sans avoir lu.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dialogue")
bool bConfirmOnEnter = false;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ConfirmButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CancelButton;
/** Optionnel : une boite dont le texte est fige dans le Blueprint reste valide. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> MessageText;
private:
UFUNCTION()
void HandleConfirmClicked();
UFUNCTION()
void HandleCancelClicked();
/** Un tick par frame tant que le rebours tourne. Renvoie false pour s'arreter. */
bool TickCountdown(float DeltaTime);
void StopCountdown();
void RefreshCountdownText();
/**
* Le rebours passe par le ticker du moteur et non par un timer du monde.
*
* Deux raisons : l'ecran de reglages s'ouvre depuis le menu de PAUSE, ou les
* timers du monde sont geles -- le rebours ne repartirait jamais ; et un
* changement de resolution rate doit se defaire meme si le jeu bloque.
*/
FTSTicker::FDelegateHandle CountdownTickerHandle;
FText CountdownMessageFormat;
float CountdownRemaining = 0.f;
/** Evite de reecrire le texte 60 fois par seconde pour rien. */
int32 LastDisplayedSeconds = -1;
};
+508
View File
@@ -0,0 +1,508 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.h"
#include "CookingStation.generated.h"
class UInventoryComponent;
class UItemDataAsset;
class UNiagaraComponent;
class UPointLightComponent;
class USoundBase;
class USphereComponent;
class UStaticMeshComponent;
/**
* Ce qu'un emplacement du grill porte a l'instant t.
*
* Uniquement de l'etat : le DataAsset dit ce que l'objet EST, cette struct dit
* ou il en est. C'est la meme separation que FInventorySlot, pour la meme raison.
*/
USTRUCT(BlueprintType)
struct FCookingSlotState
{
GENERATED_BODY()
/** L'objet pose, ou nullptr. Un seul exemplaire par emplacement, comme dans Raft. */
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UItemDataAsset> Item = nullptr;
/**
* Charges restantes de l'exemplaire pose. -1 = plein.
* L'usure traverse la cuisson comme elle traverse le sol : ce qu'on
* recupere est le meme exemplaire, pas un neuf.
*/
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
int32 RemainingUses = -1;
/**
* Compte a rebours de l'etape en cours. Pas une UPROPERTY : un FTimerHandle
* n'est qu'un index dans le gestionnaire de timers, il n'y a rien a
* collecter ni a serialiser.
*/
FTimerHandle Timer;
bool IsEmpty() const { return Item == nullptr; }
};
/**
* Une buche dans le foyer : QUEL objet a ete mis, et combien de temps il tient
* encore.
*
* C'est le coeur du modele : le carburant n'est pas une jauge de secondes mais
* une pile d'objets. Cinq buches, cinq objets -- une branche qui tient dix
* secondes occupe exactement la meme place qu'une planche qui en tient cent
* quatre-vingts. Le joueur voit donc ce qu'il a donne au feu, pas un
* pourcentage abstrait, et melanger les combustibles reste lisible.
*/
USTRUCT(BlueprintType)
struct FFuelSlotState
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UItemDataAsset> Item = nullptr;
/** Secondes de combustion restantes pour CE morceau. */
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
float RemainingSeconds = 0.f;
};
/** L'etat de la station a change : allumage, depot, transformation, retrait. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCookingStationChanged);
/**
* Ce que l'appui sur Interagir va declencher, compte tenu de ce que le joueur
* vise et de ce qu'il tient.
*
* Pas un UENUM : c'est un detail d'implementation, rien a l'exterieur n'a
* besoin de le nommer.
*/
enum class ECookingAction : uint8
{
None,
TakeItem,
PlaceItem,
AddFuel
};
/**
* Resultat complet de la decision. Calcule UNE fois, par ResolveAction(), et
* servi a la fois au texte du prompt et a l'execution : deux cascades de `if`
* separees finiraient par diverger, et le joueur lirait "Light the fire" en
* obtenant autre chose.
*
* Struct de pile a duree de vie tres courte, d'ou les pointeurs bruts : rien
* ici ne peut etre collecte entre le calcul et son usage.
*/
struct FCookingInteraction
{
ECookingAction Action = ECookingAction::None;
/** Emplacement de cuisson vise, ou INDEX_NONE si le joueur regarde le foyer. */
int32 SlotIndex = INDEX_NONE;
/** Objet actif de la barre rapide. */
UItemDataAsset* HeldItem = nullptr;
/** Slot d'inventaire d'ou sortira le bois, et l'objet correspondant. */
int32 FuelSourceSlot = INDEX_NONE;
UItemDataAsset* FuelItem = nullptr;
};
/**
* Poste de cuisson pose dans le monde : feu de camp, et plus tard fourneau ou
* sechoir en changeant simplement ses reglages.
*
* Modele Raft assume : AUCUNE interface. Le joueur tient sa viande crue dans la
* barre rapide, vise un emplacement du grill, appuie sur Interagir, et regarde
* le mesh changer.
*
* Le poste ne commente jamais son propre etat : le prompt n'affiche QUE des
* actions realisables, exactement comme un objet a ramasser. Ce qui cuit, ce
* qui manque et ce qui brule se lit sur les flammes, sur le nombre de buches
* et sur le mesh de la nourriture -- l'ecrire en plus reconstruirait une
* interface par-dessus un monde qui se suffit a lui-meme.
*
* Aucun Tick. Un timer par emplacement de cuisson, un seul pour le carburant :
* celui de la buche en train de bruler. Eteindre le feu met ces timers en
* pause, et la cuisson reprend exactement ou elle s'etait arretee sans qu'une
* ligne ne compte le temps a la main.
*
* Le feu ne s'allume PAS a la main : il part des qu'il a du bois et quelque
* chose a transformer, et s'arrete des qu'il n'a plus rien a faire. Le bois ne
* part donc jamais en fumee pour rien -- ce qui evite au joueur d'avoir a
* penser a eteindre derriere lui, une corvee qui n'apporte aucune decision
* interessante.
*/
UCLASS()
class EMBERWILD_API ACookingStation : public AActor, public IInteractable
{
GENERATED_BODY()
public:
ACookingStation();
// --- IInteractable ---
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
virtual bool CanInteract_Implementation(AActor* Interactor) const override;
virtual void Interact_Implementation(AActor* Interactor) override;
virtual void OnBeginFocus_Implementation() override;
virtual void OnEndFocus_Implementation() override;
/**
* Allume le feu s'il reste du bois dans le foyer.
*
* En temps normal, personne n'appelle ceci : UpdateFireState() decide seul.
* Reste exposee pour un futur allume-feu ou un evenement scenarise.
* @return false si le foyer est vide.
*/
UFUNCTION(BlueprintCallable, Category = "Cooking")
bool Light();
/** Eteint le feu. Les cuissons en cours se figent, elles ne repartent pas de zero. */
UFUNCTION(BlueprintCallable, Category = "Cooking")
void Extinguish();
/** Vrai si au moins un emplacement porte quelque chose qui a encore une etape a franchir. */
UFUNCTION(BlueprintPure, Category = "Cooking")
bool HasPendingWork() const;
UFUNCTION(BlueprintPure, Category = "Cooking")
bool IsLit() const { return bIsLit; }
/**
* Combien de morceaux de bois le foyer peut contenir.
*
* C'est le nombre de buches taguees dans le Blueprint : la capacite est une
* decision de mise en scene, et la regler a deux endroits garantirait qu'ils
* finissent par se contredire. FallbackFuelCapacity ne sert qu'aux postes
* sans buche visible, un four ferme par exemple.
*/
UFUNCTION(BlueprintPure, Category = "Cooking")
int32 GetFuelCapacity() const { return FuelIndicators.Num() > 0 ? FuelIndicators.Num() : FallbackFuelCapacity; }
/** Morceaux de bois actuellement dans le foyer. */
UFUNCTION(BlueprintPure, Category = "Cooking")
int32 GetFuelCount() const { return Fuel.Num(); }
/** Secondes de combustion cumulees sur tout ce qui est dans le foyer. */
UFUNCTION(BlueprintPure, Category = "Cooking")
float GetFuelRemaining() const;
/** Secondes avant la prochaine transformation, ou 0 si l'emplacement n'attend rien. */
UFUNCTION(BlueprintPure, Category = "Cooking")
float GetSlotTimeRemaining(int32 SlotIndex) const;
UFUNCTION(BlueprintPure, Category = "Cooking")
const TArray<FCookingSlotState>& GetSlots() const { return Slots; }
UPROPERTY(BlueprintAssignable, Category = "Cooking")
FOnCookingStationChanged OnStationChanged;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/**
* Nombre d'emplacements de cuisson existant dans la classe. Les composants
* sont crees au constructeur, donc ce nombre est fige a la compilation --
* c'est le prix a payer pour pouvoir les positionner A LA SOURIS dans le
* viewport du Blueprint. Des composants engendres a l'execution ne seraient
* reglables qu'en tapant des coordonnees a l'aveugle.
*/
static constexpr int32 MaxCookingSlots = 4;
// ------------------------------------------------------------------
// Composants
// ------------------------------------------------------------------
/** Le foyer lui-meme : bois, pierres, structure. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UStaticMeshComponent> BaseMesh;
/**
* Ce que le trace d'interaction vient toucher. Sphere dediee et non le mesh,
* pour la meme raison que sur APickupItem : rien ne garantit qu'un mesh
* d'asset pack ait une collision.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TObjectPtr<USphereComponent> InteractionSphere;
/**
* Un mesh par emplacement de cuisson, qui porte la nourriture posee. Sa
* transform sert a la fois a l'afficher et a savoir ce que le joueur vise :
* pas besoin d'une collision par emplacement, donc pas une requete physique
* de plus.
*
* Positionne-les dans le viewport du Blueprint. Ils sont caches au demarrage
* tant qu'ils sont vides, mais restent visibles dans l'editeur pour que tu
* puisses les placer.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TArray<TObjectPtr<UStaticMeshComponent>> SlotMeshes;
/** Flamme. Active tant que le feu brule, coupee des qu'il s'eteint. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UNiagaraComponent> FireEffect;
/**
* Halo du foyer, allume en meme temps que la flamme.
*
* Regle intensite, couleur et rayon directement sur le composant dans le
* Blueprint : ce sont des proprietes natives, les redeclarer ici en
* doublon garantirait qu'un jour les deux se contredisent.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UPointLightComponent> FireLight;
// ------------------------------------------------------------------
// Reglages
// ------------------------------------------------------------------
/** Emplacements de cuisson reellement utilises. Les autres restent caches. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "1", ClampMax = "4"))
int32 CookingSlotCount = 2;
/**
* Echelle appliquee au mesh de la nourriture posee.
* Les meshes d'objets sont dimensionnes pour etre poses au sol ; sur un
* grill ils sont souvent trop gros.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "0.01"))
float CookingMeshScale = 1.f;
/**
* Demi-angle du cone de visee autour de chaque emplacement, en degres.
*
* Au-dela, on considere que le joueur vise le foyer et pas un emplacement :
* c'est ce qui lui permet d'ajouter du bois ou d'allumer le feu sans devoir
* eviter le grill du regard.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "1", ClampMax = "89"))
float SlotAimHalfAngle = 25.f;
/** Multiplie la vitesse de cuisson. Un fourneau va plus vite que des braises. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "0.01"))
float CookSpeedMultiplier = 1.f;
/**
* Autorise a reprendre un objet qui n'a pas fini de cuire.
*
* Decoche par defaut : ce qu'on a mis a cuire est engage, on attend. Un
* objet qui a fini sa cuisson reste toujours recuperable, y compris pendant
* qu'il se dirige vers le brule -- sans quoi il serait impossible de sauver
* sa viande a temps, ce qui viderait la sur-cuisson de tout son sens.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
bool bAllowTakingRawItems = false;
/**
* Ajoute les secondes restantes aux textes du prompt.
*
* Decoche en jeu : un chiffre qui s'egrene est de l'interface deguisee, et
* le joueur doit lire l'avancement sur le mesh, pas sur un compteur. Coche
* le temps d'un reglage pour verifier tes durees.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
bool bShowRemainingTimeInPrompt = false;
/** Active le Custom Depth quand le poste est vise, comme les ramassables. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
bool bHighlightOnFocus = true;
// ------------------------------------------------------------------
// Reglages : carburant
// ------------------------------------------------------------------
/**
* Tag a poser sur les meshes de buches qui figurent le contenu du foyer.
* Leur NOMBRE fait la capacite du feu, et leur ordre alphabetique l'ordre
* d'apparition : la derniere posee est la premiere a bruler.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
FName FuelIndicatorTag = TEXT("FuelLog");
/** Capacite des postes qui n'ont aucune buche visible. Ignore des qu'il y en a une. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel", meta = (ClampMin = "1"))
int32 FallbackFuelCapacity = 5;
/**
* Chaque buche prend le mesh de l'objet qu'elle represente.
*
* Decoche par defaut : les buches d'un feu de camp sont posees a la main
* dans le Blueprint, avec leur orientation et leur imbrication soignees, et
* y substituer un mesh d'objet gacherait ce travail. Coche-le pour un
* support ou l'on veut distinguer d'un coup d'oeil branches et planches.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
bool bFuelIndicatorsUseItemMesh = false;
/** Bois deja dans le foyer au demarrage. Laisse vide pour un feu a alimenter. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
TObjectPtr<UItemDataAsset> StartingFuelItem;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel", meta = (ClampMin = "0"))
int32 StartingFuelCount = 0;
// ------------------------------------------------------------------
// Sons. Tous facultatifs : un champ vide ne joue rien.
// ------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
TObjectPtr<USoundBase> LightSound;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
TObjectPtr<USoundBase> ExtinguishSound;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
TObjectPtr<USoundBase> PlaceItemSound;
private:
/**
* Quel emplacement de cuisson le joueur vise, ou INDEX_NONE s'il regarde le
* 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. */
FCookingInteraction ResolveAction(AActor* Interactor) const;
/** Depose l'objet actif de la barre rapide sur l'emplacement de cuisson. */
bool PlaceItemFromHand(int32 SlotIndex, AActor* Interactor);
/** Rend l'objet a l'inventaire. Refuse et laisse tout en place si c'est plein. */
bool TakeItemToInventory(int32 SlotIndex, AActor* Interactor);
/**
* Met UN morceau de bois dans le foyer, pris dans un slot d'inventaire.
*
* Un exemplaire par appui, jamais la pile entiere : donner du bois est un
* geste irreversible, et vider vingt branches d'un coup sur un feu qui n'en
* demandait qu'une ne se rattrape pas. Le joueur dose lui-meme.
*/
bool AddFuelFrom(int32 InventorySlotIndex, AActor* Interactor);
/**
* Ou prendre du bois : la main d'abord, sinon le premier slot qui en
* contient. La main est prioritaire parce que c'est le seul moyen pour le
* joueur de CHOISIR ce qu'il brule.
*/
static int32 FindFuelSource(const UInventoryComponent* Inventory, UItemDataAsset*& OutItem);
/** Peut-on retirer ce qui se trouve sur cet emplacement de cuisson. */
bool CanTakeFromSlot(int32 SlotIndex) const;
/** Arme le compte a rebours de l'etape courante de l'emplacement. */
void StartSlotTimer(int32 SlotIndex);
/** Fin d'etape : l'objet devient son resultat, et on enchaine s'il y a une suite. */
void HandleSlotFinished(int32 SlotIndex);
/** Recopie le WorldMesh de l'objet pose, ou cache le composant si l'emplacement est vide. */
void RefreshSlotMesh(int32 SlotIndex);
/**
* Allume ou eteint selon qu'il y ait, ou non, du bois ET du travail.
*
* Point de decision UNIQUE : toutes les actions qui changent le contenu du
* foyer ou du grill finissent ici. Laisser chaque action decider elle-meme
* d'allumer aurait fini par oublier un cas -- typiquement le dernier objet
* retire, qui laisserait le feu bruler dans le vide.
*/
void UpdateFireState();
/** La buche du dessus a fini de bruler : on passe a la suivante. */
void HandleFuelConsumed();
/** Arme le timer sur la buche en cours de combustion. */
void StartFuelTimer();
/** Recopie le restant du timer dans la buche courante avant de le modifier. */
void SyncFuelFromTimer();
/**
* Allume ou coupe tout ce qui se voit quand le feu brule.
*
* Un seul endroit pour la flamme et la lumiere : le jour ou s'ajoute une
* boucle sonore ou un panache de fumee, il n'y aura toujours qu'un point a
* modifier au lieu de deux appels a retrouver dans Light() et Extinguish().
*/
void ApplyFireVisuals(bool bActive);
/** Retrouve les buches par leur tag, une fois pour toutes, au demarrage. */
void CacheFuelIndicators();
/**
* Montre exactement autant de buches qu'il y a de morceaux dans le foyer.
*
* Plus aucun calcul de palier ni de timer d'affichage depuis que le
* carburant est une pile d'objets : une buche visible EST un objet stocke.
*/
void RefreshFuelIndicators();
/**
* Ce que cet objet va devenir sur le feu, et en combien de temps.
* La cuisson passe avant la combustion : voir le commentaire de bCanBurn
* sur UItemDataAsset.
*/
static UItemDataAsset* GetNextStage(const UItemDataAsset* Item, float& OutDuration);
/** Inventaire du pawn qui interagit, ou nullptr. */
static UInventoryComponent* GetInventoryOf(AActor* Interactor);
/**
* 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;
/**
* Le contenu du foyer, dans l'ordre ou il a ete donne. L'index 0 est celui
* qui brule ; quand il est consomme, tout le monde avance d'un cran et la
* derniere buche disparait du foyer.
*/
/**
* 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;
/**
* Buches de mise en scene, resolues au BeginPlay et triees par nom.
* Transient : ces pointeurs designent des composants du Blueprint, il n'y a
* rien a sauvegarder, seulement a garder du GC le temps de la partie.
*/
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();
};
+190
View File
@@ -0,0 +1,190 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CraftingRecipeDataAsset.h"
#include "CraftingComponent.generated.h"
class UCraftingRecipeBook;
class UInventoryComponent;
class UItemDataAsset;
/**
* Pourquoi une fabrication a echoue. Sert a l'interface, pour dire au joueur
* ce qui manque au lieu de laisser un bouton grise sans explication.
*/
UENUM(BlueprintType)
enum class ECraftFailureReason : uint8
{
None,
InvalidRecipe,
NoRecipeBook,
StationUnavailable,
MissingIngredients
};
/**
* Etat d'un ingredient pour une recette donnee : ce qu'il faut, ce qu'on a.
*
* C'est le composant qui fait le rapprochement, pas le widget. Une ligne
* d'ingredient n'a plus qu'a afficher deux nombres et choisir une couleur --
* elle n'interroge jamais l'inventaire elle-meme.
*/
USTRUCT(BlueprintType)
struct FCraftIngredientStatus
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Crafting")
TObjectPtr<UItemDataAsset> Item = nullptr;
UPROPERTY(BlueprintReadOnly, Category = "Crafting")
int32 RequiredQuantity = 0;
UPROPERTY(BlueprintReadOnly, Category = "Crafting")
int32 OwnedQuantity = 0;
bool IsSatisfied() const { return OwnedQuantity >= RequiredQuantity; }
};
/** Une fabrication vient d'aboutir. Pour le son, l'animation, une notification. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCraftSucceeded, UCraftingRecipeDataAsset*, Recipe);
/** Les postes accessibles ont change : l'interface doit revoir sa liste. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAvailableStationsChanged);
/**
* Le resultat n'est pas entre dans l'inventaire. A quelqu'un de le poser
* dans le monde -- voir le commentaire de Craft().
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCraftOverflow, UItemDataAsset*, Item, int32, Quantity, int32, RemainingUses);
/**
* Fabrication d'objets a partir d'un livre de recettes et de l'inventaire du
* meme acteur.
*
* Sur le pawn : fabriquer est une capacite du corps, au meme titre que porter
* un sac. Le composant ne connait ni l'interface ni la classe du pawn -- un
* futur etabli pourra porter le meme composant avec son propre livre.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UCraftingComponent : public UActorComponent
{
GENERATED_BODY()
public:
UCraftingComponent();
virtual void InitializeComponent() override;
/**
* Recettes a afficher, dans l'ordre du livre.
*
* Ne renvoie que celles dont le poste est ACCESSIBLE : les mains vides, on
* ne voit pas les recettes d'etabli. C'est le modele choisi -- une seule
* fenetre de craft dont le contenu depend d'ou l'on se trouve.
*
* @param StationFilter filtre d'affichage supplementaire, purement visuel.
* Passe Count pour tout voir. Un poste demande ici mais non
* accessible ne fait rien apparaitre : le filtre restreint, il
* n'autorise jamais.
*
* Pas de valeur par defaut sur StationFilter : UHT refuse une entree
* UMETA(Hidden) comme defaut, et dé-cacher Count le rendrait selectionnable
* dans le champ Required Station d'une recette. L'appelant l'ecrit.
*/
UFUNCTION(BlueprintCallable, Category = "Crafting")
void GatherRecipes(TArray<UCraftingRecipeDataAsset*>& OutRecipes, ECraftingStation StationFilter) const;
/** Peut-on fabriquer cette recette maintenant, et sinon pourquoi. */
UFUNCTION(BlueprintCallable, Category = "Crafting")
bool CanCraft(const UCraftingRecipeDataAsset* Recipe, ECraftFailureReason& OutReason) const;
/** Le detail requis / possede de chaque ingredient, dans l'ordre de la recette. */
UFUNCTION(BlueprintCallable, Category = "Crafting")
void GatherIngredientStatus(const UCraftingRecipeDataAsset* Recipe, TArray<FCraftIngredientStatus>& OutStatus) const;
/**
* Consomme les ingredients et ajoute le resultat a l'inventaire.
*
* L'ordre compte : on consomme AVANT d'ajouter. Refuser une fabrication
* parce que l'inventaire est plein serait faux -- consommer trois branches
* libere justement la case ou poser le resultat. Et si malgre tout il ne
* rentre pas, l'objet part dans OnCraftOverflow pour etre pose au sol :
* aucune ressource ne disparait jamais, ni a l'entree ni a la sortie.
*
* @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);
// ------------------------------------------------------------------
// Postes accessibles
//
// Etat courant, pas de la configuration : {Hands} en permanence, plus
// Workbench tant qu'on est devant un etabli. C'est une CONDITION de
// fabrication, a ne pas confondre avec le filtre d'affichage de
// GatherRecipes -- melanger les deux rendrait un jour un craft impossible
// a cause d'un bouton d'interface.
// ------------------------------------------------------------------
UFUNCTION(BlueprintPure, Category = "Crafting|Stations")
bool HasStation(ECraftingStation Station) const;
/**
* A appeler quand le joueur entre dans le rayon d'un poste.
*
* Les appels se COMPTENT : deux etablis dont les portees se chevauchent, ce
* qui arrive des qu'on construit une base un peu dense, s'ajoutent tous les
* deux. Sortir du premier ne doit pas retirer l'acces alors qu'on est encore
* devant le second.
*/
UFUNCTION(BlueprintCallable, Category = "Crafting|Stations")
void AddAvailableStation(ECraftingStation Station);
/** A appeler quand il s'en eloigne. Hands ne peut pas etre retire. */
UFUNCTION(BlueprintCallable, Category = "Crafting|Stations")
void RemoveAvailableStation(ECraftingStation Station);
UPROPERTY(BlueprintAssignable, Category = "Crafting")
FOnCraftSucceeded OnCraftSucceeded;
UPROPERTY(BlueprintAssignable, Category = "Crafting")
FOnAvailableStationsChanged OnAvailableStationsChanged;
UPROPERTY(BlueprintAssignable, Category = "Crafting")
FOnCraftOverflow OnCraftOverflow;
protected:
/** Ton DA_RecipeBook. Sans lui le composant ne connait aucune recette. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
TObjectPtr<UCraftingRecipeBook> RecipeBook;
/** Resolu une fois a l'initialisation : chercher le composant a chaque clic serait absurde. */
UPROPERTY(Transient, BlueprintReadOnly, Category = "Crafting")
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.
*
* Pas une UPROPERTY : ni la cle ni la valeur ne sont des references a
* collecter, et cet etat n'a rien a regler dans l'editeur.
*/
TMap<ECraftingStation, int32> StationRefCounts;
};
@@ -0,0 +1,59 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "CraftingComponent.h"
#include "CraftingIngredientRowWidget.generated.h"
class UImage;
class UTextBlock;
/**
* Une ligne de cout dans le panneau de detail : l'icone d'un ingredient, son
* nom, et "2/3" colore selon qu'on l'a ou non.
*
* Elle ne consulte jamais l'inventaire : UCraftingComponent lui remet un
* FCraftIngredientStatus deja calcule. Une ligne qui irait compter elle-meme
* referait le travail autant de fois qu'il y a d'ingredients, et pourrait
* afficher autre chose que ce sur quoi le bouton Fabriquer s'appuie.
*/
UCLASS(Abstract)
class EMBERWILD_API UCraftingIngredientRowWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** Affiche un ingredient et applique la couleur qui va avec. */
UFUNCTION(BlueprintCallable, Category = "Crafting")
void SetIngredient(const FCraftIngredientStatus& Status);
protected:
/** Facultatif : l'icone de la ressource demandee. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UImage> IngredientIcon;
/** Facultatif : le nom de la ressource. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> IngredientNameText;
/** Obligatoire : c'est lui qui porte le "2/3". A nommer "IngredientCountText". */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> IngredientCountText;
/** Couleur du compteur quand la ressource est en quantite suffisante. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
FLinearColor EnoughColor = FLinearColor(0.35f, 0.85f, 0.35f, 1.f);
/** Couleur du compteur quand il en manque. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
FLinearColor MissingColor = FLinearColor(0.9f, 0.3f, 0.3f, 1.f);
/**
* Mise en forme du compteur. {0} = possede, {1} = requis.
* Exposee pour pouvoir passer a "2 / 3" ou "2 sur 3" sans recompiler.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
FText CountFormat = NSLOCTEXT("Crafting", "IngredientCountFormat", "{0}/{1}");
};
@@ -0,0 +1,40 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CraftingRecipeBook.generated.h"
class UCraftingRecipeDataAsset;
/**
* La liste ordonnee des recettes du jeu.
*
* Un catalogue plutot qu'un scan de l'AssetManager : le contenu est
* deterministe, rien ne se charge par surprise au runtime, et surtout l'ORDRE
* d'affichage de la grille devient une donnee de design qu'on regle a la
* souris. Un scan d'assets rendrait cet ordre dependant du nom des fichiers.
*
* Un catalogue plutot qu'une liste posee sur le composant, aussi : un etabli
* devra pointer son propre livre, et le pawn n'a pas a etre l'endroit ou vit
* la table de craft du jeu.
*/
UCLASS(BlueprintType)
class EMBERWILD_API UCraftingRecipeBook : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/** L'ordre de ce tableau est l'ordre d'affichage dans la grille de craft. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
TArray<TObjectPtr<UCraftingRecipeDataAsset>> Recipes;
// Pas de UFUNCTION ici : UHT refuse un TObjectPtr en valeur de retour. Le
// Blueprint lit de toute facon Recipes directement, c'est une UPROPERTY.
const TArray<TObjectPtr<UCraftingRecipeDataAsset>>& GetRecipes() const { return Recipes; }
#if WITH_EDITOR
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
};
@@ -0,0 +1,124 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CraftingRecipeDataAsset.generated.h"
class UItemDataAsset;
class UTexture2D;
/**
* Poste de travail necessaire pour realiser une recette.
*
* Ce n'est PAS un filtre d'affichage : c'est une condition de realisation.
* Le UCraftingComponent tient la liste des stations dont le joueur dispose a
* l'instant t -- {Hands} tout seul, {Hands, Workbench} devant un etabli -- et
* une recette n'est realisable que si la sienne s'y trouve. Le filtre de
* l'interface est une notion separee, purement visuelle : melanger les deux
* finirait par rendre un craft impossible parce qu'un bouton d'affichage est
* mal positionne, et ce genre de bug ne se comprend jamais sur le moment.
*
* Fourneau et feu de cuisson n'ont volontairement PAS leur entree ici : ils ne
* passent par aucun menu. Ce sont des acteurs poses dans le monde, facon Raft,
* ou l'on depose une ressource et ou l'on voit le mesh changer d'etat. Leur
* donnee et leur file d'attente leur appartiendront, une recette de menu n'a
* rien a y faire.
*/
UENUM(BlueprintType)
enum class ECraftingStation : uint8
{
Hands,
Workbench,
// Sentinelle, jamais assignee a une recette. Elle sert a parcourir l'enum
// pour construire les boutons de filtre sans avoir a les enumerer a la main.
Count UMETA(Hidden)
};
/** Une ligne de cout : tant d'exemplaires de tel objet. */
USTRUCT(BlueprintType)
struct FCraftIngredient
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
TObjectPtr<UItemDataAsset> Item = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe", meta = (ClampMin = "1"))
int32 Quantity = 1;
};
/**
* Une recette : ce qu'il faut, ce que ca donne, ou ca se fabrique.
*
* Un asset par recette, comme un DataAsset par objet. Trois comportements de
* craft n'existeront jamais : la recette est de la donnee pure, c'est le
* composant qui l'execute.
*
* Elle ne contient aucun etat -- ni "deja decouverte", ni "en cours de
* fabrication". Le jour ou les recettes se debloqueront en jeu, la liste des
* RecipeId connus vivra dans la sauvegarde, pas ici : sinon decouvrir une
* recette la debloquerait pour toutes les parties a la fois.
*/
UCLASS(BlueprintType)
class EMBERWILD_API UCraftingRecipeDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/**
* Identifiant stable, independant du nom de l'asset.
* Servira a la sauvegarde des recettes decouvertes : renommer un asset ne
* doit pas faire oublier au joueur ce qu'il avait debloque.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
FName RecipeId;
/** Ce que la fabrication consomme. Une recette sans cout est acceptee (objet gratuit). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
TArray<FCraftIngredient> Ingredients;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
TObjectPtr<UItemDataAsset> ResultItem;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe", meta = (ClampMin = "1"))
int32 ResultQuantity = 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
ECraftingStation RequiredStation = ECraftingStation::Hands;
// Pas de duree de fabrication ici : le craft de menu est instantane, comme
// dans Valheim ou Raft. La fabrication qui prend du temps est celle des
// postes qui travaillent SANS le joueur, et ceux-la ne sont pas des recettes.
/**
* Nom affiche, quand celui du resultat ne suffit pas.
* Laisse vide dans l'immense majorite des cas : dupliquer le nom de l'objet
* dans la recette, c'est se garantir deux textes qui divergeront un jour.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe|Display")
FText DisplayNameOverride;
/** Icone affichee dans la grille de craft. Vide = celle du resultat. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe|Display")
TObjectPtr<UTexture2D> IconOverride;
/** Le nom a afficher, override compris. Passe toujours par ici, jamais par ResultItem directement. */
UFUNCTION(BlueprintPure, Category = "Recipe")
FText GetDisplayName() const;
/** L'icone a afficher, override compris. */
UFUNCTION(BlueprintPure, Category = "Recipe")
UTexture2D* GetIcon() const;
#if WITH_EDITOR
/**
* Une recette sans resultat ne planterait pas : elle ne ferait simplement
* RIEN, en consommant les ingredients. C'est exactement le genre de bug qu'on
* cherche pendant une heure -- autant que l'editeur le refuse a la sauvegarde.
*/
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
};
@@ -0,0 +1,73 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "CraftingRecipeSlotWidget.generated.h"
class UCraftingRecipeDataAsset;
class UCraftingWidget;
class UImage;
class UTextBlock;
/**
* Une case de la grille de craft : l'icone d'une recette, cliquable.
*
* Passive comme la case d'inventaire : elle n'interroge jamais le composant de
* fabrication, c'est la page parente qui lui pousse son etat. Elle ne gere que
* son propre clic, parce que c'est elle qui recoit l'evenement souris.
*/
UCLASS(Abstract)
class EMBERWILD_API UCraftingRecipeSlotWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** Appele une fois a la creation : dit a la case quelle recette elle represente. */
void InitSlot(UCraftingRecipeDataAsset* InRecipe, UCraftingWidget* InOwningGrid);
/** Affiche ou masque le cadre de selection. */
UFUNCTION(BlueprintCallable, Category = "Crafting")
void SetSelected(bool bSelected);
/**
* Assombrit la case quand les ressources manquent.
* La recette reste visible et cliquable : le joueur doit pouvoir consulter
* ce qui lui manque, sinon il ne sait pas quoi aller chercher.
*/
UFUNCTION(BlueprintCallable, Category = "Crafting")
void SetCraftable(bool bCraftable);
UCraftingRecipeDataAsset* GetRecipe() const { return Recipe; }
protected:
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
/** A nommer exactement "RecipeIcon" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> RecipeIcon;
/** Facultatif : le nom sous l'icone, si tu veux une grille legendee. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> RecipeNameText;
/** Facultatif : cadre affiche quand la case est selectionnee. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> SelectionBorder;
/** Teinte de l'icone quand la recette est realisable. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
FLinearColor CraftableTint = FLinearColor::White;
/** Teinte quand il manque des ressources. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
FLinearColor UncraftableTint = FLinearColor(0.3f, 0.3f, 0.3f, 1.f);
private:
UPROPERTY(Transient)
TObjectPtr<UCraftingRecipeDataAsset> Recipe;
/** La page parente, a prevenir du clic. */
TWeakObjectPtr<UCraftingWidget> OwningGrid;
};
+65
View File
@@ -0,0 +1,65 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CraftingRecipeDataAsset.h"
#include "Interactable.h"
#include "CraftingStation.generated.h"
class UStaticMeshComponent;
/**
* Un poste de travail pose dans le monde : etabli, et ce qui suivra.
*
* Il ne fabrique rien lui-meme et ne contient aucune recette. Tout ce qu'il
* fait, c'est ouvrir l'ecran de fabrication en annoncant SON type de poste --
* le reste appartient au UCraftingComponent du joueur, qui filtrera les
* recettes en consequence.
*
* L'acces au poste ne dure que le temps de l'ecran : c'est le controller qui
* l'accorde a l'ouverture et le reprend a la fermeture. Un etabli qui donnerait
* son acces "par proximite" obligerait a surveiller la distance en permanence,
* pour un resultat que le joueur comprendrait moins bien -- ici on fabrique a
* l'etabli parce qu'on est en train de s'en servir.
*
* Un seul BP_CraftingStation suffit pour tous les postes : c'est StationType
* qui change par instance, comme ItemData sur BP_Pickup.
*/
UCLASS()
class EMBERWILD_API ACraftingStation : public AActor, public IInteractable
{
GENERATED_BODY()
public:
ACraftingStation();
// --- IInteractable ---
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
virtual void Interact_Implementation(AActor* Interactor) override;
protected:
/**
* Le visuel ET la surface d'interaction.
*
* Pas de sphere dediee ici, contrairement a APickupItem : un poste est un
* meuble construit, dont le mesh a une vraie collision simple. Une sphere
* autour d'un volume pareil serait touchee AVANT lui -- le trace s'arrete au
* premier bloqueur -- et le prompt apparaitrait en visant le vide autour, ou
* masquerait un objet pose au pied du meuble.
*
* Corollaire : le mesh assigne doit avoir une collision simple. Sans elle,
* le trace le traverse, puisqu'il est lance en bTraceComplex = false.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Crafting")
TObjectPtr<UStaticMeshComponent> Mesh;
/** Le poste que cet acteur rend accessible pendant qu'on s'en sert. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
ECraftingStation StationType = ECraftingStation::Workbench;
/** Texte affiche quand le joueur vise le poste. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
FText InteractionPrompt;
};
+160
View File
@@ -0,0 +1,160 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "CraftingRecipeDataAsset.h"
#include "CraftingWidget.generated.h"
class UButton;
class UCraftingComponent;
class UCraftingIngredientRowWidget;
class UCraftingRecipeSlotWidget;
class UImage;
class UInventoryComponent;
class UPanelWidget;
class UTextBlock;
class UUniformGridPanel;
/**
* La page de fabrication : la grille de recettes a gauche, le detail de la
* recette choisie a droite.
*
* Elle ne calcule rien. UCraftingComponent lui dit quelles recettes afficher,
* lesquelles sont realisables, et fabrique quand on clique. Elle s'abonne a
* l'inventaire et aux postes accessibles pour se rafraichir -- jamais de Tick
* pour relire un etat.
*/
UCLASS(Abstract)
class EMBERWILD_API UCraftingWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/**
* (Re)branche la page sur le pawn actuellement possede.
* Indispensable apres un respawn : le nouveau pawn a de nouveaux composants,
* et les anciens abonnements pointeraient vers des objets detruits.
*/
void BindToOwningPawn();
/** Relit la liste, l'etat de chaque case et le panneau de detail. */
UFUNCTION(BlueprintCallable, Category = "Crafting")
void Refresh();
/** Appele par une case quand on clique dessus. */
void NotifyRecipeClicked(UCraftingRecipeDataAsset* InRecipe);
/**
* Restreint l'affichage a un seul poste. Count = tout ce qui est accessible.
* Purement visuel : ce filtre ne peut jamais rendre une recette realisable.
*/
UFUNCTION(BlueprintCallable, Category = "Crafting")
void SetStationFilter(ECraftingStation InFilter);
protected:
/** A nommer exactement "RecipeGrid" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UUniformGridPanel> RecipeGrid;
/** L'icone du resultat, dans le panneau de droite. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> ResultIcon;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> ResultNameText;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CraftButton;
/** Facultatif : la description de l'objet produit. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> ResultDescriptionText;
/** Facultatif : "x2" quand la recette produit plusieurs exemplaires. Masque sinon. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> ResultQuantityText;
/**
* Facultatif : le panneau de droite dans son ensemble. Masque tant qu'aucune
* recette n'est choisie, pour ne pas afficher un cadre vide au premier
* affichage.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> DetailsPanel;
/** Facultatif : message affiche quand aucune recette n'est disponible ici. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> EmptyGridMessage;
/**
* Le conteneur des lignes de cout. Declare en UPanelWidget et non en
* UVerticalBox : ca laisse le choix d'un Wrap Box ou d'un Grid Panel dans le
* Blueprint sans toucher au C++, puisqu'on ne fait qu'y ajouter des enfants.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UPanelWidget> IngredientList;
/** Ton WBP_RecipeSlot. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
TSubclassOf<UCraftingRecipeSlotWidget> RecipeSlotClass;
/** Ton WBP_IngredientRow. Sans elle, la liste de cout reste vide. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
TSubclassOf<UCraftingIngredientRowWidget> IngredientRowClass;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting", meta = (ClampMin = "1"))
int32 ColumnCount = 5;
private:
UFUNCTION()
void HandleInventoryChanged();
UFUNCTION()
void HandleStationsChanged();
UFUNCTION()
void HandleCraftClicked();
/** Recree les cases uniquement si la liste affichee a change. */
void RebuildGrid(const TArray<UCraftingRecipeDataAsset*>& NewRecipes);
/** Pousse l'etat realisable/non a chaque case et met a jour le panneau. */
void RefreshStates();
void RefreshDetails();
/**
* Remplit la liste de cout de la recette choisie.
*
* Les lignes sont RECYCLEES et non recreees : cette fonction repasse a
* chaque changement d'inventaire, donc a chaque ramassage. Detruire puis
* reconstruire trois widgets par branche ramassee serait du gaspillage.
*/
void RefreshIngredients();
UCraftingComponent* ResolveCrafting() const;
/** Detache les deux abonnements. Appele avant de rebrancher, et a la destruction. */
void UnbindDelegates();
TWeakObjectPtr<UCraftingComponent> BoundCrafting;
TWeakObjectPtr<UInventoryComponent> BoundInventory;
UPROPERTY(Transient)
TObjectPtr<UCraftingRecipeDataAsset> SelectedRecipe;
/** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */
UPROPERTY(Transient)
TArray<TObjectPtr<UCraftingRecipeSlotWidget>> RecipeSlots;
UPROPERTY(Transient)
TArray<TObjectPtr<UCraftingIngredientRowWidget>> IngredientRows;
ECraftingStation StationFilter = ECraftingStation::Count;
};
@@ -0,0 +1,77 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "CustomizationCategoryWidget.generated.h"
class UBorder;
class UButton;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCustomizationCategoryClicked, int32, CategoryIndex);
/**
* Une ligne de la colonne de gauche : « Corps », « Cheveux », « Peau »...
*
* Elle ne sait pas ce qu'elle designe -- juste un libelle et un index, comme
* USettingRowWidget ignore ce qu'il regle. C'est l'ecran qui traduit l'index en
* categorie, et c'est ce qui permet de n'avoir qu'UN widget a dessiner pour les
* onze entrees.
*/
UCLASS(Abstract)
class EMBERWILD_API UCustomizationCategoryWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
UPROPERTY(BlueprintAssignable, Category = "Customization")
FOnCustomizationCategoryClicked OnCategoryClicked;
UFUNCTION(BlueprintCallable, Category = "Customization")
void Setup(int32 InIndex, const FText& InLabel);
UFUNCTION(BlueprintCallable, Category = "Customization")
void SetSelected(bool bInSelected);
/**
* L'index de la categorie que porte cette rangee.
*
* Indispensable a l'ecran : les categories vides etant sautees, le rang de
* la rangee dans la colonne ne vaut PAS son index de categorie.
*/
UFUNCTION(BlueprintPure, Category = "Customization")
int32 GetCategoryIndex() const { return CategoryIndex; }
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CategoryButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> CategoryLabel;
/** Optionnel : on peut aussi marquer la selection par la couleur du texte. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UBorder> SelectionBorder;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
FLinearColor SelectedLabelColor = FLinearColor(1.f, 0.78f, 0.35f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
FLinearColor UnselectedLabelColor = FLinearColor(0.8f, 0.8f, 0.8f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
FLinearColor SelectedBorderColor = FLinearColor(1.f, 1.f, 1.f, 0.12f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
FLinearColor UnselectedBorderColor = FLinearColor(1.f, 1.f, 1.f, 0.f);
private:
UFUNCTION()
void HandleButtonClicked();
int32 CategoryIndex = 0;
};
@@ -0,0 +1,95 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "CustomizationOptionWidget.generated.h"
class UBorder;
class UButton;
class UImage;
class UTextBlock;
class UTexture2D;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCustomizationOptionClicked, int32, OptionIndex);
/**
* Une case de la grille de customisation.
*
* La MEME classe sert les vignettes de pieces et les pastilles de couleur :
* dans les deux cas c'est un bouton carre qui porte un visuel et sait s'il est
* selectionne. Deux widgets auraient eu la meme logique de selection a tenir a
* jour en double, pour la seule difference d'afficher une texture plutot qu'un
* aplat.
*
* Classe Abstract : on cree un WBP_CustomizationOption qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API UCustomizationOptionWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
/** Previent d'un clic du joueur, jamais d'un rafraichissement. */
UPROPERTY(BlueprintAssignable, Category = "Customization")
FOnCustomizationOptionClicked OnOptionClicked;
/**
* Mode piece : affiche la vignette du catalogue.
*
* @param Icon peut etre nul -- la case retombe alors sur son numero. Une
* option sans vignette doit rester choisissable, sinon le catalogue
* est inutilisable tant que les trente-six textures ne sont pas
* rendues, et on veut justement les capturer depuis cet ecran.
*/
UFUNCTION(BlueprintCallable, Category = "Customization")
void SetupAsPart(int32 InIndex, UTexture2D* Icon);
/** Mode couleur : la pastille EST le visuel, aucune texture necessaire. */
UFUNCTION(BlueprintCallable, Category = "Customization")
void SetupAsColor(int32 InIndex, const FLinearColor& Color);
/** Encadre la case si elle porte le choix courant. */
UFUNCTION(BlueprintCallable, Category = "Customization")
void SetSelected(bool bInSelected);
UFUNCTION(BlueprintPure, Category = "Customization")
int32 GetOptionIndex() const { return OptionIndex; }
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> OptionButton;
/** Porte la vignette en mode piece, la couleur en mode couleur. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> OptionImage;
/**
* Le numero de repli, montre uniquement quand une piece n'a pas de
* vignette. BindWidgetOptional : un WBP qui s'en passe reste valide, il
* affichera simplement des cases vides.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> IndexText;
/** Le cadre de selection. Optionnel : on peut le rendre autrement. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UBorder> SelectionBorder;
/** Couleur du cadre quand la case porte le choix courant. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
FLinearColor SelectedBorderColor = FLinearColor(1.f, 0.78f, 0.35f, 1.f);
/** Couleur du cadre au repos. Transparente par defaut : il disparait. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
FLinearColor UnselectedBorderColor = FLinearColor(1.f, 1.f, 1.f, 0.f);
private:
UFUNCTION()
void HandleButtonClicked();
int32 OptionIndex = 0;
};
+362
View File
@@ -0,0 +1,362 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Engine/EngineTypes.h"
#include "FootstepSoundsDataAsset.h"
#include "FootstepComponent.generated.h"
class ACharacter;
class UCharacterMovementComponent;
class USoundAttenuation;
class USoundBase;
class USoundConcurrency;
/**
* Les reglages d'une allure : sa foulee et son volume.
*
* Groupes dans une struct pour la meme raison que FHeadBobProfile : trois
* allures a plat feraient neuf lignes melangees dans le Details panel, alors
* que la struct donne un repli par allure.
*/
USTRUCT(BlueprintType)
struct FFootstepGaitSettings
{
GENERATED_BODY()
FFootstepGaitSettings() = default;
FFootstepGaitSettings(float InStrideLength, float InVolume)
: StrideLength(InStrideLength)
, Volume(InVolume)
{
}
/**
* Distance parcourue entre deux pas, en cm.
*
* C'est le reglage central du systeme, et il se calcule plutot qu'il ne se
* devine : pour que les pas tombent en rythme avec le balancement de tete,
* il faut StrideLength = Vitesse / (2 x StrideFrequency du profil de
* UHeadBobComponent) -- le "2" parce qu'une foulee compte deux pas. Les
* valeurs par defaut sortent de ce calcul avec les vitesses du pawn.
*
* Corollaire a retenir : changer WalkSpeed ou SprintSpeed sans retoucher ici
* desynchronise le son de l'image, et ca s'entend avant de se voir.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "10", UIMax = "300"))
float StrideLength = 180.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "2"))
float Volume = 1.f;
/**
* Variation aleatoire de hauteur, en plus ou en moins autour de 1.
*
* Le pack ne livre qu'un ou deux echantillons par surface et par allure :
* sans ce grain, une course en ligne droite repete exactement le meme son
* trois fois par seconde et l'oreille l'entend comme une boucle. Quelques
* pourcents suffisent, au-dela le personnage change de poids a chaque pas.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "0.4"))
float PitchVariation = 0.06f;
};
/**
* Bruits de pas : mesure la distance parcourue, cherche la matiere sous les
* pieds, joue le son qui va avec.
*
* ---------------------------------------------------------------------------
* Pourquoi la distance et pas un AnimNotify
* ---------------------------------------------------------------------------
* C'est le choix structurant. Trois raisons de ne pas accrocher le son aux
* animations :
*
* - Le corps 3e personne du joueur local est en bOwnerNoSee. Faire dependre
* son propre son d'un mesh qu'il ne voit pas est fragile, et le devient
* d'autant plus le jour ou l'on optimisera son tick d'animation.
* - Les animations du pack n'ont aucun notify, et il faudrait en poser dans le
* BlendSpace, ou ils se declenchent selon l'echantillon dominant : le
* passage marche -> course y saute ou double un pas.
* - Les bras FPS sont sur BasePose_Skeleton, qui n'a rien a voir avec
* SK_BodyA. Aucun notify ne peut servir aux deux.
*
* La distance, elle, donne un espacement regulier, se regle en PIE sans
* recompiler, et se comporte identiquement sur le pawn local et sur ceux des
* amis.
*
* ---------------------------------------------------------------------------
* Reseau : rien n'est replique, et rien n'a a l'etre
* ---------------------------------------------------------------------------
* Exactement le raisonnement de UPlayerBodyAnimInstance : tout ce que ce
* composant lit DERIVE d'un etat deja synchronise -- la vitesse par le
* CharacterMovement, l'accroupissement par ACharacter, le mode de deplacement
* par la replication du mouvement. Chaque machine calcule donc les pas de tous
* les pawns qu'elle connait. Un Multicast par pas, a trois pas par seconde et
* par joueur, serait du trafic pur pour un resultat identique.
*
* Le son du joueur local part en 2D, celui des autres en 3D a leurs pieds :
* un pas local joue en 3D depuis les chevilles est attenue et sonne creux.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UFootstepComponent : public UActorComponent
{
GENERATED_BODY()
public:
UFootstepComponent();
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/**
* Coupe ou rallume les pas. Pour la mort, une cinematique, un mode fantome.
* Coupe, le compteur de foulee est remis a zero : rallumer ne doit pas
* declencher le pas reste en attente depuis l'autre bout de la carte.
*/
UFUNCTION(BlueprintCallable, Category = "Footsteps")
void SetFootstepsEnabled(bool bEnabled);
/** Remet le cycle de foulee a zero. Pour une teleportation ou un respawn. */
UFUNCTION(BlueprintCallable, Category = "Footsteps")
void ResetStride();
/**
* Joue un pas immediatement, a l'allure courante. Sert aux montages et aux
* AnimNotify si l'on veut un jour caler un geste precis sur un son.
*/
UFUNCTION(BlueprintCallable, Category = "Footsteps")
void PlayFootstepNow();
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/**
* Saut et reception, sur TOUTES les machines.
*
* Et surtout pas ACharacter::Landed / OnJumped, qui semblent pourtant faits
* pour ca : Landed vient de ProcessLanded, qui n'est appele que la ou le
* mouvement est reellement simule, et OnJumped que la ou l'input est lu. Ni
* l'un ni l'autre n'existe sur le pawn d'un ami vu depuis notre machine, ou
* le mouvement arrive deja fait. Le mode de deplacement, lui, EST replique,
* donc ce delegue est diffusé partout -- c'est le seul signal qui couvre les
* quatre joueurs.
*/
UFUNCTION()
void HandleMovementModeChanged(ACharacter* Character, EMovementMode PrevMovementMode, uint8 PreviousCustomMode);
// ------------------------------------------------------------------
// Assets
// ------------------------------------------------------------------
/** La table surface -> sons. Sans elle le composant ne fait rien. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
TObjectPtr<UFootstepSoundsDataAsset> SoundBank;
/**
* Attenuation appliquee aux pas des AUTRES joueurs (le pas local est en 2D).
*
* A remplir : le pack ne fournit aucune attenuation, donc sans cet asset les
* cues sont jouees en 3D sans courbe de distance et s'entendent d'un bout a
* l'autre de la carte au meme volume.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
TObjectPtr<USoundAttenuation> Attenuation;
/**
* Concurrence, facultative. A quatre joueurs qui courent, huit a douze pas
* par seconde peuvent se superposer ; une regle a 4 ou 5 voix evite que les
* pas mangent le reste du mixage.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
TObjectPtr<USoundConcurrency> Concurrency;
// ------------------------------------------------------------------
// Reglages generaux
// ------------------------------------------------------------------
/** Coupe entierement l'effet sans avoir a vider la banque de sons. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
bool bEnableFootsteps = true;
/**
* Joue le pas du joueur local en 2D plutot qu'a la position de ses pieds.
* Decoche pour l'entendre spatialise comme celui des autres -- utile pour
* regler l'attenuation sans avoir besoin d'un second joueur.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
bool bPlayLocalStepsIn2D = true;
/**
* Multiplicateur applique au seul pas local. Sous 1 par defaut : un son 2D
* arrive a pleine puissance dans les deux oreilles, la ou celui des autres
* passe par l'attenuation.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "2"))
float LocalVolumeScale = 0.7f;
/** Joue le pas final quand le personnage s'immobilise. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
bool bPlayStopSounds = true;
// ------------------------------------------------------------------
// Allures
// ------------------------------------------------------------------
/** Accroupi. Valeurs par defaut dans le constructeur. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
FFootstepGaitSettings CrouchGait;
/** Marche normale. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
FFootstepGaitSettings WalkGait;
/** Sprint. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
FFootstepGaitSettings SprintGait;
/**
* En dessous de cette vitesse horizontale (cm/s), on considere le
* personnage immobile. Meme seuil et meme raison que UHeadBobComponent :
* marcher dans un mur laisse l'input actif alors que le corps n'avance plus.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0"))
float IdleSpeedThreshold = 10.f;
/**
* Au-dessus de cette vitesse horizontale (cm/s), on passe sur l'allure de
* sprint. A caler entre WalkSpeed et SprintSpeed du pawn, comme le seuil
* homonyme du balancement de tete -- les deux doivent basculer ensemble,
* sinon la tete accelere avant les pieds.
*
* La vitesse REELLE et pas AFpsPlayer::IsSprinting() : le composant reste
* ainsi utilisable par un PNJ ou un animal, qui n'ont pas de touche Maj.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0"))
float SprintSpeedThreshold = 520.f;
/**
* Fraction de foulee deja "parcourue" au demarrage du mouvement.
*
* A 0, le premier pas ne tombe qu'apres une foulee entiere : le personnage
* part en silence sur presque deux metres, ce qui se lit comme un temps de
* reaction du jeu. A 1 il claque avant que le corps ait bouge. La moitie
* place le premier pas juste apres l'entree en mouvement.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0", ClampMax = "1"))
float FirstStepFraction = 0.5f;
// ------------------------------------------------------------------
// Saut et reception
// ------------------------------------------------------------------
/**
* Vitesse de chute (cm/s) en dessous de laquelle la reception est muette.
* Descendre une marche d'escalier passe techniquement par un instant de
* chute ; sans ce seuil, la moindre bosse du terrain claque.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Air", meta = (ClampMin = "0"))
float LandMinFallSpeed = 150.f;
/** Volume du saut et de la reception. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Air", meta = (ClampMin = "0", UIMax = "2"))
float AirVolume = 1.f;
// ------------------------------------------------------------------
// Trace de sol
// ------------------------------------------------------------------
/**
* Profondeur sondee sous les pieds, en cm. Quelques dizaines suffisent : le
* trace part du centre de la capsule et on ne cherche le sol que lorsqu'on
* marche dessus.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace", meta = (ClampMin = "1"))
float TraceDepth = 60.f;
/**
* Trace en collision COMPLEXE, et ce n'est pas negociable pour ce systeme.
*
* Un PhysicalMaterial pose sur un MATERIAU n'est rendu que par un trace
* per-poly : le trace simple remonte celui du BodySetup du mesh, qui est
* presque toujours vide. Le symptome d'un decochage est immediat et trompeur
* -- tout le monde marche sur DefaultSurface, comme si la table n'etait pas
* remplie.
*
* Le cout est nul a l'echelle du jeu : deux a trois traces par seconde et
* par personnage, jamais un par frame.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace")
bool bTraceComplex = true;
/** Canal du trace de sol. Visibility couvre tout le decor du projet. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace")
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
private:
/** L'allure correspondant a l'etat reel du CharacterMovement. */
EFootstepGait SelectGait(float HorizontalSpeed) const;
/** Les reglages de l'allure courante. */
const FFootstepGaitSettings& GetGaitSettings(EFootstepGait Gait) const;
/**
* Cherche la matiere sous les pieds et rend les sons correspondants.
* @param OutLocation ou jouer le son : le point d'impact, ou les pieds si
* le trace n'a rien touche (le sol peut etre hors du canal trace).
*/
const FFootstepSurfaceSounds* QueryGroundSounds(FVector& OutLocation) const;
/** Joue une cue, en 2D chez le proprietaire et en 3D partout ailleurs. */
void PlayCue(USoundBase* Sound, float Volume, float PitchVariation, const FVector& Location) const;
/** Vrai quand le son doit sortir des haut-parleurs de CETTE machine. */
bool ShouldPlaySounds() const;
// ------------------------------------------------------------------
// Etat
// ------------------------------------------------------------------
/** Distance horizontale accumulee depuis le dernier pas, en cm. */
float DistanceSinceLastStep = 0.f;
/** Position de l'acteur au tick precedent, pour mesurer le deplacement reel. */
FVector LastLocation = FVector::ZeroVector;
/**
* Vrai des le premier tick valide. Sans lui, le premier deplacement mesure
* partirait de l'origine du monde et declencherait une salve de pas.
*/
bool bHasLastLocation = false;
/** Etat du tick precedent, pour detecter le demarrage et l'arret. */
bool bWasMoving = false;
/** Allure du dernier pas joue : c'est elle qui choisit le son d'arret. */
EFootstepGait LastGait = EFootstepGait::Jog;
/**
* Nombre de pas depuis l'entree en mouvement. Le son d'arret n'est joue
* qu'a partir de un : un a-coup de collision suffit sinon a faire claquer
* un pas final alors que le personnage n'a pas bouge.
*/
int32 StepsSinceMoveStart = 0;
/**
* Plus grande vitesse de chute observee, en cm/s et positive.
*
* Mesuree pendant la chute plutot que lue a l'impact : au moment ou le mode
* repasse a Walking, le moteur a deja remis Velocity.Z a zero. Un tick a
* 20 Hz l'echantillonne largement assez -- et le cas ou il n'a pas eu le
* temps de tourner du tout est precisement celui d'une chute negligeable,
* qu'on veut de toute facon silencieuse.
*/
float MaxFallSpeed = 0.f;
/** Caches : lus a chaque tick, pas question d'y caster. */
TObjectPtr<ACharacter> OwnerCharacter;
TObjectPtr<UCharacterMovementComponent> OwnerMovement;
};
@@ -0,0 +1,140 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Chaos/ChaosEngineInterface.h"
#include "Engine/DataAsset.h"
#include "FootstepSoundsDataAsset.generated.h"
class USoundBase;
/**
* Les trois allures pour lesquelles le pack livre un son.
*
* Les noms sont ceux des fichiers du pack et PAS ceux des etats du jeu, alors
* que la tentation serait grande de les appeler Crouch / Walk / Sprint. La
* correspondance est etablie une seule fois, dans UFootstepComponent, et elle
* n'est pas l'identite : le personnage marche a 400 cm/s, ce qui est deja un
* trot -- d'ou accroupi = Walk, marche = Jog, sprint = Run. Nommer l'enum
* d'apres les etats obligerait a ecrire "le son Walk est dans le champ Crouch"
* dans le Details panel, ce qui est exactement le genre de traduction qu'on
* finit par se tromper en relisant.
*/
UENUM(BlueprintType)
enum class EFootstepGait : uint8
{
/** Le pas le plus discret du pack. Utilise quand le personnage est accroupi. */
Walk,
/** Allure de marche normale du jeu (WalkSpeed). */
Jog,
/** Sprint. */
Run
};
/**
* La banque de sons d'UNE surface : les huit cues que le pack fournit pour
* chacune de ses matieres.
*
* Tous les champs sont facultatifs. Une case vide ne produit aucun son et
* n'est pas une erreur -- c'est ce qui permet de commencer avec trois surfaces
* remplies et d'ajouter les autres au fil du temps, ou de renoncer aux sons
* d'arret sans toucher au code.
*/
USTRUCT(BlueprintType)
struct FFootstepSurfaceSounds
{
GENERATED_BODY()
// ------------------------------------------------------------------
// Les pas eux-memes, un par allure.
// ------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Steps")
TObjectPtr<USoundBase> WalkStep;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Steps")
TObjectPtr<USoundBase> JogStep;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Steps")
TObjectPtr<USoundBase> RunStep;
// ------------------------------------------------------------------
// Le pas final, joue quand le personnage s'immobilise.
//
// Ce n'est pas un ornement : sans lui, une course qui s'arrete coupe la
// serie de pas net, comme si le personnage s'etait teleporte. Le pack les
// livre exprès, avec un son plus traine que le pas courant.
// ------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stop")
TObjectPtr<USoundBase> WalkStop;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stop")
TObjectPtr<USoundBase> JogStop;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stop")
TObjectPtr<USoundBase> RunStop;
// ------------------------------------------------------------------
// Saut et reception. Une seule variante chacun, quelle que soit l'allure.
// ------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Air")
TObjectPtr<USoundBase> Jump;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Air")
TObjectPtr<USoundBase> Land;
/** Le pas courant d'une allure, ou nullptr si la case est vide. */
USoundBase* GetStepSound(EFootstepGait Gait) const;
/** Le pas final d'une allure, ou nullptr si la case est vide. */
USoundBase* GetStopSound(EFootstepGait Gait) const;
};
/**
* La table de correspondance surface physique -> banque de sons.
*
* Un seul asset a remplir pour tout le jeu, et le SEUL endroit a toucher pour
* ajouter une matiere. Le composant n'y range aucun etat : il le lit, c'est
* tout, ce qui permet de le partager entre le joueur, les futurs PNJ et les
* animaux sans se poser de question.
*
* La cle est le SurfaceType declare dans Project Settings > Physics, pas un
* nom ni un chemin d'asset : c'est le seul identifiant que le moteur nous
* rende depuis un trace, et il tient dans un octet.
*/
UCLASS(BlueprintType)
class EMBERWILD_API UFootstepSoundsDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/**
* Une entree par matiere du jeu. Les surfaces absentes de cette table
* tombent sur DefaultSurface.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Footsteps")
TMap<TEnumAsByte<EPhysicalSurface>, FFootstepSurfaceSounds> Surfaces;
/**
* Le repli, et il n'est pas facultatif en pratique.
*
* Un decor n'est jamais tague a 100 % : il restera toujours un rocher de
* demo, un mesh importe a la va-vite ou un acteur de test sans materiau. Le
* symptome d'un repli vide est un silence total sur ces surfaces, qu'on
* confond immediatement avec "le systeme ne marche pas". Mettre ici les
* cues de terre est le choix sur.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Footsteps")
FFootstepSurfaceSounds DefaultSurface;
/**
* Les sons d'une surface, ou DefaultSurface si elle n'est pas dans la table.
* Ne rend jamais nullptr : l'appelant n'a qu'a tester les cues elles-memes.
*/
const FFootstepSurfaceSounds& GetSurfaceSounds(EPhysicalSurface Surface) const;
};
@@ -0,0 +1,175 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "FpsPlayer.h"
#include "FpsArmsAnimInstance.generated.h"
/**
* AnimInstance des bras en vue premiere personne.
*
* Elle ne DECIDE de rien : elle lit l'etat du pawn une fois par frame et le
* republie sous forme de variables. C'est la machine a etats du Blueprint enfant
* (ABP_FpsArms) qui choisit l'animation. La separation est volontaire -- une
* transition d'animation se regle a l'oeil, avec des durees de fondu, et ca n'a
* rien a faire dans du C++ qu'il faut recompiler.
*
* Corollaire : ces variables ne sont JAMAIS repliquees. Les bras ne sont vus que
* par leur proprietaire, donc chaque machine calcule les siens a partir d'un etat
* de pawn que la replication a deja synchronise (vitesse, sprint, accroupi).
*/
UCLASS()
class EMBERWILD_API UFpsArmsAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
virtual void NativeInitializeAnimation() override;
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
protected:
// ------------------------------------------------------------------
// Etat publie vers le Blueprint d'animation
// ------------------------------------------------------------------
/** Vitesse horizontale en cm/s. Sert aussi a moduler le PlayRate de la course. */
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float GroundSpeed = 0.f;
/** Vrai quand le joueur se deplace REELLEMENT, freinage exclu. */
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsMoving = false;
/**
* Intention brute de sprint : la touche est enfoncee, rien de plus.
*
* NE PAS la brancher sur une transition de la machine a etats -- c'est
* exactement ce qui faisait courir les bras d'un joueur immobile qui appuyait
* sur Maj. Elle reste publiee parce qu'elle est la seule a dire ce que le
* joueur VEUT (un effet de FOV, un cout de stamina) ; pour l'animation, c'est
* bIsRunning qu'on lit.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsSprinting = false;
/**
* Sprint EFFECTIF : au sol, debout, et le joueur avance pour de bon.
*
* Toute la composition est faite ici, en un seul endroit, plutot que
* reconstruite dans chaque transition du Blueprint. Une machine a etats a
* autant d'entrees vers Run que d'etats voisins ; recomposer la condition a
* la main dans chacune, c'est se garantir d'en oublier une.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsRunning = false;
/** Deplacement au sol, debout, sans sprint. Complementaire exact de bIsRunning. */
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsWalking = false;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsFalling = false;
/**
* La reception qui vient d'avoir lieu, ou None tant qu'on est en l'air.
*
* C'est elle qui pilote la SORTIE de l'etat de chute, et il n'y a rien a
* composer autour : les trois valeurs menent aux trois etats de sortie
* (reception normale, reception encaissee, retour direct a la locomotion) et
* s'excluent par construction. Le seuil sous lequel une chute ne merite
* aucune animation vit sur le pawn -- c'est le meme que celui de la secousse
* de camera, pour que les deux effets ne se contredisent jamais.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
ELandingImpact LandingImpact = ELandingImpact::None;
/**
* Vitesse verticale signee (cm/s), positive vers le haut.
*
* C'est elle, et pas bIsFalling, qui distingue un saut d'une chute : le
* moteur appelle « falling » tout ce qui n'est pas au sol, donc sortir d'un
* rebord jouerait l'animation de detente si on se fiait au seul booleen.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float VerticalSpeed = 0.f;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsCrouching = false;
/**
* Le joueur tient un objet VISIBLE dans la main.
*
* A brancher sur un « Blend Poses by bool » A L'INTERIEUR de l'etat de
* deplacement, jamais sur une transition vers un etat jumeau. Deux etats
* « marche avec » et « marche sans » doubleraient la machine et, surtout,
* equiper un outil en pleine course claquerait d'un cycle a l'autre. Un blend
* fond les deux poses, l'outil apparaissant pendant que la foulee continue.
*
* C'est le nombre de MESHES montes qu'elle reflete, pas le contenu de la case
* active : la plupart des ressources n'ont pas bShowInHand, et les tenir ne
* doit pas changer la posture des bras.
*/
UPROPERTY(BlueprintReadOnly, Category = "Held Item")
bool bHasItemInHand = false;
/**
* Vitesse de lecture du cycle de deplacement, a brancher sur la broche
* Play Rate du noeud d'animation.
*
* Le projet a trois vitesses de jeu (accroupi 200, marche 400, sprint 650)
* pour un seul cycle d'animation. Plutot qu'un etat par vitesse -- trois
* seuils, donc trois occasions de claquer quand on les franchit -- on etire
* le meme cycle. La cadence des bras suit alors la vitesse en continu, y
* compris pendant l'acceleration.
*
* Un vrai etat Sprint n'aura de sens que le jour ou l'animateur livrera une
* animation de sprint a la POSTURE differente. Le meme cycle joue plus vite
* n'est pas une animation de sprint.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float LocomotionPlayRate = 1.f;
/**
* En dessous de cette vitesse (cm/s) on considere le joueur immobile.
*
* Un seuil strictement a zero ferait clignoter la transition course/repos
* pendant le freinage, ou la vitesse decroit en passant par des valeurs
* minuscules mais non nulles.
*/
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0"))
float MovingSpeedThreshold = 10.f;
/**
* Vitesse (cm/s) a laquelle le cycle tourne a sa cadence d'origine.
*
* A regler a l'oeil dans ABP_FpsArms : c'est la vitesse ou les bras ont l'air
* naturels sans etirement. Par defaut la vitesse de marche du pawn, donc le
* sprint accelere d'environ 60 %.
*/
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "1"))
float ReferenceLocomotionSpeed = 400.f;
/**
* Bornes du Play Rate. Sans le plancher, un joueur qui rampe a 20 cm/s
* regarderait des bras quasi immobiles ; sans le plafond, une future vitesse
* bonus donnerait des bras en accelere qui sortent du registre credible.
*/
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0.1"))
float MinLocomotionPlayRate = 0.6f;
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0.1"))
float MaxLocomotionPlayRate = 1.8f;
private:
/**
* Cache du pawn. TryGetPawnOwner() fait un Cast a chaque appel, et cette
* fonction tourne a chaque frame sur un mesh anime.
*
* TWeakObjectPtr et pas un pointeur nu : apres un respawn le pawn est detruit,
* et cet objet peut lui survivre d'une frame.
*/
TWeakObjectPtr<AFpsPlayer> OwningPlayer;
};
+897
View File
@@ -0,0 +1,897 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "FpsPlayer.generated.h"
class APickupItem;
class UAnimMontage;
class UCameraComponent;
class UCharacterAppearanceComponent;
class UCraftingComponent;
class UFootstepComponent;
class UHeadBobComponent;
class UHeldItemComponent;
class UInputAction;
class UInputMappingContext;
class UInteractionComponent;
class UInventoryComponent;
class UItemDataAsset;
class USurvivalStatsComponent;
struct FInputActionValue;
/**
* Ce que vaut la réception qui vient d'avoir lieu.
*
* Produit par le PAWN, qui est le seul à connaître les deux seuils, et republié
* tel quel par UFpsArmsAnimInstance. Un enum plutôt que deux booléens parce que
* les trois cas s'excluent par construction : la machine à états compare, elle
* n'a aucune condition à recomposer, et il n'existe aucune combinaison où deux
* transitions seraient valides en même temps — c'est-à-dire prises au hasard.
*
* Remis à None dès l'entrée en chute (voir Tick), donc il vaut None pendant tout
* le vol : une transition n'a jamais à vérifier bIsFalling en plus.
*/
UENUM(BlueprintType)
enum class ELandingImpact : uint8
{
/** En l'air, ou chute trop faible pour mériter une animation. */
None,
/** Réception normale (JumpLanding). */
Soft,
/**
* Réception encaissée (HardLanding) — c'est-à-dire, très exactement, une
* chute qui a coûté de la vie. Le seuil n'est pas un réglage d'animation,
* c'est `FallDamageMinSpeed`.
*/
Hard
};
/**
* Ressort amorti à une dimension, partagé par les effets de caméra.
*
* Pourquoi un ressort plutôt qu'une courbe paramétrée par le temps : un ressort
* n'a pas d'animation « en cours » qu'il faudrait annuler pour en jouer une
* autre. Une nouvelle impulsion s'ajoute simplement à l'état courant, position
* et vitesse restent continues, donc le mouvement est interruptible à n'importe
* quel instant sans jamais produire de saut. C'est exactement ce que le crouch
* demande quand le joueur martèle la touche.
*/
struct FCameraSpring
{
/** Déplacement courant, dans l'unité de ce que le ressort pilote. */
float Offset = 0.f;
/** Vitesse courante, par seconde. */
float Velocity = 0.f;
/** Impulsion pas encore versée, étalée sur RampTime pour éviter l'à-coup. */
float PendingImpulse = 0.f;
void AddImpulse(float Strength) { PendingImpulse += Strength; }
/** Remet le ressort au repos, sans transition (respawn, téléportation). */
void Reset()
{
Offset = 0.f;
Velocity = 0.f;
PendingImpulse = 0.f;
}
/**
* Intègre le ressort d'un pas de temps et renvoie le nouvel Offset.
*
* @param Frequency fréquence propre en Hz : vitesse de retour au repos.
* @param Damping 1 = retour net sans rebond, en dessous ça oscille.
* @param RampTime durée sur laquelle une impulsion est versée (0 = d'un coup).
* @param MaxOffset garde-fou d'amplitude.
*/
float Update(float Step, float Frequency, float Damping, float RampTime, float MaxOffset);
};
/**
* Personnage joueur en vue première personne.
* Les assets d'input (Input Mapping Context + Input Actions) se branchent
* dans le Blueprint enfant (BP_FpsPlayer), catégorie "Input".
*/
UCLASS()
class EMBERWILD_API AFpsPlayer : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AFpsPlayer();
// 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;
/** Vrai tant que le joueur sprinte (utile pour l'anim ou la stamina plus tard). */
UFUNCTION(BlueprintPure, Category = "Mouvement")
bool IsSprinting() const { return bIsSprinting; }
UFUNCTION(BlueprintPure, Category = "Camera")
UCameraComponent* GetFirstPersonCamera() const { return FirstPersonCamera; }
UFUNCTION(BlueprintPure, Category = "Camera")
UHeadBobComponent* GetHeadBob() const { return HeadBob; }
/** Les bras vus par le joueur local. C'est ici que viendra l'objet en main. */
UFUNCTION(BlueprintPure, Category = "Animation")
USkeletalMeshComponent* GetFirstPersonArms() const { return FirstPersonArms; }
/** La réception en cours, ou None en l'air. Lue par les AnimInstance. */
UFUNCTION(BlueprintPure, Category = "Animation")
ELandingImpact GetLandingImpact() const { return LandingImpact; }
/**
* Joue un montage sur les bras du joueur LOCAL, et seulement lui.
*
* Rien n'est replique et c'est voulu : les bras sont en OnlyOwnerSee, donc
* les animer chez le serveur ou chez un ami ferait tourner un squelette que
* personne ne rend. Le geste vu par les autres joueurs viendra du mesh 3e
* personne, avec ses propres animations, le jour ou l'animateur les livrera.
*
* @return la duree du montage en secondes, 0 s'il n'a pas pu etre joue.
*/
UFUNCTION(BlueprintCallable, Category = "Animation")
float PlayArmsMontage(UAnimMontage* Montage, float PlayRate = 1.f);
UFUNCTION(BlueprintPure, Category = "Interaction")
UInteractionComponent* GetInteractionComponent() const { return InteractionComponent; }
UFUNCTION(BlueprintPure, Category = "Inventaire")
UInventoryComponent* GetInventoryComponent() const { return InventoryComponent; }
UFUNCTION(BlueprintPure, Category = "Crafting")
UCraftingComponent* GetCraftingComponent() const { return CraftingComponent; }
UFUNCTION(BlueprintPure, Category = "Survie")
USurvivalStatsComponent* GetSurvivalStats() const { return SurvivalStats; }
UFUNCTION(BlueprintPure, Category = "Apparence")
UCharacterAppearanceComponent* GetAppearanceComponent() const { return AppearanceComponent; }
UFUNCTION(BlueprintPure, Category = "Inventaire")
UHeldItemComponent* GetHeldItemComponent() const { return HeldItemComponent; }
UFUNCTION(BlueprintPure, Category = "Audio")
UFootstepComponent* GetFootstepComponent() const { return FootstepComponent; }
// ------------------------------------------------------------------
// Commandes console d'apparence
//
// Elles vivent ICI et non sur UCharacterAppearanceComponent, ou leur place
// serait pourtant plus naturelle : la chaine de routage des commandes
// (UPlayer::Exec, dans Player.cpp) interroge le monde, le PlayerInput, le
// controller, le PAWN, le HUD, le GameMode, le CheatManager, le GameState
// et le camera manager -- jamais les composants, AActor ne surchargeant pas
// ProcessConsoleExec pour les parcourir. Une UFUNCTION(Exec) posee sur un
// composant est silencieusement ignoree, sans le moindre message.
//
// Le pawn est le maillon le plus proche du composant dans cette chaine,
// d'ou ces trois delegations d'une ligne. Elles disparaitront le jour ou
// l'ecran de customisation existera.
// ------------------------------------------------------------------
/** `EmberPart hair 3` -- body, head, hair, eyebrows, beard, mustache, eyes. */
UFUNCTION(Exec, Category = "Apparence")
void EmberPart(const FString& Part, int32 Index);
/** `EmberColor skin 2` -- skin, hair, eye, underwear. Index dans la palette. */
UFUNCTION(Exec, Category = "Apparence")
void EmberColor(const FString& Part, int32 ColorIndex);
/** Ecrit l'apparence courante et le nombre d'options de chaque categorie. */
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; }
/**
* Coupe les etats maintenus avant de desactiver les inputs du pawn.
*
* Sprint et saut sont declenches par Started et arretes par Completed.
* Si on desactive l'input pendant que la touche est enfoncee, le Completed
* n'a plus personne pour l'ecouter et l'etat reste bloque : on rouvrirait
* le jeu en sprintant sans toucher a Maj.
*/
UFUNCTION(BlueprintCallable, Category = "Input")
void ClearTransientInputStates();
/**
* Retire des objets d'un slot et les fait apparaitre au sol devant le joueur.
* @param Quantity nombre d'exemplaires ; passe un grand nombre pour tout jeter.
* @return true si quelque chose est reellement sorti de l'inventaire.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool DropSlot(int32 SlotIndex, int32 Quantity);
/**
* Meme chose, depuis un inventaire quelconque : celui d'un coffre ouvert,
* typiquement.
*
* C'est le PAWN qui pose au sol et pas le composant, pour la meme raison
* que le surplus de fabrication : un UInventoryComponent ne sait pas
* engendrer un acteur, et un coffre qui deposerait lui-meme laisserait le
* butin dans sa propre caisse. L'objet part donc toujours devant les yeux
* du joueur, quel que soit le contenant d'ou il sort.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool DropFromInventory(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity);
/** Vide l'inventaire entier en un tas autour du personnage. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void DropAllItems();
UFUNCTION(BlueprintPure, Category = "Survie")
bool IsDead() const { return bIsDead; }
/**
* Secoue la caméra verticalement. Strength est la vitesse initiale en cm/s
* donnée à la caméra ; positif = vers le haut, négatif = vers le bas.
* Réutilisable pour une explosion, un recul d'arme, un coup reçu.
*/
UFUNCTION(BlueprintCallable, Category = "Camera")
void AddCameraPunch(float Strength);
/**
* Recopie les reglages du joueur dans le pawn.
*
* Appelee au BeginPlay ET a chaque fois que USurvivalUserSettings diffuse :
* apres un respawn le pawn est un NOUVEL acteur, une valeur poussee une
* seule fois serait perdue avec le cadavre. UFUNCTION obligatoire, c'est ce
* qui permet le AddDynamic sur le delegue.
*/
UFUNCTION()
void ApplyUserSettings();
/**
* Pose la transform de la camera juste avant que la vue soit lue.
*
* Le gestionnaire de camera passe par ici pour construire le point de vue :
* c'est le seul endroit ou l'on est certain de lire la position de l'acteur
* et la demi-hauteur de la capsule au MEME instant. Le Tick du pawn, lui,
* n'a aucune dependance de tick vis-a-vis du CharacterMovement (verifie dans
* Character.cpp : seul le Mesh en recoit une), donc la frame ou la capsule
* change de taille pouvait etre calculee avec l'ancienne demi-hauteur --
* soit un decalage de 28 cm sur une frame, a chaque accroupissement.
* Bonus : le regard n'a plus une frame de retard sur la souris.
*/
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;
/**
* Les deux moities de « le controller est arrive ».
*
* UHeldItemComponent ne peut lire la case active QUE chez le joueur local,
* et son BeginPlay peut tomber avant que le pawn soit possede -- chez un
* client, le pawn et son controller arrivent par deux chemins independants,
* exactement comme le PlayerState. Un joueur qui reapparait avec une hache
* deja en case 1 aurait alors la main vide jusqu'a ce qu'il touche a son
* inventaire. PossessedBy couvre le serveur, OnRep_Controller le client, et
* RefreshFromInventory() est idempotente.
*/
virtual void PossessedBy(AController* NewController) override;
virtual void OnRep_Controller() 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)
virtual void OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) override;
virtual void OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) override;
// Réception au sol et décollage
virtual void Landed(const FHitResult& Hit) override;
virtual void OnJumped_Implementation() override;
// ------------------------------------------------------------------
// Composants
// ------------------------------------------------------------------
/** Caméra placée à hauteur des yeux, suit la rotation du controller. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
TObjectPtr<UCameraComponent> FirstPersonCamera;
/**
* Bras vue première personne, visibles du seul propriétaire.
*
* Attachés à la CAMÉRA et non à la capsule : ils doivent suivre le tangage du
* regard, sinon lever les yeux les laisserait à l'horizontale. Bénéfice
* gratuit au passage — ils héritent du ressort d'impact et de la bascule
* d'accroupissement, donc les bras encaissent l'atterrissage avec la caméra
* sans une ligne de code de plus.
*
* Sa transform relative se règle À L'ŒIL dans BP_FpsPlayer : les valeurs du
* constructeur ne sont qu'un point de départ, elles dépendent entièrement de
* l'origine du FBX livré par l'animateur.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation")
TObjectPtr<USkeletalMeshComponent> FirstPersonArms;
/**
* Montage joué sur les bras au ramassage d'un objet (MTG_GrabItem).
* Vide = aucun geste, le ramassage fonctionne quand même.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
TObjectPtr<UAnimMontage> GrabMontage;
// ------------------------------------------------------------------
// Dégâts de chute
//
// C'est ce bloc qui définit la réception encaissée, et pas l'inverse :
// `ELandingImpact::Hard` signifie exactement « cet atterrissage a coûté de la
// vie ». Un seuil d'animation réglé de son côté finirait forcément par
// diverger de celui des dégâts, et le joueur verrait son personnage encaisser
// sans rien perdre — ou pire, perdre de la vie en se réceptionnant
// souplement. Une animation qui ment sur l'état du jeu est pire que pas
// d'animation du tout.
//
// L'atterrissage est un ÉTAT de la machine et non un montage : les trois
// animations livrées (JumpStart, JumpFallLoop, JumpLanding) forment une
// chaîne dont le milieu est une BOUCLE, et une boucle ne se joue pas en
// montage.
// ------------------------------------------------------------------
/**
* Vitesse de chute (cm/s) à partir de laquelle on prend des dégâts.
*
* Doit rester nettement au-dessus de la retombée d'un saut sur place
* (~JumpZVelocity, 450 par défaut), sinon sauter coûte de la vie et le
* déplacement devient une punition.
*
* À 800 cm/s, c'est une chute d'environ 3,2 m (v = racine(2·g·h), g = 980).
* Ne pas raisonner en hauteur pour régler cette valeur : la hauteur ne se
* mesure nulle part dans le moteur, alors que la vitesse d'impact est
* exactement ce que le CharacterMovement a sous la main — et elle tient
* compte, gratuitement, d'un saut fait en tombant.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fall Damage", meta = (ClampMin = "0"))
float FallDamageMinSpeed = 800.f;
/** Vitesse (cm/s) au-delà de laquelle les dégâts plafonnent (~20 m de chute). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fall Damage", meta = (ClampMin = "1"))
float FallDamageMaxSpeed = 2000.f;
/**
* Dégâts pris pile au seuil. Volontairement non nul : franchir la limite
* doit se sentir, sinon le seuil n'existe que dans le code.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fall Damage", meta = (ClampMin = "0"))
float FallDamageMin = 15.f;
/** Dégâts pris à FallDamageMaxSpeed et au-delà. À 100, la chute est mortelle. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fall Damage", meta = (ClampMin = "0"))
float FallDamageMax = 100.f;
/** Les dégâts de la chute qui vient d'avoir lieu, 0 sous le seuil. */
UFUNCTION(BlueprintPure, Category = "Fall Damage")
float ComputeFallDamage(float FallSpeed) const;
/**
* Balancement de tête procédural : respiration à l'arrêt, pas à la marche,
* foulée au sprint. Tous ses réglages sont dans le Details panel du composant,
* pas ici : cinq profils de cinq valeurs noieraient la catégorie "Camera".
*
* Il ne touche pas la caméra lui-même, il expose deux décalages que
* ApplyCameraTransform additionne — cette fonction reste le seul écrivain.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
TObjectPtr<UHeadBobComponent> HeadBob;
/** Détecte et déclenche les interactions avec le monde. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction")
TObjectPtr<UInteractionComponent> InteractionComponent;
/**
* Inventaire du joueur. Sur le pawn et non sur le controller :
* un cadavre qui garde son butin fait partie du gameplay de survie.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
TObjectPtr<UInventoryComponent> InventoryComponent;
/**
* Fabrication. Sur le pawn comme l'inventaire : c'est ce corps qui fabrique,
* et il lit l'inventaire du meme acteur.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Crafting")
TObjectPtr<UCraftingComponent> CraftingComponent;
/** Vie, faim, soif. Sur le pawn : c'est ce corps qui a faim. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Survie")
TObjectPtr<USurvivalStatsComponent> SurvivalStats;
/**
* Monte la tete, les cheveux, la barbe et les couleurs sur le mesh 3e
* personne.
*
* Sur le pawn et non sur le controller, alors que l'apparence, elle, vivra
* sur le PlayerState : ce composant ne STOCKE rien, il assemble un corps.
* Or c'est le corps qui meurt et reapparait, et un nouveau pawn doit se
* remonter tout seul.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Apparence")
TObjectPtr<UCharacterAppearanceComponent> AppearanceComponent;
/**
* Montre dans la main l'objet de la case active de la barre rapide.
*
* Sur le pawn : ce sont ces bras-la et ce corps-la qui tiennent l'objet, et
* un cadavre ne tient plus rien. Il lit l'inventaire du meme acteur.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
TObjectPtr<UHeldItemComponent> HeldItemComponent;
/**
* Bruits de pas.
*
* Sur le pawn et sur TOUS les pawns, pas seulement celui du joueur local :
* le composant ne replique rien, chaque machine calcule les pas de tous les
* corps qu'elle connait a partir de leur vitesse deja synchronisee. Le
* mettre sur le controller n'aurait donc donne les pas que d'un joueur.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Audio")
TObjectPtr<UFootstepComponent> FootstepComponent;
// ------------------------------------------------------------------
// Enhanced Input : assets à assigner dans le Blueprint
// ------------------------------------------------------------------
/** Contexte ajouté au joueur au BeginPlay (IMC_Default). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputMappingContext> DefaultMappingContext;
/** Déplacement, valeur Axis2D (X = droite/gauche, Y = avant/arrière). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> MoveAction;
/** Regard souris / stick droit, valeur Axis2D. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> LookAction;
/** Saut, valeur Digital (bool). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> JumpAction;
/** Sprint, valeur Digital (bool) — maintenu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> SprintAction;
/** Accroupi, valeur Digital (bool) — maintenu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> CrouchAction;
/** Interaction, valeur Digital (bool). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> InteractAction;
/**
* Utilise l'objet actif de la barre rapide, valeur Digital (bool).
* A mapper sur le clic gauche.
*
* Bindee sur le PAWN volontairement : quand l'inventaire est ouvert, les
* inputs du pawn sont coupes, donc le clic gauche sert au glisser-deposer
* de l'UI sans qu'on ait le moindre test a ecrire.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> UseItemAction;
// ------------------------------------------------------------------
// Réglages : mouvement
// ------------------------------------------------------------------
/** Vitesse de marche normale (cm/s). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mouvement", meta = (ClampMin = "0"))
float WalkSpeed = 400.f;
/** Vitesse en sprint (cm/s). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mouvement", meta = (ClampMin = "0"))
float SprintSpeed = 650.f;
/** Vitesse accroupi (cm/s). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mouvement", meta = (ClampMin = "0"))
float CrouchSpeed = 200.f;
// ------------------------------------------------------------------
// Réglages : objets jetés au sol
// ------------------------------------------------------------------
/**
* Classe engendrée quand on jette un objet. Assigne ton BP_Pickup_Test :
* son ItemData est écrasé au spawn, un seul Blueprint suffit pour tout.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inventaire|Drop")
TSubclassOf<APickupItem> DroppedPickupClass;
/** Distance devant les yeux où l'objet est déposé (cm). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire|Drop", meta = (ClampMin = "10"))
float DropDistance = 120.f;
/** Hauteur au-dessus du sol trouvé, pour éviter que l'objet s'y encastre. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire|Drop")
float DropGroundOffset = 5.f;
// ------------------------------------------------------------------
// Réglages : regard
// ------------------------------------------------------------------
/**
* Sensibilité horizontale : degrés de rotation par unité de souris.
* 1.0 = brut (beaucoup trop rapide), ~0.4 est un bon point de départ.
*
* ATTENTION : ces trois valeurs, comme BaseFOV, sont ECRASEES au BeginPlay
* par USurvivalUserSettings. Elles ne servent plus que de repli si l'objet
* de réglages est absent ; pour changer la sensibilité, passe par le menu.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (ClampMin = "0.01"))
float LookSensitivityX = 0.4f;
/** Sensibilité verticale. Souvent réglée un peu plus basse que l'horizontale. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (ClampMin = "0.01"))
float LookSensitivityY = 0.4f;
/** Inverse l'axe vertical de la souris. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
bool bInvertLookY = false;
/**
* Hauteur de l'oeil AU-DESSUS DES PIEDS, debout (cm).
* 160 cm correspond a un personnage d'environ 1m76.
* Mesure depuis le sol et non depuis la capsule : c'est ce qui permet
* d'annuler le saut instantane de la capsule quand elle retrecit.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
float EyeHeightStanding = 160.f;
/** Hauteur de l'oeil au-dessus des pieds, accroupi (cm). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
float EyeHeightCrouched = 104.f;
// ------------------------------------------------------------------
// Réglages : animation d'accroupissement
// ------------------------------------------------------------------
/** Durée en secondes de la transition debout <-> accroupi. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.01"))
float CrouchTransitionDuration = 0.4f;
/**
* Tangage (degrés) au pic de la bascule. Positif = le regard plonge.
*
* Le tilt n'est plus une courbe indexée sur l'avancement de la transition
* mais un ressort relancé à chaque changement d'état : c'est ce qui permet
* de s'accroupir et de se relever coup sur coup sans que la caméra saute.
* L'impulsion est normalisée pour que le pic tombe bien sur cette valeur.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi")
float CrouchTiltPitch = 3.f;
/**
* Roulis (degrés) au pic de la bascule : bascule sur le côté.
* A 0 par defaut, le mouvement reste purement vertical.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi")
float CrouchTiltRoll = 0.f;
/**
* Fréquence du ressort de bascule (Hz). Le pic est atteint après environ
* 1/(2*PI*f) secondes : 0.9 Hz place le sommet vers 0.18 s, soit à peu près
* au milieu d'une transition de 0.4 s. Plus haut = plus sec et plus court.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.1"))
float CrouchTiltFrequency = 0.9f;
/** Amortissement de la bascule. 1 = aucun rebond ; en dessous, la tête oscille. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.05", ClampMax = "2.0"))
float CrouchTiltDamping = 1.f;
/**
* Durée (s) sur laquelle l'impulsion de bascule est versée dans le ressort.
* A 0 la rotation démarre à pleine vitesse angulaire en une frame, ce qui
* se lit comme un claquement ; quelques centièmes suffisent à l'adoucir.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0"))
float CrouchTiltRampTime = 0.05f;
// ------------------------------------------------------------------
// Réglages : impacts caméra (saut / atterrissage)
// ------------------------------------------------------------------
/**
* Fréquence du ressort de caméra en Hz : à quelle vitesse elle revient
* en place après un choc. Plus haut = plus sec.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0.1"))
float PunchFrequency = 3.f;
/**
* Amortissement du ressort. 1.0 = retour net sans rebond,
* 0.5 = un rebond visible, en dessous ça oscille longtemps.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0.05", ClampMax = "2.0"))
float PunchDamping = 0.85f;
/**
* Durée (s) sur laquelle une impulsion est injectée dans le ressort.
* A 0 elle est appliquée d'un coup : la caméra part à pleine vitesse en
* une frame, ce qui se lit comme un a-coup. Quelques centiemes suffisent
* a rendre l'entree en mouvement progressive.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0"))
float PunchImpulseRampTime = 0.06f;
/** Degrés de tangage ajoutés par cm de déplacement vertical de la caméra. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact")
float PunchPitchPerCm = 0.35f;
/** Amplitude maximale du déplacement vertical (cm), garde-fou. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0"))
float PunchMaxOffset = 50.f;
/**
* Impulsion (cm/s) à l'atterrissage, quand l'impact est au maximum.
* Ordre de grandeur : le deplacement en cm de la camera vaut environ
* cette valeur x 0.018 (avec les reglages de ressort par defaut).
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0"))
float LandPunchStrength = 2000.f;
/** En dessous de cette vitesse de chute (cm/s), aucun effet à l'atterrissage. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0"))
float LandPunchMinFallSpeed = 150.f;
/**
* Vitesse de chute (cm/s) à partir de laquelle l'effet est au maximum.
* Un saut sur place retombe a peu pres a JumpZVelocity (450), donc cette
* valeur doit rester proche pour qu'un saut normal se sente deja bien.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "1"))
float LandPunchMaxFallSpeed = 900.f;
/**
* Impulsion (cm/s) au décollage du saut. Positif = la caméra s'enfonce
* un instant (compression des jambes) ; mets une valeur négative pour
* qu'elle parte vers le haut à la place.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact")
float JumpPunchStrength = 550.f;
// ------------------------------------------------------------------
// Réglages : champ de vision
// ------------------------------------------------------------------
/** FOV au repos, en degrés. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|FOV", meta = (ClampMin = "30", ClampMax = "150"))
float BaseFOV = 90.f;
/** Degrés de FOV ajoutés en sprint. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|FOV")
float SprintFOVOffset = 8.f;
/** Vitesse de transition du FOV. Plus haut = plus brusque. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|FOV", meta = (ClampMin = "0.1"))
float FOVInterpSpeed = 8.f;
// ------------------------------------------------------------------
// Handlers d'input
// ------------------------------------------------------------------
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartSprint();
void StopSprint();
void StartCrouch();
void StopCrouch();
void Interact();
/**
* Relachement de la touche de sprint / d'accroupissement.
*
* Separes de StopSprint et StopCrouch, qui restent des ordres inconditionnels :
* en mode bascule le relachement ne doit rien faire, alors que la mort ou
* l'ouverture de l'inventaire doivent bel et bien couper le sprint.
*/
void HandleSprintReleased();
void HandleCrouchReleased();
/**
* Clic gauche : lance le GESTE de l'objet actif de la barre rapide.
*
* Elle ne produit plus l'effet elle-même — c'est le marqueur
* `UAnimNotify_ItemAction` posé dans le montage qui le fait, au moment où le
* geste frappe. Sans montage à jouer, elle retombe sur l'effet immédiat :
* un objet qui n'a pas d'animation n'aura jamais de marqueur, et le priver
* d'effet serait un objet cassé plutôt qu'un objet sans animation.
*/
void UseSelectedItem();
public:
/**
* L'objet en main agit MAINTENANT. Appelée par l'AnimNotify du montage.
*
* Publique et `BlueprintCallable` pour cette seule raison : c'est le point
* d'entrée de l'animation vers le gameplay, y compris depuis un notify
* Blueprint. Elle ne fait rien d'autre que ce que faisait la seconde moitié
* de `UseSelectedItem` : router vers l'autorité.
*/
UFUNCTION(BlueprintCallable, Category = "Items")
void TriggerHeldItemAction();
private:
/**
* La case visée par le clic, retenue jusqu'à l'impact.
*
* Relire la case active au moment du marqueur serait faux : trois dixièmes
* de seconde séparent l'appui de l'impact, et la molette peut passer sur un
* autre objet entre-temps — le coup de hache partirait alors avec la pomme
* qu'on vient de sélectionner. Changer d'objet coupe bien le montage en
* cours (`PlayEquipMontage` ne saute pas), mais seulement quand le nouvel
* objet a un montage d'équipement : ce n'est pas une garantie sur laquelle
* s'appuyer.
*/
int32 PendingUseSlotIndex = INDEX_NONE;
// ------------------------------------------------------------------
// 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);
/**
* Recopie l'état des animations dans la transform de la caméra.
* Séparée de l'intégration : appelée à la fois en fin de Tick et depuis
* CalcCamera, elle doit rester idempotente et sans notion de temps.
*/
void ApplyCameraTransform();
/** Relance la bascule de tête. Direction : +1 on s'accroupit, -1 on se relève. */
void AddCrouchTilt(float Direction);
/** Branche sur OnDied du composant de survie. */
UFUNCTION()
void HandleDeath();
/**
* Le resultat d'une fabrication n'est pas entre dans l'inventaire : on le
* pose aux pieds du joueur plutot que de le laisser disparaitre.
*
* C'est le pawn qui fait ca et non UCraftingComponent : le composant n'a pas
* a savoir comment on engendre un objet dans le monde, et un etabli qui
* porterait le meme composant deposerait son surplus sur sa propre table.
*/
UFUNCTION()
void HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses);
/** Fait apparaitre un pickup deja configure a un endroit donne. */
APickupItem* SpawnPickup(const struct FInventorySlot& Slot, const FVector& Location);
/** Descend une position jusqu'au sol, pour ne pas laisser d'objet en l'air. */
FVector PlaceOnGround(const FVector& Start) const;
bool bIsDead = false;
/** Progression de l'animation d'accroupissement : 0 = debout, 1 = accroupi. */
float CrouchAlpha = 0.f;
/** Valeur visée par CrouchAlpha. */
float CrouchTargetAlpha = 0.f;
/** Déplacement vertical de la caméra (cm) : sauts, atterrissages, explosions. */
FCameraSpring PunchSpring;
/**
* Bascule de tête pendant l'accroupissement, en unités normalisées :
* son pic vaut 1, ce sont CrouchTiltPitch et CrouchTiltRoll qui le
* convertissent en degrés.
*/
FCameraSpring CrouchTiltSpring;
/** Vitesse verticale mémorisée pendant la chute, pour calibrer l'atterrissage. */
float LastFallSpeed = 0.f;
/**
* La réception en cours, lue par les AnimInstance.
*
* Écrite à deux endroits et deux seulement : remise à None dans Tick tant
* qu'on est en l'air, posée à sa vraie valeur dans Landed(). C'est ce qui
* garantit qu'elle vaut None pendant tout le vol — sans quoi la machine à
* états prendrait sa transition de réception avec la valeur du saut
* précédent, dès la première image de chute.
*/
ELandingImpact LandingImpact = ELandingImpact::None;
/** 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. */
bool bLoggedLookTypeWarning = false;
// ------------------------------------------------------------------
// Copies locales des réglages joueur, rafraîchies par ApplyUserSettings
// ------------------------------------------------------------------
/** 0 = caméra parfaitement fixe, 1 = amplitude d'origine. */
float CameraShakeMultiplier = 1.f;
bool bSprintToggleMode = false;
bool bCrouchToggleMode = false;
};
@@ -0,0 +1,663 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
// 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"
#include "FpsPlayerController.generated.h"
class AStorageContainer;
class UConfirmDialogWidget;
class UCraftingComponent;
class UHotbarWidget;
class UInputAction;
class UInventoryComponent;
class UInteractionPromptWidget;
class UInventoryScreenWidget;
class UPauseMenuWidget;
class UScreenFadeComponent;
class USettingsMenuWidget;
class USurvivalStatsWidget;
class UUserWidget;
struct FInputActionValue;
/**
* Controller du joueur. C'est ici que vit l'UI, et pas dans le personnage :
* le controller survit a la mort du pawn, alors que le personnage non.
* Si tu creais le HUD dans AFpsPlayer, il disparaitrait a chaque respawn.
*/
UCLASS()
class EMBERWILD_API AFpsPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AFpsPlayerController();
/** Ouvre ou ferme l'ecran, toujours sur l'onglet Inventaire. */
UFUNCTION(BlueprintCallable, Category = "UI")
void ToggleInventory();
/**
* Ouvre l'ecran directement sur l'onglet Fabrication.
* Depuis l'onglet Inventaire elle bascule, depuis l'onglet Fabrication elle referme.
*/
UFUNCTION(BlueprintCallable, Category = "UI")
void ToggleCrafting();
/**
* Ouvre l'ecran de fabrication depuis un poste de travail.
*
* L'acces au poste ne dure QUE le temps de l'ecran : il est accorde ici et
* repris a la fermeture. C'est le contrat de l'interaction -- on fabrique a
* l'etabli parce qu'on est en train de s'en servir, pas parce qu'on en a
* croise un un jour.
*
* Le contrôleur s'en charge et non l'etabli : c'est lui qui sait quand
* l'ecran se referme, et lui seul peut donc rendre le poste au bon moment.
*/
UFUNCTION(BlueprintCallable, Category = "UI")
void OpenCraftingAtStation(ECraftingStation Station);
UFUNCTION(BlueprintPure, Category = "UI")
bool IsInventoryOpen() const { return bInventoryOpen; }
/**
* Ouvre l'ecran d'inventaire avec le panneau du coffre a cote : le sac a
* gauche, son contenu a droite.
*
* Ce n'est PAS un ecran a part. Le sac du joueur reste exactement au meme
* endroit qu'à l'ouverture par Tab -- c'est tout l'interet, un ecran
* distinct le ferait sauter a chaque fois qu'on ouvre un coffre.
*
* Comme pour l'etabli, c'est le controller qui pilote et non le coffre : lui
* seul sait quand l'ecran se referme, et donc quand refermer le couvercle.
*/
UFUNCTION(BlueprintCallable, Category = "UI|Storage")
void OpenStorage(AStorageContainer* Container);
/** Referme l'ecran de coffre. Sans effet s'il n'y en a pas d'ouvert. */
UFUNCTION(BlueprintCallable, Category = "UI|Storage")
void CloseStorage();
UFUNCTION(BlueprintPure, Category = "UI|Storage")
bool IsStorageOpen() const { return bStorageOpen; }
/**
* Echap, quel que soit le chemin par lequel il arrive.
*
* Une seule touche pour cinq usages, d'ou une priorite explicite : boite de
* confirmation, reglages, coffre, inventaire, puis pause. Sans cet ordre on
* se retrouve avec deux ecrans ouverts en meme temps.
*
* Publique parce qu'elle a DEUX appelants : l'action Enhanced Input quand le
* viewport tient le focus clavier, et le NativeOnKeyDown d'un widget quand
* c'est lui qui l'a -- les touches n'atteignent alors plus l'input du jeu.
*/
void HandleEscapeInput();
/** Ferme le menu de pause et relance le monde. */
UFUNCTION(BlueprintCallable, Category = "UI|Pause")
void ResumeGame();
/** Ouvre la demande de confirmation. Ne quitte rien par elle-meme. */
UFUNCTION(BlueprintCallable, Category = "UI|Pause")
void RequestQuitToMainMenu();
UFUNCTION(BlueprintPure, Category = "UI|Pause")
bool IsPauseMenuOpen() const { return bPauseMenuOpen; }
/** Ouvre l'ecran de reglages par-dessus le menu de pause. */
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();
/**
* 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;
protected:
virtual void BeginPlay() override;
/** Serveur uniquement. Chez un client, c'est AcknowledgePossession qui arrive. */
virtual void OnPossess(APawn* InPawn) override;
/**
* Le pendant CLIENT de OnPossess.
*
* OnPossess n'existe QUE sur le serveur : chez un client le pawn arrive par
* replication et cette fonction n'est jamais appelee. Comme c'est elle qui
* construisait le HUD, un joueur qui se connectait n'avait aucune interface --
* ni viseur, ni barre rapide, ni jauges. Le piege est d'autant plus fourbe
* que l'hote, lui, est bien le serveur et voit son HUD parfaitement.
*/
virtual void AcknowledgePossession(APawn* P) override;
virtual void SetupInputComponent() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
// ------------------------------------------------------------------
// Fondus
//
// La mécanique elle-même vit dans UScreenFadeComponent, partagé avec le
// menu principal. Ici on ne garde que les durées propres au jeu.
// La couleur et le WBP_Fade se règlent sur le composant.
// ------------------------------------------------------------------
/** Le voile. Assigne-lui ton WBP_Fade dans BP_FpsPlayerController. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UI|Fondu")
TObjectPtr<UScreenFadeComponent> ScreenFade;
/** Décoche pour démarrer sans fondu, pratique pendant les tests. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu")
bool bFadeInOnStart = true;
/**
* Délai maximal d'attente du pawn avant d'éclaircir quand même.
*
* Filet de sécurité pur : si la possession n'arrivait jamais, l'écran
* resterait noir pour toujours et le joueur croirait le jeu planté. Mieux
* vaut une vue bancale qu'un écran mort.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "1", Units = "s"))
float StartupFadeTimeout = 10.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1"))
float StartupFadeDuration = 1.5f;
/** Durée du fondu au noir quand le joueur meurt. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1"))
float DeathFadeDuration = 1.f;
/**
* Temps total entre la mort et la réapparition.
* Doit rester supérieur à DeathFadeDuration, sinon le joueur réapparaît
* avant que l'écran soit entièrement noir.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1"))
float RespawnDelay = 3.f;
/**
* Ouverture de l'inventaire, valeur Digital (bool).
* L'input d'interface vit sur le controller et non sur le pawn : ouvrir
* son sac doit rester possible meme sans corps a piloter.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> ToggleInventoryAction;
/**
* Ouverture directe sur l'onglet Fabrication, valeur Digital (bool).
* Sur le controller comme ToggleInventoryAction : elle doit repondre alors
* meme que l'input du pawn est coupe, ecran ouvert.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> ToggleCraftingAction;
/**
* Jette l'objet survole, valeur Digital (bool). Ctrl enfonce = un seul
* exemplaire au lieu de la pile entiere.
* Sur le controller aussi : le pawn a ses inputs coupes quand l'inventaire
* est ouvert, donc un binding sur le pawn ne repondrait jamais.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> DropItemAction;
/**
* Selection directe d'un slot de la barre rapide, valeur Axis1D.
*
* Une seule action pour toutes les touches : dans IMC_Default, mappe les
* touches 1 a 6 sur cette action et donne a chacune un modificateur
* Scalar different (1, 2, 3...). Le handler lit la valeur et en deduit
* le slot. Six actions separees seraient six assets a maintenir.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> HotbarSelectAction;
/** Molette de souris pour defiler dans la barre, valeur Axis1D. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> HotbarScrollAction;
/**
* Echap : ferme l'inventaire s'il est ouvert, sinon bascule la pause.
*
* ATTENTION : coche "Trigger When Paused" sur l'asset IA_Pause. Un jeu en
* pause ne fait plus qu'un tick reduit ou seules les actions portant ce
* drapeau sont evaluees -- sans lui, on peut mettre en pause mais plus
* jamais en sortir au clavier.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> PauseAction;
/**
* Viseur, affiche en permanence. Widget separe du prompt : ce dernier se
* met en Collapsed des qu'on ne vise plus rien, et emporterait le viseur
* avec lui s'ils partageaient le meme widget.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UUserWidget> CrosshairClass;
/** A assigner dans BP_FpsPlayerController : ton WBP_InteractionPrompt. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UInteractionPromptWidget> InteractionPromptClass;
/**
* Ton WBP_InventoryScreen : l'ecran a onglets qui contient la grille
* d'inventaire ET la page de fabrication. Ce n'est plus WBP_Inventory
* directement, celui-ci est devenu une page a l'interieur.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UInventoryScreenWidget> InventoryScreenClass;
/**
* Distance au-dela de laquelle l'ecran de coffre se referme tout seul.
*
* Sans elle, on viderait un coffre depuis l'autre bout de la carte : rien
* n'oblige le joueur a rester devant une fois l'ecran ouvert.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Storage", meta = (ClampMin = "50", Units = "cm"))
float MaxStorageDistance = 400.f;
/** Ton WBP_Hotbar, affiche en permanence. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UHotbarWidget> HotbarClass;
/** Ton WBP_SurvivalStats : les trois jauges. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<USurvivalStatsWidget> SurvivalStatsClass;
/** Ton WBP_PauseMenu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Pause")
TSubclassOf<UPauseMenuWidget> PauseMenuClass;
/**
* Ton WBP_ConfirmDialog, le meme que celui du menu principal. Laisse-le
* assigne : sans lui on retourne au menu sans rien demander, et la partie
* est perdue sur un clic.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Pause")
TSubclassOf<UConfirmDialogWidget> QuitToMenuDialogClass;
/** La question posee avant d'abandonner la partie. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Pause")
FText QuitToMenuMessage;
/**
* Ton WBP_Settings, le meme que celui du menu principal. C'est tout
* l'interet de son delegue OnCloseRequested : l'ecran ignore qui l'ouvre.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Reglages")
TSubclassOf<USettingsMenuWidget> SettingsMenuClass;
/**
* Ta map de menu principal.
*
* TSoftObjectPtr comme dans AMainMenuPlayerController : une reference dure
* ferait charger la map du menu en meme temps que celle du jeu.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Pause")
TSoftObjectPtr<UWorld> MainMenuLevel;
/** Fondu au noir avant de rendre la main au menu principal. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1"))
float QuitToMenuFadeDuration = 1.f;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<UUserWidget> Crosshair;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<UInteractionPromptWidget> InteractionPrompt;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<UInventoryScreenWidget> InventoryScreen;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<UHotbarWidget> Hotbar;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<USurvivalStatsWidget> SurvivalStatsWidget;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI|Pause")
TObjectPtr<UPauseMenuWidget> PauseMenu;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI|Pause")
TObjectPtr<UConfirmDialogWidget> QuitToMenuDialog;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI|Reglages")
TObjectPtr<USettingsMenuWidget> SettingsMenu;
private:
void CreateHudWidgets();
/**
* Tout ce qu'il faut faire quand ce controller prend un pawn, quel que soit
* le cote du reseau : creer le HUD s'il manque, rebrancher les widgets sur
* le nouveau corps, ecouter sa mort.
*
* Appelee par OnPossess (serveur) ET AcknowledgePossession (client), donc
* potentiellement deux fois chez l'hote qui est les deux a la fois. Elle
* doit rester idempotente -- d'ou AddUniqueDynamic sur OnDied, et
* CreateHudWidgets qui ne cree que ce qui n'existe pas encore.
*/
void SetupForPossessedPawn(APawn* InPawn);
/**
* Éclaircit l'écran d'arrivée, une seule fois, et seulement quand le joueur
* a réellement un corps.
*
* La condition n'est PAS « ai-je un pawn » mais « la caméra est-elle sur mon
* pawn ». Chez un client, le contrôleur commence sur un `SpectatorPawn` né
* au centre du monde -- sous le terrain -- et y reste encore un instant
* APRÈS `AcknowledgePossession`. Mesuré sur une vraie connexion : au moment
* où la possession est acquittée, `GetViewTarget()` vaut toujours
* `SpectatorPawn_0`. Éclaircir là montre le dessous de la carte.
*
* Tant que la caméra n'y est pas, on repasse toutes les `StartupFadeRetry`
* secondes. L'écran reste noir pendant ce temps -- le rendu du monde est
* suspendu par le GameInstance depuis le chargement de la map.
*/
void TryStartupFadeIn();
/**
* Éclaircit pour de bon, sans vérifier la caméra.
* Appelée par le filet de sécurité : mieux vaut une vue bancale qu'un écran
* noir définitif, qui se lit comme un plantage.
*/
void CompleteStartupFadeIn();
/** L'éclaircissement lui-même, après le temps de chauffe du monde. */
void StartStartupFade();
void OpenPauseMenu();
/** Oui : fondu au noir, puis retour au menu principal. */
UFUNCTION()
void HandleQuitToMenuConfirmed();
/**
* L'hote s'en va : ce client doit partir aussi.
*
* Une RPC explicite et non un simple abandon de connexion. Quand l'hote
* ferme sa partie, le moteur ne previent personne : les clients restent
* dans un monde mort, avec le pawn fantome de l'hote encore plante devant
* eux, jusqu'a un timeout qui peut ne jamais venir. On leur dit donc de
* partir, et ils partent avec leur propre fondu.
*/
UFUNCTION(Client, Reliable)
void Client_ReturnToMainMenu();
/**
* Lance le depart : ferme les ecrans, previent le GameInstance, fond au noir.
*
* Idempotente -- un client peut recevoir l'ordre de l'hote alors qu'il avait
* deja clique Quitter de son cote, et deux fondus simultanes se
* marcheraient dessus.
*/
void BeginQuitToMainMenu();
/**
* Envoie l'ordre de depart a tous les AUTRES joueurs. Serveur uniquement.
*
* Appele AVANT le fondu de l'hote et non apres : les clients ont ainsi
* toute la duree de ce fondu pour recevoir le message et lancer le leur.
* Prevenir a la fin donnerait un client qui decouvre la coupure sans
* explication, ecran de jeu fige.
*/
void KickRemoteClientsToMainMenu();
/** Non : on referme la boite et on rend la main au menu de pause. */
UFUNCTION()
void HandleQuitToMenuCancelled();
/** Le fondu est termine : on charge la map du menu, si c'est ce qu'on attendait. */
UFUNCTION()
void HandleFadeFinished();
/**
* Le joueur a accepte une invitation EN PLEINE PARTIE : on fond au noir
* avant que le voyage vers l'autre hote parte.
*
* bQuittingToMainMenu reste faux : ce n'est pas nous qui chargeons la map
* suivante, c'est USessionSubsystem. HandleFadeFinished doit donc laisser
* passer ce fondu-la sans rien faire.
*/
UFUNCTION()
void HandleSessionTravelPending();
/** L'ecran de reglages demande a se fermer : on rend la main au menu de pause. */
UFUNCTION()
void HandleSettingsCloseRequested();
/**
* Point d'entree unique de l'ouverture et de la fermeture de l'ecran.
*
* Tab et la touche de fabrication passent tous les deux par ici : le mode
* d'input, la souris et la barre rapide sont ainsi decides a un seul endroit.
* Deux chemins separes finiraient par diverger.
*/
void SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab);
/** Rend le poste accorde par le dernier OpenCraftingAtStation. Sans effet sinon. */
void ReleaseActiveStation();
/**
* Rend le coffre ouvert : couvercle referme, panneau masque, surveillance
* arretee. Sans effet s'il n'y en a pas.
*
* Separee de CloseStorage() pour la meme raison que ReleaseActiveStation :
* c'est la fermeture de l'ECRAN qui l'appelle, quelle qu'en soit la cause --
* Tab, Echap, la mort, un autre coffre. Un seul point de sortie, donc aucun
* chemin qui laisse un couvercle leve derriere soi.
*/
void ReleaseActiveStorage();
/**
* Mode d'input et curseur pour un ecran plein cadre.
*
* Partage par l'inventaire et le coffre : deux copies de ces huit lignes
* finiraient par diverger, et le symptome -- une camera qui ne repond plus,
* un sprint bloque -- se cherche tres longtemps.
*/
void SetUiInputMode(bool bUiOpen);
/** Le joueur s'est-il trop eloigne du coffre ouvert. Appele par un timer. */
void CheckStorageDistance();
/** Raccourci vers le composant de fabrication du pawn possede. */
UCraftingComponent* GetPawnCrafting() const;
/** L'ecran a change d'onglet : la barre rapide n'a rien a faire sous la page de craft. */
UFUNCTION()
void HandleInventoryTabChanged(bool bInventoryTabActive);
/**
* Etat unique de la barre rapide : visible partout, sauf par-dessus l'onglet
* Fabrication. Passer par une seule fonction evite les cas ou la fermeture
* de l'ecran laisserait la barre cachee.
*/
void UpdateHotbarVisibility();
/**
* Relaie le survol d'une case de la barre rapide vers le panneau de detail
* de l'ecran d'inventaire.
*
* Ignore ecran ferme : sans curseur il n'y a pas de survol, mais un widget
* masque sous la souris peut encore emettre sa sortie -- et decrire un objet
* dans un panneau invisible n'aurait aucun sens.
*/
UFUNCTION()
void HandleHotbarSlotHovered(UInventoryComponent* Inventory, int32 SlotIndex, bool bHovered);
/** Jette la pile survolee dans la grille, si l'inventaire est ouvert. */
void DropHoveredItem();
void HandleHotbarSelect(const FInputActionValue& Value);
void HandleHotbarScroll(const FInputActionValue& Value);
/** Raccourci vers l'inventaire du pawn possede. */
UInventoryComponent* GetPawnInventory() const;
/** Branche sur OnDied du pawn possede. */
UFUNCTION()
void HandlePawnDied();
/**
* 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);
/**
* 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. */
bool bStartupFadePending = false;
FTimerHandle StartupFadeTimeoutHandle;
/** Réessai tant que la caméra n'est pas sur le pawn. */
FTimerHandle StartupFadeRetryHandle;
/** Période de réessai, en secondes. Assez court pour ne pas se voir. */
static constexpr float StartupFadeRetry = 0.05f;
/** Surveillance de l'eloignement, actif seulement pendant qu'un coffre est ouvert. */
FTimerHandle StorageDistanceTimerHandle;
/** Le coffre actuellement ouvert, pour lui refermer son couvercle. */
TWeakObjectPtr<AStorageContainer> ActiveStorage;
bool bStorageOpen = false;
bool bInventoryOpen = false;
/** Onglet Fabrication affiche. N'a de sens que si bInventoryOpen est vrai. */
bool bCraftingTabActive = false;
/**
* Poste accorde par l'ecran actuellement ouvert, a rendre a la fermeture.
* Count = aucun, l'ecran a ete ouvert au clavier et pas depuis un etabli.
*/
ECraftingStation ActiveStation = ECraftingStation::Count;
/**
* Visibilite d'origine de la barre rapide, relevee a sa creation.
* On la restaure telle quelle plutot que de forcer Visible : le Blueprint a
* peut-etre choisi SelfHitTestInvisible, et l'ecraser changerait
* silencieusement son comportement au clic.
*/
ESlateVisibility HotbarDefaultVisibility = ESlateVisibility::Visible;
bool bPauseMenuOpen = false;
/**
* La boite de confirmation avale Echap elle-meme quand elle a le focus,
* mais un clic ailleurs le lui retire : sans ce booleen, Echap refermerait
* alors le menu de pause en laissant la question affichee dans le vide.
*/
bool bQuitToMenuDialogOpen = false;
bool bSettingsOpen = false;
/** Distingue le fondu de sortie de tous les autres dans HandleFadeFinished. */
bool bQuittingToMainMenu = false;
};
+234
View File
@@ -0,0 +1,234 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HeadBobComponent.generated.h"
class ACharacter;
class UCharacterMovementComponent;
/**
* Les reglages d'un etat de deplacement : immobile, marche, sprint, accroupi, en l'air.
*
* Regroupes dans une struct plutot qu'a plat sur le composant pour deux raisons.
* D'abord le Details panel : cinq etats x cinq valeurs a plat feraient vingt-cinq
* lignes melangees, alors que la struct donne un repli par etat. Ensuite le code :
* passer d'un etat a l'autre est une interpolation champ a champ, et une struct
* permet de l'ecrire une seule fois au lieu d'une par valeur.
*/
USTRUCT(BlueprintType)
struct FHeadBobProfile
{
GENERATED_BODY()
FHeadBobProfile() = default;
FHeadBobProfile(float InStrideFrequency, float InVerticalAmount, float InLateralAmount, float InRollAmount, float InPitchAmount)
: StrideFrequency(InStrideFrequency)
, VerticalAmount(InVerticalAmount)
, LateralAmount(InLateralAmount)
, RollAmount(InRollAmount)
, PitchAmount(InPitchAmount)
{
}
/**
* Foulees par seconde. Une foulee compte DEUX pas : l'oscillation verticale
* bat donc au double de cette valeur, une bosse par pied qui touche le sol.
* A 1.1, les pas tombent a 2.2 Hz, ce qui correspond a une marche normale.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "4"))
float StrideFrequency = 1.1f;
/** Amplitude verticale en cm, mesuree du repos au sommet (le debattement total vaut le double). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "10"))
float VerticalAmount = 1.6f;
/** Amplitude laterale en cm : le balancement gauche-droite du buste. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "10"))
float LateralAmount = 1.2f;
/** Roulis en degres au pic du balancement lateral. La tete penche dans le sens du pas. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (UIMin = "-3", UIMax = "3"))
float RollAmount = 0.4f;
/**
* Tangage en degres au pic vertical. A 0 par defaut, et ce n'est pas un oubli :
* le tangage deplace le reticule, donc il degrade la visee. A n'ouvrir que si
* l'on assume un jeu ou l'on ne vise pas au pixel.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (UIMin = "-3", UIMax = "3"))
float PitchAmount = 0.f;
};
/**
* Balancement de tete procedural, sans la moindre animation authoree.
*
* Le principe tient en une phase unique, avancee chaque frame par la frequence de
* l'etat courant, et une amplitude qui glisse d'un etat a l'autre. C'est ce qui
* rend le passage marche -> sprint continu : ce n'est pas une animation qu'on
* coupe pour en lancer une autre, ce sont deux jeux de coefficients entre lesquels
* on interpole pendant que la phase, elle, ne s'interrompt jamais. Remettre la
* phase a zero a chaque changement d'etat produirait exactement le "clac" qu'on
* cherche a eviter.
*
* Le composant ne touche PAS a la camera. Il expose un decalage de position et un
* decalage de rotation, que AFpsPlayer::ApplyCameraTransform additionne aux siens.
* C'est la condition pour que cette fonction reste le seul endroit du projet qui
* ecrit la transform de la camera -- elle est rappelee depuis CalcCamera, et deux
* ecrivains se marcheraient dessus une frame sur deux.
*
* Rien n'est replique et rien n'a a l'etre : le balancement n'existe que dans les
* yeux du joueur local. Les bras suivent gratuitement, puisqu'ils sont attaches a
* la camera.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UHeadBobComponent : public UActorComponent
{
GENERATED_BODY()
public:
UHeadBobComponent();
/**
* Fait avancer la phase et recalcule les decalages.
*
* Appelee par le pawn depuis son Tick, et surtout PAS depuis un TickComponent a
* lui : l'ordre de tick entre un acteur et ses composants n'est pas garanti sans
* dependance explicite, et le resultat serait un decalage lu une frame apres
* avoir ete calcule -- invisible a 120 fps, franchement mou a 30.
*/
void UpdateBob(float DeltaTime);
/** Decalage a ajouter a la position RELATIVE de la camera (X avant, Y droite, Z haut), en cm. */
UFUNCTION(BlueprintPure, Category = "Head Bob")
FVector GetLocationOffset() const { return LocationOffset; }
/** Decalage a ajouter a la rotation de la camera, en degres. */
UFUNCTION(BlueprintPure, Category = "Head Bob")
FRotator GetRotationOffset() const { return RotationOffset; }
/**
* Remet le balancement au repos sans transition.
* Pour une teleportation ou une prise de controle : on ne veut pas que la tete
* finisse le pas d'avant a l'autre bout de la carte.
*/
UFUNCTION(BlueprintCallable, Category = "Head Bob")
void ResetBob();
/**
* Facteur global pousse par les reglages joueur (curseur "secousses de camera").
* A 0 la tete ne bouge plus du tout : c'est un confort d'accessibilite, pas une
* option cosmetique, et il doit valoir pour le balancement comme pour les chocs.
*/
UFUNCTION(BlueprintCallable, Category = "Head Bob")
void SetIntensityScale(float NewScale);
protected:
virtual void BeginPlay() override;
// ------------------------------------------------------------------
// Reglages generaux
// ------------------------------------------------------------------
/** Coupe entierement l'effet, sans avoir a remettre les cinq profils a zero. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob")
bool bEnableHeadBob = true;
/**
* Multiplicateur d'amplitude commun aux cinq profils.
* C'est le curseur a bouger pour un reglage d'ensemble ("un peu moins de tout")
* sans avoir a retoucher vingt valeurs une par une.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "3"))
float GlobalIntensity = 1.f;
/**
* Vitesse de fondu d'un profil vers un autre (facteur d'interpolation
* exponentielle, par seconde). Plus haut = le sprint prend son amplitude plus
* vite. Trop haut, le changement d'etat redevient une cassure.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0.1", UIMax = "20"))
float ProfileBlendSpeed = 6.f;
// ------------------------------------------------------------------
// Seuils de choix d'etat
// ------------------------------------------------------------------
/**
* En dessous de cette vitesse horizontale (cm/s), on est considere immobile.
* Le seuil se lit sur la vitesse REELLE et non sur la touche enfoncee : marcher
* dans un mur laisse l'input actif alors que le corps ne bouge plus, et sans ce
* detail la tete continuerait de gigoter face a la cloison.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Seuils", meta = (ClampMin = "0"))
float IdleSpeedThreshold = 10.f;
/**
* Au-dessus de cette vitesse horizontale (cm/s), on bascule sur le profil de
* sprint. A regler entre WalkSpeed et SprintSpeed du pawn -- 400 et 650 par
* defaut, d'ou ces 520 : assez haut pour que la phase d'acceleration ne compte
* pas comme du sprint, assez bas pour que le sprint reel soit toujours detecte.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Seuils", meta = (ClampMin = "0"))
float SprintSpeedThreshold = 520.f;
// ------------------------------------------------------------------
// Profils, un par etat. Valeurs par defaut dans le constructeur.
// ------------------------------------------------------------------
/** Immobile : une respiration lente, quasi purement verticale. C'est ce qui evite la statue. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
FHeadBobProfile IdleProfile;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
FHeadBobProfile WalkProfile;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
FHeadBobProfile SprintProfile;
/** Accroupi : plus lent et plus discret que la marche, le corps est ramasse. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
FHeadBobProfile CrouchProfile;
/**
* En l'air : tout a zero, y compris la frequence -- la phase se fige donc en vol
* et repart d'ou elle en etait a la reception, ce qui evite que le premier pas
* apres un saut tombe sur un pied arbitraire. Le profil reste editable malgre
* tout, au cas ou l'on veuille un flottement pendant la chute.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
FHeadBobProfile AirProfile;
private:
/** Choisit le profil vise a partir de l'etat reel du CharacterMovement. */
const FHeadBobProfile& SelectTargetProfile() const;
/** Rapproche BlendedProfile de Target, champ par champ. */
void BlendTowards(const FHeadBobProfile& Target, float Step);
/**
* Position dans le cycle de foulee, en radians, ramenee dans [0, 2*PI[.
* Le repli n'est pas cosmetique : sans lui la valeur grossit indefiniment et la
* precision du float se degrade, jusqu'a un balancement qui devient saccade au
* bout d'une longue partie.
*/
float StridePhase = 0.f;
/** Profil courant, resultat du fondu entre les profils d'etat. */
FHeadBobProfile BlendedProfile;
FVector LocationOffset = FVector::ZeroVector;
FRotator RotationOffset = FRotator::ZeroRotator;
/** Pousse par les reglages joueur, se multiplie a GlobalIntensity. */
float IntensityScale = 1.f;
/** Caches : ces deux pointeurs sont lus a chaque frame, pas question d'y caster. */
TObjectPtr<ACharacter> OwnerCharacter;
TObjectPtr<UCharacterMovementComponent> OwnerMovement;
};
+339
View File
@@ -0,0 +1,339 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ItemDataAsset.h"
#include "HeldItemComponent.generated.h"
class AFpsPlayer;
class UAnimInstance;
class UItemDataAsset;
class UStaticMeshComponent;
/** Diffuse a chaque changement d'objet en main, nul compris. L'UI et l'anim s'y abonnent. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHeldItemChanged, UItemDataAsset*, NewItem);
/**
* Ce qu'a fait PlayUseMontage, du point de vue de l'appelant.
*
* Trois etats et pas un booleen, parce que l'appelant en tire trois conduites
* differentes : un geste qui demarre porte son propre marqueur d'impact et il
* n'y a plus qu'a attendre ; un geste deja en cours en enverra un lui aussi ;
* une absence de montage, elle, ne produira JAMAIS de marqueur et doit retomber
* sur l'effet immediat. Les confondre donne soit un objet sans montage qui ne
* fait plus rien du tout, soit un clic martele qui declenche l'effet a chaque
* image pendant que le bras, lui, ne rejoue pas.
*/
UENUM(BlueprintType)
enum class EUseMontageResult : uint8
{
/** Le geste vient de demarrer : son AnimNotify declenchera l'effet. */
Started,
/** Le meme geste tournait deja ; on ne l'a pas relance. */
AlreadyPlaying,
/** Rien a jouer -- l'objet n'a pas de UseMontage, ou les bras sont distants. */
NoMontage
};
/**
* Montre dans la main l'objet occupant la case active de la barre rapide.
*
* Un systeme = un composant, comme le reste du projet. Celui-ci ne decide de
* rien et ne consomme rien : il regarde l'inventaire et pose deux meshes. Ce que
* l'objet FAIT quand on clique reste dans AFpsPlayer::PerformUseItem.
*
* Deux meshes et pas un, parce qu'il y a deux corps a habiller :
*
* - les BRAS (FirstPersonArms), visibles du seul proprietaire, attaches a la
* camera. C'est la hache qu'on voit soi-meme.
* - le CORPS (le Mesh du Character), invisible pour son proprietaire mais vu
* par les trois autres joueurs -- et dont on percoit sa propre ombre.
*
* Les bras recopient du porteur leur « visible pour moi seul ». Le mesh du corps,
* lui, se cache par la VISIBILITE chez le seul joueur qui controle ce pawn --
* bOwnerNoSee y couperait son ombre, voir UpdateLocalVisibility().
*
* ------------------------------------------------------------------
* Reseau : c'est le CLIENT qui pilote, toujours
* ------------------------------------------------------------------
*
* UInventoryComponent::SelectedHotbarIndex n'est PAS replique -- c'est une
* intention purement locale, et il n'y a aucune raison de la mettre sur le
* reseau. Le serveur ne peut donc pas deviner ce que le joueur a en main : lire
* l'inventaire d'un pawn distant y trouverait toujours la case 0.
*
* Ce qui se replique est donc l'OBJET lui-meme, pas l'index. Le proprietaire
* l'applique en local et l'annonce ; le serveur l'enregistre et le rediffuse aux
* autres. Exactement le schema de bIsSprinting, COND_SkipOwner compris : renvoyer
* a l'emetteur une valeur qui a un aller-retour de retard ne peut que faire
* clignoter ce qu'il tient deja.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UHeldItemComponent : public UActorComponent
{
GENERATED_BODY()
public:
UHeldItemComponent();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/** L'objet actuellement montre dans la main. Nul = les mains sont vides. */
UFUNCTION(BlueprintPure, Category = "Held Item")
UItemDataAsset* GetHeldItem() const { return HeldItem; }
/**
* Vrai quand un mesh est effectivement monte dans la main.
*
* Different de `GetHeldItem() != nullptr`, et la nuance compte pour
* l'animation : la plupart des ressources du jeu n'ont pas bShowInHand, donc
* les tenir ne met rien dans la paume. Se fier a l'objet ferait jouer la
* posture « objet en main » sur des mains vides, ce qui se voit tout de suite.
*/
UFUNCTION(BlueprintPure, Category = "Held Item")
bool HasItemInHand() const { return bItemVisibleInHand; }
/**
* Relit la case active et met la main a jour.
*
* Sans effet ailleurs que chez le joueur local : ce composant est le seul du
* projet dont la SOURCE de verite vive uniquement sur la machine du
* proprietaire. Idempotente, donc appelable a la volee.
*/
UFUNCTION(BlueprintCallable, Category = "Held Item")
void RefreshFromInventory();
/**
* Joue le geste du clic gauche avec l'objet en main.
*
* Appelee par le PAWN au moment de l'input, donc chez le joueur local et
* AVANT tout aller-retour reseau : un coup de pioche qui attendrait la
* reponse du serveur partirait avec un temps de retard sur le clic, et c'est
* la premiere chose qu'on sent dans un jeu.
*
* Il ne fait QUE l'animation : l'effet de l'objet part du marqueur
* UAnimNotify_ItemAction pose dans le montage, pour tomber au moment de
* l'impact et pas a l'appui. D'ou le retour a trois etats, que l'appelant
* doit lire -- voir EUseMontageResult.
*
* @return ce qu'il est advenu du geste.
*/
UFUNCTION(BlueprintCallable, Category = "Held Item")
EUseMontageResult PlayUseMontage();
UPROPERTY(BlueprintAssignable, Category = "Held Item")
FOnHeldItemChanged OnHeldItemChanged;
/**
* Os ou socket des BRAS auquel l'objet s'accroche.
*
* C'est un OS du rig et pas un socket taille sur mesure, d'ou les offsets
* par objet dans UItemDataAsset : creer un socket par outil demanderait de
* rouvrir le squelette a chaque nouvel objet, alors que l'offset se regle
* dans le DataAsset qu'on est deja en train d'ecrire.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held Item")
FName FirstPersonSocket = TEXT("RightHand_Holder_JTN");
/** Os de la main droite du mannequin UE5, porteur du mesh 3e personne. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held Item")
FName ThirdPersonSocket = TEXT("hand_r");
/**
* Geste du clic gauche quand les mains sont VIDES (MTG_Punch).
*
* Ici et pas sur un DataAsset, pour la raison evidente qu'il n'y a pas
* d'objet a qui le demander. Ici et pas sur AFpsPlayer a cote de GrabMontage,
* parce que c'est ce composant qui choisit quel geste part au clic : deux
* moities de la meme decision dans deux classes finiraient par diverger.
*
* Il ne sert QUE quand aucun objet n'occupe la case active. Un objet en main
* sans UseMontage ne joue rien -- retomber sur le poing ferait boxer le
* joueur en mangeant une pomme.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item")
TObjectPtr<class UAnimMontage> UnarmedUseMontage;
// ------------------------------------------------------------------
// Accroupissement
//
// Des MONTAGES et pas des etats de la machine, alors que CrouchIdle et
// CrouchWalk, eux, sont bien des etats. La frontiere est la meme que partout
// ailleurs : ce qui DURE est un etat, ce qui ARRIVE est un montage. Plier les
// genoux prend trois dixiemes de seconde et se superpose au cycle en cours.
//
// Raison technique par-dessus, et elle est decisive : un etat dont la sortie
// depend de la fin de l'animation utilise « Automatic Rule Based on Sequence
// Player in State », qui a besoin d'un Sequence Player pour lire sa duree. Un
// etat qui contiendrait un choix entre deux animations -- avec et sans objet
// -- lui retire cette certitude, et il faudrait ecrire des durees en dur dans
// les transitions, a re-regler a chaque animation livree.
//
// Ici et pas sur UItemDataAsset : le geste est GENERIQUE, deux variantes en
// tout. Le jour ou un outil voudra la sienne, un champ optionnel sur son
// DataAsset primera sur ceux-ci, sans rien casser.
// ------------------------------------------------------------------
/** Geste d'accroupissement, mains vides (MTG_EnterCrouch). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item|Crouch")
TObjectPtr<class UAnimMontage> UnarmedEnterCrouchMontage;
/** Meme geste, un objet en main. Vide = on rejoue la version mains vides. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item|Crouch")
TObjectPtr<class UAnimMontage> HeldEnterCrouchMontage;
/** Geste de relevee, mains vides (MTG_ExitCrouch). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item|Crouch")
TObjectPtr<class UAnimMontage> UnarmedExitCrouchMontage;
/** Meme geste, un objet en main. Vide = on rejoue la version mains vides. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item|Crouch")
TObjectPtr<class UAnimMontage> HeldExitCrouchMontage;
/**
* Joue le geste d'accroupissement ou de relevee, selon ce qu'on tient.
*
* Appelee par AFpsPlayer depuis OnStartCrouch / OnEndCrouch, qui sont deja
* surchargees : le pawn sait exactement quand la capsule change de taille,
* personne n'a a le deviner.
*/
UFUNCTION(BlueprintCallable, Category = "Held Item|Crouch")
bool PlayCrouchMontage(bool bEntering);
protected:
virtual void BeginPlay() override;
virtual void OnUnregister() override;
private:
/**
* L'objet en main. Un pointeur d'asset se replique comme n'importe quelle
* reference d'objet -- c'est deja ce que fait FInventorySlot::Item, et c'est
* pour ca qu'on peut se permettre d'envoyer l'objet plutot qu'un index.
*
* COND_SkipOwner : le proprietaire vient de l'appliquer en local, le lui
* renvoyer ecraserait son etat courant par une valeur en retard.
*/
UPROPERTY(ReplicatedUsing = OnRep_HeldItem)
TObjectPtr<UItemDataAsset> HeldItem;
UFUNCTION()
void OnRep_HeldItem();
/** Point d'entree unique : applique en local, puis annonce au serveur. */
void SetHeldItem(UItemDataAsset* NewItem);
UFUNCTION(Server, Reliable)
void Server_SetHeldItem(UItemDataAsset* NewItem);
/** Monte ou demonte les deux meshes selon HeldItem. Tourne sur TOUTES les machines. */
void ApplyHeldVisuals();
/**
* Joue le geste de sortie d'outil sur les bras du joueur LOCAL.
*
* Appelee depuis SetHeldItem et pas depuis ApplyHeldVisuals, alors que les
* deux disent « l'objet en main a change » : ApplyHeldVisuals tourne sur
* toutes les machines (OnRep, serveur), et un geste de bras n'a de sens que
* chez celui qui les voit.
*/
void PlayEquipMontage();
/**
* Joue un montage sur les bras du joueur LOCAL, et lui seul.
*
* @param bSkipIfAlreadyPlaying vrai pour le clic gauche : marteler le bouton
* relancerait le geste depuis le debut a chaque image, ce qui donne un
* bras qui tremble au lieu de frapper. Faux pour l'equipement, ou
* changer d'outil doit couper net le geste precedent.
* @return true si un montage a demarre.
*/
bool PlayLocalArmsMontage(class UAnimMontage* Montage, bool bSkipIfAlreadyPlaying);
/**
* Branche ou debranche la couche d'animation de l'objet sur les bras.
*
* Locale elle aussi, et pour la meme raison que le montage : les bras sont en
* OnlyOwnerSee. Chez l'hote, lier une couche sur le squelette de bras d'un ami
* ferait evaluer un graphe que personne ne rend.
*
* Idempotente : elle compare a la couche deja liee et ne fait rien si elle est
* la bonne. C'est ce qui permet de l'appeler a chaque rafraichissement
* d'inventaire sans relancer le graphe.
*/
void UpdateArmsAnimLayer();
/**
* Cache le mesh du CORPS chez le joueur qui controle ce pawn, et lui seul.
*
* Par la VISIBILITE et non par bOwnerNoSee : sur un UStaticMeshComponent,
* bCastHiddenShadow n'honore que la premiere -- cache par le proprietaire,
* l'objet perd son ombre en meme temps que son mesh. Detail dans le .cpp.
*/
void UpdateLocalVisibility();
/**
* Cree les deux composants de mesh s'ils manquent.
*
* A la demande et pas dans le constructeur : le composant n'a alors aucune
* hypothese sur l'ordre de creation des sous-objets d'AFpsPlayer, et un pawn
* qui ne tient jamais rien ne paie pas deux composants pour rien.
*/
void EnsureHeldMeshComponents();
/**
* Cree un mesh d'objet en main attache a Parent, ou nullptr si impossible.
*
* @param bCastWorldShadow l'objet projette-t-il une ombre dans le monde.
* Vrai pour la main du corps, faux pour les bras -- un mesh colle a
* l'objectif projetterait une ombre enorme en travers du decor.
*/
UStaticMeshComponent* CreateHeldMeshComponent(FName ComponentName, class USkeletalMeshComponent* Parent,
FName SocketName, bool bCastWorldShadow);
/** Les deux delegues d'inventaire menent ici. */
UFUNCTION()
void HandleInventoryChanged();
UFUNCTION()
void HandleSelectedHotbarSlotChanged(int32 NewIndex);
/** Le pawn porteur, ou nullptr si le composant vit ailleurs. */
AFpsPlayer* GetOwningPlayer() const;
/** Mesh vu par le seul proprietaire, accroche aux bras. */
UPROPERTY(Transient)
TObjectPtr<UStaticMeshComponent> FirstPersonHeldMesh;
/** Mesh vu par les autres joueurs, accroche a la main du corps. */
UPROPERTY(Transient)
TObjectPtr<UStaticMeshComponent> ThirdPersonHeldMesh;
/**
* Un mesh est-il reellement monte en main, en cache.
*
* Recopie de la decision prise par ApplyHeldVisuals plutot que recalculee :
* l'AnimInstance la lit a chaque frame, et surtout deux facons de repondre a
* la meme question finiraient par diverger.
*/
bool bItemVisibleInHand = false;
/**
* La couche actuellement liee aux bras, ou nulle.
*
* Indispensable, et pas un simple cache : LinkAnimClassLayers AJOUTE une
* couche, elle ne remplace pas la precedente. Sans garder ce qui est en
* place pour le delier, passer de la hache a la pioche laisserait les deux
* branchees -- et c'est la premiere qui gagnerait sur les fonctions que la
* seconde n'implemente pas.
*/
UPROPERTY(Transient)
TSubclassOf<UAnimInstance> LinkedArmsLayer;
/** Evite de repeter l'avertissement de socket introuvable a chaque montage. */
bool bLoggedMissingSocket = false;
};
+95
View File
@@ -0,0 +1,95 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HotbarWidget.generated.h"
class UHorizontalBox;
class UInventoryComponent;
class UInventorySlotWidget;
/**
* Le curseur entre ou sort d'une case de la barre rapide.
*
* La barre n'affiche rien elle-meme : le panneau de detail vit dans l'ecran
* d'inventaire, qu'elle ne connait pas. C'est le PlayerController qui tient les
* deux widgets et qui fait le pont -- il est deja le proprietaire du HUD.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnHotbarSlotHovered, UInventoryComponent*, Inventory, int32, SlotIndex, bool, bHovered);
/**
* Barre rapide en bas de l'ecran, affichee en permanence.
*
* Elle ne possede aucune donnee : elle montre les N premiers slots de
* l'inventaire. Deplacer un objet de la grille vers la barre, c'est
* simplement le deplacer vers un slot d'index bas -- le glisser-deposer
* existant s'en occupe deja sans aucun code supplementaire.
*/
UCLASS(Abstract)
class EMBERWILD_API UHotbarWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Remet a jour contenu et selection. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void Refresh();
/** (Re)branche sur l'inventaire du pawn possede. Voir UInventoryWidget. */
void BindToOwningPawn();
/**
* Destination du clic droit pour les cases de la barre.
*
* La barre reste affichee et cliquable par-dessus l'ecran de coffre : sans
* ca, le clic droit marcherait dans la grille et pas dans la barre, ce qui
* passerait pour un bug. Le controller lui pousse le coffre a l'ouverture
* et nullptr a la fermeture.
*/
void SetQuickTransferTarget(UInventoryComponent* InTarget);
UPROPERTY(BlueprintAssignable, Category = "Inventory")
FOnHotbarSlotHovered OnSlotHovered;
protected:
/** A nommer exactement "SlotContainer" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UHorizontalBox> SlotContainer;
/** Le meme WBP_InventorySlot que la grille : une seule apparence a maintenir. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire")
TSubclassOf<UInventorySlotWidget> SlotWidgetClass;
/** Espacement entre les cases. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "0"))
float SlotPadding = 4.f;
private:
UFUNCTION()
void HandleInventoryChanged();
UFUNCTION()
void HandleSelectionChanged(int32 NewIndex);
/** Abonne aux cases : relaie le survol vers l'exterieur. */
UFUNCTION()
void HandleSlotHovered(int32 SlotIndex, bool bHovered);
void RebuildSlots(int32 DesiredCount);
UInventoryComponent* ResolveInventory() const;
TWeakObjectPtr<UInventoryComponent> BoundInventory;
/** Destination du clic droit, recopiee sur chaque case creee. */
TWeakObjectPtr<UInventoryComponent> QuickTransferTarget;
/** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */
UPROPERTY(Transient)
TArray<TObjectPtr<UInventorySlotWidget>> SlotWidgets;
};
+77
View File
@@ -0,0 +1,77 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Interactable.generated.h"
/**
* Classe technique requise par Unreal. On ne la manipule jamais directement,
* elle sert uniquement au systeme de reflection : c'est elle qu'on passe a
* Implements<UInteractable>() et a Execute_XXX().
*/
UINTERFACE(MinimalAPI, BlueprintType)
class UInteractable : public UInterface
{
GENERATED_BODY()
};
/**
* A implementer par tout ce avec quoi le joueur peut interagir : objets a
* ramasser, portes, coffres, etablis, plantes a recolter.
*
* Les fonctions sont des BlueprintNativeEvent : une classe C++ surcharge
* XXX_Implementation(), un Blueprint surcharge l'evenement du meme nom.
* Les deux fonctionnent, et l'appelant n'a pas a savoir lequel des deux
* il a en face -- c'est tout l'interet de passer par une interface.
*
* IMPORTANT : pour appeler ces fonctions depuis l'exterieur, utilise
* IInteractable::Execute_Interact(Actor, Interactor) et jamais
* Actor->Interact() directement. Seul le wrapper Execute_ sait router
* l'appel vers une implementation Blueprint.
*/
class EMBERWILD_API IInteractable
{
GENERATED_BODY()
public:
/**
* Texte affiche au joueur quand il vise l'objet ("Ramasser", "Ouvrir"...).
* Renvoie du FText et non du FString : c'est le type localisable d'Unreal,
* indispensable si tu traduis le jeu un jour.
*
* Interactor est fourni parce que le texte depend souvent de CELUI qui vise
* et d'ou il vise : un feu de camp nomme l'emplacement precis qu'on regarde,
* un coffre verrouille annonce quelle cle il attend. Sans lui, l'objet
* devrait aller chercher le joueur local par un chemin detourne -- ce qui
* marche exactement jusqu'au jour ou il y a deux joueurs.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
FText GetInteractionPrompt(AActor* Interactor) const;
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const { return FText::GetEmpty(); }
/**
* Permet de refuser l'interaction selon le contexte : porte verrouillee,
* inventaire plein, competence manquante. Renvoyer false empeche l'objet
* d'etre meme selectionne par le composant d'interaction.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
bool CanInteract(AActor* Interactor) const;
virtual bool CanInteract_Implementation(AActor* Interactor) const { return true; }
/** L'interaction elle-meme. Interactor est le pawn du joueur. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void Interact(AActor* Interactor);
virtual void Interact_Implementation(AActor* Interactor) {}
/** Le joueur commence a viser l'objet : surbrillance, son, animation. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void OnBeginFocus();
virtual void OnBeginFocus_Implementation() {}
/** Le joueur ne vise plus l'objet. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void OnEndFocus();
virtual void OnEndFocus_Implementation() {}
};
@@ -0,0 +1,166 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Engine/EngineTypes.h"
#include "InteractionComponent.generated.h"
/**
* Diffuse a chaque changement de cible visee. NewFocus vaut nullptr quand le
* joueur ne vise plus rien : c'est le signal pour cacher le prompt a l'ecran.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInteractionFocusChanged, AActor*, NewFocus, AActor*, OldFocus);
/**
* Diffuse quand le TEXTE du prompt change, ce qui n'est pas la meme chose qu'un
* changement de cible : un feu de camp affiche un compte a rebours, et le fait
* de balayer d'un emplacement a l'autre ne change pas l'acteur vise. S'abonner
* uniquement a OnFocusChanged laisserait ces deux cas figes a l'ecran.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteractionPromptChanged, const FText&, NewPrompt);
/**
* A poser sur le pawn du joueur. Lance regulierement un trace depuis le point
* de vue de la camera, garde en memoire l'objet IInteractable vise, et declenche
* l'interaction a la demande.
*
* Le composant ne connait aucun type d'objet concret : il ne parle qu'a
* l'interface. Tu peux ajouter des portes, des coffres ou des PNJ sans jamais
* revenir toucher a ce fichier.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UInteractionComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInteractionComponent();
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/**
* 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();
/** L'acteur actuellement vise, ou nullptr. */
UFUNCTION(BlueprintPure, Category = "Interaction")
AActor* GetFocusedActor() const { return FocusedActor.Get(); }
/**
* Texte a afficher a l'ecran, vide si rien n'est vise.
*
* Renvoie une valeur DEJA calculee : le prompt est reconstruit une fois par
* trace, pas une fois par lecture. Un widget peut donc l'interroger sans
* jamais declencher le moindre travail.
*/
UFUNCTION(BlueprintPure, Category = "Interaction")
const FText& GetFocusedPrompt() const { return CachedPrompt; }
/** Branche ton widget la-dessus pour afficher / cacher le prompt. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnInteractionFocusChanged OnFocusChanged;
/** Le texte affiche a change. C'est a ca qu'un widget de prompt s'abonne. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnInteractionPromptChanged OnPromptChanged;
protected:
/** Portee du trace en cm. 250 = un peu plus d'un bras et demi. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction", meta = (ClampMin = "1"))
float InteractionRange = 250.f;
/**
* Rayon de la sphere balayee. Un trace en ligne pure oblige a viser au pixel
* pres et rend le jeu penible ; quelques cm de tolerance changent tout.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction", meta = (ClampMin = "0"))
float TraceRadius = 8.f;
/**
* Intervalle entre deux traces, en secondes. Inutile de tracer 120 fois par
* seconde pour un affichage que l'oeil lit a 20 Hz.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction", meta = (ClampMin = "0"))
float TraceInterval = 0.05f;
/** Canal de collision teste. Visibility convient dans la majorite des cas. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
/** Affiche le trace dans le monde, tres utile quand rien ne repond. */
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();
/** Vrai si l'acteur implemente l'interface et accepte l'interaction. */
bool IsValidInteractable(AActor* Actor) const;
/** Change la cible en notifiant l'ancienne et la nouvelle. */
void SetFocusedActor(AActor* NewFocus);
/** Point de vue reel du joueur, camera comprise. */
bool GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
/** Reconstruit CachedPrompt et diffuse uniquement si le texte a reellement change. */
void RefreshPrompt();
/** Cible courante. TWeakObjectPtr : si l'objet est detruit, on ne garde pas un pointeur mort. */
TWeakObjectPtr<AActor> FocusedActor;
/**
* Vrai si on avait une cible au dernier changement diffuse.
* Indispensable : quand la cible se detruit (pickup ramasse), FocusedActor
* se vide tout seul, et une simple comparaison de pointeurs ne verrait
* jamais la transition "j'avais une cible" -> "je n'en ai plus".
*/
bool bHasFocusedActor = false;
/**
* Dernier texte diffuse. Le garder permet deux choses : ne reveiller l'UI
* que sur un vrai changement, et servir GetFocusedPrompt() sans recalculer.
*/
FText CachedPrompt;
float TimeSinceLastTrace = 0.f;
};
@@ -0,0 +1,73 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InteractionPromptWidget.generated.h"
class UInteractionComponent;
class UTextBlock;
/**
* Affiche "Appuyez sur E pour Ramasser Objet" quand le joueur vise quelque
* chose d'interactif, et se cache sinon.
*
* La classe est Abstract : on ne l'instancie jamais directement, on cree un
* Widget Blueprint (WBP_InteractionPrompt) qui en herite. Le C++ porte la
* logique, le Blueprint porte la mise en page. C'est la separation standard
* en UMG, l'equivalent d'un prefab d'UI avec son script en Unity.
*
* Le widget ne fait AUCUN travail par frame : il s'abonne au delegue
* OnPromptChanged du composant d'interaction et ne se met a jour que lorsque
* le texte change reellement.
*
* OnPromptChanged et pas OnFocusChanged : le texte peut bouger sans que la
* cible change -- un feu de camp qui egrene son compte a rebours, ou le regard
* qui glisse d'un emplacement du grill a l'autre sur le meme acteur.
*/
UCLASS(Abstract)
class EMBERWILD_API UInteractionPromptWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** (Re)branche sur le composant d'interaction du pawn possede. */
void BindToOwningPawn();
protected:
/**
* meta = (BindWidget) : Unreal relie automatiquement ce pointeur au widget
* nomme exactement "PromptText" dans le Widget Blueprint enfant. Si tu
* oublies de le creer ou que tu le renommes, le Blueprint refuse de
* compiler avec un message clair -- pas de crash silencieux au runtime.
*/
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> PromptText;
/**
* Texte affiche. {0} sera remplace par la touche, {1} par l'action.
* Editable dans le Widget Blueprint pour ajuster la formulation.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FText PromptFormat;
UInteractionPromptWidget(const FObjectInitializer& ObjectInitializer);
private:
/** Appele par le composant d'interaction a chaque changement du texte affiche. */
UFUNCTION()
void HandlePromptChanged(const FText& NewPrompt);
/** Recalcule le texte et la visibilite. */
void Refresh();
/** Nom lisible de la touche actuellement mappee sur l'action Interagir. */
FText GetInteractKeyDisplayName() const;
/** Le composant auquel on est abonne, pour pouvoir se desabonner proprement. */
TWeakObjectPtr<UInteractionComponent> BoundComponent;
};
@@ -0,0 +1,397 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"
class UItemDataAsset;
/**
* Contenu d'un emplacement. Item nul = slot vide.
* Le tableau de slots a toujours exactement la taille de l'inventaire :
* l'index dans le tableau EST la position dans la grille, ce qui rend
* l'affichage et le futur drag & drop triviaux.
*/
USTRUCT(BlueprintType)
struct FInventorySlot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
TObjectPtr<UItemDataAsset> Item = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
int32 Quantity = 0;
/**
* Charges restantes de l'exemplaire courant, pour les objets a usages
* multiples. Etat par instance, d'ou sa place ici et pas sur le DataAsset :
* une canette entamee ne doit pas entamer toutes les canettes du jeu.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
int32 RemainingUses = 0;
bool IsEmpty() const { return Item == nullptr || Quantity <= 0; }
void Clear()
{
Item = nullptr;
Quantity = 0;
RemainingUses = 0;
}
};
/** Diffuse a chaque modification du contenu ou de la taille. L'UI s'y abonne. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryChanged);
/** Diffuse quand le joueur change de slot actif dans la barre rapide. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSelectedHotbarSlotChanged, int32, NewIndex);
/**
* Inventaire en grille a slots, facon Raft / Valheim.
*
* Le nombre de slots est dynamique : BaseSlotCount fixe le minimum, et
* BonusSlotCount permet a un sac equipe de l'agrandir. Retirer le sac
* refuse si les objets ne rentrent plus, plutot que de les faire
* disparaitre en silence.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInventoryComponent();
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".
* Sert a rendre son etat a un objet ramasse au sol.
* @return le nombre d'exemplaires qui N'ONT PAS pu rentrer (0 = tout est passe).
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 AddItem(UItemDataAsset* Item, int32 Quantity = 1, int32 RemainingUses = -1);
/**
* Consomme une utilisation du slot.
* Retire un exemplaire quand il ne reste plus de charge.
* @return true si quelque chose a ete consomme.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool ConsumeUse(int32 SlotIndex);
/**
* Retire des objets, en vidant d'abord les piles les plus petites.
* @return le nombre d'exemplaires reellement retires.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 RemoveItem(UItemDataAsset* Item, int32 Quantity = 1);
/** Combien d'exemplaires de cet objet, toutes piles confondues. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetItemCount(const UItemDataAsset* Item) const;
/** Combien d'exemplaires pourraient encore rentrer, sans rien modifier. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetRoomFor(const UItemDataAsset* Item) const;
/**
* Un seul tableau, deux conteneurs distincts :
* index 0 .. HotbarSlotCount-1 -> barre rapide
* index HotbarSlotCount .. fin -> grille d'inventaire (+ bonus de sac)
*
* Un objet range dans la barre n'apparait PAS dans la grille, et
* inversement. Ils partagent le meme tableau uniquement pour que le
* glisser-deposer entre les deux ne demande aucune conversion d'index.
*/
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetTotalSlotCount() const { return FMath::Max(0, HotbarSlotCount + BackpackSlotCount + BonusSlotCount); }
/** Premier index affiche par la grille, juste apres la barre rapide. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetBackpackStartIndex() const { return GetHotbarSlotCount(); }
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetUsedSlotCount() const;
UFUNCTION(BlueprintPure, Category = "Inventaire")
const TArray<FInventorySlot>& GetSlots() const { return Slots; }
/**
* Deplace la pile entiere d'un slot vers un autre.
* - cible vide -> la pile entiere part
* - meme objet -> fusion jusqu'a MaxStackSize, le reste demeure a la source
* - objet different -> echange des deux slots
* @return true si quelque chose a change.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool MoveItem(int32 FromIndex, int32 ToIndex);
/**
* Deplace seulement une partie d'une pile.
*
* Un transfert partiel vers un slot occupe par un objet DIFFERENT est
* refuse : il n'existe aucune facon sensee d'echanger la moitie d'une pile
* contre autre chose. Seul un deplacement total peut provoquer un echange.
*
* @return true si quelque chose a change.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity);
// ------------------------------------------------------------------
// Transferts entre deux inventaires (coffres)
// ------------------------------------------------------------------
/**
* Deplace le contenu d'un slot vers un autre, dans le MEME inventaire ou
* dans un autre. C'est desormais l'unique implementation du deplacement :
* MoveItem() et MoveItemQuantity() ne sont que des appels avec From == To.
*
* Statique parce qu'elle n'appartient ni a la source ni a la destination --
* elle arbitre entre les deux, et doit diffuser OnInventoryChanged sur les
* DEUX. Oublier la seconde diffusion laisse la grille d'en face afficher un
* etat perime jusqu'au prochain evenement, ce qui se lit comme un objet
* qui disparait.
*
* @param Quantity nombre d'exemplaires ; MAX_int32 pour la pile entiere.
* @return true si quelque chose a change.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
static bool TransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity);
/**
* Envoie le contenu d'un slot vers un autre inventaire sans designer de
* case d'arrivee : c'est le clic droit de l'ecran de coffre.
*
* Passe par AddItem(), donc exactement comme un ramassage -- piles entamees
* d'abord, puis barre rapide, puis grille. Ce qui ne rentre pas reste sur
* place : un transfert rapide ne perd jamais rien.
*
* @return le nombre d'exemplaires reellement partis.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
static int32 QuickTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To);
/**
* Deverse tout ce qui peut entrer vers un autre inventaire : les boutons
* "Tout ranger" et "Tout prendre".
*
* @param FirstIndex premier slot considere. Le joueur passe
* GetBackpackStartIndex() pour que "Tout ranger" laisse sa barre
* rapide tranquille -- ranger ses outils dans le coffre a chaque
* retour d'expedition serait une punition, pas un confort.
* @return le nombre d'exemplaires reellement partis.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 TransferAllTo(UInventoryComponent* To, int32 FirstIndex = 0);
/**
* Transforme ce composant en contenant du monde : NewSlotCount cases de
* grille et AUCUNE barre rapide, pour que GetBackpackStartIndex() vaille 0
* et que la grille affiche la totalite du coffre.
*
* A appeler avant que le composant contienne quoi que ce soit -- typiquement
* depuis PreInitializeComponents() de l'acteur porteur. Reduire la taille
* d'un conteneur deja rempli fait disparaitre ce qui depasse.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void ConfigureAsContainer(int32 NewSlotCount);
/** Index du premier slot libre, ou INDEX_NONE si l'inventaire est plein. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 FindFirstEmptySlot() const;
/**
* Retire jusqu'a Quantity exemplaires d'un slot precis et renvoie ce qui a
* ete retire. Sert au drop au sol et, plus tard, au transfert vers un coffre.
*
* Different de RemoveItem() qui pioche par type sans se soucier du slot :
* ici le joueur a designe une case, on ne touche qu'a celle-la.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool TakeFromSlot(int32 SlotIndex, int32 Quantity, FInventorySlot& OutTaken);
/**
* Change le bonus de slots apporte par l'equipement (sac).
* @return false si reduire la taille laisserait des objets dehors ;
* dans ce cas rien n'est modifie et le sac ne doit pas etre retire.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool SetBonusSlotCount(int32 NewBonusSlots);
UPROPERTY(BlueprintAssignable, Category = "Inventaire")
FOnInventoryChanged OnInventoryChanged;
// ------------------------------------------------------------------
// Barre rapide
// ------------------------------------------------------------------
//
// La barre rapide n'est PAS un jeu de slots separe : ce sont les
// HotbarSlotCount premiers slots de l'inventaire, simplement affiches
// deux fois. Consequence directe : le glisser-deposer entre la barre et
// la grille fonctionne sans une ligne de code de plus, puisque ce sont
// les memes index dans le meme tableau.
/** Change le slot actif. Un index hors de la barre est ignore. */
UFUNCTION(BlueprintCallable, Category = "Inventaire|Barre rapide")
void SelectHotbarSlot(int32 Index);
/** Avance ou recule dans la barre, en boucle. */
UFUNCTION(BlueprintCallable, Category = "Inventaire|Barre rapide")
void CycleHotbarSelection(int32 Delta);
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
int32 GetSelectedHotbarIndex() const { return SelectedHotbarIndex; }
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
int32 GetHotbarSlotCount() const { return FMath::Min(HotbarSlotCount, Slots.Num()); }
/** Contenu du slot actif. C'est ce que le joueur "a en main". */
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
FInventorySlot GetSelectedSlot() const;
UPROPERTY(BlueprintAssignable, Category = "Inventaire|Barre rapide")
FOnSelectedHotbarSlotChanged OnSelectedHotbarSlotChanged;
protected:
/** Cases affichees dans la grille d'inventaire. N'inclut PAS la barre rapide. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "0"))
int32 BackpackSlotCount = 20;
/** Slots supplementaires apportes par l'equipement (sac). Passe par SetBonusSlotCount(). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
int32 BonusSlotCount = 0;
/**
* Cases de la barre rapide, independantes de celles de la grille.
* Elles occupent les premiers index du tableau.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire|Barre rapide", meta = (ClampMin = "0"))
int32 HotbarSlotCount = 10;
/**
* 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();
/** Regroupe les slots occupes au debut du tableau. */
void CompactSlots();
/**
* Regles de deplacement entre deux cases, sans savoir a quel inventaire
* chacune appartient. Ne diffuse rien : c'est l'appelant qui sait combien
* de composants ont bouge.
*/
static bool ApplySlotMove(FInventorySlot& From, FInventorySlot& To, int32 Quantity);
/**
* Verse un slot dans un autre inventaire via AddItem(). Ne diffuse que du
* cote destination (AddItem s'en charge) : la source reste a la charge de
* l'appelant, qui peut en enchainer vingt.
* @return exemplaires reellement partis.
*/
int32 PushSlotInto(int32 SlotIndex, UInventoryComponent& To);
/**
* Diffusion differee. Un "Tout ranger" deplace jusqu'a vingt piles : sans
* regroupement, l'UI se reconstruirait vingt fois pour un seul clic.
* Toutes les modifications passent par BroadcastChanged() plutot que par
* OnInventoryChanged.Broadcast() directement, sinon le lot fuit.
*/
void BroadcastChanged();
void BeginBatch();
void EndBatch();
int32 SelectedHotbarIndex = 0;
int32 BatchDepth = 0;
bool bBatchDirty = false;
};
@@ -0,0 +1,36 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/DragDropOperation.h"
#include "InventoryDragDropOperation.generated.h"
class UInventoryComponent;
/**
* Ce qui voyage entre la case qu'on saisit et celle ou on lache.
*
* On transporte un INDEX et non le contenu du slot : si l'inventaire change
* pendant le glisser (ramassage automatique, script), la cible relira l'etat
* reel au moment du drop au lieu d'appliquer une donnee perimee.
*/
UCLASS()
class EMBERWILD_API UInventoryDragDropOperation : public UDragDropOperation
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, Category = "Inventaire")
int32 SourceSlotIndex = INDEX_NONE;
/**
* Combien d'exemplaires on deplace. Egal a la pile entiere pour un
* glisser normal, la moitie avec Maj, un seul avec Ctrl.
*/
UPROPERTY(BlueprintReadOnly, Category = "Inventaire")
int32 Quantity = 0;
UPROPERTY(BlueprintReadOnly, Category = "Inventaire")
TWeakObjectPtr<UInventoryComponent> SourceInventory;
};
@@ -0,0 +1,200 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InventoryScreenWidget.generated.h"
class AStorageContainer;
class UButton;
class UCraftingWidget;
class UInventoryComponent;
class UInventoryWidget;
class UTextBlock;
class UWidgetSwitcher;
/**
* L'onglet visible a change. true = Inventaire, false = Fabrication.
*
* L'ecran se contente d'annoncer. Ce que ca implique pour le reste du HUD --
* masquer la barre rapide, par exemple -- ne le regarde pas : il serait alors
* un widget reutilisable qui connait la pile de son hote, exactement ce qu'on
* evite partout ailleurs.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryScreenTabChanged, bool, bInventoryTabActive);
/**
* L'ecran que Tab ouvre : une barre d'onglets et deux pages, Inventaire et
* Fabrication.
*
* Il ne contient aucune logique de l'une ni de l'autre. Les pages restent des
* widgets autonomes -- la grille d'inventaire n'a pas change d'une ligne en
* devenant une page -- et l'ecran ne fait que les montrer, les rebrancher apres
* un respawn, et relayer au controller ce dont il a besoin.
*
* Classe Abstract : on cree un WBP_InventoryScreen qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API UInventoryScreenWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
/** Rebranche les deux pages sur le pawn possede. A appeler depuis OnPossess. */
void BindToOwningPawn();
/**
* Index de la case d'inventaire survolee, ou INDEX_NONE.
*
* Renvoie INDEX_NONE des que l'onglet Fabrication est affiche : sans ca, la
* touche "jeter" agirait sur une case invisible, sous le panneau de craft.
*/
int32 GetHoveredSlotIndex() const;
/**
* Meme chose, avec l'inventaire auquel la case appartient. Depuis les
* coffres, l'index seul ne designe plus rien de facon unique.
*/
bool GetHoveredSlot(UInventoryComponent*& OutInventory, int32& OutIndex) const;
/**
* Survol d'une case exterieure a l'ecran -- la barre rapide, qui reste
* affichee par-dessus lui. Alimente le panneau de detail de la page
* d'inventaire, seul panneau existant.
*/
void NotifyExternalSlotHovered(UInventoryComponent* Inventory, int32 SlotIndex, bool bHovered);
/** Revient sur l'onglet Inventaire. Appele a chaque ouverture par Tab. */
UFUNCTION(BlueprintCallable, Category = "UI")
void ShowInventoryTab();
/** Bascule sur l'onglet Fabrication. Appele par la touche dediee. */
UFUNCTION(BlueprintCallable, Category = "UI")
void ShowCraftingTab();
UPROPERTY(BlueprintAssignable, Category = "UI")
FOnInventoryScreenTabChanged OnTabChanged;
// ------------------------------------------------------------------
// Panneau de coffre
//
// Le contenu d'un coffre s'affiche A COTE de la grille du joueur, dans ce
// meme ecran, et non dans un ecran a part. La raison est du ressenti pur :
// le sac ne bouge pas d'un pixel entre "j'ouvre mon sac" et "j'ouvre un
// coffre". Deux ecrans distincts le feraient sauter a chaque fois.
//
// Techniquement c'est une deuxieme instance du MEME WBP_Inventory que
// InventoryPage. Elle ne peut pas vivre a l'interieur de WBP_Inventory :
// un Widget Blueprint qui se contient lui-meme est une reference
// circulaire, et UMG refuse de le compiler.
// ------------------------------------------------------------------
/** Branche et affiche le panneau de droite. Force l'onglet Inventaire. */
void ShowStorage(AStorageContainer* Container);
/** Masque et debranche le panneau. Sans effet s'il est deja cache. */
void HideStorage();
UFUNCTION(BlueprintPure, Category = "UI|Storage")
bool IsStorageVisible() const { return bStorageVisible; }
protected:
/**
* Absorbe ou traite les depots qui ne visent aucune page.
* Meme regle que dans la grille : lache hors des cases, l'objet est jete.
*/
virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UWidgetSwitcher> TabSwitcher;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> InventoryTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CraftingTabButton;
/**
* Ton WBP_Inventory, pose comme enfant du WidgetSwitcher et renomme
* "InventoryPage". Les deux pages sont obligatoires : un ecran a onglets
* auquel il manque une page n'a aucun sens, autant que le Blueprint refuse
* de compiler.
*/
UPROPERTY(meta = (BindWidget))
TObjectPtr<UInventoryWidget> InventoryPage;
/** Ton WBP_Crafting, renomme "CraftingPage". */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCraftingWidget> CraftingPage;
/**
* Tout le bloc de droite -- cadre, titre, grille, boutons -- qu'on montre et
* qu'on cache d'un bloc.
*
* BindWidgetOptional et non BindWidget, comme tous les elements de coffre
* qui suivent : l'ecran doit rester valide dans un projet ou personne n'a
* encore construit ce panneau. Sans ca, ajouter les coffres casserait la
* compilation de WBP_InventoryScreen tant que la mise en page n'est pas
* faite -- et donc l'inventaire entier.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> StoragePanel;
/** Un deuxieme WBP_Inventory, renomme "StorageGrid". DECOCHE-lui Auto Bind To Pawn. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UInventoryWidget> StorageGrid;
/** Nom du coffre ouvert. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> StorageTitle;
/** Deverse la grille du sac dans le coffre. La barre rapide n'y passe pas. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> StoreAllButton;
/** Vide le coffre dans le sac, dans la limite de la place disponible. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> TakeAllButton;
private:
UFUNCTION()
void HandleStoreAllClicked();
UFUNCTION()
void HandleTakeAllClicked();
/** Inventaire du pawn possede, ou nullptr. */
UInventoryComponent* GetPlayerInventory() const;
UFUNCTION()
void HandleInventoryTabClicked();
UFUNCTION()
void HandleCraftingTabClicked();
/**
* Affiche une page et grise son bouton d'onglet.
*
* On passe le widget et non un index en dur : l'ordre des enfants du
* WidgetSwitcher se change d'un glisser dans la Designer, et un 0/1 code ici
* inverserait silencieusement les onglets le jour ou ca arrive.
*/
void ShowPage(UWidget* Page);
bool IsInventoryTabActive() const;
/**
* Le panneau de coffre est affiche.
*
* Tant qu'il l'est, l'onglet Fabrication est grise : fabriquer avec un
* coffre ouvert a cote n'a pas de sens tant que ses ressources ne comptent
* pas dans les recettes, et laisser le bouton actif inviterait a un clic
* dont le resultat serait incomprehensible.
*/
bool bStorageVisible = false;
TWeakObjectPtr<UInventoryComponent> BoundStorage;
};
@@ -0,0 +1,135 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InventoryComponent.h"
#include "InventorySlotWidget.generated.h"
class UImage;
class UProgressBar;
class UTextBlock;
/**
* Le curseur est entre sur cette case, ou en est sorti.
*
* Un delegue et non un pointeur vers la grille : la barre rapide pose les
* memes cases sans etre un UInventoryWidget, et elle a autant besoin du survol
* depuis que l'inventaire affiche un panneau de detail.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInventorySlotHoverChanged, int32, SlotIndex, bool, bHovered);
/**
* Une case de la grille d'inventaire : une icone et une quantite.
*
* Volontairement passive pour l'affichage : elle ne va jamais chercher
* l'information elle-meme, c'est la grille parente qui lui pousse son contenu
* via SetSlot(). Elle est en revanche autonome pour le glisser-deposer, parce
* que c'est elle qui recoit les evenements souris.
*/
UCLASS(Abstract)
class EMBERWILD_API UInventorySlotWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
/** Appele une fois a la creation : dit a la case qui elle represente. */
void InitSlot(UInventoryComponent* InInventory, int32 InSlotIndex);
/** Le conteneur qui a cree cette case s'y abonne pour suivre le survol. */
UPROPERTY()
FOnInventorySlotHoverChanged OnHoverChanged;
/** Affiche le contenu d'un slot, ou se vide si le slot est vide. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void SetSlot(const FInventorySlot& InSlot);
/**
* Transforme cette instance en fantome de glisser : le fond de case
* disparait pour ne laisser flotter que l'objet lui-meme.
*/
void ConfigureAsDragVisual();
/** Affiche ou masque le cadre de selection (barre rapide). */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void SetSelected(bool bSelected);
/**
* Inventaire vers lequel le CLIC DROIT envoie le contenu de cette case.
*
* Nul en temps normal : hors de l'ecran de coffre il n'existe aucun "autre
* cote" evident, et deviner une destination reviendrait a faire disparaitre
* des objets sur un clic malheureux. C'est l'ecran qui sait, et qui pousse.
*/
void SetQuickTransferTarget(UInventoryComponent* InTarget);
protected:
// --- Glisser-deposer ---
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual void NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation) override;
virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override;
// --- Survol, pour que la touche "jeter" sache sur quoi agir ---
virtual void NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual void NativeOnMouseLeave(const FPointerEvent& InMouseEvent) override;
/** A nommer exactement "ItemIcon" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> ItemIcon;
/** A nommer exactement "QuantityText". */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> QuantityText;
/**
* Fond de case. BindWidgetOptional et non BindWidget : le Blueprint peut
* s'en passer sans casser la compilation, c'est purement decoratif.
* Nomme-le "SlotBackground" pour qu'il soit masque pendant le glisser.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> SlotBackground;
/**
* Cadre affiche quand la case est le slot actif de la barre rapide.
* Optionnel : la grille d'inventaire ne s'en sert jamais.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> SelectionBorder;
/**
* Petite barre d'usure en bas de la case, pour les objets a usages
* multiples. Masquee automatiquement sur tout le reste.
* A nommer "UsesBar" dans le Widget Blueprint.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> UsesBar;
/** Opacite du fantome qui suit le curseur pendant le glisser. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire", meta = (ClampMin = "0.05", ClampMax = "1.0"))
float DragVisualOpacity = 0.85f;
/** Opacite de la case d'origine tant que le glisser est en cours. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float DraggedSlotOpacity = 0.3f;
private:
/** Rebranche l'opacite normale, que le glisser ait abouti ou non. */
UFUNCTION()
void HandleDragFinished(UDragDropOperation* Operation);
TWeakObjectPtr<UInventoryComponent> OwningInventory;
/** Destination du clic droit. Voir SetQuickTransferTarget(). */
TWeakObjectPtr<UInventoryComponent> QuickTransferTarget;
int32 SlotIndex = INDEX_NONE;
/** Dernier contenu recu, pour construire le fantome de glisser. */
FInventorySlot CachedSlot;
/** Vrai si cette instance est un fantome et non une vraie case de la grille. */
bool bIsDragVisual = false;
};
+187
View File
@@ -0,0 +1,187 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InventoryWidget.generated.h"
class UInventoryComponent;
class UInventorySlotWidget;
class UItemDetailsWidget;
class UUniformGridPanel;
/**
* Grille de cases. Cree ses cases dynamiquement a partir du nombre de slots du
* composant, et se remet a jour uniquement quand l'inventaire change.
*
* Elle ne suppose PLUS qu'elle affiche le sac du joueur : le meme widget sert
* de grille de coffre en lui donnant simplement un autre UInventoryComponent.
* C'est ce qui permet d'avoir un seul WBP_Inventory pose des deux cotes de
* l'ecran de coffre -- et donc la meme apparence, sans duplication.
*
* Le nombre de cases n'est pas fige : quand un sac ajoutera des slots, la
* grille s'agrandira toute seule au prochain rafraichissement.
*/
UCLASS(Abstract)
class EMBERWILD_API UInventoryWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Reconstruit si necessaire, puis pousse le contenu dans chaque case. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void Refresh();
/** Index de la case sous le curseur, ou INDEX_NONE. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetHoveredSlotIndex() const { return HoveredSlotIndex; }
/**
* Case survolee ET inventaire auquel elle appartient.
*
* L'index seul ne suffit plus : la touche "jeter" doit savoir de quel
* conteneur sortir l'objet, et la case 3 existe aussi bien dans le sac que
* dans le coffre.
*/
bool GetHoveredSlot(UInventoryComponent*& OutInventory, int32& OutIndex) const;
/** Abonne aux cases : le curseur entre ou sort de l'une d'elles. */
UFUNCTION()
void NotifySlotHovered(int32 Index, bool bHovered);
/**
* Survol d'une case qui n'appartient PAS a cette grille -- la barre rapide.
*
* Ne touche pas a HoveredSlotIndex : celui-ci sert a la touche "jeter", qui
* doit continuer de viser la grille et elle seule. Seul le panneau de detail
* repond, parce qu'il n'y en a qu'un a l'ecran et qu'il appartient a la
* grille du sac.
*/
void NotifyExternalSlotHovered(UInventoryComponent* InInventory, int32 InSlotIndex, bool bHovered);
/**
* Autorise ou coupe le panneau de detail de cette grille.
*
* C'est l'ECRAN qui decide, jamais une case a cocher dans la Designer : le
* WBP_Inventory est instancie deux fois sur l'ecran de coffre, et un reglage
* qu'on peut oublier de decocher sur l'une des deux instances est un bug qui
* attend. Coupe : la grille de coffre n'en a jamais, et le sac perd le sien
* tant qu'un coffre est ouvert -- l'ecran est deja assez large a trois
* colonnes.
*/
void SetItemDetailsEnabled(bool bEnabled);
/**
* (Re)branche le widget sur l'inventaire du pawn actuellement possede.
* Indispensable apres un respawn : le nouveau pawn a un nouveau composant,
* et l'ancien abonnement pointerait vers un objet detruit.
*/
void BindToOwningPawn();
/**
* (Re)branche le widget sur un inventaire quelconque -- celui d'un coffre,
* typiquement. nullptr vide la grille : une grille de coffre referme ne doit
* pas continuer d'afficher un contenu qu'on ne pilote plus.
*/
void BindToInventory(UInventoryComponent* InInventory);
UFUNCTION(BlueprintPure, Category = "Inventaire")
UInventoryComponent* GetBoundInventory() const { return BoundInventory.Get(); }
/** Destination du clic droit pour toutes les cases de cette grille. */
void SetQuickTransferTarget(UInventoryComponent* InTarget);
/**
* Change le nombre de colonnes et redispose les cases.
*
* Sert a la grille de coffre : c'est le coffre pose dans le niveau qui
* decide de sa forme, pas le Widget Blueprint -- un coffre de 12 cases et
* un de 30 ne se lisent pas bien avec le meme nombre de colonnes.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void SetColumnCount(int32 NewColumnCount);
protected:
/** A nommer exactement "SlotGrid" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UUniformGridPanel> SlotGrid;
/** Le widget de case a instancier : ton WBP_InventorySlot. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire")
TSubclassOf<UInventorySlotWidget> SlotWidgetClass;
/** Nombre de cases par ligne. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "1"))
int32 ColumnCount = 6;
/**
* La grille va chercher toute seule l'inventaire du pawn a sa construction.
*
* DECOCHE-LE sur la grille de coffre. Sans ca elle s'accrocherait au pawn
* dans son NativeConstruct -- qui s'execute avant que l'ecran ait eu le
* temps de lui donner le conteneur -- et afficherait le sac du joueur des
* deux cotes de l'ecran.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire")
bool bAutoBindToPawn = true;
/**
* Le panneau de detail pose a cote de la grille, dans le meme Widget
* Blueprint. A nommer "ItemDetailsPanel".
*
* Optionnel : la grille doit rester valide dans un WBP_Inventory ou ce
* panneau n'existe pas encore, sinon ajouter cette fonctionnalite casserait
* la compilation de l'inventaire entier tant que la mise en page n'est pas
* faite.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UItemDetailsWidget> ItemDetailsPanel;
protected:
/**
* Absorbe les depots qui ne visent aucune case. Sans ca, lacher un objet
* sur le fond du panneau declencherait OnDragCancelled -- et si un jour on
* y branche le drop au sol, le joueur jetterait son butin par accident.
*/
virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override;
private:
UFUNCTION()
void HandleInventoryChanged();
/** Cree ou supprime des cases pour coller au nombre de slots. */
void RebuildGrid(int32 DesiredCount);
/** Detruit toutes les cases : leur inventaire a change, elles sont perimees. */
void ClearSlotWidgets();
/** Retrouve l'inventaire du pawn possede. */
UInventoryComponent* ResolveInventory() const;
/**
* Point de passage UNIQUE du survol : met a jour l'index et le panneau de
* detail. Ne fait rien si l'index ne change pas.
*/
void SetHoveredSlot(int32 NewIndex);
/** Pousse la case survolee dans le panneau, ou l'efface. */
void UpdateItemDetails();
TWeakObjectPtr<UInventoryComponent> BoundInventory;
/** Destination du clic droit, recopiee sur chaque case creee. */
TWeakObjectPtr<UInventoryComponent> QuickTransferTarget;
int32 HoveredSlotIndex = INDEX_NONE;
/** Voir SetItemDetailsEnabled(). */
bool bItemDetailsEnabled = true;
/** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */
UPROPERTY(Transient)
TArray<TObjectPtr<UInventorySlotWidget>> SlotWidgets;
};
+362
View File
@@ -0,0 +1,362 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ItemDataAsset.generated.h"
class UMaterialInterface;
class UStaticMesh;
class UTexture2D;
/**
* Famille d'outil, pour l'appariement avec les ressources recoltables.
*
* C'est le TYPE qui decide de ce qu'on peut abattre, jamais le mesh ni le nom :
* un arbre acceptera { Axe }, un filon { Pickaxe }. None n'est pas un outil du
* tout -- c'est la valeur de tous les objets existants, qui ne changent donc pas
* de comportement.
*/
UENUM(BlueprintType)
enum class EToolType : uint8
{
None UMETA(DisplayName = "Not a tool"),
Axe,
Pickaxe,
/** Sentinelle de comptage, jamais assignee a un objet. */
Count UMETA(Hidden)
};
/**
* Definition d'un TYPE d'objet : "Bois", "Pomme", "Corde".
* Un asset par type, cree dans le Content Browser, edite a la main.
*
* C'est l'equivalent exact d'un ScriptableObject Unity : une donnee partagee
* que tout le monde reference et que personne ne duplique. Cent buches dans
* le monde pointent toutes vers le meme UItemDataAsset "Bois".
*
* Regle a ne jamais enfreindre : ceci decrit ce qu'est un objet, jamais son
* etat. La quantite, la durabilite, l'usure appartiennent a l'inventaire ou
* a l'acteur, pas ici -- sinon ramasser une pomme modifierait toutes les
* pommes du jeu.
*/
UCLASS(BlueprintType)
class EMBERWILD_API UItemDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/** Identifiant stable, utilise par la sauvegarde et les recettes de craft. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
FName ItemId;
/** Nom affiche au joueur. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
FText DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item", meta = (MultiLine = "true"))
FText Description;
/** Icone affichee dans la grille d'inventaire. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UTexture2D> Icon;
/** Mesh utilise quand l'objet est pose au sol. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UStaticMesh> WorldMesh;
/**
* Materiau force sur le WorldMesh. Vide = celui du mesh.
*
* Indispensable des qu'un pack partage une geometrie entre plusieurs
* aliments : une chaine de cuisson demande un visuel par etape, et sans ca
* il faudrait trois meshes distincts la ou un seul suffit avec trois
* teintes. N'ecrase que le PREMIER slot de materiau -- un mesh qui en
* expose plusieurs garde les autres tels quels.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UMaterialInterface> WorldMeshMaterial;
/**
* Nombre maximum d'exemplaires par slot.
* 1 = ne s'empile pas (outils, armes), 20 = ressource classique.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item", meta = (ClampMin = "1"))
int32 MaxStackSize = 1;
/**
* Nombre d'utilisations avant que l'objet soit consomme.
* 1 = objet a usage unique. 3 = une canette qu'on boit en trois gorgees.
*
* Les valeurs de restauration sont appliquees A CHAQUE utilisation, pas
* reparties : une canette a 3 usages et 20 de soif rend 20 par gorgee.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item", meta = (ClampMin = "1"))
int32 MaxUses = 1;
/**
* Taille de pile reellement applicable.
*
* Un objet a charges ne s'empile jamais : deux canettes entamees a des
* niveaux differents ne peuvent pas partager un slot, il faudrait stocker
* deux compteurs dans une seule case. La regle est centralisee ici pour
* qu'aucun appelant ne puisse l'oublier.
*/
UFUNCTION(BlueprintPure, Category = "Item")
int32 GetEffectiveMaxStack() const { return MaxUses > 1 ? 1 : FMath::Max(1, MaxStackSize); }
/**
* Pose le mesh et le materiau de l'objet sur un composant.
*
* Un DataAsset ne devrait pas piloter un composant, mais la regle "mesh plus
* materiau optionnel" a deja deux appelants -- l'objet au sol et l'objet sur
* le feu -- et en aura d'autres, l'objet en main le premier. La dupliquer
* garantit qu'un jour l'un des deux oubliera le materiau.
*/
UFUNCTION(BlueprintCallable, Category = "Item")
void ApplyWorldVisualsTo(class UStaticMeshComponent* Component) const;
/**
* Slots ajoutes a l'inventaire quand cet objet est equipe comme sac.
* Laisse a 0 pour tout ce qui n'est pas un contenant.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Backpack", meta = (ClampMin = "0"))
int32 BonusInventorySlots = 0;
// ------------------------------------------------------------------
// En main
//
// Ce qu'on voit quand l'objet occupe la case active de la barre rapide.
// C'est UHeldItemComponent qui lit ce bloc ; il ne concerne donc que le
// visuel, jamais ce que l'objet fait.
// ------------------------------------------------------------------
/**
* Cet objet apparait-il dans la main quand il est selectionne.
*
* Faux par defaut, et c'est delibere : les deux offsets ci-dessous se reglent
* a l'oeil, objet par objet, sur un socket donne. Un vrai par defaut ferait
* surgir toutes les ressources deja creees dans le poing du joueur, chacune
* de travers, sans que personne l'ait demande.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held")
bool bShowInHand = false;
/**
* Couche d'animation des BRAS quand cet objet est en main (ABP_Tool_Axe...).
*
* C'est un « Linked Anim Layer » : un Animation Blueprint qui implemente
* l'interface de couche de ABP_FpsArms et REMPLACE ses cycles le temps qu'on
* tienne l'objet. Vide = les bras gardent leurs cycles mains nues.
*
* Ce champ est la raison d'etre de tout le mecanisme, et elle est humaine
* plutot que technique : ajouter un outil ne doit demander NI recompilation
* C++, NI ouverture de ABP_FpsArms. Un animateur livre son AnimBP de couche,
* on le glisse ici, c'est fini. L'alternative -- un enum de posture lu par un
* « Blend Poses by Enum » -- coutait une valeur d'enum, une recompilation, et
* un branchement a la main dans CHAQUE etat de la machine : le travail
* croissait en outils x etats, pas en outils.
*
* Contrainte a tenir : la couche doit etre batie sur le meme squelette que
* les bras, et implementer la meme interface. Une classe qui n'implemente
* rien se lie sans erreur et ne change simplement rien -- symptome silencieux
* dont il faut se souvenir.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held", meta = (EditCondition = "bShowInHand"))
TSubclassOf<class UAnimInstance> ArmsAnimLayer;
/**
* Mesh tenu en main. Vide = on reprend WorldMesh.
*
* A ne remplir que si l'objet au sol et l'objet en main different vraiment --
* une buche posee en tas contre une buche unique, typiquement. Une hache,
* elle, est la meme des deux cotes.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held", meta = (EditCondition = "bShowInHand"))
TObjectPtr<UStaticMesh> HeldMesh;
/**
* Ajustement du mesh sur le socket des BRAS (vue premiere personne).
*
* Necessaire parce qu'on s'attache a un OS du rig et pas a un socket taille
* pour l'objet : le pivot d'un mesh d'outil tombe presque toujours sur son
* centre, jamais sur le manche. Se regle a l'oeil en PIE.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held", meta = (EditCondition = "bShowInHand"))
FTransform FirstPersonHeldOffset;
/**
* Ajustement sur la main du CORPS (ce que les autres joueurs voient).
*
* Separe du precedent, et il le restera : les bras FPS et le mannequin UE5
* sont deux rigs distincts, avec deux echelles et deux orientations d'os. Une
* transform commune donnerait forcement une hache de travers d'un cote.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held", meta = (EditCondition = "bShowInHand"))
FTransform ThirdPersonHeldOffset;
/**
* Geste joue sur les BRAS quand cet objet arrive en main (MTG_Axe_Equip...).
*
* Un montage et pas un etat de la machine a etats : sortir un outil est un
* geste PONCTUEL qui doit se superposer a ce que les bras faisaient deja --
* meme raison que LandMontage. Un etat imposerait de decider combien de temps
* y rester, et changer d'outil en pleine course y bloquerait la foulee.
*
* Vide = l'objet apparait sans geste, ce qui reste parfaitement jouable.
* Rien n'est replique : les bras ne sont vus que par leur proprietaire.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held")
TObjectPtr<class UAnimMontage> EquipMontage;
/**
* Geste du CLIC GAUCHE avec cet objet en main (MTG_Pickaxe_Attack...).
*
* « Use » et pas « Attack » : le clic gauche est une seule action dont le
* sens depend de ce qu'on tient -- frapper avec une pioche, boire a une
* gourde, allumer une torche. Un champ par verbe finirait en AttackMontage,
* EatMontage, DrinkMontage pour un seul bouton.
*
* Il ne fait QUE l'animation. Ce que l'objet produit reste dans
* AFpsPlayer::PerformUseItem, cote serveur -- l'un se voit tout de suite chez
* soi, l'autre doit faire autorite.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held")
TObjectPtr<class UAnimMontage> UseMontage;
/** Le mesh a montrer en main, en appliquant le repli sur WorldMesh. */
UFUNCTION(BlueprintPure, Category = "Item|Held")
UStaticMesh* GetHeldMesh() const { return HeldMesh ? HeldMesh.Get() : WorldMesh.Get(); }
/**
* Pose le mesh tenu en main et son materiau sur un composant.
* Pendant exact d'ApplyWorldVisualsTo, dont c'est le second appelant annonce.
*/
UFUNCTION(BlueprintCallable, Category = "Item|Held")
void ApplyHeldVisualsTo(class UStaticMeshComponent* Component) const;
// ------------------------------------------------------------------
// Outil
//
// Volontairement reduit au TYPE pour l'instant : la puissance de recolte et
// le cout en durabilite d'un coup n'ont pas encore de lecteur, et inventer
// leur forme avant d'ecrire les ressources recoltables serait deviner.
//
// L'usure, elle, est deja tranchee : ce sera MaxUses / RemainingUses, comme
// une canette. Un outil neuf part donc a MaxUses charges, l'inventaire les
// suit deja au sol comme dans un coffre, et WBP_ItemDetails affiche deja la
// barre correspondante -- rien a ajouter le jour venu.
// ------------------------------------------------------------------
/** Ce que cet objet sait recolter. None = ce n'est pas un outil. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool")
EToolType ToolType = EToolType::None;
UFUNCTION(BlueprintPure, Category = "Item|Tool")
bool IsTool() const { return ToolType != EToolType::None; }
// ------------------------------------------------------------------
// Consommable
// ------------------------------------------------------------------
/** Coche pour qu'un clic gauche consomme l'objet et applique ses effets. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable")
bool bIsConsumable = false;
/** Faim rendue a la consommation. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
float HungerRestore = 0.f;
/** Soif rendue. Une valeur negative deshydrate -- utile pour l'eau salee. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
float ThirstRestore = 0.f;
/** Vie rendue. Negative pour un aliment avarie ou toxique. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
float HealthRestore = 0.f;
// ------------------------------------------------------------------
// Cuisson
//
// Modele en CHAINE D'OBJETS, sans recette : cuire n'est pas un choix que le
// joueur fait dans un menu, c'est une propriete de l'objet. "Steak cru"
// pointe vers "Steak cuit", qui pointe vers "Steak brule". Chaque maillon
// est un asset complet, donc le changement de mesh sur le feu ne coute pas
// une ligne : c'est le WorldMesh du maillon suivant.
//
// Ce qui n'est PAS ici, et ne doit jamais y venir : le temps deja passe sur
// le feu. C'est de l'etat, il vit sur ACookingStation. Le mettre dans le
// DataAsset ferait cuire tous les steaks du monde en meme temps.
// ------------------------------------------------------------------
/** Cet objet se transforme quand on le pose sur un feu allume. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking")
bool bCanBeCooked = false;
/** Secondes de feu ACTIF necessaires. Un feu eteint met le compteur en pause. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBeCooked", ClampMin = "0.1", Units = "s"))
float CookDuration = 20.f;
/** Ce que l'objet devient une fois cuit. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBeCooked"))
TObjectPtr<UItemDataAsset> CookedResult;
/**
* L'objet se degrade s'il reste sur le feu.
*
* Volontairement independant de bCanBeCooked : un objet peut bruler sans
* jamais avoir ete cuisinable (une planche jetee dans les flammes), et un
* plat peut rester indefiniment au chaud sans jamais se gacher. Une seule
* chaine commune imposerait l'un ou l'autre a tout le monde.
*
* Si les deux sont coches, la CUISSON passe en premier : c'est le maillon
* cuit qui portera bCanBurn pour continuer vers le brule.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking")
bool bCanBurn = false;
/** Secondes de feu actif avant que l'objet se gache. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBurn", ClampMin = "0.1", Units = "s"))
float BurnDuration = 40.f;
/** Ce que l'objet devient une fois brule. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBurn"))
TObjectPtr<UItemDataAsset> BurntResult;
/**
* Le feu accepte-t-il cet objet sur un emplacement de cuisson.
*
* Un maillon final -- le steak brule -- repond non : on ne peut plus le
* poser, et il ne fait plus rien s'il est deja la. La regle est centralisee
* ici pour que la station n'ait pas a la reconstruire a chaque appel.
*/
UFUNCTION(BlueprintPure, Category = "Item|Cooking")
bool CanBePlacedOnFire() const
{
return (bCanBeCooked && CookedResult != nullptr) || (bCanBurn && BurntResult != nullptr);
}
// ------------------------------------------------------------------
// Carburant
// ------------------------------------------------------------------
/** Cet objet peut alimenter un feu. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Fuel")
bool bIsFuel = false;
/** Secondes de combustion apportees par exemplaire. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Fuel", meta = (EditCondition = "bIsFuel", ClampMin = "1", Units = "s"))
float FuelDuration = 60.f;
#if WITH_EDITOR
/**
* Une chaine de cuisson mal branchee ne planterait pas : la viande resterait
* simplement crue sur le feu, sans rien dire. C'est exactement le genre de
* bug qu'on cherche une heure -- autant que l'editeur le refuse a la
* sauvegarde.
*/
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
};
+114
View File
@@ -0,0 +1,114 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ItemDetailsWidget.generated.h"
class UImage;
class UInventoryComponent;
class UProgressBar;
class UTextBlock;
/**
* Le panneau qui decrit l'objet survole : icone, nom, description, usure.
* Rien d'autre -- volontairement.
*
* Il ne connait ni la grille ni l'ecran. On lui pousse un couple
* (inventaire, index) et il lit -- et NON une copie du FInventorySlot :
* le contenu peut changer pendant le survol (une charge consommee, un craft
* qui vide la pile), donc le panneau s'abonne a OnInventoryChanged de
* l'inventaire affiche et se relit tout seul. Jamais de Tick.
*
* Aucun texte en dur ici en dehors du format numerique de l'usure : la mise en
* page et les libelles appartiennent au Widget Blueprint.
*/
UCLASS(Abstract)
class EMBERWILD_API UItemDetailsWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Affiche la case InSlotIndex de InInventory. Index invalide = etat vide. */
void ShowSlot(UInventoryComponent* InInventory, int32 InSlotIndex);
/** Repasse en etat vide et coupe l'abonnement. */
void Clear();
/**
* Efface, mais seulement si c'est bien CETTE case qui est affichee.
*
* Le panneau est unique et trois conteneurs le nourrissent -- grille, barre
* rapide, et la grille elle-meme quand elle se rebranche. Passer de l'un a
* l'autre emet "entre dans B" PUIS "sort de A" : un effacement
* inconditionnel viderait l'affichage qui vient d'arriver.
*/
void ClearSlot(const UInventoryComponent* InInventory, int32 InSlotIndex);
protected:
/** A nommer exactement "ItemIcon" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> ItemIcon;
/** A nommer exactement "ItemNameText". */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> ItemNameText;
/** Pense a cocher "Auto Wrap Text" dessus, sinon le panneau s'elargit. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> ItemDescriptionText;
/**
* Bloc d'usure entier -- barre et texte ensemble -- replie sur tout objet
* qui n'a pas de charges multiples. Sans lui, la barre et son texte
* devraient etre replies separement et finiraient un jour desynchronises.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> UsesPanel;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> UsesBar;
/** "3 / 5", pose par-dessus la barre. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> UsesText;
/**
* Le conteneur qui empile tout le panneau -- a renommer "ContentRoot", ce
* n'est pas un widget de plus.
*
* Il est masque en bloc quand rien n'est survole. Sans lui, la barre de
* separation resterait seule a l'ecran : c'est une decoration du Blueprint,
* le C++ n'a aucune raison de la connaitre individuellement.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> ContentRoot;
private:
UFUNCTION()
void HandleInventoryChanged();
/** Relit la case et repeint tout le panneau. */
void Refresh();
/**
* Etat vide : plus rien a l'ecran.
*
* Le panneau n'a pas de fond, donc vide il est simplement invisible -- et sa
* largeur est tenue par le Size Box du Blueprint, pas par son contenu. C'est
* ce qui empeche la grille de sauter lateralement quand le curseur quitte
* une case.
*/
void ClearDisplay();
/** (Re)branche OnInventoryChanged. Un seul abonnement a la fois. */
void BindInventory(UInventoryComponent* InInventory);
TWeakObjectPtr<UInventoryComponent> BoundInventory;
int32 SlotIndex = INDEX_NONE;
};
+115
View File
@@ -0,0 +1,115 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InputCoreTypes.h"
#include "KeyCaptureWidget.generated.h"
class UButton;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnKeyCaptured, FKey, CapturedKey);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnKeyCaptureCancelled);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnKeyConflictAnswered, bool, bConfirmed);
/** Ce que le voile attend a un instant donne. */
UENUM()
enum class EKeyCaptureState : uint8
{
/** Ferme. */
Idle,
/** Ouvert, en attente d'une touche. Les boutons sont replies. */
Capturing,
/** Ouvert, la touche est prise : on demande quoi faire. Les boutons apparaissent. */
Conflict
};
/**
* Le voile de remappage, en deux temps : "Appuie sur une touche", puis
* eventuellement "cette touche est deja prise, on la vole ?".
*
* Les deux etapes vivent dans le MEME widget et non dans deux ecrans successifs.
* Le conflit fait partie du geste de remappage : refermer le voile pour ouvrir
* une boite par-dessus produirait un clignotement et ferait perdre le fil de ce
* qu'on etait en train de modifier. UConfirmDialogWidget garde les questions
* generiques -- reinitialiser, abandonner une partie, valider une resolution.
*
* Classe Abstract : on cree un WBP_KeyCapture qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API UKeyCaptureWidget : public UUserWidget
{
GENERATED_BODY()
public:
UKeyCaptureWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeOnInitialized() override;
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
UPROPERTY(BlueprintAssignable, Category = "Reglages")
FOnKeyCaptured OnKeyCaptured;
UPROPERTY(BlueprintAssignable, Category = "Reglages")
FOnKeyCaptureCancelled OnCaptureCancelled;
UPROPERTY(BlueprintAssignable, Category = "Reglages")
FOnKeyConflictAnswered OnConflictAnswered;
/** Affiche le voile, ecrit la consigne, replie les boutons et prend le focus. */
UFUNCTION(BlueprintCallable, Category = "Reglages")
void BeginCapture(const FText& ActionLabel);
/**
* Bascule le voile deja ouvert sur la question de conflit.
*
* Ne referme rien : c'est tout l'interet d'avoir garde un seul widget.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void ShowConflict(const FText& Message);
UFUNCTION(BlueprintCallable, Category = "Reglages")
void EndCapture();
UFUNCTION(BlueprintPure, Category = "Reglages")
EKeyCaptureState GetCaptureState() const { return State; }
protected:
/** Optionnel : un voile dont le texte est fige dans le Blueprint reste valide. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> PromptText;
/**
* Boutons de la seule etape de conflit. Replies pendant la capture : les
* afficher inertes inviterait a cliquer alors qu'on attend une touche.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> ConfirmButton;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> CancelButton;
/** Doit contenir {0}, remplace par le nom de l'action en cours de remappage. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
FText PromptFormat;
private:
UFUNCTION()
void HandleConfirmClicked();
UFUNCTION()
void HandleCancelClicked();
/** Facteur commun a NativeOnKeyDown et NativeOnMouseButtonDown. */
FReply CaptureKey(const FKey& Key);
/** Affiche ou replie les deux boutons selon l'etat. */
void UpdateButtonVisibility();
EKeyCaptureState State = EKeyCaptureState::Idle;
};
+54
View File
@@ -0,0 +1,54 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "InputCoreTypes.h"
/**
* Traduction d'une touche de reference vers la disposition clavier du systeme.
*
* LE PROBLEME. Un IMC stocke des FKey -- EKeys::W --, c'est-a-dire des LETTRES.
* Or Windows livre au moteur un virtual key code deja traduit par la disposition
* active : sur un clavier AZERTY, la touche en haut a gauche du bloc de
* deplacement envoie VK_Z, qu'Unreal lit donc comme EKeys::Z. Le mapping "W" ne
* se declenche jamais, et le joueur pousse son personnage en arriere en croyant
* avancer. Rien de tout ca n'est visible en developpant sur un clavier QWERTY.
*
* LA REPONSE. On ne raisonne plus en lettres mais en POSITIONS. Chaque touche
* par defaut de l'IMC est lue comme une position physique sur un clavier QWERTY
* US -- c'est le referentiel dans lequel les IMC sont ecrits -- et l'on demande
* a l'OS quelle touche occupe cette position sur la disposition courante. La
* geometrie de la main est preservee : le bloc WASD devient ZQSD en AZERTY,
* reste WASD en QWERTZ (seuls Y et Z y sont echanges), et suit les positions en
* Dvorak ou Colemak. Aucune table par pays a maintenir, et les dispositions
* qu'on n'a jamais vues sont traitees comme les autres.
*
* PORTEE. Seule la zone alphanumerique bouge d'une disposition a l'autre. Les
* modificateurs, la barre d'espace, Echap, les fonctions, les fleches, le pave
* numerique et la manette occupent la meme position partout : ils ne figurent
* pas dans la table de reference et ressortent donc inchanges. C'est aussi ce
* qui garantit qu'on ne casse rien -- une touche inconnue est rendue telle quelle.
*
* WINDOWS UNIQUEMENT pour l'instant. Sur les autres plateformes la touche est
* rendue inchangee, soit exactement le comportement d'avant.
*/
namespace KeyboardLayout
{
/**
* Rend la touche qui occupe, sur la disposition active, la position que
* ReferenceKey occupe sur un clavier QWERTY US.
*
* Rend ReferenceKey inchangee si la touche n'est pas concernee par la
* disposition (espace, modificateurs, manette...), si la plateforme n'est pas
* geree, ou si l'OS ne sait pas repondre. Jamais de touche invalide en sortie.
*/
EMBERWILD_API FKey RemapKeyToActiveLayout(const FKey& ReferenceKey);
/**
* L'identifiant de la disposition active, pour le journal : "0000040C" pour
* le francais, "00000409" pour l'anglais US. Sert au diagnostic quand un
* testeur signale des touches inattendues -- c'est la premiere chose a lire.
*/
EMBERWILD_API FString GetActiveLayoutId();
}
@@ -0,0 +1,27 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "MainMenuGameMode.generated.h"
/**
* GameMode de la map de menu.
*
* Son role principal est NEGATIF : empecher AFpsPlayer de spawner. La map de
* menu etant une copie de la map de jeu, sans ce GameMode Override dans ses
* World Settings, le joueur apparaitrait derriere le menu avec son HUD, ses
* jauges et son inventaire.
*
* Divergence Unity a garder en tete : ici le GameMode est une propriete DE LA
* MAP, pas un objet qu'on pose dedans. Il se choisit dans World Settings.
*/
UCLASS()
class EMBERWILD_API AMainMenuGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AMainMenuGameMode();
};
@@ -0,0 +1,241 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MainMenuPlayerController.generated.h"
class ACharacterPreviewActor;
class AMenuCameraSpot;
class UCharacterCustomizationWidget;
class UConfirmDialogWidget;
class UMainMenuWidget;
class UScreenFadeComponent;
class USettingsMenuWidget;
/**
* Ce qu'on fait une fois l'ecran devenu noir.
*
* Quitter n'y figure pas : la sortie est seche, sans fondu, une fois la
* confirmation obtenue.
*/
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,
/**
* On ne quitte PAS la map : le mannequin est pose dans MenuScene, on ne fait
* que porter la vue sur lui. Le fondu sert a masquer la bascule -- un
* travelling depuis l'angle de menu traverserait le decor.
*/
OpenCustomization,
/** Retour du mannequin vers un angle de menu, meme mecanique en sens inverse. */
CloseCustomization
};
/**
* Controller du menu principal. Il ne possede aucun pawn : il pose simplement
* sa vue sur une AMenuCameraSpot tiree au hasard et affiche le menu par-dessus.
*
* Comme dans le jeu, l'UI vit sur le controller et pas sur un acteur du monde.
*/
UCLASS()
class EMBERWILD_API AMainMenuPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AMainMenuPlayerController();
/**
* 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 StartGame();
/** Ouvre la demande de confirmation. Ne quitte rien par elle-meme. */
UFUNCTION(BlueprintCallable, Category = "Menu")
void QuitGame();
/** Ouvre l'ecran de reglages par-dessus le menu. */
UFUNCTION(BlueprintCallable, Category = "Menu")
void OpenSettings();
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/**
* Ta map de jeu.
*
* TSoftObjectPtr et non une reference dure : une reference dure ferait
* charger la map de jeu EN MEME TEMPS que le menu, alors qu'on ne la veut
* qu'au clic sur Jouer. C'est l'equivalent d'une Addressable en Unity, par
* opposition a un champ public qui tire tout l'asset avec lui.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
TSoftObjectPtr<UWorld> GameLevel;
/** Ton WBP_MainMenu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
TSubclassOf<UMainMenuWidget> MainMenuClass;
/**
* Ton WBP_ConfirmDialog. Si tu le laisses vide, le bouton Quitter ferme le
* jeu directement -- pratique pour tester, mais laisse-le assigne.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
TSubclassOf<UConfirmDialogWidget> QuitDialogClass;
/** La question posee avant de fermer le jeu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
FText QuitDialogMessage;
/** Ton WBP_Settings, exactement le meme que celui du menu de pause. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
TSubclassOf<USettingsMenuWidget> SettingsMenuClass;
/**
* Ton WBP_CharacterCustomization. Laisse-le vide pour retrouver l'ancien
* comportement -- Jouer lance directement la partie. Pratique pour tester
* autre chose sans traverser l'ecran a chaque fois.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu|Customization")
TSubclassOf<UCharacterCustomizationWidget> CustomizationMenuClass;
/**
* Le mannequin pose dans MenuScene. Cherche par classe au BeginPlay plutot
* qu'assigne ici : une reference d'acteur dans un CDO de controller ne
* survit pas au chargement de la map.
*/
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu|Customization")
TObjectPtr<ACharacterPreviewActor> CharacterPreview;
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu|Customization")
TObjectPtr<UCharacterCustomizationWidget> CustomizationMenu;
/**
* Duree de CHAQUE moitie du fondu entre le menu et l'ecran de customisation.
* Le passage complet dure donc le double.
*
* Plus court que le fondu de depart vers la partie : on ne change pas de
* map, il n'y a rien a charger, et une attente ici ne serait qu'une attente.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu|Customization", meta = (ClampMin = "0.05"))
float CustomizationFadeDuration = 0.35f;
/** Fondu d'arrivee sur le menu, au lancement du jeu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu", meta = (ClampMin = "0.1"))
float OpeningFadeDuration = 1.5f;
/** Fondu de depart, quand on quitte le menu pour la partie. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu", meta = (ClampMin = "0.1"))
float LeaveFadeDuration = 1.f;
/** Le fondu, partage avec AFpsPlayerController. Assigne-lui ton WBP_Fade. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Menu")
TObjectPtr<UScreenFadeComponent> ScreenFade;
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu")
TObjectPtr<UMainMenuWidget> MainMenu;
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu")
TObjectPtr<UConfirmDialogWidget> QuitDialog;
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu")
TObjectPtr<USettingsMenuWidget> SettingsMenu;
private:
/** Tire un angle de vue au hasard, en evitant celui du dernier passage. */
AMenuCameraSpot* PickCameraSpot();
/** L'eclaircissement lui-meme, apres la chauffe du decor. */
void StartOpeningFade();
/** Le fondu vient de se terminer : on execute l'action mise de cote. */
UFUNCTION()
void HandleFadeFinished();
/** Oui : on ferme le jeu, immediatement. */
UFUNCTION()
void HandleQuitConfirmed();
/** Non : on referme la boite et on rend la main au menu. */
UFUNCTION()
void HandleQuitCancelled();
/** L'ecran de reglages demande a se fermer : on rend la main au menu. */
UFUNCTION()
void HandleSettingsCloseRequested();
/**
* Ouvre l'ecran de customisation et pose la vue sur le mannequin.
*
* L'ordre compte : SetViewTarget d'abord, widget ensuite. L'inverse
* afficherait la colonne par-dessus l'ancien cadrage pendant toute la duree
* du blend.
*/
void OpenCustomization();
/** Le joueur a valide : on soumet l'apparence, puis on lance la partie. */
UFUNCTION()
void HandleCustomizationConfirmed();
/** Le joueur renonce : retour au menu, rien n'est ecrit. */
UFUNCTION()
void HandleCustomizationCancelled();
/** Referme l'ecran et rend la vue a l'angle de menu. */
void CloseCustomization();
/**
* 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);
/**
* Une invitation vient d'aboutir : on fond au noir avant que le voyage
* parte. C'est le seul chemin d'entree en partie qui ne passait pas par le
* bouton Jouer, et donc le seul qui coupait l'image d'un coup.
*/
UFUNCTION()
void HandleTravelPending();
/** Ferme le jeu sans rien demander. */
void CloseGame();
EMenuPendingAction PendingAction = EMenuPendingAction::None;
};
+58
View File
@@ -0,0 +1,58 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MainMenuWidget.generated.h"
class UButton;
/**
* Le menu principal : quelques boutons par-dessus la vue du monde.
*
* Classe Abstract comme les autres widgets du projet : on cree un
* WBP_MainMenu qui en herite, le C++ porte la logique et le Blueprint la mise
* en page.
*
* Le widget ne decide de rien, il appelle AMainMenuPlayerController. Le jour ou
* un bouton devra faire une action reseau, elle sera deja du bon cote : un
* widget n'existe que sur la machine du joueur local, le serveur ne le voit pas.
*/
UCLASS(Abstract)
class EMBERWILD_API UMainMenuWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
protected:
/**
* 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;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> QuitButton;
/** BindWidgetOptional : un WBP_MainMenu sans bouton Reglages reste valide. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> SettingsButton;
private:
UFUNCTION()
void HandlePlayClicked();
UFUNCTION()
void HandleQuitClicked();
UFUNCTION()
void HandleSettingsClicked();
};
+33
View File
@@ -0,0 +1,33 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Camera/CameraActor.h"
#include "MenuCameraSpot.generated.h"
/**
* Un angle de vue possible pour le fond du menu principal.
*
* Pose-en autant que tu veux dans la map de menu : le controller en tire une au
* hasard a chaque lancement.
*
* Une classe dediee plutot qu'un tag "MenuCamera" sur une ACameraActor -- le
* reflexe Unity : la recherche est typee au lieu d'etre une chaine de
* caracteres, et la classe a un endroit ou porter ses propres reglages.
*/
UCLASS()
class EMBERWILD_API AMenuCameraSpot : public ACameraActor
{
GENERATED_BODY()
public:
AMenuCameraSpot();
/**
* Decoche pour retirer cet angle du tirage sans supprimer l'acteur.
* Pratique pour mettre un cadrage de cote le temps de le retravailler.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Menu")
bool bEnabled = true;
};
+63
View File
@@ -0,0 +1,63 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Info.h"
#include "MusicSubsystem.h"
#include "MusicDirector.generated.h"
class UMusicPlaylistDataAsset;
/**
* Declare la musique du jeu. UN SEUL exemplaire suffit, dans la premiere map
* chargee -- typiquement la scene de menu.
*
* Il ne joue rien lui-meme et ne survit pas a sa map : il transmet sa consigne a
* UMusicSubsystem, qui, lui, vit sur le GameInstance et traverse tous les
* changements de niveau. C'est pour ca que les maps suivantes n'ont besoin de
* rien : la musique continue toute seule, et la regle de transition a deja ete
* posee ici.
*
* En poser un second dans une autre map reste possible : il change alors de
* playlist a l'arrivee sur celle-la.
*
* Derive d'AInfo : aucun visuel, aucun tick, et AInfo se declare non
* spatialement charge, donc il est present des le demarrage meme en World
* Partition -- un acteur ordinaire hors de la zone de streaming ne ferait jamais
* son BeginPlay.
*/
UCLASS(ClassGroup = "Audio", meta = (DisplayName = "Music Director"))
class EMBERWILD_API AMusicDirector : public AInfo
{
GENERATED_BODY()
public:
AMusicDirector();
protected:
virtual void BeginPlay() override;
/**
* La playlist a lancer en arrivant sur cette map.
*
* Si c'est deja elle qui tourne, elle n'est pas relancee. La laisser vide veut
* dire "je ne change pas la musique en cours", et n'a d'interet que sur un
* second Director pose uniquement pour changer la regle de transition.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Musique")
TObjectPtr<UMusicPlaylistDataAsset> Playlist;
/**
* Ce que devient le morceau en cours a CHAQUE changement de scene suivant.
*
* Continue : il poursuit sa route par-dessus le chargement.
* New : il s'eteint et le lecteur en demarre un autre -- tirage aleatoire
* ou morceau suivant, selon le mode de la playlist.
*
* La consigne est envoyee au lecteur, qui la garde : les autres maps n'ont
* rien a declarer.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Musique")
EMusicLevelChangeMode LevelChangeMode = EMusicLevelChangeMode::Continue;
};
@@ -0,0 +1,85 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "MusicPlaylistDataAsset.generated.h"
class USoundBase;
/**
* Une liste de morceaux et la facon de les enchainer.
*
* DataAsset et pas une variable sur un acteur : c'est de la DONNEE, au meme titre
* qu'un UItemDataAsset. On en cree autant qu'on veut (menu, exploration, nuit,
* combat) sans toucher au code, et la meme playlist peut etre referencee par
* plusieurs maps -- ce qui est exactement ce qui permet au menu et au jeu de
* partager une musique qui ne se coupe pas entre les deux.
*
* Aucun etat ici : l'index en cours, l'ordre tire au sort et le volume vivent
* dans UMusicSubsystem. Un DataAsset est partage par toutes les references, y
* mettre de l'etat le partagerait aussi.
*/
UCLASS(BlueprintType)
class EMBERWILD_API UMusicPlaylistDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/** Nom lisible, pour un futur affichage "en cours de lecture". */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist")
FText DisplayName;
/**
* Les morceaux, dans l'ordre de lecture quand l'aleatoire est desactive.
*
* Les cases vides sont ignorees a la lecture : une reference oubliee ne coupe
* pas la musique, elle est simplement sautee.
*
* IMPORTANT : aucun de ces sons ne doit boucler sur lui-meme (ni "Looping" sur
* la SoundWave, ni un noeud Looping dans la SoundCue). L'enchainement repose
* sur OnAudioFinished, qui n'est jamais diffuse par un son qui boucle.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist")
TArray<TObjectPtr<USoundBase>> Tracks;
/**
* Mode par defaut au demarrage de CETTE playlist.
*
* Le joueur peut ensuite basculer en cours de route via
* UMusicSubsystem::SetShuffleEnabled ; cette valeur n'est relue qu'au
* prochain demarrage de playlist.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist")
bool bShuffle = true;
/** Faux = la musique s'arrete quand tous les morceaux ont ete joues une fois. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist")
bool bLoop = true;
/**
* Silence entre deux morceaux, tire au hasard dans l'intervalle.
*
* Une musique d'ambiance qui respire quelques secondes se remarque beaucoup
* moins qu'un flux continu, et c'est ce qui evite la lassitude sur une longue
* session. A zero, l'enchainement est immediat.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", Units = "s"))
float MinGapBetweenTracks = 0.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", Units = "s"))
float MaxGapBetweenTracks = 0.f;
/** Correction de niveau propre a cette playlist, par-dessus le volume joueur. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", ClampMax = "2.0"))
float VolumeMultiplier = 1.f;
/** Fondu entrant au demarrage de la playlist (pas entre deux morceaux). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", Units = "s"))
float FadeInDuration = 2.f;
/** Fondu sortant quand une autre playlist prend la main, ou a l'arret. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", Units = "s"))
float FadeOutDuration = 2.f;
};
+212
View File
@@ -0,0 +1,212 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Containers/Ticker.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "MusicSubsystem.generated.h"
class UAudioComponent;
class UMusicPlaylistDataAsset;
class USoundBase;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMusicTrackChanged, USoundBase*, NewTrack);
/**
* Ce qui arrive au morceau en cours quand on change de scene.
*
* La question ne se pose que parce que le lecteur, lui, ne s'arrete jamais :
* sans consigne, la musique du menu se poursuit telle quelle dans la partie, au
* milieu du morceau ou elle en etait.
*/
UENUM(BlueprintType)
enum class EMusicLevelChangeMode : uint8
{
/** Le morceau en cours poursuit sa route, comme si de rien n'etait. */
Continue UMETA(DisplayName = "Continue"),
/**
* Le morceau en cours s'eteint et le suivant demarre : nouveau tirage si
* l'aleatoire est actif, morceau suivant de la liste sinon. La playlist, elle,
* ne change pas.
*/
New UMETA(DisplayName = "New")
};
/**
* Le lecteur de musique du jeu. Un seul, pour toute la duree de l'application.
*
* UGameInstanceSubsystem et pas un acteur : il naît avec le GameInstance et meurt
* avec le jeu, exactement comme USurvivalGameInstance. Aucune map ne le possede,
* donc aucun OpenLevel ne le detruit -- c'est le seul endroit d'ou une musique
* peut traverser le passage menu -> partie sans se couper. Le moteur l'instancie
* tout seul, il n'y a rien a poser dans une scene ni a cocher nulle part.
*
* Le composant audio lui-meme est cree via CreateSound2D avec
* bPersistAcrossLevelTransition : le moteur le construit alors SANS monde et sans
* proprietaire, et lui pose bIgnoreForFlushing -- le drapeau que le vidage du
* peripherique audio, declenche a chaque changement de niveau, saute
* explicitement. Sans ce parametre le son s'arreterait net au chargement, meme si
* l'objet C++, lui, survivait.
*
* Effet de bord voulu : ces composants sont marques bIsUISound, donc ils
* continuent de jouer quand le jeu est en pause. Le menu de pause garde sa
* musique sans une ligne de plus.
*/
UCLASS()
class EMBERWILD_API UMusicSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
/** Raccourci depuis n'importe quel objet ayant un monde. */
UFUNCTION(BlueprintPure, Category = "Musique", meta = (WorldContext = "WorldContextObject", DisplayName = "Get Music Subsystem"))
static UMusicSubsystem* Get(const UObject* WorldContextObject);
/**
* Demarre une playlist.
*
* Si c'est DEJA celle qui joue, ne fait rien : c'est ce comportement qui rend
* la transition menu -> jeu inaudible quand les deux maps designent la meme
* playlist. Si c'en est une autre, croisement entre l'ancienne qui s'eteint et
* la nouvelle qui entre.
*
* @param bForceRestart Repart du debut meme si la playlist est deja en cours.
*/
UFUNCTION(BlueprintCallable, Category = "Musique")
void PlayPlaylist(UMusicPlaylistDataAsset* Playlist, bool bForceRestart = false);
/** @param FadeOutDuration Negatif = utiliser le fondu declare par la playlist. */
UFUNCTION(BlueprintCallable, Category = "Musique")
void StopMusic(float FadeOutDuration = -1.f);
/** Passe au morceau suivant, dans l'ordre courant (aleatoire ou non). */
UFUNCTION(BlueprintCallable, Category = "Musique")
void SkipToNextTrack();
/**
* Bascule aleatoire / ordre de la liste.
*
* Ne coupe jamais le morceau en cours : seul l'enchainement change. Le curseur
* se recale sur le morceau qui joue, donc en repassant en mode ordonne la
* lecture reprend juste apres lui au lieu de sauter au debut de la liste.
*/
UFUNCTION(BlueprintCallable, Category = "Musique")
void SetShuffleEnabled(bool bEnabled);
UFUNCTION(BlueprintPure, Category = "Musique")
bool IsShuffleEnabled() const { return bShuffleEnabled; }
/**
* Ce que devient le morceau en cours au prochain changement de scene.
*
* La consigne vit ICI et pas sur l'acteur qui l'a posee : l'acteur meurt avec
* sa map, alors que le changement de scene se produit justement apres. Une
* seule map -- le menu -- suffit donc a la declarer pour tout le jeu.
*/
UFUNCTION(BlueprintCallable, Category = "Musique")
void SetLevelChangeMode(EMusicLevelChangeMode Mode) { LevelChangeMode = Mode; }
UFUNCTION(BlueprintPure, Category = "Musique")
EMusicLevelChangeMode GetLevelChangeMode() const { return LevelChangeMode; }
/**
* Attenuation propre au LECTEUR, 0 a 1. Ce n'est PAS le reglage du joueur.
*
* Le volume choisi dans les reglages passe par la SoundClass de musique (voir
* USurvivalAudioSettings), pas par ici : deux chemins vers le meme volume
* finiraient par se multiplier sans que personne ne s'en souvienne. Celui-ci
* reste disponible pour ce qui est de l'ordre du gameplay -- baisser la musique
* pendant un dialogue, par exemple.
*/
UFUNCTION(BlueprintCallable, Category = "Musique")
void SetMusicVolume(float NewVolume);
UFUNCTION(BlueprintPure, Category = "Musique")
float GetMusicVolume() const { return MusicVolume; }
UFUNCTION(BlueprintPure, Category = "Musique")
bool IsPlaying() const { return bIsPlaying; }
UFUNCTION(BlueprintPure, Category = "Musique")
USoundBase* GetCurrentTrack() const;
UFUNCTION(BlueprintPure, Category = "Musique")
UMusicPlaylistDataAsset* GetCurrentPlaylist() const { return CurrentPlaylist; }
/** Diffuse a chaque changement de morceau, nullptr a l'arret. */
UPROPERTY(BlueprintAssignable, Category = "Musique")
FOnMusicTrackChanged OnMusicTrackChanged;
private:
/** Fin NATURELLE d'un morceau uniquement -- voir FadeOutAndRelease. */
UFUNCTION()
void HandleTrackFinished();
/** Applique LevelChangeMode a chaque map chargee. */
void HandlePostLoadMap(UWorld* LoadedWorld);
/** (Re)construit l'ordre de lecture du cycle a venir. */
void BuildPlayOrder();
void AdvanceAndPlay(float FadeInDuration);
void PlayTrackAtCursor(float FadeInDuration);
UAudioComponent* CreateMusicComponent(USoundBase* Sound);
/**
* Detache puis eteint un composant : apres cet appel, plus rien de ce qu'il
* diffuse ne revient vers nous.
*/
void FadeOutAndRelease(TObjectPtr<UAudioComponent>& Component, float FadeOutDuration);
void ScheduleGap(float DelaySeconds);
void CancelPendingGap();
float GetEffectiveVolume() const;
UPROPERTY()
TObjectPtr<UMusicPlaylistDataAsset> CurrentPlaylist;
/**
* UPROPERTY obligatoire : ces composants n'ont ni monde ni acteur proprietaire,
* cette reference est la SEULE qui les tienne hors de portee du ramasse-miettes.
*/
UPROPERTY()
TObjectPtr<UAudioComponent> MusicComponent;
/** La voie de sortie, le temps d'un croisement. Une seule a la fois. */
UPROPERTY()
TObjectPtr<UAudioComponent> FadingOutComponent;
/**
* Index dans Tracks, deja melangee si l'aleatoire est actif.
*
* Un ordre pre-melange plutot qu'un tirage a chaque morceau : le tirage
* independant rejoue le meme titre deux fois de suite une fois sur N, et sur
* une playlist de cinq morceaux ca s'entend tout de suite.
*/
TArray<int32> PlayOrder;
int32 OrderCursor = INDEX_NONE;
int32 LastPlayedTrackIndex = INDEX_NONE;
bool bShuffleEnabled = true;
bool bIsPlaying = false;
float MusicVolume = 1.f;
EMusicLevelChangeMode LevelChangeMode = EMusicLevelChangeMode::Continue;
/** Abonnement au chargement de map, retire a la fermeture du jeu. */
FDelegateHandle PostLoadMapHandle;
/**
* Le silence entre deux morceaux passe par le ticker de Core, pas par un
* FTimerHandle : un timer appartient au monde, donc il disparaîtrait au
* chargement de la map suivante -- et il ne tourne pas en pause. Le ticker de
* Core ignore les deux.
*/
FTSTicker::FDelegateHandle GapTickerHandle;
};
+102
View File
@@ -0,0 +1,102 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PauseMenuWidget.generated.h"
class UButton;
/**
* Le menu de pause : trois boutons par-dessus la partie figee.
*
* Comme UMainMenuWidget, il ne decide de rien -- il appelle
* AFpsPlayerController. Mettre SetPause ou OpenLevel ici serait doublement
* faux : un widget n'existe que sur la machine du joueur local, et la mise en
* pause appartient a l'autorite.
*
* Classe Abstract : on cree un WBP_PauseMenu qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API UPauseMenuWidget : public UUserWidget
{
GENERATED_BODY()
public:
UPauseMenuWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/**
* Echap, quand c'est CE widget qui tient le focus clavier.
*
* Indispensable et pas seulement confortable : les widgets UMG sont des
* FRERES du SViewport dans l'arbre Slate, jamais ses enfants. Une touche
* remonte donc le long des parents du widget focalise et n'atteint jamais le
* viewport -- Enhanced Input ne la voit pas. Or le menu de pause est le seul
* ecran in-game plein de UButton, et un bouton prend le focus au clic : sans
* ce relais, Echap cesse de refermer le menu des le premier clic dedans.
*/
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
/** Affiche le menu et lui rend le focus clavier. */
void Open();
/** Masque le menu. Le focus revient au viewport avec le mode d'input. */
void Close();
/**
* Actif (cliquable et focalise) ou simplement lisible derriere un ecran pose
* par-dessus -- reglages, boite de confirmation.
*
* Redonner le focus en redevenant actif est la moitie qui manque le plus
* souvent : l'ecran qui passait devant l'avait pris pour lui, et personne ne
* le rend en se fermant.
*/
void SetInteractive(bool bInteractive);
protected:
/** Reprend la partie. Nomme exactement "ResumeButton" dans le WBP. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ResumeButton;
/** Retour au menu principal, apres confirmation. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> QuitToMenuButton;
/**
* BindWidgetOptional : un WBP_PauseMenu sans bouton Reglages reste valide.
*
* Pas de bouton "Quitter le jeu" ici, volontairement : la sortie se fait
* depuis le menu principal, ou l'on repasse de toute facon.
*/
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();
UFUNCTION()
void HandleSettingsClicked();
UFUNCTION()
void HandleQuitToMenuClicked();
UFUNCTION()
void HandleInviteClicked();
};
+181
View File
@@ -0,0 +1,181 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.h"
#include "PickupItem.generated.h"
class UBoxComponent;
class UItemDataAsset;
class UStaticMeshComponent;
/**
* Un objet pose dans le monde. Il ne DECRIT pas l'objet, il ne fait que
* pointer vers son UItemDataAsset et porter une quantite.
*
* Consequence pratique : pour creer un nouveau type de ramassable, tu n'as
* plus besoin d'un Blueprint par objet. Un seul BP_Pickup suffit, tu changes
* juste l'ItemData sur chaque instance posee dans le niveau.
*/
UCLASS()
class EMBERWILD_API APickupItem : public AActor, public IInteractable
{
GENERATED_BODY()
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);
// --- IInteractable ---
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
virtual bool CanInteract_Implementation(AActor* Interactor) const override;
virtual void Interact_Implementation(AActor* Interactor) override;
virtual void OnBeginFocus_Implementation() override;
virtual void OnEndFocus_Implementation() override;
protected:
/**
* Sans garde WITH_EDITOR : on veut aussi que le mesh se pose correctement
* sur les pickups engendres a l'execution quand on jette un objet.
*/
virtual void OnConstruction(const FTransform& Transform) override;
/** Visuel uniquement, sans collision. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UStaticMeshComponent> Mesh;
/**
* Volume que le trace d'interaction vient toucher.
*
* Volontairement separe du mesh : beaucoup de meshes d'asset packs, en
* particulier la vegetation, n'ont aucune geometrie de collision. Avec un
* volume dedie, le pickup fonctionne quel que soit le visuel.
*
* Une BOITE et non une sphere, contrairement a la premiere version : la
* sphere englobante d'un objet allonge a pour rayon sa DEMI-DIAGONALE, ce qui
* est le pire cas geometrique possible. Une branche de 120x20x15 donnait une
* sphere de 62 cm de rayon, soit vingt-sept fois le volume de l'objet. Le
* symptome n'etait pas cosmetique : deux pickups poses cote a cote voyaient
* leurs spheres se chevaucher, et le trace retenait le premier volume touche
* plutot que l'objet reellement vise.
*
* Elle epouse toute seule le mesh pose (voir FitInteractionBox) : un seul
* BP_Pickup sert des centaines d'objets, une taille fixe serait forcement
* fausse pour la plupart.
*
* Contrairement a celle du coffre, elle ne bloque QUE le canal Visibility :
* un objet au sol se traverse, il ne barre pas le passage.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UBoxComponent> InteractionBox;
/**
* 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.
* 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, 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();
/**
* Cale la boite d'interaction sur le mesh actuellement pose.
*
* Elle lit le mesh du COMPOSANT et pas celui de l'ItemData : le cas
* bUseMeshFromItemData decoche -- un mesh impose a la main sur une instance --
* a autant besoin d'une boite a sa taille que les autres.
*/
void FitInteractionBox();
/**
* Reprend automatiquement le WorldMesh de l'ItemData dans l'editeur.
* Decoche pour imposer un mesh different sur une instance precise.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
bool bUseMeshFromItemData = true;
/**
* La boite d'interaction epouse toute seule le mesh pose.
*
* Coche par defaut : changer l'ItemData d'une instance doit suffire, sans
* avoir a retailler un volume a la main derriere. Decoche-le pour un objet
* qu'on veut volontairement ramassable de plus loin, ou de plus pres.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Interaction")
bool bAutoFitInteractionBox = true;
/**
* Marge ajoutee sur chaque axe, en cm.
*
* Additive et surtout pas multiplicative : c'est tout l'interet du passage a
* une boite. Multiplier la demi-epaisseur d'une branche -- 7 cm -- par 1.2
* ajoute un centimetre et demi, autant dire rien, alors que le meme facteur
* rallonge son grand axe de 12 cm. La tolerance de visee ne depend pas de la
* taille de l'objet, elle se compte en centimetres.
*
* Garde-la petite : le trace d'interaction est deja un balayage spherique
* tolerant, gonfler la boite ferait surtout voler la cible aux voisins.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Interaction",
meta = (EditCondition = "bAutoFitInteractionBox", ClampMin = "0", Units = "cm"))
float InteractionBoxPadding = 4.f;
/**
* Demi-dimension plancher, appliquee axe par axe, en cm.
*
* Une baie ou un clou donneraient une boite de deux centimetres, impossible
* a viser meme avec la tolerance du balayage. Axe par axe et non sur le
* volume entier : une branche a besoin qu'on epaississe son epaisseur, pas
* qu'on rallonge sa longueur.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Interaction",
meta = (EditCondition = "bAutoFitInteractionBox", ClampMin = "0", Units = "cm"))
float MinInteractionExtent = 10.f;
/** Active le Custom Depth quand l'objet est vise. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
bool bHighlightOnFocus = true;
};
@@ -0,0 +1,150 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "PlayerBodyAnimInstance.generated.h"
class AFpsPlayer;
/**
* AnimInstance du corps vu a la TROISIEME personne -- celui que les autres
* joueurs voient, et dont on ne percoit soi-meme que l'ombre.
*
* Meme partage des roles que UFpsArmsAnimInstance : elle ne DECIDE de rien, elle
* lit l'etat du pawn une fois par frame et le republie en variables. C'est la
* machine a etats du Blueprint enfant qui choisit l'animation, parce qu'une
* transition se regle a l'oeil avec des durees de fondu et n'a rien a faire dans
* du C++ qu'il faut recompiler.
*
* La difference avec les bras est reseau, et elle est essentielle : les bras ne
* sont vus que par leur proprietaire, ce corps-ci est vu par TOUT LE MONDE.
* Chaque machine evalue donc les quatre corps de la partie. Et pourtant rien
* n'est replique ici -- tout est DERIVE d'un etat que le moteur ou le projet
* synchronisent deja :
*
* Velocity -> replique par le mouvement du Character
* bIsCrouched -> UPROPERTY repliquee d'ACharacter
* bIsSprinting -> repliquee par AFpsPlayer (c'est a ca qu'elle sert)
* AimPitch -> RemoteViewPitch, replique par APawn sans rien demander
*
* Repliquer une seule de ces variables serait envoyer deux fois la meme
* information -- exactement le reproche fait a UPlayerNameplateComponent.
*/
UCLASS()
class EMBERWILD_API UPlayerBodyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
virtual void NativeInitializeAnimation() override;
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
protected:
// ------------------------------------------------------------------
// Etat publie vers le Blueprint d'animation
// ------------------------------------------------------------------
/** Vitesse horizontale en cm/s : l'axe du BlendSpace de deplacement. */
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float GroundSpeed = 0.f;
/**
* Direction du deplacement par rapport a l'orientation du corps, en degres :
* 0 = on avance, 90 = on va vers la droite, 180 = on recule.
*
* Elle est non nulle en permanence dans ce jeu et ce n'est pas un detail :
* bUseControllerRotationYaw garde le corps aligne sur la camera, donc un pas
* de cote est un VRAI pas de cote, jamais un demi-tour. Le pack ne livre que
* des cycles vers l'avant, donc personne ne la lit encore ; elle est publiee
* parce que le jour ou les cycles lateraux arriveront, il n'y aura qu'un
* BlendSpace a brancher et pas une classe a rouvrir.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float LocomotionDirection = 0.f;
/** Vrai quand le joueur se deplace REELLEMENT, freinage exclu. */
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsMoving = false;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsSprinting = false;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsFalling = false;
/**
* Vitesse verticale en cm/s : positif = on monte, negatif = on tombe.
*
* C'est elle, et elle seule, qui distingue un SAUT d'une CHUTE. bIsFalling
* vaut vrai dans les deux cas : le moteur appelle "falling" tout ce qui n'est
* pas au sol, detente comprise. Sans ce signe, sortir d'une falaise jouerait
* l'animation de detente -- un personnage qui prend son elan pour tomber.
*
* Gratuite sur le reseau comme le reste : le CharacterMovement replique deja
* la velocite des pawns distants, on ne fait que lire sa composante Z.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float VerticalSpeed = 0.f;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsCrouching = false;
/**
* Tangage du regard, en degres, borne a -90..90. Positif = le joueur regarde
* vers le haut.
*
* Gratuit sur le reseau : APawn compresse et replique deja RemoteViewPitch
* pour ses pawns distants, et GetBaseAimRotation() rend la bonne valeur des
* deux cotes sans qu'on ait une seule propriete a declarer. Personne ne la
* lit tant qu'il n'y a pas d'AimOffset -- mais le jour ou un ami doit voir
* qu'on regarde le ciel, c'est cette variable qu'on branchera.
*/
UPROPERTY(BlueprintReadOnly, Category = "Aim")
float AimPitch = 0.f;
/**
* Vitesse de lecture du cycle de deplacement.
*
* A NE BRANCHER QUE si l'on joue une animation unique. Avec le BlendSpace du
* pack, qui interpole deja marche et course sur l'axe de la vitesse, la
* brancher etirerait une seconde fois un cycle deja etire et donnerait une
* cadence qui accelere deux fois trop vite. Elle reste ici parce que le
* BlendSpace est calibre sur les vitesses du pack et pas sur les notres
* (200 / 400 / 650) : si la foulee patine, c'est le reglage a sortir.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float LocomotionPlayRate = 1.f;
/**
* En dessous de cette vitesse (cm/s) on considere le joueur immobile.
*
* Un seuil strictement a zero ferait clignoter la transition course/repos
* pendant le freinage, ou la vitesse decroit en passant par des valeurs
* minuscules mais non nulles.
*/
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0"))
float MovingSpeedThreshold = 10.f;
/** Vitesse (cm/s) a laquelle le cycle tourne a sa cadence d'origine. */
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "1"))
float ReferenceLocomotionSpeed = 400.f;
/** Bornes du Play Rate, memes garde-fous que sur les bras. */
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0.1"))
float MinLocomotionPlayRate = 0.6f;
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0.1"))
float MaxLocomotionPlayRate = 1.8f;
private:
/**
* Cache du pawn. TryGetPawnOwner() fait un Cast a chaque appel, et cette
* fonction tourne a chaque frame sur QUATRE corps en partie a quatre.
*
* TWeakObjectPtr et pas un pointeur nu : apres un respawn le pawn est
* detruit, et cet objet peut lui survivre d'une frame.
*/
TWeakObjectPtr<AFpsPlayer> OwningPlayer;
};
@@ -0,0 +1,124 @@
// 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.
*
* 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
* meme chaine sur le reseau.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UPlayerNameplateComponent : public UWidgetComponent
{
GENERATED_BODY()
public:
UPlayerNameplateComponent();
virtual void BeginPlay() 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:
/**
* 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;
/**
* 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|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.
*
* 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:
/** Le PlayerState du pawn porteur, ou nullptr tant qu'il n'est pas arrive. */
APlayerState* GetOwnerPlayerState() const;
/** Recopie le pseudo dans le widget, uniquement s'il a change. */
void RefreshName();
/**
* 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 EMBERWILD_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,90 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ScreenFadeComponent.generated.h"
class UUserWidget;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnScreenFadeFinished);
/**
* Fondu plein ecran, pose sur un PlayerController.
*
* Un composant et non du code dans le controller : le fondu sert au jeu (mort,
* respawn) comme au menu principal, et deux implementations du meme voile
* finiraient par diverger.
*
* Le voile est un widget UMG et non le fondu camera du moteur : StartCameraFade
* ne teinte que le rendu de la SCENE et laisse tout le HUD visible par-dessus.
* On garde quand meme StartCameraFade en parallele, uniquement pour le fondu
* AUDIO, que le voile ne sait pas faire.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UScreenFadeComponent : public UActorComponent
{
GENERATED_BODY()
public:
UScreenFadeComponent();
/**
* Se declenche a la fin de CHAQUE fondu, entrant comme sortant.
* A celui qui ecoute de savoir ce qu'il attendait.
*/
UPROPERTY(BlueprintAssignable, Category = "Fondu")
FOnScreenFadeFinished OnFadeFinished;
/** Du noir vers le jeu. */
UFUNCTION(BlueprintCallable, Category = "Fondu")
void FadeIn(float Duration);
/** Du jeu vers le noir, ou l'ecran reste jusqu'au prochain FadeIn. */
UFUNCTION(BlueprintCallable, Category = "Fondu")
void FadeOut(float Duration);
/** Couvre l'ecran immediatement, sans animation ni delegue. */
UFUNCTION(BlueprintCallable, Category = "Fondu")
void SnapToBlack();
UFUNCTION(BlueprintPure, Category = "Fondu")
bool IsFading() const { return bFading; }
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/** Ton WBP_Fade : un simple rectangle plein ecran, sans logique. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Fondu")
TSubclassOf<UUserWidget> FadeWidgetClass;
/** Noir classique. Le blanc donne un reveil brutal. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Fondu")
FLinearColor FadeColor = FLinearColor::Black;
private:
/** Cree le voile au premier besoin, et pas avant : il depend du controller. */
void EnsureWidget();
/** 1 = ecran entierement couvert, 0 = voile invisible. */
void SetCoverage(float Coverage);
APlayerController* GetOwningController() const;
UPROPERTY(Transient)
TObjectPtr<UUserWidget> FadeWidget;
/** 0 = ecran couvert, 1 = jeu entierement visible. */
float Alpha = 1.f;
/** Valeur visee par Alpha : 0 pour partir au noir, 1 pour revenir. */
float Target = 1.f;
/** Duree du fondu en cours, distincte des reglages qui restent constants. */
float CurrentDuration = 1.f;
bool bFading = false;
};
+234
View File
@@ -0,0 +1,234 @@
// 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);
/**
* La connexion a abouti, le voyage part dans un instant.
*
* Existe uniquement pour laisser le temps d'un fondu : le sous-systeme sait ou
* aller, mais le voile appartient au PlayerController -- et une invitation peut
* etre acceptee depuis le menu comme en pleine partie, donc depuis deux
* controllers differents. Chacun s'abonne et fond a sa maniere.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSessionTravelPending);
/**
* 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 EMBERWILD_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;
UPROPERTY(BlueprintAssignable, Category = "Session")
FOnSessionTravelPending OnTravelPending;
/**
* Duree laissee au fondu avant que le voyage parte.
*
* C'est LE sous-systeme qui la porte et non chaque controller : les deux
* doivent fondre exactement le temps qu'il attend, sinon l'ecran voyage
* avant d'etre noir. Une seule valeur, lue par tous.
*
* Pas de UPROPERTY editable : un GameInstanceSubsystem n'a aucun asset ou
* la regler. Si elle doit bouger un jour, ce sera ici.
*/
UFUNCTION(BlueprintPure, Category = "Session")
float GetTravelFadeDuration() const { return TravelFadeDuration; }
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();
/** Le fondu a eu son temps : on voyage pour de bon. */
void CommitPendingTravel();
/** Adresse resolue de l'hote, en attente du fondu. */
FString PendingTravelURL;
FTimerHandle TravelTimer;
float TravelFadeDuration = 1.f;
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;
};
@@ -0,0 +1,73 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InputCoreTypes.h"
#include "SettingKeyRowWidget.generated.h"
class UButton;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSettingKeyRebindRequested, FName, MappingName);
/**
* Une ligne de remappage : un libelle, et un bouton portant la touche actuelle.
*
* Widget distinct de USettingRowWidget et non un mode de plus : une touche n'est
* pas un index dans une liste de paliers, il n'y a ni fleches ni curseur, et
* l'interaction est d'une autre nature -- on ouvre un voile de capture.
*
* Elle ne sait pas remapper. Elle previent via OnRebindRequested et c'est
* l'onglet qui pilote, comme partout ailleurs dans les reglages.
*
* Classe Abstract : on cree un WBP_SettingKeyRow qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingKeyRowWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
UPROPERTY(BlueprintAssignable, Category = "Reglages")
FOnSettingKeyRebindRequested OnRebindRequested;
/** Pose le libelle et la touche. Le nom de mapping est renvoye tel quel au clic. */
UFUNCTION(BlueprintCallable, Category = "Reglages")
void Setup(FName InMappingName, const FText& Label, const FKey& Key);
/** Met a jour la seule touche affichee. */
UFUNCTION(BlueprintCallable, Category = "Reglages")
void SetKey(const FKey& Key);
UFUNCTION(BlueprintPure, Category = "Reglages")
FName GetMappingName() const { return MappingName; }
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> LabelText;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> KeyButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> KeyText;
/** Couleur du texte quand l'action n'a plus aucune touche. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
FLinearColor UnboundColor = FLinearColor(0.9f, 0.2f, 0.2f);
/** Couleur normale. Reprise telle quelle a la premiere affectation valide. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
FLinearColor BoundColor = FLinearColor::White;
private:
UFUNCTION()
void HandleKeyButtonClicked();
/** Identifiant Enhanced Input du mapping, pas le libelle affiche. */
FName MappingName = NAME_None;
};
+187
View File
@@ -0,0 +1,187 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SettingRowWidget.generated.h"
class UButton;
class UProgressBar;
class USlider;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSettingRowIndexChanged, int32, NewIndex);
/**
* Une ligne de reglage : un libelle, une fleche gauche, une valeur, une fleche
* droite, et une barre de remplissage optionnelle derriere la valeur.
*
* TOUT reglage est ramene a un index dans une liste de paliers, y compris les
* nombres : "Champ de vision, 70 a 120 par pas de 5" devient un index de 0 a 10.
* La ligne ne connait donc jamais que CurrentIndex et StepCount, et c'est ce qui
* permet de n'avoir qu'UN SEUL WBP a dessiner pour tout le menu, qu'il s'agisse
* d'un choix (Maintien / Bascule) ou d'une valeur continue.
*
* Elle ne sait pas ce qu'elle regle : elle previent via OnIndexChanged, et c'est
* l'onglet qui traduit l'index en appel sur USurvivalUserSettings. Meme principe
* que UConfirmDialogWidget, qui ignore ce qu'il confirme.
*
* Classe Abstract : on cree un WBP_SettingRow qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingRowWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
/** Previent d'un changement venant du joueur, jamais d'un rafraichissement. */
UPROPERTY(BlueprintAssignable, Category = "Reglages")
FOnSettingRowIndexChanged OnIndexChanged;
/**
* Mode choix : la valeur affichee est prise telle quelle dans Options.
* Sert aussi bien a un oui/non qu'a une liste de qualites.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void SetupAsChoice(const FText& Label, const TArray<FText>& Options, int32 Index);
/**
* Mode nombre : la valeur affichee est calculee a partir de l'index.
*
* @param Step pas fin, celui du glissement. C'est lui qui definit la
* granularite de l'index.
* @param ArrowStep bond des fleches, forcement un multiple de Step. Les
* fleches se calent sur le multiple suivant plutot que
* d'ajouter betement leur bond : depuis 37 avec un bond de
* 5 on arrive a 40, pas a 42.
* @param Suffix unite ("%", "°"), peut rester vide.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void SetupAsNumber(const FText& Label, float Min, float Max, float Step, float ArrowStep, float Value, const FText& Suffix);
UFUNCTION(BlueprintPure, Category = "Reglages")
int32 GetIndex() const { return CurrentIndex; }
/** Reconvertit l'index en valeur reelle. Sans objet en mode choix. */
UFUNCTION(BlueprintPure, Category = "Reglages")
float GetNumericValue() const;
/**
* Repositionne la ligne sans rien diffuser.
*
* A utiliser pour tout rafraichissement venant des reglages : diffuser ici
* ferait boucler l'onglet sur lui-meme (il reecrit le reglage, qui rafraichit
* la ligne, qui rediffuse...).
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void SetIndexSilently(int32 NewIndex);
/** Repositionne la ligne sur une valeur reelle, sans rien diffuser. */
UFUNCTION(BlueprintCallable, Category = "Reglages")
void SetNumericValueSilently(float Value);
/**
* Derniere valeur que le JOUEUR peut atteindre, les suivantes restant
* affichables par le code.
*
* Sert a "Personnalise" en qualite globale : c'est un etat dans lequel on
* tombe en bougeant un curseur individuel, jamais un choix qu'on fait.
* Sans ca la fleche droite proposerait de le "selectionner", ce qui ne veut
* rien dire. INDEX_NONE pour lever la limite.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void SetMaxSelectableIndex(int32 InMaxSelectableIndex);
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> LabelText;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> ValueText;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> LeftArrowButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> RightArrowButton;
/**
* Le REMPLISSAGE visible, et rien d'autre.
*
* Le Slider d'Unreal ne sait pas dessiner de portion remplie : il rend une
* barre uniforme plus une poignee. On empile donc les deux -- cette
* ProgressBar pour le rendu, le Slider transparent par-dessus pour le
* geste. C'est le montage habituel, pas un bricolage.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> ValueBar;
/**
* Le GESTE, et rien d'autre : a styler entierement transparent dans le WBP.
*
* Sa course est exprimee en INDEX de palier (0 a StepCount-1) et non dans
* l'unite du reglage -- c'est ce qui garantit qu'un glissement ne peut pas
* produire une valeur hors palier, que le setter refuserait ensuite.
*
* Les deux sont facultatifs : une ligne de choix (Maintien / Bascule) les
* replie tous les deux, il n'y a rien a doser.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<USlider> ValueSlider;
private:
UFUNCTION()
void HandleLeftClicked();
UFUNCTION()
void HandleRightClicked();
/** Glissement souris : on arrondit sur le palier le plus proche. */
UFUNCTION()
void HandleSliderValueChanged(float NewValue);
/** Deplace l'index de Delta paliers et diffuse, si le mouvement est possible. */
void StepIndex(int32 Delta);
/** Index vise par une fleche : le multiple de son bond suivant ou precedent. */
int32 ArrowTargetIndex(int32 Direction) const;
/** Plus grand index atteignable par le joueur. */
int32 MaxSelectable() const;
/** Reecrit le texte, la barre et l'etat des deux fleches. */
void Refresh();
/** Vide en mode nombre. */
TArray<FText> ChoiceLabels;
FText NumericSuffix;
float NumericMin = 0.f;
float NumericStep = 1.f;
int32 CurrentIndex = 0;
/** Nombre de paliers fins, donc d'index valides. Toujours au moins 1. */
int32 StepCount = 1;
/** Bond des fleches, exprime en nombre de paliers fins. 1 en mode choix. */
int32 ArrowIndexDelta = 1;
/** INDEX_NONE = aucune limite, tous les paliers sont selectionnables. */
int32 MaxSelectableIndex = INDEX_NONE;
bool bNumeric = false;
/**
* Vrai pendant qu'on repositionne le curseur depuis le code.
*
* USlider::SetValue ne rediffuse pas son delegue aujourd'hui, mais s'en
* remettre a ce detail d'implementation coute une boucle infinie le jour ou
* il change.
*/
bool bRefreshingSlider = false;
};
@@ -0,0 +1,65 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "SettingsTabWidget.h"
#include "SettingsAudioWidget.generated.h"
class USettingRowWidget;
/**
* L'onglet "Audio" : quatre volumes et le son en arriere-plan.
*
* Meme forme que USettingsGameplayWidget -- l'onglet ne stocke rien, chaque ligne
* ecrit dans USurvivalUserSettings puis demande l'application immediate. La
* difference tient au verbe appele : ApplyAudioSettings et non
* ApplySurvivalSettings, parce que les volumes n'interessent pas le pawn et que
* rediffuser aux widgets a chaque cran de curseur ne servirait a rien.
*
* Classe Abstract : on cree un WBP_SettingsAudio qui en herite, avec un
* WBP_SettingRow par ligne, nomme exactement comme le BindWidget correspondant.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingsAudioWidget : public USettingsTabWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void RefreshFromSettings() override;
virtual void ResetToDefaults() override;
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> MasterVolumeRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> MusicVolumeRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> EffectsVolumeRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> UiVolumeRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> BackgroundSoundRow;
private:
UFUNCTION()
void HandleMasterVolumeChanged(int32 NewIndex);
UFUNCTION()
void HandleMusicVolumeChanged(int32 NewIndex);
UFUNCTION()
void HandleEffectsVolumeChanged(int32 NewIndex);
UFUNCTION()
void HandleUiVolumeChanged(int32 NewIndex);
UFUNCTION()
void HandleBackgroundSoundChanged(int32 NewIndex);
};
@@ -0,0 +1,128 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "InputCoreTypes.h"
#include "SettingsTabWidget.h"
#include "SettingsControlsWidget.generated.h"
class UEnhancedInputUserSettings;
class USettingKeyRowWidget;
/**
* L'onglet "Touches".
*
* Le stockage, la persistance et l'application des surcharges appartiennent a
* UEnhancedInputUserSettings, natif depuis 5.3 : on n'ecrit ni serialisation ni
* reconstruction d'IMC. Cet onglet n'est qu'une facade -- afficher les touches
* courantes, capturer la nouvelle, arbitrer les conflits.
*
* PREREQUIS EDITEUR : sans "Enable User Settings" dans Project Settings ->
* Enhanced Input, et sans Player Mappable Key Settings sur chaque mapping de
* IMC_Default, toutes les lignes afficheront "Non assignée".
*
* Classe Abstract : on cree un WBP_SettingsControls qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingsControlsWidget : public USettingsTabWidget
{
GENERATED_BODY()
public:
USettingsControlsWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
virtual void RefreshFromSettings() override;
virtual void ResetToDefaults() override;
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> MoveForwardRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> MoveBackwardRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> MoveLeftRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> MoveRightRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> JumpRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> SprintRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> CrouchRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> InteractRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> UseItemRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> ToggleInventoryRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> ToggleCraftingRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingKeyRowWidget> DropItemRow;
/**
* Question posee en cas de conflit. Trois emplacements :
* {0} la touche, {1} l'action qui la possede, {2} l'action qui la reclame.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
FText ConflictMessage;
private:
UFUNCTION()
void HandleRebindRequested(FName MappingName);
UFUNCTION()
void HandleKeyCaptured(FKey CapturedKey);
UFUNCTION()
void HandleConflictAnswered(bool bConfirmed);
/** Ecrit la touche en attente sur le mapping en attente, puis rafraichit. */
void ApplyPendingRebind();
/** Le sous-systeme de reglages d'input du joueur local, ou nullptr. */
UEnhancedInputUserSettings* GetInputSettings() const;
/** Toutes les lignes, dans l'ordre d'affichage. Ignore les nulles. */
TArray<USettingKeyRowWidget*> GetAllRows() const;
/** La ligne portant ce nom de mapping, ou nullptr. */
USettingKeyRowWidget* FindRow(FName MappingName) const;
/**
* Donne son identite a une ligne et retient son libelle.
*
* Le libelle vient du C++ et non du "Display Name" des Player Mappable Key
* Settings : ca t'evite de le saisir onze fois dans l'editeur, et ca garde
* les textes traduisibles au meme endroit que le reste du menu.
*/
void SetupRow(USettingKeyRowWidget* Row, FName MappingName, const FText& Label);
/** Libelle lisible d'un mapping. Retombe sur l'identifiant si inconnu. */
FText GetMappingDisplayName(FName MappingName) const;
/** Libelles retenus a la construction, pour les messages de conflit. */
TMap<FName, FText> MappingLabels;
/** Mapping en cours de remappage, et touche capturee en attente d'arbitrage. */
FName PendingMappingName = NAME_None;
FKey PendingKey;
/** Mapping a qui l'on volerait la touche si le joueur confirme. */
FName ConflictingMappingName = NAME_None;
};
@@ -0,0 +1,121 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "SettingsTabWidget.h"
#include "SettingsDisplayWidget.generated.h"
class USettingRowWidget;
class USurvivalUserSettings;
/**
* L'onglet "Affichage".
*
* Presque tout est deja ecrit dans UGameUserSettings : mode d'affichage,
* resolution, VSync, limite de FPS, echelle de resolution. On ne fait que
* l'habiller. Seule la luminosite nous appartient, parce que le gamma vit sur
* le moteur et non dans les reglages.
*
* Le changement de resolution ou de mode passe par une CONFIRMATION A REBOURS :
* c'est le seul filet contre un mode que l'ecran ne sait pas afficher, situation
* dont on ne peut sinon sortir qu'en supprimant GameUserSettings.ini a la main.
*
* Classe Abstract : on cree un WBP_SettingsDisplay qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingsDisplayWidget : public USettingsTabWidget
{
GENERATED_BODY()
public:
USettingsDisplayWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
virtual void RefreshFromSettings() override;
virtual void ResetToDefaults() override;
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> DisplayModeRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> ResolutionRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> FrameRateLimitRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> VSyncRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> ResolutionScaleRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> BrightnessRow;
/** Doit contenir {0}, remplace par les secondes restantes. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
FText VideoConfirmMessage;
/** Delai avant retour automatique au mode precedent. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages", meta = (ClampMin = "3", ClampMax = "30"))
float VideoConfirmSeconds = 10.f;
/**
* Proposer aussi les formats ultralarges (21:9) en plus du 16:9.
*
* La liste que renvoie le systeme contient TOUT ce que la carte accepte :
* 4:3, 5:4, 16:10, des modes d'un autre age. On n'en garde que deux familles.
* Le 16:9 est toujours accepte -- il represente plus des trois quarts des
* joueurs Steam a lui seul. Le 21:9 est marginal mais reel, d'ou l'option.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
bool bAllowUltrawide = true;
/** Plus petite resolution proposee. En dessous, le HUD devient illisible. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
FIntPoint MinimumResolution = FIntPoint(1280, 720);
private:
UFUNCTION()
void HandleDisplayModeChanged(int32 NewIndex);
UFUNCTION()
void HandleResolutionChanged(int32 NewIndex);
UFUNCTION()
void HandleFrameRateLimitChanged(int32 NewIndex);
UFUNCTION()
void HandleVSyncChanged(int32 NewIndex);
UFUNCTION()
void HandleResolutionScaleChanged(int32 NewIndex);
UFUNCTION()
void HandleBrightnessChanged(int32 NewIndex);
/** Reponse a la question posee apres un changement de mode ou de resolution. */
UFUNCTION()
void HandleVideoConfirmAnswered(bool bConfirmed);
/** Applique le mode video puis demande confirmation. */
void ApplyVideoModeAndConfirm();
/** Recharge la liste des resolutions de l'ecran et repositionne la ligne. */
void RebuildResolutionList();
/** Ecarte les formats non desires. Ne vide jamais la liste completement. */
void FilterResolutions(const FIntPoint& CurrentResolution);
/** Index de la resolution courante dans AvailableResolutions, 0 si inconnue. */
int32 FindCurrentResolutionIndex(const USurvivalUserSettings* Settings) const;
/** Index de la plus grande resolution proposee, INDEX_NONE si la liste est vide. */
int32 FindLargestResolutionIndex() const;
/** Resolutions proposees, dans l'ordre de la ligne. */
TArray<FIntPoint> AvailableResolutions;
};
@@ -0,0 +1,89 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "SettingsTabWidget.h"
#include "SettingsGameplayWidget.generated.h"
class USettingRowWidget;
class USurvivalUserSettings;
/**
* L'onglet "Jeu & Controles".
*
* Il ne stocke rien : chaque ligne ecrit directement dans USurvivalUserSettings,
* qui diffuse au pawn dans la foulee. C'est ce qui rend l'effet immediat sans
* bouton "Appliquer".
*
* Classe Abstract : on cree un WBP_SettingsGameplay qui en herite, avec un
* WBP_SettingRow par ligne, nomme exactement comme le BindWidget correspondant.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingsGameplayWidget : public USettingsTabWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void RefreshFromSettings() override;
virtual void ResetToDefaults() override;
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> SensitivityRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> SeparateVerticalRow;
/** Masquee tant que la sensibilite verticale n'est pas separee. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> SensitivityVerticalRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> InvertLookRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> FieldOfViewRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> CameraShakeRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> SprintModeRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> CrouchModeRow;
private:
UFUNCTION()
void HandleSensitivityChanged(int32 NewIndex);
UFUNCTION()
void HandleSeparateVerticalChanged(int32 NewIndex);
UFUNCTION()
void HandleSensitivityVerticalChanged(int32 NewIndex);
UFUNCTION()
void HandleInvertLookChanged(int32 NewIndex);
UFUNCTION()
void HandleFieldOfViewChanged(int32 NewIndex);
UFUNCTION()
void HandleCameraShakeChanged(int32 NewIndex);
UFUNCTION()
void HandleSprintModeChanged(int32 NewIndex);
UFUNCTION()
void HandleCrouchModeChanged(int32 NewIndex);
/** Ecrit puis diffuse. Tous les handlers finissent ici. */
void PushToPawn(USurvivalUserSettings* Settings);
/** Affiche ou masque la ligne de sensibilite verticale. */
void UpdateVerticalRowVisibility(bool bSeparate);
};
@@ -0,0 +1,111 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "SettingsTabWidget.h"
#include "SettingsGraphicsWidget.generated.h"
class USettingRowWidget;
class USurvivalUserSettings;
/**
* L'onglet "Graphismes" : les dix curseurs de scalability du moteur, plus une
* qualite globale qui les pousse tous.
*
* Zero logique metier ici : chaque ligne appelle un Set...Quality(0-3) de
* UGameUserSettings, le moteur fait le reste. C'est l'onglet qui rapporte le
* plus pour le moins d'effort.
*
* Vegetation et Distance d'affichage sont places EN HAUT et non dans l'ordre du
* moteur : avec SoStylized, ce sont les deux seuls qui decident vraiment si le
* jeu tourne sur une machine moyenne.
*
* Classe Abstract : on cree un WBP_SettingsGraphics qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingsGraphicsWidget : public USettingsTabWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void RefreshFromSettings() override;
virtual void ResetToDefaults() override;
protected:
/** Bas / Moyen / Élevé / Épique, plus "Personnalisé" que le joueur ne peut pas choisir. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> OverallQualityRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> ViewDistanceRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> FoliageRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> ShadowRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> TextureRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> VisualEffectRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> PostProcessRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> AntiAliasingRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> ReflectionRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> GlobalIlluminationRow;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USettingRowWidget> ShadingRow;
private:
UFUNCTION()
void HandleOverallQualityChanged(int32 NewIndex);
UFUNCTION()
void HandleViewDistanceChanged(int32 NewIndex);
UFUNCTION()
void HandleFoliageChanged(int32 NewIndex);
UFUNCTION()
void HandleShadowChanged(int32 NewIndex);
UFUNCTION()
void HandleTextureChanged(int32 NewIndex);
UFUNCTION()
void HandleVisualEffectChanged(int32 NewIndex);
UFUNCTION()
void HandlePostProcessChanged(int32 NewIndex);
UFUNCTION()
void HandleAntiAliasingChanged(int32 NewIndex);
UFUNCTION()
void HandleReflectionChanged(int32 NewIndex);
UFUNCTION()
void HandleGlobalIlluminationChanged(int32 NewIndex);
UFUNCTION()
void HandleShadingChanged(int32 NewIndex);
/** Prepare une ligne de qualite : memes quatre libelles partout. */
void SetupQualityRow(USettingRowWidget* Row, const FText& Label);
/** Pousse la scalability et rafraichit la ligne de qualite globale. */
void ApplyAndRefreshOverall();
};
@@ -0,0 +1,240 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SettingsMenuWidget.generated.h"
class UButton;
class UConfirmDialogWidget;
class UKeyCaptureWidget;
class USettingsTabWidget;
class UWidgetSwitcher;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSettingsCloseRequested);
/** Reponse a une demande de confirmation. bConfirmed est faux sur un refus ou une expiration. */
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnSettingsConfirmationResult, bool, bConfirmed);
/** Touche capturee. Invalide si le joueur a annule avec Echap. */
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnSettingsKeyCaptureResult, FKey, CapturedKey);
/**
* L'ecran de reglages : une barre d'onglets, un WidgetSwitcher, Retour et
* Reinitialiser.
*
* Il ne sait PAS qui l'a ouvert. Il demande a se fermer via OnCloseRequested et
* c'est l'appelant -- menu principal ou menu de pause -- qui decide de ce que
* "Retour" veut dire. Sans ca il faudrait un ecran par contexte.
*
* Il ne connait pas davantage le contenu de ses onglets : il appelle
* RefreshFromSettings et ResetToDefaults sur USettingsTabWidget. Ajouter
* l'onglet Graphismes ne demandera aucune ligne ici.
*
* Classe Abstract : on cree un WBP_Settings qui en herite.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingsMenuWidget : public UUserWidget
{
GENERATED_BODY()
public:
USettingsMenuWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
UPROPERTY(BlueprintAssignable, Category = "Reglages")
FOnSettingsCloseRequested OnCloseRequested;
/**
* Affiche l'ecran, rafraichit l'onglet visible et prend le focus clavier.
*
* Passe par ici et jamais par SetVisibility : sans le focus, Echap ne
* refermerait pas l'ecran.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void Open();
/** Masque l'ecran et ecrit le fichier de reglages. */
UFUNCTION(BlueprintCallable, Category = "Reglages")
void Close();
/**
* ZOrder de la boite de confirmation, impose par celui qui ouvre l'ecran.
*
* L'ecran ne le choisit PAS lui-meme : il est ouvert depuis deux contextes
* dont les empilements n'ont rien a voir (50 au menu principal, 150 en jeu).
* Un numero code en dur ici obligerait le widget a savoir qui l'affiche,
* exactement ce qu'on evite avec OnCloseRequested. A appeler AVANT Open().
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void SetDialogZOrder(int32 InZOrder) { DialogZOrder = InZOrder; }
/**
* Pose une question au joueur pour le compte d'un onglet.
*
* L'ecran est le seul a posseder une boite : un onglet qui creerait la
* sienne devrait a son tour se demander a quel ZOrder la poser, et on
* repartirait pour un tour.
*
* @param MessageOrFormat texte simple, ou format contenant {0} si
* CountdownSeconds est superieur a zero.
* @param CountdownSeconds 0 pour une question sans limite de temps.
* @return false si aucune boite n'est configuree -- l'appelant doit alors
* decider seul, sa reponse n'arrivera jamais.
*/
bool RequestConfirmation(const FText& MessageOrFormat, float CountdownSeconds, const FOnSettingsConfirmationResult& OnResult);
/**
* Ouvre le voile de capture de touche pour le compte d'un onglet.
*
* Meme raison que la boite : un voile pose DANS l'onglet serait clippe a la
* zone du WidgetSwitcher au lieu de couvrir l'ecran, et devrait a son tour
* se trouver un ZOrder.
*
* @return false si aucun voile n'est configure -- la reponse n'arrivera
* jamais, l'appelant doit renoncer.
*/
bool RequestKeyCapture(const FText& ActionLabel, const FOnSettingsKeyCaptureResult& OnResult);
/**
* Bascule le voile deja ouvert sur la question de conflit.
*
* Le voile RESTE ouvert entre la capture et la reponse : c'est le meme geste
* pour le joueur, il n'a pas a le voir disparaitre puis revenir.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void ShowKeyCaptureConflict(const FText& Message, const FOnSettingsConfirmationResult& OnAnswer);
/** Referme le voile. A appeler quand le remappage est termine ou abandonne. */
UFUNCTION(BlueprintCallable, Category = "Reglages")
void CloseKeyCapture();
protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UWidgetSwitcher> TabSwitcher;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> BackButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ResetButton;
/**
* Les cinq onglets sont OBLIGATOIRES : un WBP_Settings sans eux ne compile pas.
*
* BindWidgetOptional echoue en silence -- un nom mal orthographie donnait un
* bouton parfaitement inerte, sans le moindre message. Pour un element dont
* l'absence n'a aucun sens, on veut l'erreur a la compilation du Blueprint.
*/
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> GameplayTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> DisplayTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> GraphicsTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ControlsTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> AudioTabButton;
/**
* Ton WBP_ConfirmDialog, a renseigner dans les Class Defaults de
* WBP_Settings. Laisse-le vide et "Reinitialiser" agit sans rien demander.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Reglages")
TSubclassOf<UConfirmDialogWidget> ResetDialogClass;
/** La question posee avant de reinitialiser l'onglet visible. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
FText ResetDialogMessage;
/** Ton WBP_KeyCapture. Sans lui, l'onglet Touches ne peut rien remapper. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Reglages")
TSubclassOf<UKeyCaptureWidget> KeyCaptureClass;
UPROPERTY(Transient, BlueprintReadOnly, Category = "Reglages")
TObjectPtr<UConfirmDialogWidget> ResetDialog;
UPROPERTY(Transient, BlueprintReadOnly, Category = "Reglages")
TObjectPtr<UKeyCaptureWidget> KeyCapture;
private:
UFUNCTION()
void HandleBackClicked();
UFUNCTION()
void HandleResetClicked();
/** Reponse a la question du bouton Reinitialiser. */
UFUNCTION()
void HandleResetAnswered(bool bConfirmed);
UFUNCTION()
void HandleDialogConfirmed();
UFUNCTION()
void HandleDialogCancelled();
/** Referme la boite et rend la reponse a qui a pose la question. */
void FinishConfirmation(bool bConfirmed);
/** Cree la boite au premier besoin. Renvoie nullptr si aucune classe n'est assignee. */
UConfirmDialogWidget* EnsureDialog();
UFUNCTION()
void HandleGameplayTabClicked();
UFUNCTION()
void HandleDisplayTabClicked();
UFUNCTION()
void HandleGraphicsTabClicked();
UFUNCTION()
void HandleControlsTabClicked();
UFUNCTION()
void HandleAudioTabClicked();
UFUNCTION()
void HandleKeyCaptured(FKey CapturedKey);
UFUNCTION()
void HandleKeyCaptureCancelled();
UFUNCTION()
void HandleKeyConflictAnswered(bool bConfirmed);
/** A qui rendre la reponse du conflit affiche dans le voile. */
FOnSettingsConfirmationResult PendingConflictAnswer;
/** Change de page et rafraichit la nouvelle. Ignore un index inexistant. */
void ShowTab(int32 Index);
/** La page actuellement affichee, ou nullptr si le switcher est vide. */
USettingsTabWidget* GetActiveTab() const;
/** Grise les boutons dont la page n'existe pas encore. */
void UpdateTabButtonStates();
/** A qui rendre la reponse de la boite ouverte. Vide quand rien n'est pose. */
FOnSettingsConfirmationResult PendingConfirmation;
/** A qui rendre la touche capturee. Vide quand le voile est ferme. */
FOnSettingsKeyCaptureResult PendingKeyCapture;
bool bResetDialogOpen = false;
bool bKeyCaptureOpen = false;
/** Valeur de repli si l'appelant oublie SetDialogZOrder : devant le fondu exclu. */
int32 DialogZOrder = 500;
};
@@ -0,0 +1,47 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SettingsTabWidget.generated.h"
class USettingsMenuWidget;
/**
* Base commune a tous les onglets de reglages.
*
* Deux verbes, rien de plus : l'ecran parent rafraichit ou reinitialise l'onglet
* visible sans jamais savoir ce qu'il contient. Ajouter un onglet ne demande
* donc aucune ligne dans USettingsMenuWidget.
*/
UCLASS(Abstract)
class EMBERWILD_API USettingsTabWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** Relit les reglages et repositionne les lignes, sans rien diffuser. */
virtual void RefreshFromSettings() {}
/** Remet a l'usine les seuls reglages de cet onglet, puis se rafraichit. */
virtual void ResetToDefaults() {}
/**
* L'ecran se presente a ses onglets a la construction.
*
* Pointeur explicite plutot qu'un GetTypedOuter au moment de s'en servir :
* un onglet doit pouvoir demander une confirmation, et faire dependre ca de
* la forme de l'arbre de widgets casserait le jour ou on l'imbrique
* autrement.
*
* Defini dans le .cpp et non inline : TWeakObjectPtr exige un type complet
* pour s'assigner, et inclure l'en-tete de l'ecran ici ferait dependre tous
* les onglets de lui.
*/
void SetOwningMenu(USettingsMenuWidget* InMenu);
protected:
UPROPERTY(Transient, BlueprintReadOnly, Category = "Reglages")
TWeakObjectPtr<USettingsMenuWidget> OwningMenu;
};
+260
View File
@@ -0,0 +1,260 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.h"
#include "StorageContainer.generated.h"
class UBoxComponent;
class UInventoryComponent;
class UItemDataAsset;
class USoundBase;
class UStaticMeshComponent;
/**
* Contenu initial d'un coffre pose dans le niveau.
*
* De la donnee de level design, pas du gameplay : un coffre de depart, une
* cache a trouver. Verse une seule fois au BeginPlay, jamais relu ensuite.
*/
USTRUCT(BlueprintType)
struct FStorageStartingItem
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UItemDataAsset> Item = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage", meta = (ClampMin = "1"))
int32 Quantity = 1;
};
/**
* Coffre pose dans le monde.
*
* Il ne contient AUCUN modele de donnees a lui : son contenu est un
* UInventoryComponent, exactement le meme que celui du joueur, simplement
* configure sans barre rapide. C'est ce qui rend le glisser-deposer entre le
* sac et le coffre gratuit -- ce sont les memes cases, le meme FInventorySlot,
* et donc la durabilite d'une hache traverse le coffre sans une ligne de code.
*
* Toute la collision tient dans UNE boite, et les deux meshes n'en portent
* aucune. C'est la difference avec ACraftingStation, qui se contente de son
* mesh : un coffre est en deux morceaux dont l'un TOURNE. Se reposer sur leurs
* collisions imposerait que le pack en fournisse deux correctes, et laisserait
* surtout un trou dans la surface visable des que le couvercle se leve --
* viser le haut du coffre ouvert ne toucherait plus rien.
*
* La boite est fille de la caisse et ne bouge donc jamais : le coffre se vise
* exactement pareil ouvert ou ferme. Elle bloque aussi le joueur physiquement,
* ce qui evite d'avoir a se demander si le mesh du pack sait le faire.
*
* Aucun Tick au repos : il ne s'active que le temps d'animer le couvercle, et
* se rendort des que l'angle est atteint.
*/
UCLASS()
class EMBERWILD_API AStorageContainer : public AActor, public IInteractable
{
GENERATED_BODY()
public:
AStorageContainer();
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;
UFUNCTION(BlueprintPure, Category = "Storage")
UInventoryComponent* GetStorage() const { return Storage; }
/** Nom affiche en titre de l'ecran de coffre. */
UFUNCTION(BlueprintPure, Category = "Storage")
FText GetDisplayName() const { return DisplayName; }
/** Cases par ligne souhaitees pour la grille de l'ecran. */
UFUNCTION(BlueprintPure, Category = "Storage")
int32 GetColumnCount() const { return ColumnCount; }
/**
* Ouvre ou ferme le couvercle.
*
* Appele par le controller, pas par le coffre lui-meme : c'est lui qui sait
* quand l'ecran se referme, que ce soit par Echap, par la mort du joueur ou
* 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);
UFUNCTION(BlueprintPure, Category = "Storage")
bool IsOpen() const { return bOpen; }
protected:
/**
* Dimensionne le composant AVANT que son InitializeComponent() fige la
* taille par defaut. PostInitializeComponents serait deja trop tard, et le
* constructeur trop tot -- il ne connait pas les valeurs reglees sur
* l'instance posee dans le niveau.
*/
virtual void PreInitializeComponents() override;
virtual void BeginPlay() override;
/** Redimensionne la boite quand un mesh change dans le Blueprint. */
virtual void OnConstruction(const FTransform& Transform) override;
// ------------------------------------------------------------------
// Composants
// ------------------------------------------------------------------
/** La caisse : SM_Chest_Bottom. Racine, purement visuelle. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UStaticMeshComponent> BaseMesh;
/**
* L'unique collision du coffre : ce que le trace d'interaction vient
* toucher, et ce qui empeche le joueur de traverser.
*
* Fille de la caisse et non du couvercle : elle ne suit surtout pas ce
* dernier. Un coffre ouvert doit se viser au meme endroit qu'un coffre
* ferme, sinon le prompt disparait au moment precis ou l'on s'en sert.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UBoxComponent> InteractionBox;
/**
* Le couvercle : SM_Chest_Top, enfant direct de la caisse.
*
* C'est LUI qu'on tourne, donc la charniere est le pivot que l'artiste a
* donne au mesh -- un StaticMeshComponent tourne toujours autour de
* l'origine de son mesh. Rien a placer a la main, mais rien a rattraper non
* plus : si un pack posait ce pivot au centre du couvercle, l'ouverture se
* verrait de travers et il faudrait corriger le pivot dans l'editeur de
* mesh, pas ici.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UStaticMeshComponent> LidMesh;
/** Le contenu. Un inventaire ordinaire, sans barre rapide. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UInventoryComponent> Storage;
// ------------------------------------------------------------------
// Reglages
// ------------------------------------------------------------------
/**
* Nombre de cases. Reglable PAR INSTANCE : le meme Blueprint donne un petit
* coffre de 12 et un grand de 30 selon celui qu'on pose.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage", meta = (ClampMin = "1", ClampMax = "60"))
int32 SlotCount = 20;
/** Cases par ligne dans l'ecran. Purement visuel. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage", meta = (ClampMin = "1"))
int32 ColumnCount = 6;
/** Nom affiche en titre de l'ecran. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage")
FText DisplayName;
/** Texte affiche quand le joueur vise le coffre. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage")
FText InteractionPrompt;
/** Verse dans le coffre au demarrage. Laisse vide pour un coffre a remplir. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage")
TArray<FStorageStartingItem> StartingItems;
// ------------------------------------------------------------------
// Collision
// ------------------------------------------------------------------
/**
* La boite epouse toute seule les deux meshes, couvercle ferme.
*
* Coche par defaut : changer SM_Chest_v1 pour le v3 doit suffire, sans
* avoir a retailler une boite a la main derriere. Decoche-le seulement si
* tu veux une zone volontairement differente du volume du coffre.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Collision")
bool bAutoFitInteractionBox = true;
/**
* Marge ajoutee autour des meshes par l'ajustement automatique.
*
* Garde-la petite. Le trace d'interaction est deja un balayage spherique
* tolerant : gonfler la boite ne rendrait pas le coffre plus facile a viser,
* ca l'empecherait juste de se coller a un mur.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Collision", meta = (ClampMin = "0", Units = "cm"))
float InteractionBoxPadding = 0.f;
// ------------------------------------------------------------------
// Couvercle
// ------------------------------------------------------------------
/**
* Rotation RELATIVE du couvercle ouvert. Le repos vaut toujours zero.
*
* Un FRotator complet et pas un simple angle : selon l'orientation dans
* laquelle le mesh a ete exporte, la charniere peut tomber sur le pitch, le
* yaw ou le roll. Regle les trois a l'oeil dans le viewport plutot que
* d'aller comprendre quel axe local du mesh pointe ou.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Lid")
FRotator OpenRotation = FRotator(-100.f, 0.f, 0.f);
/** Duree de l'ouverture, en secondes. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Lid", meta = (ClampMin = "0.05", Units = "s"))
float LidDuration = 0.35f;
// ------------------------------------------------------------------
// Sons. Facultatifs : un champ vide ne joue rien.
// ------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Sound")
TObjectPtr<USoundBase> OpenSound;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Sound")
TObjectPtr<USoundBase> CloseSound;
private:
/** Recopie LidAlpha dans la rotation du couvercle. */
void ApplyLidRotation();
/** 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. */
float LidAlpha = 0.f;
};
@@ -0,0 +1,120 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
// Sound/SoundClass.h en entier, et pas une simple declaration anticipee :
// TStrongObjectPtr exige un type COMPLET, la ou TObjectPtr s'en passait.
#include "Sound/SoundClass.h"
#include "UObject/StrongObjectPtr.h"
#include "SurvivalAudioSettings.generated.h"
/**
* Ou le C++ trouve les SoundClass du jeu. Reglable dans Project Settings, section
* "Survival Audio".
*
* UDeveloperSettings et pas des chemins codes en dur dans le .cpp : renommer ou
* deplacer SC_Music casserait silencieusement le curseur Musique, sans la moindre
* erreur de compilation. Ici la reference est un asset comme un autre, suivie par
* le redirecteur de l'editeur et par le cooker.
*
* Ces references sont SOFT : un UDeveloperSettings est construit tres tot, bien
* avant que le gestionnaire d'assets ne soit pret. Une reference dure forcerait le
* chargement des SoundClass au demarrage du moteur, ce qui est exactement le genre
* de dependance qui fait echouer un cook.
*
* A NOTER : le SoundMix, lui, n'est PAS ici. Il est deja declare dans
* Project Settings > Audio > Default Base Sound Mix, d'ou le moteur le reactive a
* chaque chargement de map -- c'est precisement ce qu'on veut, et le redeclarer ici
* ouvrirait la porte a deux references qui divergent.
*/
UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Survival Audio"))
class EMBERWILD_API USurvivalAudioSettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
/** Range la page sous "Game" et non sous "Plugins". */
virtual FName GetCategoryName() const override;
/**
* Le CDO, seul exemplaire qui existe.
*
* Non const alors que rien n'est modifie du point de vue du joueur : les
* accesseurs ci-dessous memorisent l'asset resolu dans un champ Transient, comme
* le fait UAudioSettings pour sa propre SoundClass par defaut.
*/
static USurvivalAudioSettings* Get();
/**
* Le sommet de la hierarchie : toutes les autres classes en descendent, donc son
* volume les multiplie toutes.
*/
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
TSoftObjectPtr<USoundClass> MasterSoundClass;
/**
* Imposee au lecteur de musique par UMusicSubsystem, donc les morceaux eux-memes
* n'ont rien a declarer -- ajouter une piste a une playlist suffit.
*/
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
TSoftObjectPtr<USoundClass> MusicSoundClass;
/**
* A declarer aussi comme Default Sound Class dans Project Settings > Audio :
* tout son qui n'annonce pas sa classe atterrit alors ici, ce qui evite qu'un
* bruitage oublie echappe au curseur Effets.
*/
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
TSoftObjectPtr<USoundClass> EffectsSoundClass;
/** A poser a la main sur les sons d'interface (survol, validation). */
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
TSoftObjectPtr<USoundClass> UiSoundClass;
USoundClass* GetMasterSoundClass();
USoundClass* GetMusicSoundClass();
USoundClass* GetEffectsSoundClass();
USoundClass* GetUiSoundClass();
private:
/**
* Charge au premier appel, puis rend le pointeur memorise.
*
* Le cache n'est pas qu'une optimisation : il fait aussi office d'ancre pour le
* ramasse-miettes. Une SoundClass chargee par un LoadSynchronous et referencee
* nulle part serait collectee, et le curseur cesserait d'agir en pleine partie.
*/
USoundClass* Resolve(const TSoftObjectPtr<USoundClass>& Asset, TStrongObjectPtr<USoundClass>& Cache);
/**
* TStrongObjectPtr et surtout PAS UPROPERTY(Transient) : c'est ce qui plantait
* le jeu package au demarrage, alors que tout allait bien dans l'editeur.
*
* Le CDO d'un UDeveloperSettings naît avant CloseDisregardForGC, donc il vit dans
* le pool des objets PERMANENTS. Le moteur ne les scanne jamais -- ils sont
* immortels par construction -- et exige en echange qu'ils ne referencent que des
* objets du root set, sans quoi ils garderaient un pointeur sur un objet collecte.
* VerifyGCAssumptions le controle et c'est une erreur FATALE :
*
* Disregard for GC object Default__SurvivalAudioSettings referencing SoundClass
* SC_Master which is not part of root set. Property: CachedMasterSoundClass
* Fatal error: Encountered 3 object(s) breaking Disregard for GC assumptions.
*
* Invisible dans l'editeur, ou les SoundClass sont deja chargees quand le pool se
* ferme, donc permanentes elles aussi. En jeu package elles arrivent apres, et le
* jeu meurt au premier ramassage -- pendant le chargement de la premiere map.
*
* TStrongObjectPtr ancre l'asset exactement comme le faisait la UPROPERTY, mais
* range sa reference dans GGCObjectReferencer, le seul objet que VerifyGCAssumptions
* saute explicitement. Le CDO ne reference donc plus rien, et l'asset reste vivant.
*/
TStrongObjectPtr<USoundClass> CachedMasterSoundClass;
TStrongObjectPtr<USoundClass> CachedMusicSoundClass;
TStrongObjectPtr<USoundClass> CachedEffectsSoundClass;
TStrongObjectPtr<USoundClass> CachedUiSoundClass;
};
@@ -0,0 +1,255 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "SurvivalGameInstance.generated.h"
class UInputMappingContext;
class ULocalPlayer;
/**
* Le seul objet qui survit aux changements de map.
*
* C'est l'equivalent le plus proche d'un singleton DontDestroyOnLoad d'Unity :
* il naît au lancement du jeu et meurt a sa fermeture. Tout le reste --
* GameMode, GameState, PlayerController, pawns, widgets -- est detruit et
* recree a chaque OpenLevel.
*
* Y mettre : les reglages, la sauvegarde en cours, et plus tard l'hebergement
* et le rejoindre-une-partie. Surtout pas d'etat de gameplay, qui appartient au
* monde et doit disparaitre avec lui.
*/
UCLASS()
class EMBERWILD_API USurvivalGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
virtual void Init() override;
virtual void Shutdown() override;
/** Nom de la derniere camera de menu utilisee. NAME_None au premier lancement. */
UFUNCTION(BlueprintPure, Category = "Menu")
FName GetLastMenuCameraName() const { return LastMenuCameraName; }
UFUNCTION(BlueprintCallable, Category = "Menu")
void SetLastMenuCameraName(FName SpotName) { LastMenuCameraName = SpotName; }
/**
* Declare les IMC remappables aupres des reglages d'input du joueur.
*
* Appelee A LA DEMANDE, par l'ecran de reglages avant de lire les touches, et
* non depuis un evenement de demarrage. Raison : UserSettings n'est cree que
* dans UEnhancedInputLocalPlayerSubsystem::PlayerControllerChanged, et
* ULocalPlayer diffuse ses evenements AVANT de prevenir ses sous-systemes --
* tout hook precoce arrive donc trop tot, quel qu'il soit.
*
* Idempotente : le moteur ignore un contexte deja connu.
*/
UFUNCTION(BlueprintCallable, Category = "Input")
void RegisterMappableContexts();
/**
* Traduit les touches par defaut vers la disposition clavier du joueur.
*
* Les IMC sont ecrits en QWERTY US -- EKeys::W pour avancer -- et Windows livre
* au moteur une touche DEJA traduite par la disposition : en AZERTY, la touche
* du haut du bloc de deplacement arrive comme EKeys::Z et le mapping "W" ne se
* declenche jamais. On repose donc chaque touche a la position equivalente.
* Le detail du mecanisme est dans KeyboardLayout.h.
*
* Ne touche QUE les mappings que le joueur n'a pas remappes lui-meme : un
* choix explicite l'emporte toujours sur une deduction. Et sur un clavier
* QWERTY la conversion rend la touche d'origine, donc rien n'est ecrit du tout
* et le profil reste vierge.
*
* Declenchee UNE FOIS par RegisterMappableContexts(), au premier enregistrement
* des contextes ; ensuite seul "Reinitialiser" la redemande. Elle appartient a
* l'initialisation, pas au rafraichissement d'ecran -- l'onglet Touches se
* rafraichit apres chaque remappage, et repasser dessus reprendrait sa touche
* au joueur qui aurait justement choisi celle de l'IMC (la lettre W sur un
* AZERTY, par exemple : identique au defaut, donc indiscernable d'un mapping
* jamais touche).
*/
UFUNCTION(BlueprintCallable, Category = "Input")
void ApplyKeyboardLayoutToDefaultKeys();
/**
* Un depart VOULU vers le menu commence : le filet de secours reseau doit
* se taire.
*
* Sans ce drapeau, la connexion qui tombe pendant le fondu de sortie
* declencherait HandleNetworkFailure, qui chargerait le menu sur-le-champ
* et couperait le fondu en plein milieu -- exactement le saut d'image qu'on
* cherchait a eviter en fondant.
*/
void BeginReturnToMainMenu() { bReturningToMainMenu = true; }
/** On est arrive au menu : le filet redevient actif pour la partie suivante. */
void ClearReturnToMainMenu() { bReturningToMainMenu = false; }
/**
* Rend la main au rendu du monde, coupe depuis le chargement de la map.
*
* A n'appeler QUE lorsque le voile de fondu est deja noir : le monde
* reapparait alors derriere lui, invisible.
*
* Pourquoi couper le rendu plutot que poser un widget. Mesure sur une vraie
* connexion : 2,4 secondes s'ecoulent entre `LoadMap` chez le client et la
* naissance de son PlayerController, que le serveur lui envoie. Pendant ce
* temps la map est chargee et le moteur la rend depuis une vue par defaut --
* sous le terrain. Il n'existe alors AUCUN acteur a qui accrocher un voile,
* et le composant de fondu vit justement sur ce controller absent. Le
* viewport, lui, survit aux changements de map et n'appartient a personne.
*
* Attend aussi la fin du streaming avant de rendre la main : c'est le seul
* instant du chargement ou l'on sait que rien ne sera montre tant qu'on n'a
* pas fini. Sans ca, le joueur voit le terrain passer de sa version
* grossiere a la vraie, et les modeles apparaitre un a un.
*/
UFUNCTION(BlueprintCallable, Category = "Chargement")
void ResumeWorldRendering();
/**
* Laisse le monde rendre SceneWarmupFrames images derriere le voile noir,
* puis rappelle OnComplete.
*
* Compte des IMAGES et non des secondes, et c'est tout l'interet. Ce qui
* reste a se raffiner apres le flush de streaming -- Nanite, les ombres
* virtuelles, la convergence de Lumen -- progresse a chaque image RENDUE,
* pas avec le temps qui passe. Une duree fixe donnerait quarante images sur
* une machine rapide et cinq sur une machine lente : exactement celle qui en
* aurait eu le plus besoin.
*/
void BeginSceneWarmup(FSimpleDelegate OnComplete);
protected:
/**
* Temps maximal accordé au streaming avant de montrer le monde, en secondes.
*
* Ce blocage se produit ECRAN NOIR, donc il ne se voit pas -- c'est du temps
* de chargement, pas une saccade. Le dépasser vaut mieux que de montrer un
* terrain qui se raffine sous les yeux du joueur, mais on le borne quand
* même : une machine lente ne doit pas rester bloquée indéfiniment.
*
* Réglable sur BP_SurvivalGameInstance.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0", Units = "s"))
float StreamingFlushTimeLimit = 5.f;
/**
* Nombre d'images rendues derriere le voile noir avant de l'eclaircir.
* Voir BeginSceneWarmup(). Reglable sur BP_SurvivalGameInstance.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0"))
int32 SceneWarmupFrames = 45;
/**
* Plafond de la chauffe, en secondes.
*
* Le compte d'images reste la mesure PRINCIPALE -- c'est la bonne unite.
* Ceci n'est qu'un garde-fou : sur une machine tres lente, 45 images
* pourraient prendre plusieurs secondes d'ecran noir, et mieux vaut un
* decor pas tout a fait converge qu'une attente qui inquiete.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0.1", Units = "s"))
float SceneWarmupMaxSeconds = 2.f;
/**
* Les mapping contexts dont les touches sont remappables. Mets-y IMC_Default.
*
* Ici et PAS sur le pawn : le menu principal n'a aucun pawn, et l'ecran de
* reglages doit pourtant afficher les touches. C'est le critere habituel --
* "est-ce que ca doit survivre au changement de map ?" -- qui envoie cette
* liste sur le GameInstance.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TArray<TObjectPtr<UInputMappingContext>> MappableContexts;
private:
/**
* L'hote a quitte, ou la connexion est tombee.
*
* Sans ce filet, un client dont l'hote ferme le jeu reste planté dans un
* monde qui ne repond plus : le moteur coupe la connexion, mais rien ne le
* ramene nulle part. C'est ici et pas sur le PlayerController parce que
* celui-ci est justement detruit par l'echec -- le GameInstance est le seul
* objet dont on soit certain qu'il vive encore a cet instant.
*/
void HandleNetworkFailure(UWorld* World, UNetDriver* NetDriver, ENetworkFailure::Type FailureType, const FString& ErrorString);
/** Meme filet pour un voyage qui echoue en route. */
void HandleTravelFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ErrorString);
/**
* Ferme la session en cours et rouvre le menu principal.
*
* La map du menu n'est PAS une propriete de plus : c'est deja la
* GameDefaultMap du projet. En dupliquer le chemin ici donnerait deux
* valeurs a tenir en phase, et celle-ci ne serait jamais testee.
*/
void ReturnToMainMenu();
/**
* Une map va etre chargee : on coupe le rendu jusqu'a ce qu'un controller
* soit pret.
*
* PreLoadMap et surtout PAS PostLoadMapWithWorld : ce dernier est diffuse
* APRES le BeginPlay des acteurs. Sur une map chargee localement -- le menu,
* ou la partie chez l'hote -- le controller rendait donc la main au rendu,
* et on la lui reprenait juste apres. Ecran noir definitif, jusqu'au filet
* de securite. Chez un client ca passait par accident, son controller
* arrivant plus tard.
*/
void HandlePreLoadMap(const FString& MapName);
void SetWorldRenderingSuspended(bool bSuspended);
/** Une image de chauffe vient de passer : on decompte, ou on rappelle. */
void TickSceneWarmup();
/** Images restantes a rendre avant d'eclaircir. */
int32 RemainingWarmupFrames = 0;
/** Debut de la chauffe, pour appliquer le plafond en secondes. */
double WarmupStartTime = 0.0;
/** Ce qu'on fait une fois la chauffe finie -- eclaircir, cote appelant. */
FSimpleDelegate SceneWarmupCompleted;
FDelegateHandle NetworkFailureHandle;
FDelegateHandle TravelFailureHandle;
FDelegateHandle PreLoadMapHandle;
/**
* Filet : si aucun controller ne rend la main -- connexion qui n'aboutit
* pas, spawn rate -- on rallume quand meme. Un ecran noir definitif se lit
* comme un plantage, et c'est pire que le defaut qu'on corrige.
*/
FTimerHandle RenderingResumeTimeout;
/** Un retour au menu est deja en cours, voulu ou non. */
bool bReturningToMainMenu = false;
/**
* La disposition clavier a deja ete appliquee dans cette session.
*
* Pas de UPROPERTY(config) : ce n'est PAS "l'a-t-on deja fait un jour". Les
* surcharges posees, elles, sont persistees par Enhanced Input, et elles
* suffisent a rendre l'operation sans effet au lancement suivant. Ce drapeau
* ne protege que du re-declenchement en cours de session.
*/
bool bKeyboardLayoutApplied = false;
/**
* Stocke un NOM et pas un index : les index changent des qu'on ajoute ou
* supprime une camera dans la map, et on retomberait sur autre chose.
*
* Ne sert qu'a eviter de retirer deux fois de suite le meme angle quand le
* joueur revient au menu -- d'ou le Transient, ca n'a pas a etre sauvegarde.
*/
UPROPERTY(Transient)
FName LastMenuCameraName = NAME_None;
};
@@ -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 EMBERWILD_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;
};
@@ -0,0 +1,28 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "SurvivalGameState.generated.h"
/**
* L'etat du monde que TOUT LE MONDE doit connaitre.
*
* Vide pour l'instant, pose des maintenant parce que la rebrancher plus tard
* suppose de retoucher le GameMode et tous les Blueprints qui en derivent.
*
* Le piege a retenir pour la coop : AGameMode n'existe QUE sur le serveur.
* Sur un client, GetGameMode() renvoie nullptr, toujours -- ce n'est pas un bug
* mais le design du framework. Le GameMode arbitre, il ne se montre pas.
* Le GameState, lui, est replique a tout le monde : c'est le seul endroit ou
* un client peut lire l'etat de la partie.
*
* Y mettra : l'heure du jour, le numero de jour, la seed du monde, l'etat des
* ressources partagees. Pas les stats d'un joueur, qui vont sur son PlayerState.
*/
UCLASS()
class EMBERWILD_API ASurvivalGameState : public AGameStateBase
{
GENERATED_BODY()
};
@@ -0,0 +1,73 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "CharacterAppearanceTypes.h"
#include "SurvivalPlayerState.generated.h"
/**
* 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 -- 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 EMBERWILD_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;
};
@@ -0,0 +1,145 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SurvivalStatsComponent.generated.h"
/** Diffuse a chaque changement de vie, faim ou soif. L'UI s'y abonne. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalStatsChanged);
/** Diffuse une seule fois, quand la vie atteint zero. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalDeath);
/**
* Vie, faim et soif du personnage.
*
* Les valeurs descendent sur un timer a basse frequence et non dans Tick :
* une jauge qui perd trois points par minute n'a aucune raison d'etre
* recalculee 120 fois par seconde.
*
* Les vitesses sont exprimees PAR MINUTE, parce que c'est l'unite dans
* laquelle on raisonne quand on equilibre un jeu de survie -- "cent points
* de faim en trente minutes" se lit, "0.055 par seconde" non.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API USurvivalStatsComponent : public UActorComponent
{
GENERATED_BODY()
public:
USurvivalStatsComponent();
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);
UFUNCTION(BlueprintCallable, Category = "Survie")
void ApplyDamage(float Amount);
UFUNCTION(BlueprintPure, Category = "Survie")
float GetHealth() const { return Health; }
UFUNCTION(BlueprintPure, Category = "Survie")
float GetHunger() const { return Hunger; }
UFUNCTION(BlueprintPure, Category = "Survie")
float GetThirst() const { return Thirst; }
/** Ratios 0-1, directement utilisables par une ProgressBar. */
UFUNCTION(BlueprintPure, Category = "Survie")
float GetHealthPercent() const { return MaxHealth > 0.f ? Health / MaxHealth : 0.f; }
UFUNCTION(BlueprintPure, Category = "Survie")
float GetHungerPercent() const { return MaxHunger > 0.f ? Hunger / MaxHunger : 0.f; }
UFUNCTION(BlueprintPure, Category = "Survie")
float GetThirstPercent() const { return MaxThirst > 0.f ? Thirst / MaxThirst : 0.f; }
UFUNCTION(BlueprintPure, Category = "Survie")
bool IsDead() const { return bDead; }
UPROPERTY(BlueprintAssignable, Category = "Survie")
FOnSurvivalStatsChanged OnStatsChanged;
UPROPERTY(BlueprintAssignable, Category = "Survie")
FOnSurvivalDeath OnDied;
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Survie|Vie", meta = (ClampMin = "1"))
float MaxHealth = 100.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Survie|Faim", meta = (ClampMin = "1"))
float MaxHunger = 100.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Survie|Soif", meta = (ClampMin = "1"))
float MaxThirst = 100.f;
/** Points de faim perdus par minute. 3.3 vide une jauge pleine en ~30 min. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie|Faim", meta = (ClampMin = "0"))
float HungerDecayPerMinute = 3.3f;
/** La soif descend plus vite que la faim : c'est le besoin le plus pressant. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie|Soif", meta = (ClampMin = "0"))
float ThirstDecayPerMinute = 5.f;
/** Vie perdue par minute quand la faim est a zero. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie|Vie", meta = (ClampMin = "0"))
float StarvationDamagePerMinute = 6.f;
/** Vie perdue par minute quand la soif est a zero. Cumulable avec la faim. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie|Vie", meta = (ClampMin = "0"))
float DehydrationDamagePerMinute = 9.f;
/**
* Secondes entre deux mises a jour. Une seconde suffit largement et reste
* assez fin pour que les barres bougent de facon fluide a l'oeil.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Survie", meta = (ClampMin = "0.05"))
float UpdateInterval = 1.f;
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();
};
@@ -0,0 +1,73 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SurvivalStatsWidget.generated.h"
class UProgressBar;
class USurvivalStatsComponent;
/**
* Les trois jauges de survie : vie, faim, soif.
*
* Aucun Tick : le widget s'abonne a OnStatsChanged, qui ne se declenche
* qu'une fois par seconde ou lors d'une consommation.
*/
UCLASS(Abstract)
class EMBERWILD_API USurvivalStatsWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
UFUNCTION(BlueprintCallable, Category = "Survie")
void Refresh();
/** (Re)branche sur les stats du pawn possede. Voir UInventoryWidget. */
void BindToOwningPawn();
protected:
/** A nommer exactement "HealthBar", "HungerBar" et "ThirstBar". */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UProgressBar> HealthBar;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UProgressBar> HungerBar;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UProgressBar> ThirstBar;
/** En dessous de ce ratio, la barre passe en couleur d'alerte. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie", meta = (ClampMin = "0", ClampMax = "1"))
float CriticalThreshold = 0.2f;
/** Couleur d'alerte, commune aux trois jauges. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie")
FLinearColor CriticalColor = FLinearColor(0.9f, 0.1f, 0.1f, 1.f);
private:
UFUNCTION()
void HandleStatsChanged();
USurvivalStatsComponent* ResolveStats() const;
/** Applique le remplissage et la couleur d'une barre. */
void ApplyBar(UProgressBar* Bar, float Percent, const FLinearColor& BaseColor) const;
TWeakObjectPtr<USurvivalStatsComponent> BoundStats;
/**
* Couleurs d'origine relevees au NativeConstruct.
*
* On les capture depuis les widgets eux-memes plutot que de les redeclarer
* en C++ : la palette reste entierement dans le Designer, et le code ne
* fait que basculer vers l'alerte puis revenir.
*/
FLinearColor HealthBaseColor = FLinearColor::White;
FLinearColor HungerBaseColor = FLinearColor::White;
FLinearColor ThirstBaseColor = FLinearColor::White;
};
@@ -0,0 +1,372 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameUserSettings.h"
#include "CharacterAppearanceTypes.h"
#include "SurvivalUserSettings.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalSettingsApplied);
/**
* Bornes et pas des reglages numeriques.
*
* Ici et pas dans le widget : c'est l'objet de reglages qui borne ses propres
* valeurs, et l'ecran doit proposer exactement les memes paliers. Deux jeux de
* constantes finiraient par diverger et le menu afficherait des valeurs que le
* setter refuse.
*/
namespace SurvivalSettingsRange
{
// Deux granularites par reglage :
// Step = le pas FIN, celui du glissement au curseur. C'est aussi
// celui sur lequel les setters arrondissent : le stocker plus
// grossier annulerait le reglage fin juste apres.
// ArrowStep = le bond des fleches, pour traverser la plage vite.
constexpr float SensitivityMin = 5.f;
constexpr float SensitivityMax = 100.f;
constexpr float SensitivityStep = 1.f;
constexpr float SensitivityArrowStep = 5.f;
constexpr float FieldOfViewMin = 70.f;
constexpr float FieldOfViewMax = 120.f;
constexpr float FieldOfViewStep = 1.f;
constexpr float FieldOfViewArrowStep = 5.f;
constexpr float CameraShakeMin = 0.f;
constexpr float CameraShakeMax = 100.f;
constexpr float CameraShakeStep = 1.f;
constexpr float CameraShakeArrowStep = 10.f;
/**
* Luminosite en POURCENTAGE et non en gamma.
*
* Le gamma utile va de 0.8 a 1.4 ; affiche tel quel il tiendrait dans un
* intervalle de 0.6 que la ligne, qui n'affiche que des entiers, arrondirait
* a "1" partout. On stocke donc 80 a 140 et on divise a l'application.
*/
constexpr float BrightnessMin = 80.f;
constexpr float BrightnessMax = 140.f;
constexpr float BrightnessStep = 1.f;
constexpr float BrightnessArrowStep = 10.f;
/**
* Gamma d'affichage a 100 % de luminosite.
*
* C'est la valeur du moteur (BaseEngine.ini), PAS un neutre a 1.0 :
* GEngine->DisplayGamma est un exposant, pas un multiplicateur. Y ecrire 1.0
* assombrit tout l'ecran -- et comme GEngine est global, l'editeur avec.
*/
constexpr float BaseDisplayGamma = 2.2f;
/** Echelle de resolution, en pourcentage de la resolution native. */
constexpr float ResolutionScaleMin = 50.f;
constexpr float ResolutionScaleMax = 100.f;
constexpr float ResolutionScaleStep = 1.f;
constexpr float ResolutionScaleArrowStep = 10.f;
/**
* Multiplicateurs reels correspondant aux extremites de l'echelle affichee.
*
* Le joueur regle un nombre de 5 a 100 parce que "0.4" ne veut rien dire ;
* le pawn, lui, veut le multiplicateur. Le defaut de 35 retombe sur ~0.41,
* soit la valeur qui etait codee en dur dans AFpsPlayer.
*/
constexpr float LookMultiplierAtMin = 0.05f;
constexpr float LookMultiplierAtMax = 1.2f;
/** Les quatre volumes partagent la meme echelle : 0 a 100 %. */
constexpr float VolumeMin = 0.f;
constexpr float VolumeMax = 100.f;
constexpr float VolumeStep = 1.f;
constexpr float VolumeArrowStep = 10.f;
/**
* Exposant applique au pourcentage avant d'en faire une amplitude.
*
* L'oreille repond au logarithme de l'amplitude, pas a l'amplitude : un
* curseur envoye tel quel donne un reglage inutilisable, ou tout se joue dans
* les vingt derniers pourcents et ou la moitie basse ne s'entend deja plus.
* Elever au carre est l'approximation habituelle -- 50 % rendent 0,25 en
* amplitude, soit environ la moitie du volume PERCU. 0 reste le silence exact.
*/
constexpr float VolumeCurveExponent = 2.f;
}
/**
* Les reglages du joueur, et le seul endroit qui fasse autorite dessus.
*
* Sous-classe de UGameUserSettings et non un objet a nous : le moteur fournit
* deja le singleton, sa persistance dans Saved/Config/Windows/GameUserSettings.ini
* et tout le volet video (resolution, VSync, qualite). C'est l'equivalent d'un
* PlayerPrefs + QualitySettings + Screen d'Unity reunis, sauf qu'il est deja
* ecrit. On l'etend, on ne le remplace pas.
*
* Pour que le moteur instancie CETTE classe et pas la sienne, Config/DefaultEngine.ini
* contient :
* [/Script/Engine.Engine]
* GameUserSettingsClassName=/Script/EmberWild.SurvivalUserSettings
*
* Les UPROPERTY(config) sont lues et ecrites automatiquement : aucun code de
* serialisation a maintenir.
*/
UCLASS()
class EMBERWILD_API USurvivalUserSettings : public UGameUserSettings
{
GENERATED_BODY()
public:
USurvivalUserSettings();
/**
* Le raccourci typé vers le singleton. GEngine->GetGameUserSettings()
* renvoie la classe de base ; ce helper evite un cast a chaque appel.
*/
UFUNCTION(BlueprintPure, Category = "Reglages", meta = (DisplayName = "Get Survival User Settings"))
static USurvivalUserSettings* Get();
/**
* Previent tout le monde que les reglages de jeu ont change.
*
* Indispensable a cause du respawn : le pawn est un NOUVEL acteur a chaque
* mort, et une valeur poussee une fois dans l'ancien serait perdue. Chaque
* pawn lit les reglages a son BeginPlay et s'abonne ici pour la suite.
*/
UPROPERTY(BlueprintAssignable, Category = "Reglages")
FOnSurvivalSettingsApplied OnSurvivalSettingsApplied;
/**
* Diffuse les reglages de jeu, sans toucher au volet video.
*
* Distinct d'ApplyNonResolutionSettings, qui repousse aussi la qualite et la
* limite de FPS : l'appeler a chaque clic de fleche sur la sensibilite
* provoquerait un a-coup pour rien.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages")
void ApplySurvivalSettings();
/**
* Pousse les volumes sur le peripherique audio.
*
* Volontairement SEPAREE d'ApplySurvivalSettings : cinq commandes vers le
* thread audio a chaque clic de fleche sur le champ de vision seraient du
* bruit pur. Les deux sont appelees ensemble par ApplyNonResolutionSettings,
* donc le chemin de demarrage n'en oublie aucune.
*/
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
void ApplyAudioSettings();
/** Remet les seuls reglages de jeu a leur valeur d'usine. */
UFUNCTION(BlueprintCallable, Category = "Reglages")
void SetSurvivalGameplayToDefaults();
/** Remet a l'usine les seuls reglages de l'onglet Audio. */
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
void SetSurvivalAudioToDefaults();
virtual void SetToDefaults() override;
virtual void ApplyNonResolutionSettings() override;
// ------------------------------------------------------------------
// Valeurs pretes a l'emploi
//
// Le pawn ne doit jamais lire les champs bruts : ce sont des valeurs
// d'affichage (5 a 100), pas des multiplicateurs.
// ------------------------------------------------------------------
UFUNCTION(BlueprintPure, Category = "Reglages|Controles")
float GetLookMultiplierX() const;
UFUNCTION(BlueprintPure, Category = "Reglages|Controles")
float GetLookMultiplierY() const;
/** 0 = camera parfaitement fixe, 1 = amplitude d'origine. */
UFUNCTION(BlueprintPure, Category = "Reglages|Jeu")
float GetCameraShakeMultiplier() const;
// ------------------------------------------------------------------
// Accesseurs bruts, pour l'ecran de reglages
// ------------------------------------------------------------------
UFUNCTION(BlueprintPure, Category = "Reglages|Controles")
float GetLookSensitivity() const { return LookSensitivity; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
void SetLookSensitivity(float Value);
UFUNCTION(BlueprintPure, Category = "Reglages|Controles")
bool GetSeparateVerticalSensitivity() const { return bSeparateVerticalSensitivity; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
void SetSeparateVerticalSensitivity(bool bValue);
UFUNCTION(BlueprintPure, Category = "Reglages|Controles")
float GetLookSensitivityVertical() const { return LookSensitivityVertical; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
void SetLookSensitivityVertical(float Value);
UFUNCTION(BlueprintPure, Category = "Reglages|Controles")
bool GetInvertLookY() const { return bInvertLookY; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
void SetInvertLookY(bool bValue);
UFUNCTION(BlueprintPure, Category = "Reglages|Jeu")
float GetFieldOfView() const { return FieldOfView; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Jeu")
void SetFieldOfView(float Value);
UFUNCTION(BlueprintPure, Category = "Reglages|Jeu")
float GetCameraShakeScale() const { return CameraShakeScale; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Jeu")
void SetCameraShakeScale(float Value);
UFUNCTION(BlueprintPure, Category = "Reglages|Controles")
bool GetSprintToggle() const { return bSprintToggle; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
void SetSprintToggle(bool bValue);
UFUNCTION(BlueprintPure, Category = "Reglages|Controles")
bool GetCrouchToggle() const { return bCrouchToggle; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
void SetCrouchToggle(bool bValue);
UFUNCTION(BlueprintPure, Category = "Reglages|Affichage")
float GetBrightness() const { return Brightness; }
/** Ne fait qu'enregistrer : c'est ApplySurvivalSettings qui pousse le gamma. */
UFUNCTION(BlueprintCallable, Category = "Reglages|Affichage")
void SetBrightness(float Value);
/** Remet a l'usine les seuls reglages de l'onglet Affichage qui nous appartiennent. */
UFUNCTION(BlueprintCallable, Category = "Reglages|Affichage")
void SetSurvivalDisplayToDefaults();
// ------------------------------------------------------------------
// Audio
//
// Les quatre volumes sont des POURCENTAGES, comme le bouge de camera : c'est
// ce que la ligne de reglage affiche, et la conversion en amplitude (courbe
// comprise) n'appartient qu'a ApplyAudioSettings.
// ------------------------------------------------------------------
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
float GetMasterVolume() const { return MasterVolume; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
void SetMasterVolume(float Value);
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
float GetMusicVolume() const { return MusicVolume; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
void SetMusicVolume(float Value);
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
float GetEffectsVolume() const { return EffectsVolume; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
void SetEffectsVolume(float Value);
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
float GetUiVolume() const { return UiVolume; }
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
void SetUiVolume(float Value);
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
bool GetPlayInBackground() const { return bPlayInBackground; }
/** Vrai = le jeu continue de s'entendre quand la fenetre n'a plus le focus. */
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;
UPROPERTY(config)
bool bSeparateVerticalSensitivity;
UPROPERTY(config)
float LookSensitivityVertical;
UPROPERTY(config)
bool bInvertLookY;
UPROPERTY(config)
float FieldOfView;
/** Pourcentage. A 0, plus aucun impact camera : pour le mal des transports. */
UPROPERTY(config)
float CameraShakeScale;
UPROPERTY(config)
bool bSprintToggle;
UPROPERTY(config)
bool bCrouchToggle;
/** Pourcentage, converti en gamma a l'application. */
UPROPERTY(config)
float Brightness;
/** Pourcentages. Le maitre multiplie les trois autres via la hierarchie des SoundClass. */
UPROPERTY(config)
float MasterVolume;
UPROPERTY(config)
float MusicVolume;
UPROPERTY(config)
float EffectsVolume;
UPROPERTY(config)
float UiVolume;
/**
* Le moteur coupe le son des qu'il perd le focus, et c'est son defaut d'usine.
* En coop c'est justement le mauvais defaut pour qui bascule sur Discord.
*/
UPROPERTY(config)
bool bPlayInBackground;
};