Initial audio subystem for music testing and first song to loop over
This commit is contained in:
407
Source/ProjectEleri/Audio/EleriAudioSubsystem.cpp
Normal file
407
Source/ProjectEleri/Audio/EleriAudioSubsystem.cpp
Normal file
@@ -0,0 +1,407 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "EleriAudioSubsystem.h"
|
||||
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "ProjectEleri/GameEventSystem/GameEventDataAsset.h"
|
||||
#include "ProjectEleri/Settings/EleriGameSettings.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
#include "Sound/SoundClass.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
void UEleriAudioSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
// The rotation starts on its own, so that a level does not have to remember to ask for music. Safe
|
||||
// here because a game instance is handed the world it will run in before its subsystems are
|
||||
// initialised, so GetWorld() already answers with the world the music is going to live in.
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(SoundtrackTimerHandle, this, &UEleriAudioSubsystem::StartDefaultMusic, 1.f, false);
|
||||
}
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::Deinitialize()
|
||||
{
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(SoundtrackTimerHandle);
|
||||
}
|
||||
|
||||
if (UAudioComponent* Component = MusicComponent)
|
||||
{
|
||||
Component->OnAudioFinished.RemoveDynamic(this, &UEleriAudioSubsystem::HandleMusicFinished);
|
||||
Component->Stop();
|
||||
}
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
UEleriAudioSubsystem* UEleriAudioSubsystem::Get(const UObject* WorldContext)
|
||||
{
|
||||
if (WorldContext == nullptr || WorldContext->GetWorld() == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (UGameInstance* GameInstance = WorldContext->GetWorld()->GetGameInstance())
|
||||
{
|
||||
return GameInstance->GetSubsystem<UEleriAudioSubsystem>();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::PlayMusic(USoundBase* Cue, float FadeTime)
|
||||
{
|
||||
if (Cue == nullptr)
|
||||
{
|
||||
StopMusic(FadeTime);
|
||||
return;
|
||||
}
|
||||
|
||||
// An explicit request outranks whatever an event had put on, so the override is dropped and the
|
||||
// track the caller named becomes the one that plays.
|
||||
MusicOverride = nullptr;
|
||||
CancelPendingSoundtrack();
|
||||
|
||||
PlayMusicInternal(Cue, FadeTime);
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::OverrideMusic(USoundBase* Cue, float FadeTime)
|
||||
{
|
||||
if (Cue == nullptr)
|
||||
{
|
||||
ClearMusicOverride(FadeTime);
|
||||
return;
|
||||
}
|
||||
|
||||
// Recorded before the track starts, so that a track which finishes at once, a short one-shot for
|
||||
// instance, cannot be mistaken for the rotation's own and have the next one queued over it.
|
||||
MusicOverride = Cue;
|
||||
CancelPendingSoundtrack();
|
||||
|
||||
PlayMusicInternal(Cue, FadeTime);
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::ClearMusicOverride(float FadeTime)
|
||||
{
|
||||
if (MusicOverride == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MusicOverride = nullptr;
|
||||
CancelPendingSoundtrack();
|
||||
|
||||
// Fades the event's track out and a fresh rotation track in, through the same crossfade any other
|
||||
// music change uses, so coming back from an override sounds like every other track change.
|
||||
PlayMusicInternal(PickNextDefaultSoundtrack(), FadeTime);
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::PlayMusicForEvent(const UGameEventDataAsset* Event)
|
||||
{
|
||||
if (Event == nullptr || Event->MusicCue.IsNull())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Event->MusicCue.IsValid())
|
||||
{
|
||||
Event->MusicCue.LoadAsync(FLoadSoftObjectPathAsyncDelegate::CreateWeakLambda(this, [this, Event](const FSoftObjectPath& Path, UObject* Object)
|
||||
{
|
||||
PlayMusicForEvent(Event);
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
OverrideMusic(Event->MusicCue.Get());
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::StopMusic(float FadeTime)
|
||||
{
|
||||
CancelPendingSoundtrack();
|
||||
MusicOverride = nullptr;
|
||||
|
||||
if (UAudioComponent* Old = MusicComponent)
|
||||
{
|
||||
// Detached first: the fade-out will finish the component off, and that finish must not be read
|
||||
// as the current track playing out and put a new one on.
|
||||
Old->OnAudioFinished.RemoveDynamic(this, &UEleriAudioSubsystem::HandleMusicFinished);
|
||||
|
||||
const float EffectiveFade = FadeTime >= 0.f ? FadeTime : GetMusicFadeTime();
|
||||
if (EffectiveFade > 0.f)
|
||||
{
|
||||
Old->FadeOut(EffectiveFade, 0.f);
|
||||
Old->StopDelayed(EffectiveFade);
|
||||
}
|
||||
else
|
||||
{
|
||||
Old->Stop();
|
||||
}
|
||||
}
|
||||
|
||||
MusicComponent = nullptr;
|
||||
CurrentMusic = nullptr;
|
||||
OnMusicChanged.Broadcast(nullptr);
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::PlaySfx(USoundBase* Cue, float VolumeScale, float Pitch)
|
||||
{
|
||||
if (Cue == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Created rather than spawned for the same reason the music is: the sfx channel's class has to be
|
||||
// on the component before it plays, not after. The component destroys itself when the cue is done,
|
||||
// which is the whole lifetime an sfx needs.
|
||||
UAudioComponent* Component = UGameplayStatics::CreateSound2D(this, Cue, GetEffectiveVolume(EEleriAudioChannel::SFX) * VolumeScale, Pitch);
|
||||
if (Component == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Component->bIsUISound = false;
|
||||
Component->SoundClassOverride = GetRoutingSoundClass(EEleriAudioChannel::SFX);
|
||||
Component->Play();
|
||||
}
|
||||
|
||||
float UEleriAudioSubsystem::GetChannelVolume(EEleriAudioChannel Channel) const
|
||||
{
|
||||
switch (Channel)
|
||||
{
|
||||
case EEleriAudioChannel::Master:
|
||||
return MasterVolume;
|
||||
case EEleriAudioChannel::Music:
|
||||
return MusicVolume;
|
||||
case EEleriAudioChannel::SFX:
|
||||
return SfxVolume;
|
||||
default:
|
||||
return 1.f;
|
||||
}
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::SetChannelVolume(EEleriAudioChannel Channel, float Volume)
|
||||
{
|
||||
const float Clamped = FMath::Clamp(Volume, 0.f, 1.f);
|
||||
|
||||
switch (Channel)
|
||||
{
|
||||
case EEleriAudioChannel::Master:
|
||||
MasterVolume = Clamped;
|
||||
break;
|
||||
case EEleriAudioChannel::Music:
|
||||
MusicVolume = Clamped;
|
||||
break;
|
||||
case EEleriAudioChannel::SFX:
|
||||
SfxVolume = Clamped;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Music already playing is retargeted straight away, because the multiplier is the level a fade
|
||||
// moves towards and not something a fade would have to be restarted to change. Sfx already in
|
||||
// flight is left alone: it is a one-shot and will be gone before the change could be heard.
|
||||
if (UAudioComponent* Component = MusicComponent)
|
||||
{
|
||||
Component->SetVolumeMultiplier(GetEffectiveVolume(EEleriAudioChannel::Music));
|
||||
}
|
||||
}
|
||||
|
||||
float UEleriAudioSubsystem::GetEffectiveVolume(EEleriAudioChannel Channel) const
|
||||
{
|
||||
// Master multiplies rather than replaces, so a cue plays at Master * channel and the getters above
|
||||
// stay the plain per-channel numbers a menu would show rather than already-folded ones.
|
||||
if (Channel == EEleriAudioChannel::Master)
|
||||
{
|
||||
return MasterVolume;
|
||||
}
|
||||
|
||||
return GetChannelVolume(Channel) * MasterVolume;
|
||||
}
|
||||
|
||||
USoundClass* UEleriAudioSubsystem::GetRoutingSoundClass(EEleriAudioChannel Channel) const
|
||||
{
|
||||
const UEleriGameSettings* Settings = GetDefault<UEleriGameSettings>();
|
||||
if (Settings == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (const TSoftObjectPtr<USoundClass>* Found = Settings->AudioChannelSoundClasses.Find(Channel))
|
||||
{
|
||||
return Found->LoadSynchronous();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float UEleriAudioSubsystem::GetMusicFadeTime() const
|
||||
{
|
||||
const UEleriGameSettings* Settings = GetDefault<UEleriGameSettings>();
|
||||
return Settings != nullptr ? FMath::Max(0.f, Settings->MusicFadeTime) : 2.f;
|
||||
}
|
||||
|
||||
float UEleriAudioSubsystem::GetNextSoundtrackDelay() const
|
||||
{
|
||||
const UEleriGameSettings* Settings = GetDefault<UEleriGameSettings>();
|
||||
if (Settings == nullptr)
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
// A range rather than one number, so the gaps between tracks do not settle into a rhythm the ear
|
||||
// can predict and start listening for instead of the music.
|
||||
const float Low = FMath::Min(Settings->SoundtrackMinDelay, Settings->SoundtrackMaxDelay);
|
||||
const float High = FMath::Max(Settings->SoundtrackMinDelay, Settings->SoundtrackMaxDelay);
|
||||
return FMath::FRandRange(Low, High);
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::StartDefaultMusic()
|
||||
{
|
||||
if (MusicOverride != nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayMusicInternal(PickNextDefaultSoundtrack(), GetMusicFadeTime());
|
||||
}
|
||||
|
||||
USoundBase* UEleriAudioSubsystem::PickNextDefaultSoundtrack() const
|
||||
{
|
||||
const UEleriGameSettings* Settings = GetDefault<UEleriGameSettings>();
|
||||
if (Settings == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Both pools are built so that a soundtrack of one track still loops: leaving the current pick out
|
||||
// keeps the rotation from repeating itself, and when that would leave nothing to play the
|
||||
// exclusion is dropped rather than the music.
|
||||
TArray<USoundBase*> AllCues;
|
||||
TArray<USoundBase*> OtherCues;
|
||||
for (const TSoftObjectPtr<USoundBase>& SoftCue : Settings->DefaultSoundtracks)
|
||||
{
|
||||
if (USoundBase* Cue = SoftCue.LoadSynchronous())
|
||||
{
|
||||
AllCues.Add(Cue);
|
||||
if (Cue != CurrentMusic)
|
||||
{
|
||||
OtherCues.Add(Cue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TArray<USoundBase*>& Pool = OtherCues.IsEmpty() ? AllCues : OtherCues;
|
||||
if (Pool.IsEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Pool[FMath::RandRange(0, Pool.Num() - 1)];
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::CancelPendingSoundtrack()
|
||||
{
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(SoundtrackTimerHandle);
|
||||
}
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::PlayMusicInternal(USoundBase* Cue, float FadeTime)
|
||||
{
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Cue == nullptr)
|
||||
{
|
||||
StopMusic(FadeTime);
|
||||
return;
|
||||
}
|
||||
|
||||
const float EffectiveFade = FadeTime >= 0.f ? FadeTime : GetMusicFadeTime();
|
||||
|
||||
// The outgoing track is detached before it is faded, so that its own finish, which the fade-out
|
||||
// brings forward, cannot be read as the current track playing out and queue a new one over it.
|
||||
if (UAudioComponent* Old = MusicComponent)
|
||||
{
|
||||
Old->OnAudioFinished.RemoveDynamic(this, &UEleriAudioSubsystem::HandleMusicFinished);
|
||||
|
||||
if (EffectiveFade > 0.f)
|
||||
{
|
||||
Old->FadeOut(EffectiveFade, 0.f);
|
||||
Old->StopDelayed(EffectiveFade);
|
||||
}
|
||||
else
|
||||
{
|
||||
Old->Stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Created rather than spawned so the routing can be set before a single sample is heard, and so
|
||||
// the component is silent until the fade decides how loud it should fade up to.
|
||||
UAudioComponent* NewComponent = UGameplayStatics::CreateSound2D(this, Cue, GetEffectiveVolume(EEleriAudioChannel::Music));
|
||||
if (NewComponent == nullptr)
|
||||
{
|
||||
MusicComponent = nullptr;
|
||||
CurrentMusic = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
// The channel's class is what puts this cue where an artist would look for it in the mixer, and it
|
||||
// is deliberately the override and not the cue's own class, so that everything played as music ends
|
||||
// up under the music channel whatever it shipped with.
|
||||
NewComponent->bIsUISound = false;
|
||||
NewComponent->SoundClassOverride = GetRoutingSoundClass(EEleriAudioChannel::Music);
|
||||
NewComponent->OnAudioFinished.AddDynamic(this, &UEleriAudioSubsystem::HandleMusicFinished);
|
||||
|
||||
MusicComponent = NewComponent;
|
||||
CurrentMusic = Cue;
|
||||
|
||||
if (EffectiveFade > 0.f)
|
||||
{
|
||||
// Fades the fader and leaves the multiplier alone, so the level the setters keep in step with
|
||||
// stays the level the fade moves towards rather than something the fade overwrites.
|
||||
NewComponent->FadeIn(EffectiveFade, 1.f);
|
||||
}
|
||||
else
|
||||
{
|
||||
NewComponent->Play();
|
||||
}
|
||||
|
||||
OnMusicChanged.Broadcast(Cue);
|
||||
}
|
||||
|
||||
void UEleriAudioSubsystem::HandleMusicFinished()
|
||||
{
|
||||
if (MusicOverride != nullptr)
|
||||
{
|
||||
// The override belongs to whoever set it, so the rotation does not queue itself underneath it.
|
||||
return;
|
||||
}
|
||||
|
||||
// The gap is taken here, when the track has run out, rather than when it started, which is what
|
||||
// makes it a gap between tracks and not a cut-off partway through a long one.
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().SetTimer(SoundtrackTimerHandle, this, &UEleriAudioSubsystem::StartDefaultMusic, GetNextSoundtrackDelay(), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartDefaultMusic();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
173
Source/ProjectEleri/Audio/EleriAudioSubsystem.h
Normal file
173
Source/ProjectEleri/Audio/EleriAudioSubsystem.h
Normal file
@@ -0,0 +1,173 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
|
||||
#include "EleriAudioTypes.h"
|
||||
|
||||
#include "EleriAudioSubsystem.generated.h"
|
||||
|
||||
class UAudioComponent;
|
||||
class UGameEventDataAsset;
|
||||
class USoundBase;
|
||||
class USoundClass;
|
||||
|
||||
/** Fired with the cue that is playing now, or null when the music has stopped. */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMusicChanged, USoundBase*, Music);
|
||||
|
||||
/**
|
||||
* The one place the game asks for sound to come out of.
|
||||
*
|
||||
* Cues are handed to a channel (Music or SFX) rather than played directly, and each channel's cues are
|
||||
* routed through the sound class the project settings name for it. The subsystem owns one volume per
|
||||
* channel plus a master that multiplies the rest, and it is the only thing that reads them, so a menu
|
||||
* only ever has to talk to SetChannelVolume and the playing audio follows along.
|
||||
*
|
||||
* Music is more than a cue it was handed: when nothing has taken it over the subsystem rotates through
|
||||
* the default soundtracks in the settings, drawing the next one at random after a random gap, and it
|
||||
* remembers what is playing so a caller can ask. A gameplay event can take the music over with its own
|
||||
* cues, and the change in either direction is crossfaded.
|
||||
*/
|
||||
UCLASS()
|
||||
class PROJECTELERI_API UEleriAudioSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/** The subsystem for the game instance behind WorldContext, or null when there is not one yet. */
|
||||
static UEleriAudioSubsystem* Get(const UObject* WorldContext);
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Eleri|Audio")
|
||||
FOnMusicChanged OnMusicChanged;
|
||||
|
||||
// -- Music --
|
||||
|
||||
/**
|
||||
* Plays Cue as the music, crossfading away whatever was on. A call here is a command, so it also
|
||||
* drops any event override: the track the caller named is the one that plays. A null Cue is the
|
||||
* same as StopMusic.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Audio")
|
||||
void PlayMusic(USoundBase* Cue, float FadeTime = 1.f);
|
||||
|
||||
/**
|
||||
* Takes the music over with Cue, crossfading it in. The override stands until ClearMusicOverride,
|
||||
* unlike PlayMusic, and the default rotation stays quiet underneath it for as long as it does. A
|
||||
* null Cue is the same as ClearMusicOverride, so an event whose cues all failed to load can call
|
||||
* this and get its own music back.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Audio")
|
||||
void OverrideMusic(USoundBase* Cue, float FadeTime = 1.f);
|
||||
|
||||
/** Hands the music back to the default rotation, crossfading a fresh track in over the override. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Audio")
|
||||
void ClearMusicOverride(float FadeTime = 1.f);
|
||||
|
||||
/** Overrides the music with one of the event's cues, picked at random. Does nothing when the event
|
||||
* has no cues, which is the common case and is not an error. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Audio")
|
||||
void PlayMusicForEvent(const UGameEventDataAsset* Event);
|
||||
|
||||
/** Fades the music out and leaves it stopped, override and all. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Audio")
|
||||
void StopMusic(float FadeTime = 1.f);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Audio")
|
||||
USoundBase* GetCurrentMusic() const { return CurrentMusic; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Audio")
|
||||
bool IsMusicOverridden() const { return MusicOverride != nullptr; }
|
||||
|
||||
// -- SFX --
|
||||
|
||||
/** Plays Cue on the SFX channel. VolumeScale is a per-call trim on top of the channel's volume,
|
||||
* for a cue that wants to be quieter or louder than its neighbours without a settings entry. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Audio")
|
||||
void PlaySfx(USoundBase* Cue, float VolumeScale = 1.f, float Pitch = 1.f);
|
||||
|
||||
// -- Volumes --
|
||||
|
||||
/** The channel's own volume, before Master is folded in, which is the number a menu would show. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Audio")
|
||||
float GetChannelVolume(EEleriAudioChannel Channel) const;
|
||||
|
||||
/** Sets a channel's volume, clamped to 0..1. Not saved anywhere yet: it lasts for the session. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Eleri|Audio")
|
||||
void SetChannelVolume(EEleriAudioChannel Channel, float Volume);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Audio")
|
||||
float GetMasterVolume() const { return MasterVolume; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Audio")
|
||||
float GetMusicVolume() const { return MusicVolume; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri|Audio")
|
||||
float GetSfxVolume() const { return SfxVolume; }
|
||||
|
||||
protected:
|
||||
|
||||
/** Draws the next default soundtrack and plays it, unless an override owns the music. */
|
||||
void StartDefaultMusic();
|
||||
|
||||
/** A random default soundtrack, avoiding the one playing now unless that would leave nothing to
|
||||
* play, so a soundtrack of a single track still loops. Null when nothing is configured. */
|
||||
USoundBase* PickNextDefaultSoundtrack() const;
|
||||
|
||||
/**
|
||||
* The one that puts a cue on the music component: crossfades the old one out, routes the new one
|
||||
* through the music channel's class and fades it in. Null Cue stops the music. Shared by every
|
||||
* caller that changes the music so that none of them can change it differently.
|
||||
*/
|
||||
void PlayMusicInternal(USoundBase* Cue, float FadeTime);
|
||||
|
||||
/** Drops the pending next-track timer, which is what has to happen before an override, or a
|
||||
* rotation track, takes the music somewhere the timer did not plan for. */
|
||||
void CancelPendingSoundtrack();
|
||||
|
||||
/** Called by the current music component when its track plays out, so the rotation can queue the
|
||||
* next one after the configured gap. */
|
||||
UFUNCTION()
|
||||
void HandleMusicFinished();
|
||||
|
||||
/** The post-Master volume a channel actually plays at. */
|
||||
float GetEffectiveVolume(EEleriAudioChannel Channel) const;
|
||||
|
||||
/** The sound class a channel is routed through in the settings, or null when none is set, in
|
||||
* which case the cue plays on whatever class it shipped with. */
|
||||
USoundClass* GetRoutingSoundClass(EEleriAudioChannel Channel) const;
|
||||
|
||||
float GetMusicFadeTime() const;
|
||||
float GetNextSoundtrackDelay() const;
|
||||
|
||||
/** The track playing now, or the one still fading out, whichever is newer. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAudioComponent> MusicComponent;
|
||||
|
||||
/** What MusicComponent holds, kept apart from it because the component outlives nothing and the
|
||||
* question "what is playing" has an answer even between components. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<USoundBase> CurrentMusic;
|
||||
|
||||
/** Set only while a gameplay event has taken the music over. Non-null means the default rotation
|
||||
* is held back and the music is this instead. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<USoundBase> MusicOverride;
|
||||
|
||||
UPROPERTY()
|
||||
FTimerHandle SoundtrackTimerHandle;
|
||||
|
||||
UPROPERTY()
|
||||
float MasterVolume = 1.f;
|
||||
|
||||
UPROPERTY()
|
||||
float MusicVolume = 1.f;
|
||||
|
||||
UPROPERTY()
|
||||
float SfxVolume = 1.f;
|
||||
};
|
||||
24
Source/ProjectEleri/Audio/EleriAudioTypes.h
Normal file
24
Source/ProjectEleri/Audio/EleriAudioTypes.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "EleriAudioTypes.generated.h"
|
||||
|
||||
/**
|
||||
* The mixing channels every cue the game plays is routed through.
|
||||
*
|
||||
* The audio subsystem keeps one volume per channel and folds them into each other as Master * channel
|
||||
* rather than replacing one with the other, so pulling Master down takes the whole game with it while
|
||||
* a channel keeps its own setting. Which sound class each channel actually plays through is hooked up
|
||||
* in the Eleri Game Settings; Master has none, because it scales the other two rather than carrying a
|
||||
* sound of its own.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EEleriAudioChannel : uint8
|
||||
{
|
||||
Master UMETA(DisplayName = "Master"),
|
||||
Music UMETA(DisplayName = "Music"),
|
||||
SFX UMETA(DisplayName = "SFX")
|
||||
};
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
|
||||
class UTexture2D;
|
||||
class USoundBase;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EEventStage : uint8
|
||||
@@ -78,6 +79,12 @@ public:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Data")
|
||||
TArray<FTagValue> TagValueArray;
|
||||
|
||||
/** Soundtracks this event can put on. One is drawn at random and crossfaded in over whatever is
|
||||
* playing already, and it holds the music until the override is cleared. An event with none
|
||||
* leaves the music alone, which is what most of them will do. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Audio")
|
||||
TSoftObjectPtr<USoundBase> MusicCue;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Newspaper event")
|
||||
bool bIsNewspaperEvent;
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Newspaper event", meta = (EditCondition = "bIsNewspaperEvent", EditConditionHides))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "ProjectEleri/Audio/EleriAudioSubsystem.h"
|
||||
|
||||
void UGameEventSubsystem::Initialize(FSubsystemCollectionBase &Collection)
|
||||
{
|
||||
@@ -59,6 +60,17 @@ bool UGameEventSubsystem::StartEvent(FSoftObjectPath EventPath)
|
||||
EventData->EventStage = EEventStage::STARTED;
|
||||
else
|
||||
EventData->EventStage = EEventStage::COMPLETED;
|
||||
|
||||
// A starting event takes the music over with its own cues, when it has any. Done here rather
|
||||
// than at whatever happens to start an event, so that every caller changes the music the same
|
||||
// way and an event with no cues stays silent on the subject.
|
||||
if (EventData->EventStage == EEventStage::STARTED)
|
||||
{
|
||||
if (UEleriAudioSubsystem* AudioSubsystem = UEleriAudioSubsystem::Get(this))
|
||||
{
|
||||
AudioSubsystem->PlayMusicForEvent(EventData->EventPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Data/GameplayStatEffect.h"
|
||||
#include "Templates/SubclassOf.h"
|
||||
#include "UObject/SoftObjectPtr.h"
|
||||
#include "ProjectEleri/Audio/EleriAudioTypes.h"
|
||||
|
||||
#include "EleriGameSettings.generated.h"
|
||||
|
||||
@@ -17,6 +18,8 @@ class UNpcDataAsset;
|
||||
class UNpcScheduleAsset;
|
||||
class UItemDataAsset;
|
||||
class UGameplayEffect;
|
||||
class USoundBase;
|
||||
class USoundClass;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -56,6 +59,25 @@ public:
|
||||
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Materials")
|
||||
TSoftObjectPtr<UMaterialInterface> SpriteShadowMaterial;
|
||||
|
||||
/** Which sound class each channel's cues are routed through when the audio subsystem plays them.
|
||||
* Master has no entry because it carries no sound of its own, it scales the other two. */
|
||||
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Audio")
|
||||
TMap<EEleriAudioChannel, TSoftObjectPtr<USoundClass>> AudioChannelSoundClasses;
|
||||
|
||||
/** The soundtracks the audio subsystem rotates through while no event has taken the music over. */
|
||||
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Audio")
|
||||
TArray<TSoftObjectPtr<USoundBase>> DefaultSoundtracks;
|
||||
|
||||
/** Bounds of the random gap left between two default soundtracks, in seconds. */
|
||||
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Audio", meta = (ClampMin = "0.0"))
|
||||
float SoundtrackMinDelay = 20.f;
|
||||
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Audio", meta = (ClampMin = "0.0"))
|
||||
float SoundtrackMaxDelay = 60.f;
|
||||
|
||||
/** How long the audio subsystem takes to crossfade from one track to the next, in seconds. */
|
||||
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Audio", meta = (ClampMin = "0.0"))
|
||||
float MusicFadeTime = 2.f;
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Eleri Game Settingss")
|
||||
|
||||
Reference in New Issue
Block a user