(Feat) Add Main Menu + Music Manger
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#include "MusicSubsystem.h"
|
||||
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "MusicPlaylistDataAsset.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
|
||||
UMusicSubsystem* UMusicSubsystem::Get(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!GEngine || !WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
|
||||
UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;
|
||||
return GameInstance ? GameInstance->GetSubsystem<UMusicSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
// Le seul endroit d'ou l'on voit passer un changement de scene : le lecteur
|
||||
// survit aux maps, donc aucun BeginPlay ni aucun EndPlay ne peut jouer ce
|
||||
// role -- et la map d'arrivee n'a justement pas a connaître le sujet.
|
||||
PostLoadMapHandle = FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMusicSubsystem::HandlePostLoadMap);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::Deinitialize()
|
||||
{
|
||||
if (PostLoadMapHandle.IsValid())
|
||||
{
|
||||
FCoreUObjectDelegates::PostLoadMapWithWorld.Remove(PostLoadMapHandle);
|
||||
PostLoadMapHandle.Reset();
|
||||
}
|
||||
|
||||
CancelPendingGap();
|
||||
|
||||
// Fondu a zero : on quitte le jeu, plus personne n'ecoute.
|
||||
FadeOutAndRelease(MusicComponent, 0.f);
|
||||
if (FadingOutComponent)
|
||||
{
|
||||
FadingOutComponent->Stop();
|
||||
FadingOutComponent = nullptr;
|
||||
}
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void UMusicSubsystem::PlayPlaylist(UMusicPlaylistDataAsset* Playlist, bool bForceRestart)
|
||||
{
|
||||
if (!Playlist || Playlist->Tracks.IsEmpty())
|
||||
{
|
||||
StopMusic();
|
||||
return;
|
||||
}
|
||||
|
||||
// Deja en cours : on la laisse tranquille. C'est tout l'interet de faire
|
||||
// pointer la map du menu et celle du jeu vers le meme asset -- le chargement
|
||||
// devient inaudible.
|
||||
if (CurrentPlaylist == Playlist && bIsPlaying && !bForceRestart)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancelPendingGap();
|
||||
|
||||
// L'ancienne musique s'eteint sur sa propre voie pendant que la nouvelle
|
||||
// entre : un composant audio ne peut pas se croiser avec lui-meme.
|
||||
FadeOutAndRelease(MusicComponent, CurrentPlaylist ? CurrentPlaylist->FadeOutDuration : Playlist->FadeOutDuration);
|
||||
|
||||
CurrentPlaylist = Playlist;
|
||||
bShuffleEnabled = Playlist->bShuffle;
|
||||
LastPlayedTrackIndex = INDEX_NONE;
|
||||
bIsPlaying = true;
|
||||
|
||||
BuildPlayOrder();
|
||||
if (PlayOrder.IsEmpty())
|
||||
{
|
||||
// Que des cases vides dans la liste.
|
||||
bIsPlaying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
PlayTrackAtCursor(Playlist->FadeInDuration);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::StopMusic(float FadeOutDuration)
|
||||
{
|
||||
CancelPendingGap();
|
||||
|
||||
const float Duration = (FadeOutDuration >= 0.f)
|
||||
? FadeOutDuration
|
||||
: (CurrentPlaylist ? CurrentPlaylist->FadeOutDuration : 0.f);
|
||||
|
||||
FadeOutAndRelease(MusicComponent, Duration);
|
||||
|
||||
bIsPlaying = false;
|
||||
CurrentPlaylist = nullptr;
|
||||
PlayOrder.Reset();
|
||||
OrderCursor = INDEX_NONE;
|
||||
|
||||
OnMusicTrackChanged.Broadcast(nullptr);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::SkipToNextTrack()
|
||||
{
|
||||
if (!bIsPlaying || !CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancelPendingGap();
|
||||
|
||||
// On LIBERE le composant au lieu de le reutiliser. Stop() et FadeOut()
|
||||
// rediffusent OnAudioFinished, et cette diffusion transite par le thread audio :
|
||||
// elle peut donc arriver une frame plus tard, soit APRES le demarrage du morceau
|
||||
// suivant, ce qui en sauterait un second. Un composant neuf, celui d'avant
|
||||
// detache, rend ce retour tardif sans effet.
|
||||
const float SkipFade = FMath::Min(CurrentPlaylist->FadeOutDuration, 1.f);
|
||||
FadeOutAndRelease(MusicComponent, SkipFade);
|
||||
|
||||
AdvanceAndPlay(SkipFade);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::SetShuffleEnabled(bool bEnabled)
|
||||
{
|
||||
if (bShuffleEnabled == bEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bShuffleEnabled = bEnabled;
|
||||
|
||||
if (!CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Le morceau en cours continue ; seule la SUITE change. On recale le curseur
|
||||
// dessus pour qu'en mode ordonne la lecture reprenne juste apres lui, et non
|
||||
// au debut de la liste.
|
||||
BuildPlayOrder();
|
||||
const int32 Found = PlayOrder.IndexOfByKey(LastPlayedTrackIndex);
|
||||
OrderCursor = (Found != INDEX_NONE) ? Found : 0;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::SetMusicVolume(float NewVolume)
|
||||
{
|
||||
MusicVolume = FMath::Clamp(NewVolume, 0.f, 1.f);
|
||||
|
||||
if (MusicComponent)
|
||||
{
|
||||
// SetVolumeMultiplier est independant du fader utilise par FadeIn/FadeOut :
|
||||
// regler le volume au milieu d'un fondu ne l'interrompt pas.
|
||||
MusicComponent->SetVolumeMultiplier(GetEffectiveVolume());
|
||||
}
|
||||
}
|
||||
|
||||
USoundBase* UMusicSubsystem::GetCurrentTrack() const
|
||||
{
|
||||
return MusicComponent ? MusicComponent->Sound : nullptr;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::HandleTrackFinished()
|
||||
{
|
||||
// N'est atteint que par une fin naturelle : toute coupure manuelle passe par
|
||||
// FadeOutAndRelease, qui detache le delegue avant d'arreter le son.
|
||||
if (!bIsPlaying || !CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float MinGap = FMath::Max(0.f, CurrentPlaylist->MinGapBetweenTracks);
|
||||
const float MaxGap = FMath::Max(MinGap, CurrentPlaylist->MaxGapBetweenTracks);
|
||||
const float Gap = FMath::FRandRange(MinGap, MaxGap);
|
||||
|
||||
if (Gap > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
ScheduleGap(Gap);
|
||||
}
|
||||
else
|
||||
{
|
||||
AdvanceAndPlay(0.f);
|
||||
}
|
||||
}
|
||||
|
||||
void UMusicSubsystem::HandlePostLoadMap(UWorld* LoadedWorld)
|
||||
{
|
||||
// En PIE plusieurs instances peuvent charger des maps en parallele : on ne
|
||||
// reagit qu'a la nôtre.
|
||||
if (!LoadedWorld || LoadedWorld->GetGameInstance() != GetGameInstance())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (LevelChangeMode != EMusicLevelChangeMode::New || !bIsPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Volontairement apres le chargement et non avant : le nouveau morceau entre
|
||||
// en meme temps que la nouvelle scene s'affiche, pas pendant l'ecran de
|
||||
// chargement ou personne ne l'associe a quoi que ce soit.
|
||||
SkipToNextTrack();
|
||||
}
|
||||
|
||||
void UMusicSubsystem::BuildPlayOrder()
|
||||
{
|
||||
PlayOrder.Reset();
|
||||
OrderCursor = 0;
|
||||
|
||||
if (!CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayOrder.Reserve(CurrentPlaylist->Tracks.Num());
|
||||
for (int32 Index = 0; Index < CurrentPlaylist->Tracks.Num(); ++Index)
|
||||
{
|
||||
// Filtre en amont : une case vide ne doit jamais devenir un morceau a jouer,
|
||||
// sinon il faudrait la sauter au moment de la lecture -- et une liste
|
||||
// entierement vide y bouclerait a l'infini.
|
||||
if (CurrentPlaylist->Tracks[Index])
|
||||
{
|
||||
PlayOrder.Add(Index);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bShuffleEnabled || PlayOrder.Num() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Fisher-Yates : chaque permutation equiprobable, une seule passe.
|
||||
for (int32 Index = PlayOrder.Num() - 1; Index > 0; --Index)
|
||||
{
|
||||
PlayOrder.Swap(Index, FMath::RandRange(0, Index));
|
||||
}
|
||||
|
||||
// Un nouveau cycle ne redemarre jamais sur le morceau qui vient de finir.
|
||||
// C'est la difference entre "aleatoire" et "aleatoire agreable a ecouter".
|
||||
if (PlayOrder[0] == LastPlayedTrackIndex)
|
||||
{
|
||||
PlayOrder.Swap(0, FMath::RandRange(1, PlayOrder.Num() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
void UMusicSubsystem::AdvanceAndPlay(float FadeInDuration)
|
||||
{
|
||||
if (!CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
++OrderCursor;
|
||||
|
||||
if (!PlayOrder.IsValidIndex(OrderCursor))
|
||||
{
|
||||
if (!CurrentPlaylist->bLoop)
|
||||
{
|
||||
StopMusic(CurrentPlaylist->FadeOutDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
BuildPlayOrder();
|
||||
if (PlayOrder.IsEmpty())
|
||||
{
|
||||
StopMusic(0.f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PlayTrackAtCursor(FadeInDuration);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::PlayTrackAtCursor(float FadeInDuration)
|
||||
{
|
||||
if (!CurrentPlaylist || !PlayOrder.IsValidIndex(OrderCursor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 TrackIndex = PlayOrder[OrderCursor];
|
||||
USoundBase* Track = CurrentPlaylist->Tracks.IsValidIndex(TrackIndex) ? CurrentPlaylist->Tracks[TrackIndex].Get() : nullptr;
|
||||
if (!Track)
|
||||
{
|
||||
// La playlist a ete editee pendant la lecture : on reconstruit plutot que
|
||||
// de jouer un index perime.
|
||||
BuildPlayOrder();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MusicComponent)
|
||||
{
|
||||
MusicComponent = CreateMusicComponent(Track);
|
||||
if (!MusicComponent)
|
||||
{
|
||||
bIsPlaying = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MusicComponent->SetSound(Track);
|
||||
}
|
||||
|
||||
MusicComponent->SetVolumeMultiplier(GetEffectiveVolume());
|
||||
|
||||
if (FadeInDuration > 0.f)
|
||||
{
|
||||
MusicComponent->FadeIn(FadeInDuration, 1.f);
|
||||
}
|
||||
else
|
||||
{
|
||||
MusicComponent->Play();
|
||||
}
|
||||
|
||||
LastPlayedTrackIndex = TrackIndex;
|
||||
OnMusicTrackChanged.Broadcast(Track);
|
||||
}
|
||||
|
||||
UAudioComponent* UMusicSubsystem::CreateMusicComponent(USoundBase* Sound)
|
||||
{
|
||||
const UGameInstance* GameInstance = GetGameInstance();
|
||||
UWorld* World = GameInstance ? GameInstance->GetWorld() : nullptr;
|
||||
if (!World || !Sound)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// bPersistAcrossLevelTransition : le composant est cree sans monde ni acteur
|
||||
// proprietaire et marque bIgnoreForFlushing, le seul moyen de traverser un
|
||||
// OpenLevel sans que le peripherique audio ne le coupe.
|
||||
// bAutoDestroy a faux : on veut le reutiliser d'un morceau au suivant, et
|
||||
// surtout recevoir son OnAudioFinished plutot que de le voir disparaître.
|
||||
UAudioComponent* Component = UGameplayStatics::CreateSound2D(
|
||||
World, Sound,
|
||||
/*VolumeMultiplier*/ 1.f, /*PitchMultiplier*/ 1.f, /*StartTime*/ 0.f,
|
||||
/*ConcurrencySettings*/ nullptr,
|
||||
/*bPersistAcrossLevelTransition*/ true,
|
||||
/*bAutoDestroy*/ false);
|
||||
|
||||
if (!Component)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Component->bIsMusic = true;
|
||||
Component->OnAudioFinished.AddDynamic(this, &UMusicSubsystem::HandleTrackFinished);
|
||||
return Component;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::FadeOutAndRelease(TObjectPtr<UAudioComponent>& Component, float FadeOutDuration)
|
||||
{
|
||||
if (!Component)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Detacher AVANT d'arreter : Stop() comme FadeOut() finissent par rediffuser
|
||||
// OnAudioFinished, qui enchainerait sur le morceau suivant alors qu'on cherche
|
||||
// justement a couper.
|
||||
Component->OnAudioFinished.RemoveDynamic(this, &UMusicSubsystem::HandleTrackFinished);
|
||||
|
||||
// Une seule voie de sortie : si un croisement precedent n'est pas fini, il est
|
||||
// coupe net. Empiler les fondus ferait monter le nombre de voix pour rien.
|
||||
if (FadingOutComponent && FadingOutComponent != Component)
|
||||
{
|
||||
FadingOutComponent->Stop();
|
||||
FadingOutComponent = nullptr;
|
||||
}
|
||||
|
||||
if (FadeOutDuration > 0.f && Component->IsPlaying())
|
||||
{
|
||||
Component->FadeOut(FadeOutDuration, 0.f);
|
||||
FadingOutComponent = Component;
|
||||
}
|
||||
else
|
||||
{
|
||||
Component->Stop();
|
||||
}
|
||||
|
||||
Component = nullptr;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::ScheduleGap(float DelaySeconds)
|
||||
{
|
||||
CancelPendingGap();
|
||||
|
||||
TWeakObjectPtr<UMusicSubsystem> WeakThis(this);
|
||||
GapTickerHandle = FTSTicker::GetCoreTicker().AddTicker(TEXT("MusicSubsystem.Gap"), DelaySeconds,
|
||||
[WeakThis](float /*DeltaTime*/)
|
||||
{
|
||||
if (UMusicSubsystem* Self = WeakThis.Get())
|
||||
{
|
||||
Self->GapTickerHandle.Reset();
|
||||
Self->AdvanceAndPlay(0.f);
|
||||
}
|
||||
return false; // un seul declenchement
|
||||
});
|
||||
}
|
||||
|
||||
void UMusicSubsystem::CancelPendingGap()
|
||||
{
|
||||
if (GapTickerHandle.IsValid())
|
||||
{
|
||||
FTSTicker::RemoveTicker(GapTickerHandle);
|
||||
GapTickerHandle.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
float UMusicSubsystem::GetEffectiveVolume() const
|
||||
{
|
||||
return MusicVolume * (CurrentPlaylist ? CurrentPlaylist->VolumeMultiplier : 1.f);
|
||||
}
|
||||
Reference in New Issue
Block a user