364 lines
12 KiB
C++
364 lines
12 KiB
C++
// 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<ACharacter>(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;
|
|
}
|