213 lines
7.7 KiB
C++
213 lines
7.7 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "Containers/Ticker.h"
|
|
#include "Subsystems/GameInstanceSubsystem.h"
|
|
#include "MusicSubsystem.generated.h"
|
|
|
|
class UAudioComponent;
|
|
class UMusicPlaylistDataAsset;
|
|
class USoundBase;
|
|
|
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMusicTrackChanged, USoundBase*, NewTrack);
|
|
|
|
/**
|
|
* Ce qui arrive au morceau en cours quand on change de scene.
|
|
*
|
|
* La question ne se pose que parce que le lecteur, lui, ne s'arrete jamais :
|
|
* sans consigne, la musique du menu se poursuit telle quelle dans la partie, au
|
|
* milieu du morceau ou elle en etait.
|
|
*/
|
|
UENUM(BlueprintType)
|
|
enum class EMusicLevelChangeMode : uint8
|
|
{
|
|
/** Le morceau en cours poursuit sa route, comme si de rien n'etait. */
|
|
Continue UMETA(DisplayName = "Continue"),
|
|
|
|
/**
|
|
* Le morceau en cours s'eteint et le suivant demarre : nouveau tirage si
|
|
* l'aleatoire est actif, morceau suivant de la liste sinon. La playlist, elle,
|
|
* ne change pas.
|
|
*/
|
|
New UMETA(DisplayName = "New")
|
|
};
|
|
|
|
/**
|
|
* Le lecteur de musique du jeu. Un seul, pour toute la duree de l'application.
|
|
*
|
|
* UGameInstanceSubsystem et pas un acteur : il naît avec le GameInstance et meurt
|
|
* avec le jeu, exactement comme USurvivalGameInstance. Aucune map ne le possede,
|
|
* donc aucun OpenLevel ne le detruit -- c'est le seul endroit d'ou une musique
|
|
* peut traverser le passage menu -> partie sans se couper. Le moteur l'instancie
|
|
* tout seul, il n'y a rien a poser dans une scene ni a cocher nulle part.
|
|
*
|
|
* Le composant audio lui-meme est cree via CreateSound2D avec
|
|
* bPersistAcrossLevelTransition : le moteur le construit alors SANS monde et sans
|
|
* proprietaire, et lui pose bIgnoreForFlushing -- le drapeau que le vidage du
|
|
* peripherique audio, declenche a chaque changement de niveau, saute
|
|
* explicitement. Sans ce parametre le son s'arreterait net au chargement, meme si
|
|
* l'objet C++, lui, survivait.
|
|
*
|
|
* Effet de bord voulu : ces composants sont marques bIsUISound, donc ils
|
|
* continuent de jouer quand le jeu est en pause. Le menu de pause garde sa
|
|
* musique sans une ligne de plus.
|
|
*/
|
|
UCLASS()
|
|
class EMBERWILD_API UMusicSubsystem : public UGameInstanceSubsystem
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
|
virtual void Deinitialize() override;
|
|
|
|
/** Raccourci depuis n'importe quel objet ayant un monde. */
|
|
UFUNCTION(BlueprintPure, Category = "Musique", meta = (WorldContext = "WorldContextObject", DisplayName = "Get Music Subsystem"))
|
|
static UMusicSubsystem* Get(const UObject* WorldContextObject);
|
|
|
|
/**
|
|
* Demarre une playlist.
|
|
*
|
|
* Si c'est DEJA celle qui joue, ne fait rien : c'est ce comportement qui rend
|
|
* la transition menu -> jeu inaudible quand les deux maps designent la meme
|
|
* playlist. Si c'en est une autre, croisement entre l'ancienne qui s'eteint et
|
|
* la nouvelle qui entre.
|
|
*
|
|
* @param bForceRestart Repart du debut meme si la playlist est deja en cours.
|
|
*/
|
|
UFUNCTION(BlueprintCallable, Category = "Musique")
|
|
void PlayPlaylist(UMusicPlaylistDataAsset* Playlist, bool bForceRestart = false);
|
|
|
|
/** @param FadeOutDuration Negatif = utiliser le fondu declare par la playlist. */
|
|
UFUNCTION(BlueprintCallable, Category = "Musique")
|
|
void StopMusic(float FadeOutDuration = -1.f);
|
|
|
|
/** Passe au morceau suivant, dans l'ordre courant (aleatoire ou non). */
|
|
UFUNCTION(BlueprintCallable, Category = "Musique")
|
|
void SkipToNextTrack();
|
|
|
|
/**
|
|
* Bascule aleatoire / ordre de la liste.
|
|
*
|
|
* Ne coupe jamais le morceau en cours : seul l'enchainement change. Le curseur
|
|
* se recale sur le morceau qui joue, donc en repassant en mode ordonne la
|
|
* lecture reprend juste apres lui au lieu de sauter au debut de la liste.
|
|
*/
|
|
UFUNCTION(BlueprintCallable, Category = "Musique")
|
|
void SetShuffleEnabled(bool bEnabled);
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Musique")
|
|
bool IsShuffleEnabled() const { return bShuffleEnabled; }
|
|
|
|
/**
|
|
* Ce que devient le morceau en cours au prochain changement de scene.
|
|
*
|
|
* La consigne vit ICI et pas sur l'acteur qui l'a posee : l'acteur meurt avec
|
|
* sa map, alors que le changement de scene se produit justement apres. Une
|
|
* seule map -- le menu -- suffit donc a la declarer pour tout le jeu.
|
|
*/
|
|
UFUNCTION(BlueprintCallable, Category = "Musique")
|
|
void SetLevelChangeMode(EMusicLevelChangeMode Mode) { LevelChangeMode = Mode; }
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Musique")
|
|
EMusicLevelChangeMode GetLevelChangeMode() const { return LevelChangeMode; }
|
|
|
|
/**
|
|
* Attenuation propre au LECTEUR, 0 a 1. Ce n'est PAS le reglage du joueur.
|
|
*
|
|
* Le volume choisi dans les reglages passe par la SoundClass de musique (voir
|
|
* USurvivalAudioSettings), pas par ici : deux chemins vers le meme volume
|
|
* finiraient par se multiplier sans que personne ne s'en souvienne. Celui-ci
|
|
* reste disponible pour ce qui est de l'ordre du gameplay -- baisser la musique
|
|
* pendant un dialogue, par exemple.
|
|
*/
|
|
UFUNCTION(BlueprintCallable, Category = "Musique")
|
|
void SetMusicVolume(float NewVolume);
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Musique")
|
|
float GetMusicVolume() const { return MusicVolume; }
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Musique")
|
|
bool IsPlaying() const { return bIsPlaying; }
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Musique")
|
|
USoundBase* GetCurrentTrack() const;
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Musique")
|
|
UMusicPlaylistDataAsset* GetCurrentPlaylist() const { return CurrentPlaylist; }
|
|
|
|
/** Diffuse a chaque changement de morceau, nullptr a l'arret. */
|
|
UPROPERTY(BlueprintAssignable, Category = "Musique")
|
|
FOnMusicTrackChanged OnMusicTrackChanged;
|
|
|
|
private:
|
|
/** Fin NATURELLE d'un morceau uniquement -- voir FadeOutAndRelease. */
|
|
UFUNCTION()
|
|
void HandleTrackFinished();
|
|
|
|
/** Applique LevelChangeMode a chaque map chargee. */
|
|
void HandlePostLoadMap(UWorld* LoadedWorld);
|
|
|
|
/** (Re)construit l'ordre de lecture du cycle a venir. */
|
|
void BuildPlayOrder();
|
|
|
|
void AdvanceAndPlay(float FadeInDuration);
|
|
void PlayTrackAtCursor(float FadeInDuration);
|
|
UAudioComponent* CreateMusicComponent(USoundBase* Sound);
|
|
|
|
/**
|
|
* Detache puis eteint un composant : apres cet appel, plus rien de ce qu'il
|
|
* diffuse ne revient vers nous.
|
|
*/
|
|
void FadeOutAndRelease(TObjectPtr<UAudioComponent>& Component, float FadeOutDuration);
|
|
|
|
void ScheduleGap(float DelaySeconds);
|
|
void CancelPendingGap();
|
|
float GetEffectiveVolume() const;
|
|
|
|
UPROPERTY()
|
|
TObjectPtr<UMusicPlaylistDataAsset> CurrentPlaylist;
|
|
|
|
/**
|
|
* UPROPERTY obligatoire : ces composants n'ont ni monde ni acteur proprietaire,
|
|
* cette reference est la SEULE qui les tienne hors de portee du ramasse-miettes.
|
|
*/
|
|
UPROPERTY()
|
|
TObjectPtr<UAudioComponent> MusicComponent;
|
|
|
|
/** La voie de sortie, le temps d'un croisement. Une seule a la fois. */
|
|
UPROPERTY()
|
|
TObjectPtr<UAudioComponent> FadingOutComponent;
|
|
|
|
/**
|
|
* Index dans Tracks, deja melangee si l'aleatoire est actif.
|
|
*
|
|
* Un ordre pre-melange plutot qu'un tirage a chaque morceau : le tirage
|
|
* independant rejoue le meme titre deux fois de suite une fois sur N, et sur
|
|
* une playlist de cinq morceaux ca s'entend tout de suite.
|
|
*/
|
|
TArray<int32> PlayOrder;
|
|
|
|
int32 OrderCursor = INDEX_NONE;
|
|
int32 LastPlayedTrackIndex = INDEX_NONE;
|
|
|
|
bool bShuffleEnabled = true;
|
|
bool bIsPlaying = false;
|
|
float MusicVolume = 1.f;
|
|
|
|
EMusicLevelChangeMode LevelChangeMode = EMusicLevelChangeMode::Continue;
|
|
|
|
/** Abonnement au chargement de map, retire a la fermeture du jeu. */
|
|
FDelegateHandle PostLoadMapHandle;
|
|
|
|
/**
|
|
* Le silence entre deux morceaux passe par le ticker de Core, pas par un
|
|
* FTimerHandle : un timer appartient au monde, donc il disparaîtrait au
|
|
* chargement de la map suivante -- et il ne tourne pas en pause. Le ticker de
|
|
* Core ignore les deux.
|
|
*/
|
|
FTSTicker::FDelegateHandle GapTickerHandle;
|
|
};
|