// 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 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 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 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 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 OwnerCharacter; TObjectPtr OwnerMovement; };