#pragma region CPL License /* Nuclex Unreal Module Copyright (C) 2014-2021 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License #include "VisualNovel/VisualNovelStoryController.h" #include "NuclexErrors.h" #include "InterfaceCast.h" #include "VisualNovel/LatentActions/FadeInAction.h" #include "VisualNovel/LatentActions/FadeOutAction.h" #include "VisualNovel/LatentActions/FlyCameraAction.h" #include "VisualNovel/LatentActions/ShowParagraphAction.h" #include #include // --------------------------------------------------------------------------------------------- // AVisualNovelStoryController::AVisualNovelStoryController() : Pawn(nullptr), cameraManager(nullptr), initialized(false), currentState() { // Set this actor to call Tick() every frame. We enable this by default because // it's an /animated/ paragraph box after all. You can turn this off in the UE editor // or in an inheriting implementation class to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.bStartWithTickEnabled = true; } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::ShowParagraph( UObject *worldContextObject, struct FLatentActionInfo latentInfo, AActor *anchor, const FString &speaker, const FString ¶graph, bool keepOpen /* = true */ ) { InitializeIfNeeded(); EnsureParagraphBoxSubscription(); // The pawn is required as a fallback (and maybe future free-positioning system) if(!IsValid(this->Pawn)) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::ShowParagraph()"), TEXT("No pawn assigned, cannot move camera") ); return; } // Obtain reference ot the current world (needed to access the latent action manager) UWorld *world = GetWorld(); if(world == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::ShowParagraph()"), TEXT("Failed to obtain current world") ); return; } // The current state is initialized as long as a paragraph box was assigned when // we called EnsureParagraphBoxSubscription() at the top. if(!this->currentState.IsValid()) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::ShowParagraph()"), TEXT("No paragraph box assigned, paragraph cannot be shown") ); return; } // Check if there's already another paragraph planned. This would indicate some kind // of logic error and prevent the player from seeing all text, so always report it. if(this->currentState->IsLatentActionRunning) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::ShowParagraph()"), TEXT("Paragraph was still active, refusing to show another one") ); return; } // Set up the next paragraph to display after the current one has been completed. // Placement is either at the anchor transform (if provided) or at a dynamically // calculated position (not implemented currently, we just flop it where the pawn is) if(IsValid(anchor)) { this->currentState->Anchor = anchor->GetTransform(); } else { this->currentState->Anchor = this->Pawn->GetTransform(); } this->currentState->Speaker = speaker; this->currentState->Paragraph = paragraph; this->currentState->Choices.Reset(); this->currentState->KeepOpen = keepOpen; this->currentState->IsLatentActionRunning = true; // we'll start it in a moment this->currentState->Confirmed = false; this->currentState->SelectedChoiceIndex = -1; #if defined(WITH_EDITOR) && (WITH_EDITOR == 1) UE_LOG( LogNuclex, Display, TEXT("%s - INFO: %s %s"), TEXT("AVisualNovelStoryController::ShowParagraph()"), TEXT("New narration-only paragraph:"), *paragraph ); #endif // Finally, this is a 'latent action' that continues to run in the background while // the Blueprint graph goes on. If this specific latent action is already running, // then it means a paragraph is already visible and the running latent action will // take care of displaying the scheduled next paragraph. FLatentActionManager &latentActionManager = world->GetLatentActionManager(); FShowParagraphAction *existingAction = ( latentActionManager.FindExistingAction( latentInfo.CallbackTarget, latentInfo.UUID ) ); if(existingAction == nullptr) { latentActionManager.AddNewAction( latentInfo.CallbackTarget, latentInfo.UUID, new FShowParagraphAction(latentInfo, this->currentState, nullptr) ); } // We've got something to do now, enable ticking SetActorTickEnabled(true); } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::RequireChoice( EDialogueChoice &selectedChoice, UObject *worldContextObject, struct FLatentActionInfo latentInfo, AActor *anchor, const FString &speaker, const FString ¶graph, const TArray &choices, bool keepOpen /* = true */ ) { InitializeIfNeeded(); EnsureParagraphBoxSubscription(); // The pawn is required as a fallback (and maybe future free-positioning system) if(!IsValid(this->Pawn)) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::RequireChoice()"), TEXT("No pawn assigned, cannot move camera") ); return; } // Obtain reference ot the current world (needed to access the latent action manager) UWorld *world = GetWorld(); if(world == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::RequireChoice()"), TEXT("Failed to obtain current world") ); return; } // The current state is initialized as long as a paragraph box was assigned when // we called EnsureParagraphBoxSubscription() at the top. if(!this->currentState.IsValid()) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::RequireChoice()"), TEXT("No paragraph box assigned, paragraph cannot be shown") ); return; } // Check if there's already another paragraph planned. This would indicate some kind // of logic error and prevent the player from seeing all text, so always report it. if(this->currentState->IsLatentActionRunning) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::RequireChoice()"), TEXT("Paragraph was still active, refusing to show another one") ); return; } // Set up the next paragraph to display after the current one has been completed. // Placement is either at the anchor transform (if provided) or at a dynamically // calculated position (not implemented currently, we just flop it where the pawn is) if(IsValid(anchor)) { this->currentState->Anchor = anchor->GetTransform(); } else { this->currentState->Anchor = this->Pawn->GetTransform(); } this->currentState->Speaker = speaker; this->currentState->Paragraph = paragraph; this->currentState->Choices = choices; this->currentState->KeepOpen = keepOpen; this->currentState->IsLatentActionRunning = true; // we'll start it in a moment this->currentState->Confirmed = false; this->currentState->SelectedChoiceIndex = -1; #if defined(WITH_EDITOR) && (WITH_EDITOR == 1) UE_LOG( LogNuclex, Display, TEXT("%s - INFO: %s %s"), TEXT("AVisualNovelStoryController::RequireChoice()"), TEXT("New multiple-choice paragraph:"), *paragraph ); #endif // Finally, this is a 'latent action' that continues to run in the background while // the Blueprint graph goes on. If this specific latent action is already running, // then it means a paragraph is already visible and the running latent action will // take care of displaying the scheduled next paragraph. FLatentActionManager &latentActionManager = world->GetLatentActionManager(); FShowParagraphAction *existingAction = ( latentActionManager.FindExistingAction( latentInfo.CallbackTarget, latentInfo.UUID ) ); if(existingAction == nullptr) { latentActionManager.AddNewAction( latentInfo.CallbackTarget, latentInfo.UUID, new FShowParagraphAction(latentInfo, this->currentState, &selectedChoice) ); } // We've got something to do now, enable ticking SetActorTickEnabled(true); } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::FadeOut( UObject *worldContextObject, struct FLatentActionInfo latentInfo, float fadeTimeInSeconds /* = 2.0 */ ) { InitializeIfNeeded(); UWorld *world = GetWorld(); if(world == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::FadeOut()"), TEXT("Failed to obtain current world") ); return; } // If the caller wants and instantaneous transition, set the camera fade level right away // (but run the latent action anyway to finish from a tick call as normal) if(fadeTimeInSeconds == 0.0f) { const bool fadeAudio = true; this->cameraManager->SetManualCameraFade(1.0f, FLinearColor::Black, fadeAudio); } FLatentActionManager &latentActionManager = world->GetLatentActionManager(); FFadeOutAction *existingAction = latentActionManager.FindExistingAction( latentInfo.CallbackTarget, latentInfo.UUID ); if(existingAction == nullptr) { latentActionManager.AddNewAction( latentInfo.CallbackTarget, latentInfo.UUID, new FFadeOutAction(latentInfo, this->cameraManager, fadeTimeInSeconds) ); } } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::FadeIn( UObject *worldContextObject, struct FLatentActionInfo latentInfo, float fadeTimeInSeconds /* = 2.0 */ ) { InitializeIfNeeded(); UWorld *world = GetWorld(); if(world == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::FadeIn()"), TEXT("Failed to obtain current world") ); return; } // If the caller wants and instantaneous transition, set the camera fade level right away // (but run the latent action anyway to finish from a tick call as normal) if(fadeTimeInSeconds == 0.0f) { const bool fadeAudio = true; this->cameraManager->SetManualCameraFade(0.0f, FLinearColor::Black, fadeAudio); } FLatentActionManager &latentActionManager = world->GetLatentActionManager(); FFadeInAction *existingAction = latentActionManager.FindExistingAction( latentInfo.CallbackTarget, latentInfo.UUID ); if(existingAction == nullptr) { latentActionManager.AddNewAction( latentInfo.CallbackTarget, latentInfo.UUID, new FFadeInAction(latentInfo, this->cameraManager, fadeTimeInSeconds) ); } } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::BlinkCamera(AActor *anchor) { InitializeIfNeeded(); // Safety checks, we don't want to crash :) if(!IsValid(this->Pawn)) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::BlinkCamera()"), TEXT("No pawn assigned, cannot move camera") ); return; } if(!IsValid(anchor)) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::BlinkCamera()"), TEXT("Anchor tranform is NULL") ); return; } // Obtain a reference to the current world (needed to find the player controller) UWorld *world = GetWorld(); if(world == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::BlinkCamera()"), TEXT("Failed to obtain current world") ); return; } // Ask for the first player controller in the world APlayerController *playerController = UGameplayStatics::GetPlayerController(world, 0); if(playerController == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::BlinkCamera()"), TEXT("Failed to obtain player controller") ); return; } // Get the global transform of the anchor scene node FTransform anchorTransform = anchor->GetTransform(); // Move the player pawn to the anchor's transform bool success; { const bool sweep = false; FHitResult *sweepResult = nullptr; success = this->Pawn->SetActorTransform( anchorTransform, sweep, sweepResult, ETeleportType::ResetPhysics ); } if(!success) { UE_LOG( LogNuclex, Warning, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::BlinkCamera()"), TEXT("APawn::SetActorTransform() failed") ); return; } // The pawn's rotation is controlled by the player controller (what a terrible design // choice!!), so we have to update the player controller's control rotation, too. playerController->SetControlRotation(anchorTransform.Rotator()); } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::FlyCamera( UObject *worldContextObject, struct FLatentActionInfo latentInfo, AActor *anchor, float flightTimeInSeconds, TEnumAsByte easingFunction ) { InitializeIfNeeded(); if(!IsValid(this->Pawn)) { UE_LOG( LogNuclex, Warning, TEXT("%s - WARNING: %s"), TEXT("AVisualNovelStoryController::FlyCamera()"), TEXT("Failed to obtain active pawn and none assigned, cannot fly camera") ); return; } if(!IsValid(anchor)) { UE_LOG( LogNuclex, Warning, TEXT("%s - WARNING: %s"), TEXT("AVisualNovelStoryController::FlyCamera()"), TEXT("Specified anchor position is invalid") ); return; } UWorld *world = GetWorld(); if(world == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::FlyCamera()"), TEXT("Failed to obtain current world") ); return; } FLatentActionManager &latentActionManager = world->GetLatentActionManager(); FFlyCameraAction *existingAction = latentActionManager.FindExistingAction( latentInfo.CallbackTarget, latentInfo.UUID ); if(existingAction == nullptr) { FTransform targetTransform = anchor->GetTransform(); latentActionManager.AddNewAction( latentInfo.CallbackTarget, latentInfo.UUID, new FFlyCameraAction( latentInfo, this->Pawn, targetTransform, flightTimeInSeconds, easingFunction.GetValue() ) ); } } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::SetPawnAsViewTarget() { InitializeIfNeeded(); if(!IsValid(this->Pawn)) { UE_LOG( LogNuclex, Warning, TEXT("%s - WARNING: %s"), TEXT("AVisualNovelStoryController::SetPawnAsViewTarget()"), TEXT("Failed to obtain active pawn and none assigned, cannot set view target") ); return; } // Sanity check, if the pawn doesn't have a camera, Unreal would silently do nothing UCameraComponent *camera = this->Pawn->FindComponentByClass(); if(camera == nullptr) { UE_LOG( LogNuclex, Warning, TEXT("%s - WARNING: %s"), TEXT("AVisualNovelStoryController::SetPawnAsViewTarget()"), TEXT("Current pawn does not contain a camera component, cannot use as view target") ); return; } // Everything checks out, make the pawn's camera the active one this->cameraManager->SetViewTarget(this->Pawn); } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::CancelAllParagraphs() { EParagraphBoxAnimationState animationState; if(this->currentState.IsValid()) { animationState = this->currentState->AnimationState; this->currentState->Detached = true; } else { animationState = IVisualNovelParagraphBox::Execute_GetAnimationState( this->subscribedParagraphBox.GetObject() ); } this->currentState = MakeShared(); this->currentState->AnimationState = animationState; this->currentState->Detached = false; this->currentState->KeepOpen = false; this->currentState->Confirmed = false; this->currentState->IsLatentActionRunning = false; if(animationState != EParagraphBoxAnimationState::Hidden) { if(this->subscribedParagraphBox) { IVisualNovelParagraphBox::Execute_Hide(this->subscribedParagraphBox.GetObject()); } } } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::BeginPlay() { Super::BeginPlay(); InitializeIfNeeded(); } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::EndPlay(const EEndPlayReason::Type endPlayReason) { UnsubscribeFromParagraphBox(); // Drop the pointers to the gameplay framework classes we looked up so Unreal Engine's // homebrew garbage collector can free them this->Pawn = nullptr; this->cameraManager = nullptr; this->initialized = false; // Drop ownership of the shared paragraph box state if(this->currentState.IsValid()) { this->currentState->Detached = true; this->currentState.Reset(); } // There is no Super::EndPlay() currently //Super::EndPlay(endPlayReason); } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::Tick(float deltaSeconds) { // This could be done without polling in the animation state callback, but I don't // want to require paragraph boxes to be implemented with reentrancy in mind. if(!this->currentState.IsValid()) { return; } // If we've been asked to close the paragraph box after confirmation, // issue the close command to the paragraph box as soon as it has been confirmed. if(this->currentState->KeepOpen == false) { if(this->currentState->IsLatentActionRunning && this->currentState->Confirmed) { bool isAlreadyHiding = ( (this->currentState->AnimationState == EParagraphBoxAnimationState::Hiding) || (this->currentState->AnimationState == EParagraphBoxAnimationState::Hidden) ); if(!isAlreadyHiding) { if(this->subscribedParagraphBox) { IVisualNovelParagraphBox::Execute_Hide(this->subscribedParagraphBox.GetObject()); // Also tell the input source that there's no receiver for UI input anymore if(this->playerInputSource) { IParagraphInputSource::Execute_EndParagraphInput( this->playerInputSource.GetObject() ); } } } } } // If there is a paragraph scheduled to display (and no latent action is currently // handling the paragraph box), bring up the scheduled paragraph if(!this->currentState->Paragraph.IsEmpty()) { if(this->subscribedParagraphBox) { if(IsValid(this->ParagraphBox)) { // Move the paragraph box to the anchor transform bool success; { const bool sweep = false; FHitResult *sweepResult = nullptr; success = this->ParagraphBox->SetActorTransform( this->currentState->Anchor, sweep, sweepResult, ETeleportType::ResetPhysics ); } if(!success) { UE_LOG( LogNuclex, Warning, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::Tick()"), TEXT("AActor::SetActorTransform() failed updating the paragraph box transform") ); } } // Bring up the new paragraph if(this->currentState->Choices.Num() <= 0) { IVisualNovelParagraphBox::Execute_ShowParagraph( this->subscribedParagraphBox.GetObject(), this->currentState->Speaker, this->currentState->Paragraph ); } else { IVisualNovelParagraphBox::Execute_ShowChoices( this->subscribedParagraphBox.GetObject(), this->currentState->Speaker, this->currentState->Paragraph, this->currentState->Choices ); } // Clear the state's variable so we don't think another pargaraph is scheduled later this->currentState->Speaker.Reset(); this->currentState->Paragraph.Reset(); this->currentState->Choices.Reset(); // Also tell the UI input source to direct all UI input to the opened paragraph box if(this->playerInputSource) { IParagraphInputSource::Execute_BeginParagraphInput( this->playerInputSource.GetObject(), this->subscribedControllable ); } } } } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::InitializeIfNeeded() { const int32 playerIndex = 0; // If we're already initialized, skip it if(this->initialized) { return; } // We need the game world this actor is part of to obtain the pawn and camera manager UWorld *world = GetWorld(); if(world == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::InitializeIfNeeded()"), TEXT("Could not obtain UWorld reference") ); return; } // If a pawn is active (and we have no explicit pawn assigned yet), automatically pick // it up as the pawn that we'll control from now on. The game can still assign another // pawn at any later time, this is only to make that setup step optional in most cases. if(!IsValid(this->Pawn)) { this->Pawn = UGameplayStatics::GetPlayerPawn(world, playerIndex); } // Ask for the first player controller in the world APlayerController *playerController = UGameplayStatics::GetPlayerController(world, 0); if(playerController == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::InitializeIfNeeded()"), TEXT("Failed to obtain player controller") ); return; } // Also check if the pawn implements the IParagraphInputSource interface this->playerInputSource = interface_cast(playerController); if(!this->playerInputSource) { UE_LOG( LogNuclex, Warning, TEXT("%s - WARNING: %s"), TEXT("AVisualNovelStoryController::InitializeIfNeeded()"), TEXT("PlayerController does not implement IParagraphInputSource -> no keyboard/gamepad") ); } // Try to obtain the camera manager. We use this for some of the story controller's // functionality, such as fading the screen in and out. if(!IsValid(this->cameraManager)) { this->cameraManager = UGameplayStatics::GetPlayerCameraManager(world, playerIndex); } if(this->cameraManager == nullptr) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::InitializeIfNeeded()"), TEXT("Failed to obtain PlayerCameraManager") ); return; } this->initialized = true; } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::EnsureParagraphBoxSubscription() { if(this->subscribedParagraphBox) { if(this->ParagraphBox == this->subscribedParagraphBox.GetObject()) { return; } UnsubscribeFromParagraphBox(); } // If this point is reached, a new or different paragraph box was assigned // Cast the paragraph box to the IVisualNovelParagraphBox interface through which // we can issue it commands (to open/close and which contents to show) this->subscribedParagraphBox = interface_cast(this->ParagraphBox); if(!this->ParagraphBox) { UE_LOG( LogNuclex, Error, TEXT("%s - ERROR: %s"), TEXT("AVisualNovelStoryController::EnsureParagraphBoxSubscription()"), TEXT("Paragraph box does not implement IVisualNovelParagraphBox -> UI disabled") ); return; } // Also attempt to cast the paragraph box to the IButtonControllableParagraph interface // so we can assign it to the player controller as the current button/keyboard input target this->subscribedControllable = interface_cast(this->ParagraphBox); if(!this->subscribedControllable) { UE_LOG( LogNuclex, Warning, TEXT("%s - WARNING: %s"), TEXT("AVisualNovelStoryController::EnsureParagraphBoxSubscription()"), TEXT("Paragraph box does not implement IButtonControllableParagraph -> no keyboard/gamepad") ); } // Initialize the tracked paragraph box state used to communicate with latent actions this->currentState = MakeShared(); this->currentState->AnimationState = IVisualNovelParagraphBox::Execute_GetAnimationState( this->subscribedParagraphBox.GetObject() ); this->currentState->Detached = false; this->currentState->KeepOpen = false; this->currentState->Confirmed = false; this->currentState->IsLatentActionRunning = false; // Subscribe to the confirmation notification that is fired when the player // confirms a paragraph or picks an option on a multi-choice question { FConfirmParagraphDelegate confirmDelegate; confirmDelegate.BindDynamic(this, &AVisualNovelStoryController::choiceAccepted); IVisualNovelParagraphBox::Execute_AddConfirmSubscriber( this->subscribedParagraphBox.GetObject(), confirmDelegate ); } // Subscribe to the animation notification that will tell us when the paragraph // UI has finished its appear or hide animation { FAnimationNotificationDelegate animationDelegate; animationDelegate.BindDynamic(this, &AVisualNovelStoryController::animationStateChanged); IVisualNovelParagraphBox::Execute_AddAnimationSubscriber( this->subscribedParagraphBox.GetObject(), animationDelegate ); } UE_LOG( LogNuclex, Display, TEXT("%s - INFO: %s %i"), TEXT("AVisualNovelStoryController::EnsureParagraphBoxSubscription()"), TEXT("Adopted a new paragraph box, initial state is"), static_cast(this->currentState->AnimationState) ); } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::UnsubscribeFromParagraphBox() { if(this->currentState.IsValid()) { this->currentState->Detached = true; this->currentState.Reset(); } if(this->subscribedParagraphBox) { UE_LOG( LogNuclex, Display, TEXT("%s - INFO: %s"), TEXT("AVisualNovelStoryController::UnsubscribeFromParagraphBox()"), TEXT("Dropping current paragraph box") ); // Subscribe to the paragraph confirmation notification { FConfirmParagraphDelegate confirmDelegate; confirmDelegate.BindDynamic(this, &AVisualNovelStoryController::choiceAccepted); IVisualNovelParagraphBox::Execute_RemoveConfirmSubscriber( this->subscribedParagraphBox.GetObject(), confirmDelegate ); } // Subscribe to the animation state change notification { FAnimationNotificationDelegate animationDelegate; animationDelegate.BindDynamic(this, &AVisualNovelStoryController::animationStateChanged); IVisualNovelParagraphBox::Execute_RemoveAnimationSubscriber( this->subscribedParagraphBox.GetObject(), animationDelegate ); } this->subscribedParagraphBox.SetInterface(nullptr); this->subscribedParagraphBox.SetObject(nullptr); this->subscribedControllable.SetInterface(nullptr); this->subscribedParagraphBox.SetObject(nullptr); } } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::animationStateChanged( EParagraphBoxAnimationState animationState ) { if(this->currentState.IsValid()) { this->currentState->AnimationState = animationState; } } // --------------------------------------------------------------------------------------------- // void AVisualNovelStoryController::choiceAccepted(int32 choiceIndex) { if(this->currentState.IsValid()) { if(this->currentState->IsLatentActionRunning) { this->currentState->SelectedChoiceIndex = choiceIndex; this->currentState->Confirmed = true; #if defined(WITH_EDITOR) && (WITH_EDITOR == 1) if(choiceIndex == -1) { UE_LOG( LogNuclex, Display, TEXT("%s - INFO: %s"), TEXT("AVisualNovelStoryController::choiceAccepted()"), TEXT("Player skipped narration") ); } else { UE_LOG( LogNuclex, Display, TEXT("%s - INFO: %s %i"), TEXT("AVisualNovelStoryController::choiceAccepted()"), TEXT("Player made choice"), static_cast(choiceIndex) ); } #endif } } } // --------------------------------------------------------------------------------------------- //