(Feat) Add Animation Fps Charater
This commit is contained in:
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user