Persistent music across levels
In one of my hobby projects, I needed some way of playing music across level transitions (if you’ve ever played Rocket League, you know exactly what I mean). However, I was unable to find any proper guide on how to do this. I found a way, and decided to make my own guide.
This is a very short and concrete example of how to initialize an audio component that you can use to play music across level transitions and across the lifetime of the game. I’ve made a deeper and more thorough explaining in my Persistent Music plugin deep dive snack which describes a full soundtrack system as the one in Rocket League.
Handling world initialization
Instead of manually placing a UAudioComponent in a level and setting it up properly, I decided to make a game instance subsystem.
This enables me to create the audio component once, and control its behaviour through said subsystem.
This requires a few moving parts though:
UCLASS(BlueprintType)
class MYGAME_API UMusicGameInstanceSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
// Subsystem lifecycle
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
private:
// World & initialization helpers
void HandlePostWorldInit(UWorld* World, const UWorld::InitializationValues IVS);
void HandleWorldBeginPlay();
// Music component
UPROPERTY(Transient)
TObjectPtr<UAudioComponent> MusicComponent = nullptr;
// State helper members
bool bPendingAutoPlay = true;
bool bAutoPlayHandled = false;
void UMusicGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
bPendingAutoPlay = true;
// Bind event to OnPostWorldInitialization
FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &ThisClass::HandlePostWorldInit);
}
void UMusicGameInstanceSubsystem::HandlePostWorldInit(UWorld* World, const UWorld::InitializationValues IVS)
{
if (!World || !World->IsGameWorld())
{
return;
}
if (World->GetGameInstance() != GetGameInstance())
{
return;
}
// Cache the UWorld and bind event to the world OnWorldBeginPlay
CachedGameWorld = World;
CachedGameWorld->OnWorldBeginPlay.AddUObject(this, &ThisClass::HandleWorldBeginPlay);
}
Playing the audio
Once the BeginPlay function is actually done on the UWorld, we can begin to play our music.
void UMusicGameInstanceSubsystem::HandleWorldBeginPlay()
{
// If we already processed auto play on game startup
if(bAutoPlayHandled) return;
// If we're not currently pending auto play on game startup
if(!bPendingAutoPlay) return;
bAutoPlayHandled = true;
#if WITH_EDITOR
// OPTIONAL: Suppress auto play in PIE
if (GetWorld()->WorldType == EWorldType::PIE)
{
bPendingAutoPlay = false;
PendingAutoPlaylist = nullptr;
return;
}
#endif
// Spawn 2D audio component
MusicComponent = UGameplayStatics::SpawnSound2D(
CachedGameWorld,
Sound, // Your actual sound
1.0f,
1.0f,
1.0f,
nullptr,
/*bPersistAcrossLevelTransition*/ true,
/*bAutoDestroy*/ false);
// Initial audio component setup
MusicComponent->bAllowSpatialization = false;
MusicComponent->Play();
// Reset auto play values
bPendingAutoPlay = false;
PendingAutoPlaylist = nullptr;
}