// Fill out your copyright notice in the Description page of Project Settings. #include "FootstepComponent.h" #include "Components/CapsuleComponent.h" #include "Engine/World.h" #include "GameFramework/Character.h" #include "GameFramework/CharacterMovementComponent.h" #include "Kismet/GameplayStatics.h" UFootstepComponent::UFootstepComponent() { PrimaryComponentTick.bCanEverTick = true; // 20 Hz, et pas la frequence d'affichage. Le composant n'accumule qu'une // DISTANCE, mesuree entre deux positions exactes : allonger l'intervalle ne // degrade donc rien, la somme des deplacements reste la meme. A 120 fps // c'est six fois moins de reveils pour un resultat identique a l'oreille. // // Un TickComponent a nous et pas un appel depuis le Tick du pawn, contrairement // a UHeadBobComponent : lui doit tourner avant que la camera soit composee, ici // une frame de retard sur un son ne s'entend pas -- et surtout ce composant doit // rester posable sur n'importe quel ACharacter, y compris ceux qu'on n'ecrit pas. PrimaryComponentTick.TickInterval = 0.05f; // Foulees calees sur les frequences de UHeadBobComponent, avec les vitesses // du pawn : StrideLength = Vitesse / (2 x StrideFrequency). // accroupi : 200 / (2 x 0.70) = 143 // marche : 400 / (2 x 1.10) = 182 // sprint : 650 / (2 x 1.55) = 210 // Elles sont ici et non en initialiseurs de membre pour que le constructeur // reste le seul endroit a lire quand on cherche les valeurs par defaut. CrouchGait = FFootstepGaitSettings(143.f, 0.35f); WalkGait = FFootstepGaitSettings(182.f, 0.80f); SprintGait = FFootstepGaitSettings(210.f, 1.00f); } void UFootstepComponent::BeginPlay() { Super::BeginPlay(); OwnerCharacter = Cast(GetOwner()); if (!OwnerCharacter) { UE_LOG(LogTemp, Warning, TEXT("UFootstepComponent : le proprietaire n'est pas un ACharacter, les bruits de pas sont desactives.")); SetComponentTickEnabled(false); return; } OwnerMovement = OwnerCharacter->GetCharacterMovement(); // Le seul signal de decollage et de reception qui existe sur les quatre // machines. Voir le commentaire de HandleMovementModeChanged. OwnerCharacter->MovementModeChangedDelegate.AddDynamic(this, &UFootstepComponent::HandleMovementModeChanged); if (!SoundBank) { UE_LOG(LogTemp, Warning, TEXT("UFootstepComponent : aucune banque de sons assignee sur %s, aucun bruit de pas ne sera joue."), *GetNameSafe(GetOwner())); } } void UFootstepComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (OwnerCharacter) { OwnerCharacter->MovementModeChangedDelegate.RemoveDynamic(this, &UFootstepComponent::HandleMovementModeChanged); } Super::EndPlay(EndPlayReason); } void UFootstepComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!bEnableFootsteps || !OwnerCharacter || !OwnerMovement || !SoundBank || !ShouldPlaySounds()) { return; } const FVector Location = OwnerCharacter->GetActorLocation(); // Premier tick valide : on se contente de memoriser la position. Sans ce // garde-fou, le premier deplacement mesure partirait de l'origine du monde // et declencherait une rafale de pas au spawn. if (!bHasLastLocation) { LastLocation = Location; bHasLastLocation = true; return; } const float Travelled = (Location - LastLocation).Size2D(); LastLocation = Location; // En l'air on ne compte pas de foulee : le vol est traite par les deux cues // de HandleMovementModeChanged. On y profite du passage pour echantillonner // la vitesse de chute, introuvable au moment de l'impact. if (OwnerMovement->IsFalling()) { MaxFallSpeed = FMath::Max(MaxFallSpeed, -OwnerMovement->Velocity.Z); return; } const float Speed = OwnerMovement->Velocity.Size2D(); const bool bIsMoving = Speed > IdleSpeedThreshold; if (bIsMoving && !bWasMoving) { // Entree en mouvement : on amorce le compteur pour que le premier pas // tombe tout de suite plutot qu'apres deux metres de silence. DistanceSinceLastStep = GetGaitSettings(SelectGait(Speed)).StrideLength * FirstStepFraction; StepsSinceMoveStart = 0; } else if (!bIsMoving && bWasMoving) { // Immobilisation : le pas final. Conditionne a un vrai pas deja joue, // sinon un simple a-coup de collision ferait claquer un arret alors que // le personnage n'a pas avance d'un centimetre. if (bPlayStopSounds && StepsSinceMoveStart > 0) { FVector StopLocation; if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(StopLocation)) { const FFootstepGaitSettings& StopSettings = GetGaitSettings(LastGait); PlayCue(Sounds->GetStopSound(LastGait), StopSettings.Volume, StopSettings.PitchVariation, StopLocation); } } DistanceSinceLastStep = 0.f; StepsSinceMoveStart = 0; } bWasMoving = bIsMoving; if (!bIsMoving) { return; } DistanceSinceLastStep += Travelled; const EFootstepGait Gait = SelectGait(Speed); const float StrideLength = GetGaitSettings(Gait).StrideLength; if (DistanceSinceLastStep >= StrideLength) { // Soustraction et pas remise a zero : le reliquat de distance doit // survivre au pas, sinon le rythme derive a chaque hoquet de framerate. DistanceSinceLastStep -= StrideLength; // Mais borne quand meme : apres une teleportation ou un long gel, le // reliquat vaudrait plusieurs foulees et partirait en rafale. DistanceSinceLastStep = FMath::Min(DistanceSinceLastStep, StrideLength); ++StepsSinceMoveStart; PlayFootstepNow(); } } void UFootstepComponent::PlayFootstepNow() { if (!OwnerMovement || !SoundBank || !ShouldPlaySounds()) { return; } const EFootstepGait Gait = SelectGait(OwnerMovement->Velocity.Size2D()); FVector StepLocation; const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(StepLocation); if (!Sounds) { return; } const FFootstepGaitSettings& Settings = GetGaitSettings(Gait); PlayCue(Sounds->GetStepSound(Gait), Settings.Volume, Settings.PitchVariation, StepLocation); // Memorise APRES coup : c'est cette allure-la que le son d'arret devra // reprendre, pas celle qu'aura le personnage une fois immobile. LastGait = Gait; } void UFootstepComponent::HandleMovementModeChanged(ACharacter* Character, EMovementMode PrevMovementMode, uint8 PreviousCustomMode) { if (!bEnableFootsteps || !OwnerMovement || !SoundBank || !ShouldPlaySounds()) { return; } const EMovementMode NewMode = OwnerMovement->MovementMode; // Decollage if (NewMode == MOVE_Falling && PrevMovementMode == MOVE_Walking) { MaxFallSpeed = 0.f; // Un saut, et pas une sortie de rebord : c'est le SIGNE de la vitesse // verticale qui tranche, exactement comme dans l'AnimBP du corps. Sans // ce test, marcher au bord d'une caisse ferait grogner le personnage. if (OwnerMovement->Velocity.Z > 0.f) { FVector JumpLocation; if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(JumpLocation)) { const FFootstepGaitSettings& Settings = GetGaitSettings(SelectGait(OwnerMovement->Velocity.Size2D())); PlayCue(Sounds->Jump, AirVolume, Settings.PitchVariation, JumpLocation); } } // Le cycle de foulee repart de zero a la reception : bWasMoving remis a // faux fait passer le premier tick au sol par la branche "entree en // mouvement", donc par FirstStepFraction. Retomber en pleine course // redonne ainsi un pas tout de suite. DistanceSinceLastStep = 0.f; StepsSinceMoveStart = 0; bWasMoving = false; return; } // Reception if (PrevMovementMode == MOVE_Falling && NewMode == MOVE_Walking) { if (MaxFallSpeed > LandMinFallSpeed) { FVector LandLocation; if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(LandLocation)) { const FFootstepGaitSettings& Settings = GetGaitSettings(SelectGait(OwnerMovement->Velocity.Size2D())); PlayCue(Sounds->Land, AirVolume, Settings.PitchVariation, LandLocation); } } MaxFallSpeed = 0.f; DistanceSinceLastStep = 0.f; StepsSinceMoveStart = 0; bWasMoving = false; } } void UFootstepComponent::SetFootstepsEnabled(bool bEnabled) { bEnableFootsteps = bEnabled; if (!bEnabled) { ResetStride(); } } void UFootstepComponent::ResetStride() { DistanceSinceLastStep = 0.f; StepsSinceMoveStart = 0; MaxFallSpeed = 0.f; bWasMoving = false; // La position memorisee aussi : un respawn deplace le pawn de plusieurs // centaines de metres, et ce deplacement ne doit pas compter comme un pas. bHasLastLocation = false; } EFootstepGait UFootstepComponent::SelectGait(float HorizontalSpeed) const { if (OwnerMovement && OwnerMovement->IsCrouching()) { return EFootstepGait::Walk; } // Le seuil se lit sur la vitesse REELLE et pas sur AFpsPlayer::IsSprinting() : // le composant reste ainsi posable sur un PNJ, qui n'a pas de touche Maj. return (HorizontalSpeed > SprintSpeedThreshold) ? EFootstepGait::Run : EFootstepGait::Jog; } const FFootstepGaitSettings& UFootstepComponent::GetGaitSettings(EFootstepGait Gait) const { switch (Gait) { case EFootstepGait::Walk: return CrouchGait; case EFootstepGait::Run: return SprintGait; default: return WalkGait; } } const FFootstepSurfaceSounds* UFootstepComponent::QueryGroundSounds(FVector& OutLocation) const { if (!SoundBank || !OwnerCharacter) { return nullptr; } const UCapsuleComponent* Capsule = OwnerCharacter->GetCapsuleComponent(); const float HalfHeight = Capsule ? Capsule->GetScaledCapsuleHalfHeight() : 0.f; const FVector Center = OwnerCharacter->GetActorLocation(); const FVector Feet = Center - FVector(0.f, 0.f, HalfHeight); // Repli de position : si le trace ne touche rien, le son part quand meme, // depuis les pieds. Un silence serait plus difficile a diagnostiquer. OutLocation = Feet; const UWorld* World = GetWorld(); if (!World) { return &SoundBank->DefaultSurface; } // bTraceComplex : voir le commentaire de la propriete. Sans lui on recupere // le PhysicalMaterial du BodySetup, presque toujours vide, et tout le decor // sonne sur DefaultSurface. FCollisionQueryParams Params(SCENE_QUERY_STAT(Footstep), bTraceComplex, OwnerCharacter); Params.bReturnPhysicalMaterial = true; FHitResult Hit; if (!World->LineTraceSingleByChannel(Hit, Center, Feet - FVector(0.f, 0.f, TraceDepth), TraceChannel, Params)) { return &SoundBank->DefaultSurface; } OutLocation = Hit.ImpactPoint; return &SoundBank->GetSurfaceSounds(UGameplayStatics::GetSurfaceType(Hit)); } void UFootstepComponent::PlayCue(USoundBase* Sound, float Volume, float PitchVariation, const FVector& Location) const { if (!Sound || Volume <= 0.f) { return; } const float Pitch = (PitchVariation > 0.f) ? FMath::FRandRange(1.f - PitchVariation, 1.f + PitchVariation) : 1.f; // Le pas du joueur local en 2D : joue en 3D depuis ses propres chevilles, il // passe par l'attenuation et sonne creux, alors qu'il devrait etre le son le // plus present du mixage. Les Cue n'ont pas de SoundClass, donc les deux // chemins tombent sur SC_SFX, la classe par defaut du projet -- le curseur // "Effets" les couvre sans qu'on ait rien a taguer. if (bPlayLocalStepsIn2D && OwnerCharacter && OwnerCharacter->IsLocallyControlled()) { UGameplayStatics::PlaySound2D(this, Sound, Volume * LocalVolumeScale, Pitch, 0.f, Concurrency, GetOwner()); return; } // PlaySoundAtLocation et pas SpawnSoundAttached : un pas est un one-shot de // quelques dixiemes de seconde, l'attacher creerait puis detruirait un // UAudioComponent trois fois par seconde et par joueur pour rien. UGameplayStatics::PlaySoundAtLocation(this, Sound, Location, FRotator::ZeroRotator, Volume, Pitch, 0.f, Attenuation, Concurrency, GetOwner()); } bool UFootstepComponent::ShouldPlaySounds() const { // Un serveur dedie n'a pas de sortie audio : inutile d'y tracer le sol pour // quatre personnages. Le projet tourne en serveur d'ecoute, mais la garde ne // coute qu'une comparaison et evite d'y repenser le jour ou ca change. const UWorld* World = GetWorld(); return World && World->GetNetMode() != NM_DedicatedServer; }