Simple cutscene manager and a test cutscene
This commit is contained in:
Binary file not shown.
Binary file not shown.
BIN
Content/LevelSequence/Quest/LS_Quest_WarmWelcome.uasset
LFS
Normal file
BIN
Content/LevelSequence/Quest/LS_Quest_WarmWelcome.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/UI/MainUI.uasset
LFS
BIN
Content/UI/MainUI.uasset
LFS
Binary file not shown.
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "DialogueComponent.h"
|
#include "DialogueComponent.h"
|
||||||
|
|
||||||
|
#include "DialogueSceneSubsystem.h"
|
||||||
#include "DialogueSubsystem.h"
|
#include "DialogueSubsystem.h"
|
||||||
#include "DialogueWidget.h"
|
#include "DialogueWidget.h"
|
||||||
#include "EleriPlayerController.h"
|
#include "EleriPlayerController.h"
|
||||||
@@ -35,6 +36,22 @@ void UDialogueComponent::BeginPlay()
|
|||||||
EleriPlayerController = Cast<AEleriPlayerController>(UGameplayStatics::GetPlayerController(this, 0));
|
EleriPlayerController = Cast<AEleriPlayerController>(UGameplayStatics::GetPlayerController(this, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UDialogueComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||||
|
{
|
||||||
|
// A component taken down mid conversation still has to let go of a cutscene it is holding: the
|
||||||
|
// sequence would otherwise be left paused on its hold point for good, and the controls it took would
|
||||||
|
// belong to a scene that nobody is left to finish.
|
||||||
|
ReleaseHeldScene();
|
||||||
|
|
||||||
|
if (UDialogueSceneSubsystem* SceneSubsystem = UDialogueSceneSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
SceneSubsystem->OnSceneHeld.RemoveAll(this);
|
||||||
|
SceneSubsystem->OnSceneFinished.RemoveAll(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Super::EndPlay(EndPlayReason);
|
||||||
|
}
|
||||||
|
|
||||||
void UDialogueComponent::OnDialogueLoaded()
|
void UDialogueComponent::OnDialogueLoaded()
|
||||||
{
|
{
|
||||||
if(UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(GetWorld()))
|
if(UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(GetWorld()))
|
||||||
@@ -77,6 +94,35 @@ void UDialogueComponent::TriggerDialogue_Async(TSoftObjectPtr<USpeechDataAsset>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UDialogueComponent::TriggerDialogue(USpeechDataAsset* DialogueAssetPath, const TMap<FString, FText>& ReplacementMap)
|
void UDialogueComponent::TriggerDialogue(USpeechDataAsset* DialogueAssetPath, const TMap<FString, FText>& ReplacementMap)
|
||||||
|
{
|
||||||
|
if (DialogueAssetPath == nullptr) return;
|
||||||
|
|
||||||
|
// A dialogue can be a cutscene. Two shapes of that are supported, and the asset picks between them
|
||||||
|
// with its hold marker: a scene that plays through and hands over at its end, and a scene that plays
|
||||||
|
// as far as its hold point and stays there while the conversation happens inside it.
|
||||||
|
const EDialogueSceneStart SceneStart = StartDialogueScene(DialogueAssetPath, EDialogueSceneTiming::OnDialogueStart);
|
||||||
|
|
||||||
|
if (SceneStart != EDialogueSceneStart::None && DialogueAssetPath->bHoldDialogueForScene)
|
||||||
|
{
|
||||||
|
PendingDialogue = DialogueAssetPath;
|
||||||
|
PendingDialogueReplacementMap = ReplacementMap;
|
||||||
|
|
||||||
|
// Both beats are listened for on the off chance the other one arrives: a held scene reports its
|
||||||
|
// hold and later its end, and a scene that plays through only ever reports its end. Whichever
|
||||||
|
// comes first starts the dialogue, and StartPendingDialogue lets go of both.
|
||||||
|
if (UDialogueSceneSubsystem* SceneSubsystem = UDialogueSceneSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
SceneSubsystem->OnSceneHeld.AddUniqueDynamic(this, &UDialogueComponent::OnDialogueSceneHeld);
|
||||||
|
SceneSubsystem->OnSceneFinished.AddUniqueDynamic(this, &UDialogueComponent::OnDialogueSceneFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartDialogueInternal(DialogueAssetPath, ReplacementMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueComponent::StartDialogueInternal(USpeechDataAsset* DialogueAssetPath, const TMap<FString, FText>& ReplacementMap)
|
||||||
{
|
{
|
||||||
UDialogueSubsystem* DialogueSubsystem = UDialogueSubsystem::Get(this);
|
UDialogueSubsystem* DialogueSubsystem = UDialogueSubsystem::Get(this);
|
||||||
if (!DialogueSubsystem) return;
|
if (!DialogueSubsystem) return;
|
||||||
@@ -140,11 +186,27 @@ void UDialogueComponent::CloseDialogue()
|
|||||||
if (!DialogueSubsystem) return;
|
if (!DialogueSubsystem) return;
|
||||||
DialogueSubsystem->EndDialogue();
|
DialogueSubsystem->EndDialogue();
|
||||||
|
|
||||||
|
// Held before the state below is cleared, because a scene that closes a conversation is played from
|
||||||
|
// the asset that is about to be forgotten.
|
||||||
|
const USpeechDataAsset* ClosedDialogue = CurrentActiveDialogue;
|
||||||
|
|
||||||
|
// A cutscene this conversation was happening inside of is let go of before anything else is tried:
|
||||||
|
// the sequence carries on from its hold point and plays its own tail, and while it is doing that no
|
||||||
|
// other scene can start anyway, which is why an end scene below is allowed to be refused.
|
||||||
|
ReleaseHeldScene();
|
||||||
|
|
||||||
SelectedDialogueOptionIndex = 0;
|
SelectedDialogueOptionIndex = 0;
|
||||||
CurrentLineIndex = 0;
|
CurrentLineIndex = 0;
|
||||||
CurrentActiveDialogue = nullptr;
|
CurrentActiveDialogue = nullptr;
|
||||||
UnbindControllerInput();
|
UnbindControllerInput();
|
||||||
EleriPlayerController->ToggleDialogueWindow(false);
|
EleriPlayerController->ToggleDialogueWindow(false);
|
||||||
|
|
||||||
|
// Played last, once the window is down and the conversation is over, so the cutscene is what the
|
||||||
|
// player is left watching rather than something the dialogue box sits on top of.
|
||||||
|
if (ClosedDialogue != nullptr)
|
||||||
|
{
|
||||||
|
StartDialogueScene(ClosedDialogue, EDialogueSceneTiming::OnDialogueEnd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDialogueComponent::AdvanceDialogue()
|
void UDialogueComponent::AdvanceDialogue()
|
||||||
@@ -276,5 +338,93 @@ TArray<FText> UDialogueComponent::GetDialogueOptionsFromPlayerSpeakList(const TA
|
|||||||
return Options;
|
return Options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EDialogueSceneStart UDialogueComponent::StartDialogueScene(const USpeechDataAsset* DialogueAsset, EDialogueSceneTiming Timing)
|
||||||
|
{
|
||||||
|
if (DialogueAsset == nullptr || DialogueAsset->Scene.IsNull() || DialogueAsset->SceneTiming != Timing)
|
||||||
|
{
|
||||||
|
return EDialogueSceneStart::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
UDialogueSceneSubsystem* SceneSubsystem = UDialogueSceneSubsystem::Get(this);
|
||||||
|
if (SceneSubsystem == nullptr)
|
||||||
|
{
|
||||||
|
return EDialogueSceneStart::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The actor the component sits on goes in as the instigator, so a listener can tell which NPC a
|
||||||
|
// cutscene belongs to without having to remember what it asked for.
|
||||||
|
//
|
||||||
|
// A scene with a hold marker is asked to hold, and one without is asked to play through: that choice
|
||||||
|
// on the asset is the whole difference between a conversation that happens inside a cutscene and one
|
||||||
|
// that happens after it.
|
||||||
|
if (!DialogueAsset->SceneHoldMarker.IsNone())
|
||||||
|
{
|
||||||
|
return SceneSubsystem->PlaySceneHeld(DialogueAsset->Scene, DialogueAsset->SceneHoldMarker, GetOwner())
|
||||||
|
? EDialogueSceneStart::StartedHeld
|
||||||
|
: EDialogueSceneStart::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SceneSubsystem->PlayScene(DialogueAsset->Scene, GetOwner())
|
||||||
|
? EDialogueSceneStart::Started
|
||||||
|
: EDialogueSceneStart::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueComponent::OnDialogueSceneFinished(ULevelSequence* Scene, AActor* Instigator)
|
||||||
|
{
|
||||||
|
bHoldingSceneForDialogue = false;
|
||||||
|
StartPendingDialogue();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueComponent::OnDialogueSceneHeld(ULevelSequence* Scene, AActor* Instigator)
|
||||||
|
{
|
||||||
|
// The scene is paused on its hold point and is staying there, so the conversation runs inside the
|
||||||
|
// cutscene and CloseDialogue is what lets it go again.
|
||||||
|
bHoldingSceneForDialogue = true;
|
||||||
|
StartPendingDialogue();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueComponent::StartPendingDialogue()
|
||||||
|
{
|
||||||
|
// Both scene events are let go of first: waiting behind a scene is a one-off, and a dialogue started
|
||||||
|
// from here may well have a scene of its own that must not be swallowed by a stale binding.
|
||||||
|
if (UDialogueSceneSubsystem* SceneSubsystem = UDialogueSceneSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
SceneSubsystem->OnSceneHeld.RemoveDynamic(this, &UDialogueComponent::OnDialogueSceneHeld);
|
||||||
|
SceneSubsystem->OnSceneFinished.RemoveDynamic(this, &UDialogueComponent::OnDialogueSceneFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
USpeechDataAsset* WaitingDialogue = PendingDialogue;
|
||||||
|
PendingDialogue = nullptr;
|
||||||
|
|
||||||
|
// A scene that was not holding a dialogue still comes through here, because the bindings are made
|
||||||
|
// before the manager decides whether it can play anything. Waiting on nothing is the ordinary case.
|
||||||
|
if (WaitingDialogue == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TMap<FString, FText> ReplacementMap = PendingDialogueReplacementMap;
|
||||||
|
PendingDialogueReplacementMap.Reset();
|
||||||
|
|
||||||
|
StartDialogueInternal(WaitingDialogue, ReplacementMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueComponent::ReleaseHeldScene()
|
||||||
|
{
|
||||||
|
if (!bHoldingSceneForDialogue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bHoldingSceneForDialogue = false;
|
||||||
|
|
||||||
|
// Letting the sequence go is all that is needed: it resumes from its hold point, plays its tail, and
|
||||||
|
// stops at the end of it, which is what returns the player's camera and their controls.
|
||||||
|
if (UDialogueSceneSubsystem* SceneSubsystem = UDialogueSceneSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
SceneSubsystem->ResumeScene();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#undef LOCTEXT_NAMESPACE
|
#undef LOCTEXT_NAMESPACE
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,26 @@
|
|||||||
struct FPlayerSpeakToDialogue;
|
struct FPlayerSpeakToDialogue;
|
||||||
class AEleriPlayerController;
|
class AEleriPlayerController;
|
||||||
class UDialogueWidget;
|
class UDialogueWidget;
|
||||||
|
class ULevelSequence;
|
||||||
class UWidgetComponent;
|
class UWidgetComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What happened when a dialogue asked for its scene, which is also the question "what is the dialogue
|
||||||
|
* waiting behind, if anything?".
|
||||||
|
*/
|
||||||
|
enum class EDialogueSceneStart : uint8
|
||||||
|
{
|
||||||
|
/** No scene was due, or the one that was due could not start: the dialogue runs on its own */
|
||||||
|
None,
|
||||||
|
|
||||||
|
/** The scene is playing through, so the dialogue waits for its end */
|
||||||
|
Started,
|
||||||
|
|
||||||
|
/** The scene has been asked to hold, so the dialogue waits for the hold point and then runs inside
|
||||||
|
* the cutscene. The scene is let go again when the dialogue ends. */
|
||||||
|
StartedHeld
|
||||||
|
};
|
||||||
|
|
||||||
UCLASS(BlueprintType, Blueprintable)
|
UCLASS(BlueprintType, Blueprintable)
|
||||||
class PROJECTELERI_API UDialogueComponent : public UActorComponent
|
class PROJECTELERI_API UDialogueComponent : public UActorComponent
|
||||||
{
|
{
|
||||||
@@ -49,6 +67,32 @@ protected:
|
|||||||
void TriggerDialogueSelect(TArray<FText>& DialogueOptions, bool bIsDialogueStart);
|
void TriggerDialogueSelect(TArray<FText>& DialogueOptions, bool bIsDialogueStart);
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void OnDialogueOptionSelected(int32 Index);
|
void OnDialogueOptionSelected(int32 Index);
|
||||||
|
/** Runs a dialogue. TriggerDialogue hands over to this once any scene in front of the dialogue has
|
||||||
|
* had its turn, so the conversation is set up in one place whichever way it was reached. */
|
||||||
|
void StartDialogueInternal(USpeechDataAsset* DialogueAsset, const TMap<FString, FText>& ReplacementMap);
|
||||||
|
|
||||||
|
/** Plays the scene the asset names, when it names one and it is due at Timing, and says what the
|
||||||
|
* dialogue is now waiting behind. None means nothing started and the dialogue carries on. */
|
||||||
|
EDialogueSceneStart StartDialogueScene(const USpeechDataAsset* DialogueAsset, EDialogueSceneTiming Timing);
|
||||||
|
|
||||||
|
/** A scene the dialogue was waiting behind has finished, so the dialogue starts. Bound on
|
||||||
|
* TriggerDialogue and let go of here, which is what keeps the wait to one conversation. */
|
||||||
|
UFUNCTION()
|
||||||
|
void OnDialogueSceneFinished(ULevelSequence* Scene, AActor* Instigator);
|
||||||
|
|
||||||
|
/** A scene the dialogue was waiting behind has reached its hold point, so the dialogue starts inside
|
||||||
|
* the cutscene. The scene stays where it is until the dialogue ends and lets it go. */
|
||||||
|
UFUNCTION()
|
||||||
|
void OnDialogueSceneHeld(ULevelSequence* Scene, AActor* Instigator);
|
||||||
|
|
||||||
|
/** Starts whatever dialogue was left waiting behind a scene, whichever beat it was waiting for, and
|
||||||
|
* lets go of both scene events so only one conversation can ever be waiting at a time. */
|
||||||
|
void StartPendingDialogue();
|
||||||
|
|
||||||
|
/** Lets go of a scene this dialogue put on hold, so the cutscene finishes and the player gets their
|
||||||
|
* controls back. Does nothing when this dialogue is not holding anything. */
|
||||||
|
void ReleaseHeldScene();
|
||||||
|
|
||||||
static TArray<FText> GetPlayerAsksFromDialogueList(const TArray<USpeechDataAsset*>& DialogueList);
|
static TArray<FText> GetPlayerAsksFromDialogueList(const TArray<USpeechDataAsset*>& DialogueList);
|
||||||
static TArray<FText> GetDialogueOptionsFromPlayerSpeakList(const TArray<FPlayerSpeakToDialogue>& SpeakList);
|
static TArray<FText> GetDialogueOptionsFromPlayerSpeakList(const TArray<FPlayerSpeakToDialogue>& SpeakList);
|
||||||
|
|
||||||
@@ -78,11 +122,27 @@ protected:
|
|||||||
TMap<int32, FPlayerSpeakToDialogue> MappedDialogueOptions;
|
TMap<int32, FPlayerSpeakToDialogue> MappedDialogueOptions;
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
TMap<FString, FText> CurrentDialogueReplacementMap;
|
TMap<FString, FText> CurrentDialogueReplacementMap;
|
||||||
|
/** The dialogue held behind a scene that is still playing, and the replacement map it was asked
|
||||||
|
* for with. Kept rather than re-derived, because the caller that asked for the dialogue is not
|
||||||
|
* around by the time the cutscene ends. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<USpeechDataAsset> PendingDialogue = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TMap<FString, FText> PendingDialogueReplacementMap;
|
||||||
|
|
||||||
|
/** True while this component is what is holding a cutscene open. It is what tells CloseDialogue
|
||||||
|
* that the scene has a tail to play before the conversation is really over. */
|
||||||
|
bool bHoldingSceneForDialogue = false;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Called when the game starts
|
// Called when the game starts
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
/** A component that is taken down mid conversation still has to let go of a cutscene it is holding,
|
||||||
|
* or the player would be left standing in a paused shot with no controls and no way back. */
|
||||||
|
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void OnDialogueLoaded();
|
void OnDialogueLoaded();
|
||||||
|
|
||||||
|
|||||||
446
Source/ProjectEleri/DialogueSystem/DialogueSceneSubsystem.cpp
Normal file
446
Source/ProjectEleri/DialogueSystem/DialogueSceneSubsystem.cpp
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "DialogueSceneSubsystem.h"
|
||||||
|
|
||||||
|
#include "EleriPlayerController.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "LevelSequence.h"
|
||||||
|
#include "LevelSequenceActor.h"
|
||||||
|
#include "LevelSequencePlayer.h"
|
||||||
|
#include "MovieScene.h"
|
||||||
|
#include "MovieSceneSequencePlaybackSettings.h"
|
||||||
|
|
||||||
|
UDialogueSceneSubsystem* UDialogueSceneSubsystem::Get(const UObject* WorldContext)
|
||||||
|
{
|
||||||
|
const UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(WorldContext);
|
||||||
|
return GameInstance != nullptr ? GameInstance->GetSubsystem<UDialogueSceneSubsystem>() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||||
|
{
|
||||||
|
Super::Initialize(Collection);
|
||||||
|
|
||||||
|
WorldCleanupHandle = FWorldDelegates::OnWorldCleanup.AddUObject(
|
||||||
|
this, &UDialogueSceneSubsystem::HandleWorldCleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::Deinitialize()
|
||||||
|
{
|
||||||
|
if (WorldCleanupHandle.IsValid())
|
||||||
|
{
|
||||||
|
FWorldDelegates::OnWorldCleanup.Remove(WorldCleanupHandle);
|
||||||
|
WorldCleanupHandle.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing here is stopped on the way out: the actor and the player live in a world that is being
|
||||||
|
// destroyed, and the controller this would hand controls back to is going with it. What is left to
|
||||||
|
// do is forget them, which is what keeps a scene that was running from being reported as still
|
||||||
|
// running in the next world.
|
||||||
|
ResetSceneTracking();
|
||||||
|
ActiveSequenceActor = nullptr;
|
||||||
|
|
||||||
|
Super::Deinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
UWorld* UDialogueSceneSubsystem::GetSceneWorld() const
|
||||||
|
{
|
||||||
|
const UGameInstance* GameInstance = GetGameInstance();
|
||||||
|
return GameInstance != nullptr ? GameInstance->GetWorld() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
APlayerController* UDialogueSceneSubsystem::GetScenePlayerController() const
|
||||||
|
{
|
||||||
|
UWorld* World = GetSceneWorld();
|
||||||
|
return World != nullptr ? World->GetFirstPlayerController() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDialogueSceneSubsystem::PlayScene(TSoftObjectPtr<ULevelSequence> Scene, AActor* Instigator, bool bDisablePlayerControl)
|
||||||
|
{
|
||||||
|
// One scene at a time. Two sequences at once would both be driving the camera and the player's
|
||||||
|
// actor, and whichever ended first would hand the controls back in the middle of the other one. So
|
||||||
|
// a request that arrives while a scene is running is refused rather than layered on top of it, and
|
||||||
|
// the caller carries on as though it had never asked. This is also what keeps a quest that changes
|
||||||
|
// status twice in one frame from cutting its own cutscene short.
|
||||||
|
if (bScenePlaying)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[DialogueScene] '%s' was asked for while '%s' is still playing, so it was not started."),
|
||||||
|
*Scene.ToString(),
|
||||||
|
ActiveScene != nullptr ? *ActiveScene->GetName() : TEXT("<unknown>"));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Scene.IsNull())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocking on the load is deliberate. The caller is asking for a cutscene it is about to hold
|
||||||
|
// everything up for anyway, and returning false here would leave a dialogue waiting on a scene
|
||||||
|
// that is never going to start. The object is already in memory whenever the caller got it from an
|
||||||
|
// asset it has loaded, which is the ordinary case.
|
||||||
|
ULevelSequence* LoadedScene = Scene.LoadSynchronous();
|
||||||
|
if (LoadedScene == nullptr)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("[DialogueScene] '%s' could not be loaded, so no scene was played."),
|
||||||
|
*Scene.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StartScene(LoadedScene, NAME_None, Instigator, bDisablePlayerControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDialogueSceneSubsystem::PlaySceneHeld(TSoftObjectPtr<ULevelSequence> Scene, FName HoldMarker, AActor* Instigator, bool bDisablePlayerControl)
|
||||||
|
{
|
||||||
|
if (bScenePlaying)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[DialogueScene] '%s' was asked for, held, while '%s' is still playing, so it was not started."),
|
||||||
|
*Scene.ToString(),
|
||||||
|
ActiveScene != nullptr ? *ActiveScene->GetName() : TEXT("<unknown>"));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Scene.IsNull())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULevelSequence* LoadedScene = Scene.LoadSynchronous();
|
||||||
|
if (LoadedScene == nullptr)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("[DialogueScene] '%s' could not be loaded, so no scene was played."),
|
||||||
|
*Scene.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hold that cannot be resolved is dropped rather than passed on: the scene still plays, it just
|
||||||
|
// plays through, which is a cutscene with a missing beat instead of a cutscene that rewinds.
|
||||||
|
FName ResolvedHold = HoldMarker;
|
||||||
|
if (!HoldMarker.IsNone() && !TryResolveHoldMarker(LoadedScene, HoldMarker))
|
||||||
|
{
|
||||||
|
ResolvedHold = NAME_None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StartScene(LoadedScene, ResolvedHold, Instigator, bDisablePlayerControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDialogueSceneSubsystem::TryResolveHoldMarker(const ULevelSequence* Scene, FName HoldMarker)
|
||||||
|
{
|
||||||
|
const UMovieScene* MovieScene = Scene != nullptr ? Scene->GetMovieScene() : nullptr;
|
||||||
|
if (MovieScene == nullptr)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[DialogueScene] '%s' has no movie scene, so it cannot be held."),
|
||||||
|
Scene != nullptr ? *Scene->GetName() : TEXT("<none>"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString Label = HoldMarker.ToString();
|
||||||
|
const int32 MarkerIndex = MovieScene->FindMarkedFrameByLabel(Label);
|
||||||
|
if (MarkerIndex == INDEX_NONE)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[DialogueScene] '%s' has no marker called '%s', so it will play through instead of holding. "
|
||||||
|
"Add the marker in Sequencer, or clear the hold marker on the dialogue."),
|
||||||
|
*Scene->GetName(), *Label);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The player resolves a marker it cannot find to its current time, and PlayTo reads a target that
|
||||||
|
// is not ahead of the cursor as a request to play backwards. A marker sitting at or before the
|
||||||
|
// start would therefore rewind the whole cutscene, so it is refused here where it can be reported.
|
||||||
|
const FFrameNumber MarkerFrame = MovieScene->GetMarkedFrames()[MarkerIndex].FrameNumber;
|
||||||
|
const TRangeBound<FFrameNumber> LowerBound = MovieScene->GetPlaybackRange().GetLowerBound();
|
||||||
|
const FFrameNumber StartFrame = LowerBound.IsOpen() ? FFrameNumber(0) : LowerBound.GetValue();
|
||||||
|
if (MarkerFrame <= StartFrame)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[DialogueScene] marker '%s' on '%s' sits at or before the start of the sequence, so it cannot be "
|
||||||
|
"held at. Move it after the first frame."),
|
||||||
|
*Label, *Scene->GetName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDialogueSceneSubsystem::StartScene(ULevelSequence* Scene, FName HoldMarker, AActor* Instigator, bool bDisablePlayerControl)
|
||||||
|
{
|
||||||
|
UWorld* World = GetSceneWorld();
|
||||||
|
if (World == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One actor for the life of the subsystem, pointed at each new sequence, rather than one actor per
|
||||||
|
// scene. A level would otherwise collect an actor for every cutscene the player watched, and taking
|
||||||
|
// such an actor down would have to happen from inside the sequence player's very own finished
|
||||||
|
// callback, which is the one moment the player is still in the middle of broadcasting to it.
|
||||||
|
ULevelSequencePlayer* Player = nullptr;
|
||||||
|
|
||||||
|
if (!IsValid(ActiveSequenceActor.Get()))
|
||||||
|
{
|
||||||
|
FMovieSceneSequencePlaybackSettings Settings;
|
||||||
|
// Not auto-played by the factory: the delegates below are bound and the control is taken before
|
||||||
|
// the sequence is allowed to run, so a scene that ends on its first frame cannot outrun its own
|
||||||
|
// setup.
|
||||||
|
Settings.bAutoPlay = false;
|
||||||
|
|
||||||
|
ALevelSequenceActor* SequenceActor = nullptr;
|
||||||
|
Player = ULevelSequencePlayer::CreateLevelSequencePlayer(World, Scene, Settings, SequenceActor);
|
||||||
|
if (Player == nullptr || SequenceActor == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveSequenceActor = SequenceActor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ActiveSequenceActor->SetSequence(Scene);
|
||||||
|
|
||||||
|
// SetSequence only takes while the sequence player is not running, which is the case here: the
|
||||||
|
// scene that used it has stopped. A refusal is reported as a scene that never started.
|
||||||
|
if (ActiveSequenceActor->GetSequence() != Scene)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player = ActiveSequenceActor->GetSequencePlayer();
|
||||||
|
if (Player == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveScene = Scene;
|
||||||
|
ActivePlayer = Player;
|
||||||
|
SceneInstigator = Instigator;
|
||||||
|
bScenePlaying = true;
|
||||||
|
bControlTakenByScene = bDisablePlayerControl;
|
||||||
|
bHoldRequested = !HoldMarker.IsNone();
|
||||||
|
bSceneHeld = false;
|
||||||
|
|
||||||
|
// Both ends are listened for rather than only OnFinished, because Stop() takes the other route out
|
||||||
|
// and a scene cut short still has to give the player their controls back.
|
||||||
|
Player->OnFinished.AddDynamic(this, &UDialogueSceneSubsystem::HandleSceneFinished);
|
||||||
|
Player->OnStop.AddDynamic(this, &UDialogueSceneSubsystem::HandleSceneStopped);
|
||||||
|
|
||||||
|
// A hold is noticed through the player's own pause. PlayTo asks the player to pause itself on the
|
||||||
|
// marker frame, so the pause arriving *is* the hold being reached. It is only bound for a scene
|
||||||
|
// that asked to hold, so a pause from anywhere else cannot be read as one.
|
||||||
|
if (bHoldRequested)
|
||||||
|
{
|
||||||
|
Player->OnPause.AddDynamic(this, &UDialogueSceneSubsystem::HandleScenePaused);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bDisablePlayerControl)
|
||||||
|
{
|
||||||
|
TakePlayerControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announced before the sequence runs, so a listener is already listening when a scene that ends on
|
||||||
|
// its first frame reports itself finished from inside this call.
|
||||||
|
OnSceneStarted.Broadcast(Scene, Instigator);
|
||||||
|
|
||||||
|
if (bHoldRequested)
|
||||||
|
{
|
||||||
|
// Plays forward and stops itself on the marker. The label has already been checked against the
|
||||||
|
// sequence, so the player cannot resolve it to the current time and run the scene backwards.
|
||||||
|
FMovieSceneSequencePlaybackParams PlaybackParams(HoldMarker.ToString(), EUpdatePositionMethod::Play);
|
||||||
|
FMovieSceneSequencePlayToParams PlayToParams;
|
||||||
|
// Inclusive, so the marked frame itself is evaluated and the hold does not land a frame early.
|
||||||
|
PlayToParams.bExclusive = false;
|
||||||
|
|
||||||
|
Player->PlayTo(PlaybackParams, PlayToParams);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Player->Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AEleriPlayerController* PlayerController = Cast<AEleriPlayerController>(GetScenePlayerController()))
|
||||||
|
{
|
||||||
|
PlayerController->GetMainGameWidget()->ToggleUiForCutscene(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::HandleScenePaused()
|
||||||
|
{
|
||||||
|
// OnPause fires for any pause the player takes, so the hold is only announced for the one that was
|
||||||
|
// asked for, and only the first time: a listener that opens a dialogue here is not opening two.
|
||||||
|
if (!bHoldRequested || bSceneHeld || !bScenePlaying)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bSceneHeld = true;
|
||||||
|
|
||||||
|
// The camera and everything else the scene was driving are still on screen and still frozen here,
|
||||||
|
// which is the whole point of the beat: a listener can put something of its own in front of it.
|
||||||
|
OnSceneHeld.Broadcast(ActiveScene, SceneInstigator.Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDialogueSceneSubsystem::HoldScene()
|
||||||
|
{
|
||||||
|
if (!bScenePlaying || !IsValid(ActivePlayer.Get()) || ActivePlayer->IsPaused())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marked as a hold before the pause is asked for, so the OnPause it causes is read as a hold point
|
||||||
|
// rather than as a pause from somewhere else.
|
||||||
|
bHoldRequested = true;
|
||||||
|
ActivePlayer->Pause();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDialogueSceneSubsystem::ResumeScene()
|
||||||
|
{
|
||||||
|
if (!bScenePlaying || !IsValid(ActivePlayer.Get()) || !ActivePlayer->IsPaused())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bSceneHeld = false;
|
||||||
|
bHoldRequested = false;
|
||||||
|
|
||||||
|
// Resumes from where it was held and runs the tail to its end, which is where the stop, the return
|
||||||
|
// of the player's camera and the hand-back of the controls all happen by themselves.
|
||||||
|
ActivePlayer->Play();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::StopScene()
|
||||||
|
{
|
||||||
|
if (!bScenePlaying || ActivePlayer == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop() reports through OnStop, which is the same path a scene that ran to its end takes, so the
|
||||||
|
// hand-back of the controls and the finished broadcast stay in the one place they are written.
|
||||||
|
ActivePlayer->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::HandleSceneFinished()
|
||||||
|
{
|
||||||
|
FinishScene();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::HandleSceneStopped()
|
||||||
|
{
|
||||||
|
FinishScene();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::FinishScene()
|
||||||
|
{
|
||||||
|
if (!bScenePlaying)
|
||||||
|
{
|
||||||
|
// OnStop and OnFinished can both arrive for one scene, since a sequence that is stopped part way
|
||||||
|
// is stopped and one that reaches its end reports both. The flag is what makes the second
|
||||||
|
// arrival a no-op instead of a second hand-back of the controls.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULevelSequence* FinishedScene = ActiveScene;
|
||||||
|
AActor* Instigator = SceneInstigator.Get();
|
||||||
|
|
||||||
|
// The controls go back before anyone is told the scene is over, because a listener that starts a
|
||||||
|
// dialogue or another scene from here has to do it in a world the player already has back.
|
||||||
|
RestorePlayerControl();
|
||||||
|
ResetSceneTracking();
|
||||||
|
|
||||||
|
if (AEleriPlayerController* PlayerController = Cast<AEleriPlayerController>(GetScenePlayerController()))
|
||||||
|
{
|
||||||
|
PlayerController->GetMainGameWidget()->ToggleUiForCutscene(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnSceneFinished.Broadcast(FinishedScene, Instigator);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::ResetSceneTracking()
|
||||||
|
{
|
||||||
|
if (ActivePlayer != nullptr)
|
||||||
|
{
|
||||||
|
// The player outlives this call, so its events have to be let go of explicitly: a scene that
|
||||||
|
// was stopped would otherwise report itself finished a second time.
|
||||||
|
ActivePlayer->OnFinished.RemoveAll(this);
|
||||||
|
ActivePlayer->OnStop.RemoveAll(this);
|
||||||
|
ActivePlayer->OnPause.RemoveAll(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActivePlayer = nullptr;
|
||||||
|
ActiveScene = nullptr;
|
||||||
|
SceneInstigator = nullptr;
|
||||||
|
bScenePlaying = false;
|
||||||
|
bControlTakenByScene = false;
|
||||||
|
|
||||||
|
// A scene that is over cannot still be held, whichever way it ended: a stopped scene leaves the
|
||||||
|
// pause behind with it, and the next scene must not inherit the hold of the one before.
|
||||||
|
bHoldRequested = false;
|
||||||
|
bSceneHeld = false;
|
||||||
|
|
||||||
|
// ActiveSequenceActor is deliberately left alone: the actor it points at is reused by the next
|
||||||
|
// scene and belongs to the world, so it is taken down by the world going away and not by a scene
|
||||||
|
// ending.
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::TakePlayerControl()
|
||||||
|
{
|
||||||
|
APlayerController* PlayerController = GetScenePlayerController();
|
||||||
|
if (PlayerController == nullptr)
|
||||||
|
{
|
||||||
|
// No controller means no player to hold back, so the scene is recorded as one that never took
|
||||||
|
// the controls: handing them back later would be acting on a controller that did not exist
|
||||||
|
// when the scene started.
|
||||||
|
bControlTakenByScene = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movement and looking only. The player stays visible and the HUD keeps drawing, because a cutscene
|
||||||
|
// that also hides both is a different thing from one that only wants the controls, and the two are
|
||||||
|
// a flag apart for a caller that wants the other.
|
||||||
|
PlayerController->SetCinematicMode(true, /*bHidePlayer=*/false, /*bAffectsHUD=*/false,
|
||||||
|
/*bAffectsMovement=*/true, /*bAffectsTurning=*/true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::RestorePlayerControl()
|
||||||
|
{
|
||||||
|
if (!bControlTakenByScene)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (APlayerController* PlayerController = GetScenePlayerController())
|
||||||
|
{
|
||||||
|
PlayerController->SetCinematicMode(false, /*bHidePlayer=*/false, /*bAffectsHUD=*/false,
|
||||||
|
/*bAffectsMovement=*/true, /*bAffectsTurning=*/true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDialogueSceneSubsystem::HandleWorldCleanup(UWorld* World, bool bSessionEnded, bool bCleanupResources)
|
||||||
|
{
|
||||||
|
(void)bSessionEnded;
|
||||||
|
(void)bCleanupResources;
|
||||||
|
|
||||||
|
// Only the world the scene is playing in matters. Another world coming down, an editor preview or a
|
||||||
|
// streaming world, must not be read as the scene being over, because the scene is still on screen
|
||||||
|
// in the first one.
|
||||||
|
if (World == nullptr || World != GetSceneWorld())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetSceneTracking();
|
||||||
|
}
|
||||||
239
Source/ProjectEleri/DialogueSystem/DialogueSceneSubsystem.h
Normal file
239
Source/ProjectEleri/DialogueSystem/DialogueSceneSubsystem.h
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/GameInstanceSubsystem.h"
|
||||||
|
#include "UObject/SoftObjectPtr.h"
|
||||||
|
|
||||||
|
#include "DialogueSceneSubsystem.generated.h"
|
||||||
|
|
||||||
|
class AActor;
|
||||||
|
class ALevelSequenceActor;
|
||||||
|
class APlayerController;
|
||||||
|
class ULevelSequence;
|
||||||
|
class ULevelSequencePlayer;
|
||||||
|
class UWorld;
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueSceneStarted, ULevelSequence*, Scene, AActor*, Instigator);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueSceneHeld, ULevelSequence*, Scene, AActor*, Instigator);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueSceneFinished, ULevelSequence*, Scene, AActor*, Instigator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one thing in the project that plays a cutscene.
|
||||||
|
*
|
||||||
|
* A dialogue scene is a level sequence and nothing more, which is what keeps writing one in Sequencer
|
||||||
|
* rather than in C++. What this subsystem adds on top of the sequence player is the part the engine
|
||||||
|
* does not do for a game: whoever asked for the scene owns the player while it runs, and the player is
|
||||||
|
* handed their controls back the moment it stops, however it stopped.
|
||||||
|
*
|
||||||
|
* That is why every caller goes through here instead of spawning an ALevelSequenceActor itself:
|
||||||
|
*
|
||||||
|
* - The scene on screen is remembered, so a second request while one is running is refused rather
|
||||||
|
* than two sequences fighting over the camera and over the player's actor.
|
||||||
|
* - The controls are taken with APlayerController::SetCinematicMode, which is a flag on the controller
|
||||||
|
* and therefore is not undone by the widget code that swaps input mapping contexts around a menu.
|
||||||
|
* - OnFinished and OnStop are treated as one event, because a sequence can end either way and the
|
||||||
|
* controls have to come back for both.
|
||||||
|
*
|
||||||
|
* A scene can also be held, which is what turns one sequence into a conversation rather than a film:
|
||||||
|
* the sequence plays as far as a marker and stays there, the camera cut and everything else it drove
|
||||||
|
* frozen on screen, while the world carries on and a dialogue plays over the top. When the dialogue is
|
||||||
|
* done the scene is let go and runs its tail, which is where the fade out and the release live. See
|
||||||
|
* PlaySceneHeld for the mechanics.
|
||||||
|
*
|
||||||
|
* A game instance subsystem, like the dialogue and quest subsystems and for the same reason: a scene
|
||||||
|
* started in one level must not still read as playing in the next, so the tracking is dropped when the
|
||||||
|
* world it was playing in is torn down.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PROJECTELERI_API UDialogueSceneSubsystem : public UGameInstanceSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
// ----- Lifetime ---------------------------------------------------------------
|
||||||
|
|
||||||
|
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||||
|
virtual void Deinitialize() override;
|
||||||
|
|
||||||
|
/** The subsystem of the game instance the context belongs to, or null when there is none. */
|
||||||
|
static UDialogueSceneSubsystem* Get(const UObject* WorldContext);
|
||||||
|
|
||||||
|
// ----- Delegates --------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Fired as a scene starts, before it is played, so a listener can set up for a scene that may
|
||||||
|
* finish inside the very call that started it. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Eleri|Dialogue")
|
||||||
|
FOnDialogueSceneStarted OnSceneStarted;
|
||||||
|
|
||||||
|
/** Fired once a scene is over and the player has their controls back. This is the event a dialogue
|
||||||
|
* waits on when a cutscene has to be seen before its first line. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Eleri|Dialogue")
|
||||||
|
FOnDialogueSceneFinished OnSceneFinished;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when a scene has reached its hold point and is paused there.
|
||||||
|
*
|
||||||
|
* This is a different beat from OnSceneFinished and not a step on the way to it: the scene is still
|
||||||
|
* on screen, still owns the camera, and is going to carry on later. A listener that starts the
|
||||||
|
* dialogue from here is what puts a conversation inside a cutscene instead of after one.
|
||||||
|
*/
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Eleri|Dialogue")
|
||||||
|
FOnDialogueSceneHeld OnSceneHeld;
|
||||||
|
|
||||||
|
// ----- Playing -----------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays Scene as a cutscene and keeps track of it until it stops.
|
||||||
|
*
|
||||||
|
* Instigator is whoever asked for the scene, reported back on both delegates so a listener can tell
|
||||||
|
* the scene a quest started from the scene a conversation started. It is optional, because a scene
|
||||||
|
* that belongs to no actor at all is a perfectly ordinary scene.
|
||||||
|
*
|
||||||
|
* bDisablePlayerControl takes the player's movement and looking away for the length of the scene
|
||||||
|
* and is the default, since that is what makes a cutscene a cutscene. Pass false for a scene that
|
||||||
|
* is only meant to drive the camera while the player keeps playing.
|
||||||
|
*
|
||||||
|
* Returns false when there is nothing to play, when the sequence could not be loaded, or when a
|
||||||
|
* scene is already on screen. A caller that gets false is free to carry on as if it had never asked,
|
||||||
|
* which is what lets a dialogue run instead of waiting for a cutscene that never started.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Dialogue")
|
||||||
|
bool PlayScene(TSoftObjectPtr<ULevelSequence> Scene, AActor* Instigator = nullptr, bool bDisablePlayerControl = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays Scene and holds it at the marker called HoldMarker instead of running it to its end.
|
||||||
|
*
|
||||||
|
* The sequence plays forward and pauses itself on reaching the marker, and pausing is all that is
|
||||||
|
* needed to hold a shot: a paused player keeps everything it was driving, so the camera cut stays
|
||||||
|
* possessed and the fade keeps its value, while the world itself carries on ticking. That is what
|
||||||
|
* lets a dialogue run on top of a held shot rather than after it. OnSceneHeld is broadcast once the
|
||||||
|
* hold has been reached; ResumeScene lets the sequence finish.
|
||||||
|
*
|
||||||
|
* A marker that is missing, or that sits before the start of the sequence, is reported and the
|
||||||
|
* scene is played straight through instead. This is deliberate: the player resolves a marker label
|
||||||
|
* it cannot find to its *current* time, which makes PlayTo run backwards, so an unresolved or
|
||||||
|
* mistyped label has to be caught here rather than left to rewind the cutscene.
|
||||||
|
*
|
||||||
|
* Returns false on the same terms as PlayScene, and also when the sequence could not be read at all.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Dialogue")
|
||||||
|
bool PlaySceneHeld(TSoftObjectPtr<ULevelSequence> Scene, FName HoldMarker, AActor* Instigator = nullptr, bool bDisablePlayerControl = true);
|
||||||
|
|
||||||
|
/** Cuts the scene on screen short. The player gets their controls back the same way they would have
|
||||||
|
* at the end of it, so nothing downstream can tell a stopped scene from a finished one. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Dialogue")
|
||||||
|
void StopScene();
|
||||||
|
|
||||||
|
/** Pauses the scene on screen where it is. Drives OnSceneHeld the same way a hold point does, which
|
||||||
|
* is what makes a hold scriptable from a blueprint instead of only from a marker. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Dialogue")
|
||||||
|
bool HoldScene();
|
||||||
|
|
||||||
|
/** Lets a held scene run on to its end. The player gets their controls back when it gets there, by
|
||||||
|
* the same path a scene that was never held takes. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Dialogue")
|
||||||
|
bool ResumeScene();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Eleri|Dialogue")
|
||||||
|
bool IsScenePlaying() const { return bScenePlaying; }
|
||||||
|
|
||||||
|
/** True while the scene on screen is paused at a hold point, which is the state a dialogue that was
|
||||||
|
* started from inside a cutscene is running in. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Eleri|Dialogue")
|
||||||
|
bool IsSceneHeld() const { return bSceneHeld; }
|
||||||
|
|
||||||
|
/** The sequence on screen, or null when there is none. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Eleri|Dialogue")
|
||||||
|
ULevelSequence* GetActiveScene() const { return ActiveScene; }
|
||||||
|
|
||||||
|
/** Whoever asked for the scene on screen, or null when nobody did */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Eleri|Dialogue")
|
||||||
|
AActor* GetSceneInstigator() const { return SceneInstigator.Get(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
/** Both ends of a sequence arrive here, because a scene that runs to its end and a scene that is
|
||||||
|
* stopped have the same thing to do: hand the player back and announce it. */
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleSceneFinished();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleSceneStopped();
|
||||||
|
|
||||||
|
/** Fired by the sequence player when it pauses, which is how a hold point is noticed: PlayTo asks
|
||||||
|
* the player to pause itself on the marker frame, so the pause *is* the arrival. Guarded so the
|
||||||
|
* hold is only ever announced once, because OnPause also fires for a pause nobody asked for. */
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleScenePaused();
|
||||||
|
|
||||||
|
/** The world the scene was playing in is going away, so there is nobody left to hand the controls
|
||||||
|
* back to and the tracking is dropped instead of claiming a scene is still running. */
|
||||||
|
void HandleWorldCleanup(UWorld* World, bool bSessionEnded, bool bCleanupResources);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
/** Puts the sequence on screen. Only reached once PlayScene has agreed there is room for one.
|
||||||
|
* HoldMarker is NAME_None for a scene that simply plays through; anything else has already been
|
||||||
|
* resolved against the sequence by TryResolveHoldMarker. */
|
||||||
|
bool StartScene(ULevelSequence* Scene, FName HoldMarker, AActor* Instigator, bool bDisablePlayerControl);
|
||||||
|
|
||||||
|
/** True when the sequence carries a usable marker of that name, meaning the scene can be held.
|
||||||
|
* Reports the two ways it cannot be: a missing label, and a label placed before the sequence
|
||||||
|
* starts, which would have PlayTo unwittingly run the scene backwards. */
|
||||||
|
static bool TryResolveHoldMarker(const ULevelSequence* Scene, FName HoldMarker);
|
||||||
|
|
||||||
|
/** The one place a scene ends, whether it ran to its end or was stopped, so that the hand-back and
|
||||||
|
* the broadcast can neither be missed nor happen twice. */
|
||||||
|
void FinishScene();
|
||||||
|
|
||||||
|
/** Forgets the sequence on screen, letting go of its delegates. The actor and the player are kept:
|
||||||
|
* the sequence actor is made once and pointed at each new scene, so it outlives any one of them,
|
||||||
|
* and it goes away with the world it was spawned into. */
|
||||||
|
void ResetSceneTracking();
|
||||||
|
|
||||||
|
/** Movement and looking are taken away from the player for the length of a scene. */
|
||||||
|
void TakePlayerControl();
|
||||||
|
|
||||||
|
/** Gives back exactly what TakePlayerControl took, and only when this subsystem is what took it. */
|
||||||
|
void RestorePlayerControl();
|
||||||
|
|
||||||
|
APlayerController* GetScenePlayerController() const;
|
||||||
|
UWorld* GetSceneWorld() const;
|
||||||
|
|
||||||
|
/** The sequence on screen, kept so a second request can be told what it collided with and so the
|
||||||
|
* finished broadcast can name the scene that ended. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<ULevelSequence> ActiveScene = nullptr;
|
||||||
|
|
||||||
|
/** The actor the sequence plays through. Made for the first scene and reused by every one after it,
|
||||||
|
* so a level does not collect an actor per cutscene the player has watched, and so the actor is
|
||||||
|
* never taken down from inside the sequence player's own finished callback. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<ALevelSequenceActor> ActiveSequenceActor = nullptr;
|
||||||
|
|
||||||
|
/** Hard, because a scene player is a plain UObject with nothing else holding on to it. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<ULevelSequencePlayer> ActivePlayer = nullptr;
|
||||||
|
|
||||||
|
TWeakObjectPtr<AActor> SceneInstigator;
|
||||||
|
|
||||||
|
bool bScenePlaying = false;
|
||||||
|
|
||||||
|
/** True when the scene on screen was asked to hold, so a pause that arrives is the hold point
|
||||||
|
* being reached rather than a pause from somewhere else. */
|
||||||
|
bool bHoldRequested = false;
|
||||||
|
|
||||||
|
/** True while the scene on screen is paused at its hold point. It is the difference between a
|
||||||
|
* scene that is waiting to be let go and one that is simply playing. */
|
||||||
|
bool bSceneHeld = false;
|
||||||
|
|
||||||
|
/** True only while this subsystem is what holds the player's controls. A scene played with
|
||||||
|
* bDisablePlayerControl false must not hand back controls it never took, because that would turn
|
||||||
|
* a player somebody else had locked back on. */
|
||||||
|
bool bControlTakenByScene = false;
|
||||||
|
|
||||||
|
FDelegateHandle WorldCleanupHandle;
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
struct FLineData;
|
struct FLineData;
|
||||||
class USpeechDataAsset;
|
class USpeechDataAsset;
|
||||||
|
class ULevelSequence;
|
||||||
|
|
||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
enum class EEventRequirementType : uint8
|
enum class EEventRequirementType : uint8
|
||||||
@@ -20,6 +21,23 @@ enum class EEventRequirementType : uint8
|
|||||||
Max UMETA(Hidden)
|
Max UMETA(Hidden)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a dialogue's scene plays.
|
||||||
|
*
|
||||||
|
* A scene at the front of a dialogue is the cutscene the conversation is about, and one at the back is
|
||||||
|
* the beat it ends on. There is no third option, because a scene that wants to run in the middle of a
|
||||||
|
* conversation is a second dialogue rather than a longer one.
|
||||||
|
*/
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDialogueSceneTiming : uint8
|
||||||
|
{
|
||||||
|
/** Before the first line, so the conversation happens after the cutscene rather than over it */
|
||||||
|
OnDialogueStart UMETA(DisplayName = "On Dialogue Start"),
|
||||||
|
|
||||||
|
/** Once the conversation is over and its window has closed */
|
||||||
|
OnDialogueEnd UMETA(DisplayName = "On Dialogue End")
|
||||||
|
};
|
||||||
|
|
||||||
USTRUCT(BlueprintType)
|
USTRUCT(BlueprintType)
|
||||||
struct FEventRequirement
|
struct FEventRequirement
|
||||||
{
|
{
|
||||||
@@ -88,6 +106,52 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Eleri|Speech", meta = (Categories = "Quests"))
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Eleri|Speech", meta = (Categories = "Quests"))
|
||||||
FGameplayTagContainer StartQuests;
|
FGameplayTagContainer StartQuests;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cutscene this dialogue plays.
|
||||||
|
*
|
||||||
|
* It goes through UDialogueSceneSubsystem, which is what holds the player still for the length of
|
||||||
|
* the sequence and gives the controls back the moment it stops. Leaving it unset makes an ordinary
|
||||||
|
* conversation, which is why nothing else on this asset depends on it.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cutscene")
|
||||||
|
TSoftObjectPtr<ULevelSequence> Scene;
|
||||||
|
|
||||||
|
/** Whether the scene opens the conversation or closes it */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cutscene", meta = (EditCondition = "!Scene.IsNull()", EditConditionHides))
|
||||||
|
EDialogueSceneTiming SceneTiming = EDialogueSceneTiming::OnDialogueStart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True to hold the dialogue until the scene has had its turn, so the lines cannot be read while the
|
||||||
|
* cutscene is still running.
|
||||||
|
*
|
||||||
|
* The turn in question is the scene's hold point when it has one, and its end when it does not: a
|
||||||
|
* scene with a hold marker is a cutscene the conversation happens *inside* of, and one without is a
|
||||||
|
* cutscene the conversation happens after.
|
||||||
|
*
|
||||||
|
* Only a scene at the front of a dialogue can hold anything: one that closes the conversation has
|
||||||
|
* nothing left to wait behind. When the scene is refused, because a quest started one of its own in
|
||||||
|
* the same breath, the dialogue goes ahead rather than waiting for a cutscene that never began.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cutscene", meta = (EditCondition = "!Scene.IsNull() && SceneTiming == EDialogueSceneTiming::OnDialogueStart", EditConditionHides))
|
||||||
|
bool bHoldDialogueForScene = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional. The name of a marker in Scene for the dialogue to happen at.
|
||||||
|
*
|
||||||
|
* When it is set the scene plays only as far as that marker and holds there — the camera cut and
|
||||||
|
* everything else it drove stay on screen — while the dialogue runs over the top of it. When the
|
||||||
|
* dialogue ends the scene is let go and plays its tail, which is where the fade out and the release
|
||||||
|
* of the camera belong. That is what makes one sequence a whole scene rather than an intro.
|
||||||
|
*
|
||||||
|
* Leave it empty for a scene that simply plays through before the dialogue starts.
|
||||||
|
*
|
||||||
|
* Author it as a Sequencer marker (the yellow ones on the timeline ruler, not a section) with its
|
||||||
|
* Label set to this name, placed after the first frame. A name that does not match a marker, or one
|
||||||
|
* placed at the very start, is reported in the log and the scene plays through instead.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cutscene", meta = (EditCondition = "!Scene.IsNull() && SceneTiming == EDialogueSceneTiming::OnDialogueStart", EditConditionHides))
|
||||||
|
FName SceneHoldMarker;
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Speech")
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Speech")
|
||||||
FText PlayerAsk;
|
FText PlayerAsk;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ public class ProjectEleri : ModuleRules
|
|||||||
"GameplayStateTreeModule",
|
"GameplayStateTreeModule",
|
||||||
"DirectiveUtilitiesRuntime",
|
"DirectiveUtilitiesRuntime",
|
||||||
"StatForge",
|
"StatForge",
|
||||||
"InteractionSystem"
|
"InteractionSystem",
|
||||||
|
"LevelSequence",
|
||||||
|
"MovieScene"
|
||||||
});
|
});
|
||||||
|
|
||||||
PrivateDependencyModuleNames.AddRange(new string[]
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
|
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
|
||||||
void FadeScreenByTime(float FadeTime = 1.f, float BlankScreenTime = 1.f);
|
void FadeScreenByTime(float FadeTime = 1.f, float BlankScreenTime = 1.f);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintImplementableEvent)
|
||||||
|
void ToggleUiForCutscene(bool bActive);
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable)
|
UFUNCTION(BlueprintCallable)
|
||||||
void TogglePlaceableUI(bool bEnabled);
|
void TogglePlaceableUI(bool bEnabled);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "QuestDataAsset.generated.h"
|
#include "QuestDataAsset.generated.h"
|
||||||
|
|
||||||
|
class ULevelSequence;
|
||||||
class UTexture2D;
|
class UTexture2D;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +53,22 @@ public:
|
|||||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest", meta = (ClampMin = 0))
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest", meta = (ClampMin = 0))
|
||||||
int32 DeadlineInDays = 0;
|
int32 DeadlineInDays = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cutscenes the quest plays as it changes stage, keyed by the status it moves to.
|
||||||
|
*
|
||||||
|
* A stage a quest enters is a status, so this is what makes a quest able to show its own beat
|
||||||
|
* without any blueprint being involved: the scene under Completed plays the moment the quest is
|
||||||
|
* handed in, and the one under Failed plays when a deadline runs out, which is what makes failing a
|
||||||
|
* quest something the player sees rather than something that happens to the journal.
|
||||||
|
*
|
||||||
|
* The scenes go through UDialogueSceneSubsystem, which is what holds the player still for the
|
||||||
|
* length of the sequence and gives the controls back the moment it stops. One scene at a time is
|
||||||
|
* played, so a quest that changes stage twice in a single frame shows the first scene it asked for
|
||||||
|
* and stays quiet about the second.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Cutscene")
|
||||||
|
TMap<EQuestStatus, TSoftObjectPtr<ULevelSequence>> StatusScenes;
|
||||||
|
|
||||||
/** The steps, in the order the journal lists them. */
|
/** The steps, in the order the journal lists them. */
|
||||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Objectives", meta = (TitleProperty = "Description"))
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Objectives", meta = (TitleProperty = "Description"))
|
||||||
TArray<FQuestObjective> Objectives;
|
TArray<FQuestObjective> Objectives;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "InventoryComponent.h"
|
#include "InventoryComponent.h"
|
||||||
#include "MyCharacter.h"
|
#include "MyCharacter.h"
|
||||||
#include "ProjectEleri/Ai/NpcScheduleComponent.h"
|
#include "ProjectEleri/Ai/NpcScheduleComponent.h"
|
||||||
|
#include "ProjectEleri/DialogueSystem/DialogueSceneSubsystem.h"
|
||||||
#include "ProjectEleri/DialogueSystem/DialogueSubsystem.h"
|
#include "ProjectEleri/DialogueSystem/DialogueSubsystem.h"
|
||||||
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
|
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
|
||||||
#include "ProjectEleri/Items/DataAssets/ItemDataAsset.h"
|
#include "ProjectEleri/Items/DataAssets/ItemDataAsset.h"
|
||||||
@@ -591,6 +592,33 @@ void UQuestSubsystem::SetQuestStatus(FQuestRuntimeState& State, EQuestStatus New
|
|||||||
// quest, which can reallocate the map this state lives in.
|
// quest, which can reallocate the map this state lives in.
|
||||||
UQuestDataAsset* Quest = State.Quest;
|
UQuestDataAsset* Quest = State.Quest;
|
||||||
OnQuestStatusChanged.Broadcast(Quest, NewStatus);
|
OnQuestStatusChanged.Broadcast(Quest, NewStatus);
|
||||||
|
|
||||||
|
// The stage's cutscene comes after the broadcast rather than before it, so a listener that reacts to
|
||||||
|
// the new status has already reacted by the time the scene takes the player's controls, and so the
|
||||||
|
// quest asset is not being read while that listener may still be writing to the quest map.
|
||||||
|
PlayStatusScene(Quest, NewStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UQuestSubsystem::PlayStatusScene(const UQuestDataAsset* Quest, EQuestStatus NewStatus)
|
||||||
|
{
|
||||||
|
if (Quest == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TSoftObjectPtr<ULevelSequence>* Scene = Quest->StatusScenes.Find(NewStatus);
|
||||||
|
if (Scene == nullptr || Scene->IsNull())
|
||||||
|
{
|
||||||
|
// Most quests have no scene for most stages, which is the ordinary case and not a fault.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UDialogueSceneSubsystem* SceneSubsystem = UDialogueSceneSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
// The player goes in as the instigator, because a quest is not an actor and the thing a scene
|
||||||
|
// listener wants to know is whose scene this is.
|
||||||
|
SceneSubsystem->PlayScene(*Scene, Cast<AActor>(GetTestContext()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UQuestSubsystem::OfferQuest(FGameplayTag QuestTag)
|
bool UQuestSubsystem::OfferQuest(FGameplayTag QuestTag)
|
||||||
|
|||||||
@@ -245,6 +245,11 @@ protected:
|
|||||||
/** Moves a quest to a new status and announces it. Does nothing when it is already there. */
|
/** Moves a quest to a new status and announces it. Does nothing when it is already there. */
|
||||||
void SetQuestStatus(FQuestRuntimeState& State, EQuestStatus NewStatus);
|
void SetQuestStatus(FQuestRuntimeState& State, EQuestStatus NewStatus);
|
||||||
|
|
||||||
|
/** Plays the cutscene the quest names for the stage it has just entered, if it names one. Called
|
||||||
|
* after the status has been announced, so a listener reacting to the stage is already listening
|
||||||
|
* by the time the scene takes the player's controls away. */
|
||||||
|
void PlayStatusScene(const UQuestDataAsset* Quest, EQuestStatus NewStatus);
|
||||||
|
|
||||||
/** Counts a step up, announcing it and completing it when it reaches what it asked for. Returns
|
/** Counts a step up, announcing it and completing it when it reaches what it asked for. Returns
|
||||||
* true when something changed. */
|
* true when something changed. */
|
||||||
bool AdvanceObjective(FQuestRuntimeState& State, FName ObjectiveId, int32 Amount = 1);
|
bool AdvanceObjective(FQuestRuntimeState& State, FName ObjectiveId, int32 Amount = 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user