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