Persistent music across levels

Persistent music across levels

Sound

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:

MusicGameInstanceSubsystem.h
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;
MusicGameInstanceSubsystem.cpp
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.

MusicGameInstanceSubsystem.cpp
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;
}