(Feat) Add footsteps

This commit is contained in:
2026-08-11 15:23:10 +02:00
parent e8e128457a
commit 176beab9fc
4776 changed files with 18806 additions and 121 deletions
@@ -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 SURVIVAL_PROJET_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,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 SURVIVAL_PROJET_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,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 SURVIVAL_PROJET_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 SURVIVAL_PROJET_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 SURVIVAL_PROJET_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 SURVIVAL_PROJET_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;
};
@@ -43,15 +43,65 @@ protected:
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;
/**
* 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.
+71
View File
@@ -11,7 +11,9 @@ class UAnimMontage;
class UCameraComponent;
class UCharacterAppearanceComponent;
class UCraftingComponent;
class UFootstepComponent;
class UHeadBobComponent;
class UHeldItemComponent;
class UInputAction;
class UInputMappingContext;
class UInteractionComponent;
@@ -126,6 +128,12 @@ public:
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
//
@@ -252,6 +260,20 @@ public:
*/
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;
@@ -300,6 +322,35 @@ protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
TObjectPtr<UAnimMontage> GrabMontage;
/**
* Réception au sol, version légère (MTG_Land).
*
* Un montage et pas un état de la machine : l'atterrissage est un geste
* PONCTUEL qui doit se superposer à ce que les bras faisaient déjà. Un état
* imposerait de décider combien de temps y rester, et retomber en pleine
* course y bloquerait le cycle de foulée le temps de l'animation.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
TObjectPtr<UAnimMontage> LandMontage;
/** Réception lourde (MTG_HardLand), au-delà de HardLandingFallSpeed. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
TObjectPtr<UAnimMontage> HardLandMontage;
/**
* Vitesse de chute (cm/s) à partir de laquelle la réception passe en lourde.
*
* Doit rester nettement au-dessus de la retombée d'un saut sur place
* (~JumpZVelocity, 450 par défaut), sinon chaque saut se termine par une
* réception encaissée et le déplacement devient poussif.
*
* Le seuil BAS, lui, est LandPunchMinFallSpeed : sous cette vitesse aucun
* montage n'est joué, exactement comme la caméra ne bouge pas. Une seule
* valeur à comprendre pour les deux effets, qui restent donc cohérents.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation", meta = (ClampMin = "0"))
float HardLandingFallSpeed = 800.f;
/**
* 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,
@@ -345,6 +396,26 @@ protected:
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
// ------------------------------------------------------------------
@@ -89,6 +89,19 @@ public:
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();
@@ -384,13 +397,6 @@ private:
/** L'éclaircissement lui-même, après le temps de chauffe du monde. */
void StartStartupFade();
/**
* Echap. Une seule touche pour trois usages, d'ou une priorite explicite :
* boite de confirmation, puis inventaire, puis pause. Sans cet ordre on se
* retrouve avec l'inventaire et la pause ouverts en meme temps.
*/
void HandlePauseInput();
void OpenPauseMenu();
/** Oui : fondu au noir, puis retour au menu principal. */
@@ -0,0 +1,197 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HeldItemComponent.generated.h"
class AFpsPlayer;
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);
/**
* 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 SURVIVAL_PROJET_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();
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");
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();
/**
* 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;
/** Evite de repeter l'avertissement de socket introuvable a chaque montage. */
bool bLoggedMissingSocket = false;
};
@@ -10,6 +10,25 @@ 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.
@@ -106,6 +125,100 @@ public:
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;
/**
* 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;
/** 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
// ------------------------------------------------------------------
@@ -6,7 +6,9 @@
#include "GameFramework/PlayerController.h"
#include "MainMenuPlayerController.generated.h"
class ACharacterPreviewActor;
class AMenuCameraSpot;
class UCharacterCustomizationWidget;
class UConfirmDialogWidget;
class UMainMenuWidget;
class UScreenFadeComponent;
@@ -27,7 +29,17 @@ enum class EMenuPendingAction : uint8
* 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
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
};
/**
@@ -102,6 +114,35 @@ protected:
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;
@@ -146,6 +187,26 @@ private:
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.
@@ -24,9 +24,39 @@ class SURVIVAL_PROJET_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))
+68 -5
View File
@@ -7,8 +7,8 @@
#include "Interactable.h"
#include "PickupItem.generated.h"
class UBoxComponent;
class UItemDataAsset;
class USphereComponent;
class UStaticMeshComponent;
/**
@@ -60,12 +60,28 @@ protected:
/**
* 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 une sphere dediee, le pickup fonctionne quel que soit le visuel.
*
* 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<USphereComponent> InteractionSphere;
TObjectPtr<UBoxComponent> InteractionBox;
/**
* Quel objet ce pickup represente. A assigner sur chaque instance.
@@ -105,6 +121,15 @@ protected:
*/
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.
@@ -112,6 +137,44 @@ protected:
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;
@@ -42,7 +42,7 @@ struct FStorageStartingItem
*
* 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 PIVOTE. Se reposer sur leurs
* 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.
@@ -137,26 +137,24 @@ protected:
* 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 pivot : elle ne suit surtout pas le
* couvercle. Un coffre ouvert doit se viser au meme endroit qu'un coffre
* 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;
/**
* La charniere. Place-la a la souris sur l'arete arriere du coffre.
* Le couvercle : SM_Chest_Top, enfant direct de la caisse.
*
* Ce composant vide n'est pas un luxe : le pivot de SM_Chest_Top est celui
* que le pack lui a donne, presque jamais la charniere. Faire tourner le
* mesh directement le ferait pivoter autour de son centre et traverser la
* caisse. Ici on tourne le pivot, et le mesh suit avec son decalage.
* 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<USceneComponent> LidPivot;
/** Le couvercle : SM_Chest_Top, enfant du pivot. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UStaticMeshComponent> LidMesh;
/** Le contenu. Un inventaire ordinaire, sans barre rapide. */
@@ -219,12 +217,12 @@ protected:
// ------------------------------------------------------------------
/**
* Rotation RELATIVE du pivot couvercle ouvert. Le repos vaut toujours zero.
* Rotation RELATIVE du couvercle ouvert. Le repos vaut toujours zero.
*
* Un FRotator complet et pas un simple angle : selon l'orientation que tu
* donnes au pivot, 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.
* 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);
@@ -244,7 +242,7 @@ protected:
TObjectPtr<USoundBase> CloseSound;
private:
/** Recopie LidAlpha dans la rotation du pivot. */
/** Recopie LidAlpha dans la rotation du couvercle. */
void ApplyLidRotation();
/** Cale la boite sur le volume combine des deux meshes, couvercle ferme. */