Simple cutscene manager and a test cutscene
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "DialogueComponent.h"
|
||||
|
||||
#include "DialogueSceneSubsystem.h"
|
||||
#include "DialogueSubsystem.h"
|
||||
#include "DialogueWidget.h"
|
||||
#include "EleriPlayerController.h"
|
||||
@@ -35,6 +36,22 @@ void UDialogueComponent::BeginPlay()
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
if (!DialogueSubsystem) return;
|
||||
@@ -139,12 +185,28 @@ void UDialogueComponent::CloseDialogue()
|
||||
UDialogueSubsystem* DialogueSubsystem = UDialogueSubsystem::Get(this);
|
||||
if (!DialogueSubsystem) return;
|
||||
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;
|
||||
CurrentLineIndex = 0;
|
||||
CurrentActiveDialogue = nullptr;
|
||||
UnbindControllerInput();
|
||||
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()
|
||||
@@ -276,5 +338,93 @@ TArray<FText> UDialogueComponent::GetDialogueOptionsFromPlayerSpeakList(const TA
|
||||
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
|
||||
|
||||
|
||||
@@ -11,8 +11,26 @@
|
||||
struct FPlayerSpeakToDialogue;
|
||||
class AEleriPlayerController;
|
||||
class UDialogueWidget;
|
||||
class ULevelSequence;
|
||||
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)
|
||||
class PROJECTELERI_API UDialogueComponent : public UActorComponent
|
||||
{
|
||||
@@ -49,6 +67,32 @@ protected:
|
||||
void TriggerDialogueSelect(TArray<FText>& DialogueOptions, bool bIsDialogueStart);
|
||||
UFUNCTION()
|
||||
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> GetDialogueOptionsFromPlayerSpeakList(const TArray<FPlayerSpeakToDialogue>& SpeakList);
|
||||
|
||||
@@ -78,11 +122,27 @@ protected:
|
||||
TMap<int32, FPlayerSpeakToDialogue> MappedDialogueOptions;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
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:
|
||||
// Called when the game starts
|
||||
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()
|
||||
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;
|
||||
class USpeechDataAsset;
|
||||
class ULevelSequence;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EEventRequirementType : uint8
|
||||
@@ -20,6 +21,23 @@ enum class EEventRequirementType : uint8
|
||||
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)
|
||||
struct FEventRequirement
|
||||
{
|
||||
@@ -88,6 +106,52 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Eleri|Speech", meta = (Categories = "Quests"))
|
||||
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")
|
||||
FText PlayerAsk;
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ public class ProjectEleri : ModuleRules
|
||||
"GameplayStateTreeModule",
|
||||
"DirectiveUtilitiesRuntime",
|
||||
"StatForge",
|
||||
"InteractionSystem"
|
||||
"InteractionSystem",
|
||||
"LevelSequence",
|
||||
"MovieScene"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
|
||||
@@ -44,6 +44,9 @@ public:
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
|
||||
void FadeScreenByTime(float FadeTime = 1.f, float BlankScreenTime = 1.f);
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent)
|
||||
void ToggleUiForCutscene(bool bActive);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void TogglePlaceableUI(bool bEnabled);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "QuestDataAsset.generated.h"
|
||||
|
||||
class ULevelSequence;
|
||||
class UTexture2D;
|
||||
|
||||
/**
|
||||
@@ -52,6 +53,22 @@ public:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest", meta = (ClampMin = 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. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Objectives", meta = (TitleProperty = "Description"))
|
||||
TArray<FQuestObjective> Objectives;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "InventoryComponent.h"
|
||||
#include "MyCharacter.h"
|
||||
#include "ProjectEleri/Ai/NpcScheduleComponent.h"
|
||||
#include "ProjectEleri/DialogueSystem/DialogueSceneSubsystem.h"
|
||||
#include "ProjectEleri/DialogueSystem/DialogueSubsystem.h"
|
||||
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.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.
|
||||
UQuestDataAsset* Quest = State.Quest;
|
||||
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)
|
||||
|
||||
@@ -245,6 +245,11 @@ protected:
|
||||
/** Moves a quest to a new status and announces it. Does nothing when it is already there. */
|
||||
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
|
||||
* true when something changed. */
|
||||
bool AdvanceObjective(FQuestRuntimeState& State, FName ObjectiveId, int32 Amount = 1);
|
||||
|
||||
Reference in New Issue
Block a user