Add Games File
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class FunnyGirlTarget : TargetRules
|
||||
{
|
||||
public FunnyGirlTarget(TargetInfo Target) : base(Target)
|
||||
{
|
||||
Type = TargetType.Game;
|
||||
DefaultBuildSettings = BuildSettingsVersion.V6;
|
||||
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7;
|
||||
ExtraModuleNames.Add("FunnyGirl");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class FunnyGirl : ModuleRules
|
||||
{
|
||||
public FunnyGirl(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "UMG" });
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
// Uncomment if you are using Slate UI
|
||||
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
// Uncomment if you are using online features
|
||||
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
|
||||
|
||||
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "FunnyGirl.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, FunnyGirl, "FunnyGirl" );
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Identity/PersonalityTraitDataAsset.h"
|
||||
@@ -0,0 +1,408 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Input/TouchInputComponent.h"
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "InputCoreTypes.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogFunnyGirlTouchInput, Log, All);
|
||||
|
||||
UTouchInputComponent::UTouchInputComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.TickInterval = 0.0f;
|
||||
}
|
||||
|
||||
void UTouchInputComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (!ResolvePlayerController() && bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogFunnyGirlTouchInput, Warning, TEXT("TouchInputComponent on %s could not find a PlayerController. Put it on a PlayerController, PlayerState-owned actor, or local player pawn."), *GetNameSafe(GetOwner()));
|
||||
}
|
||||
}
|
||||
|
||||
void UTouchInputComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
if (!bEnabled)
|
||||
{
|
||||
ResetTouchState();
|
||||
return;
|
||||
}
|
||||
|
||||
PollTouches(DeltaTime);
|
||||
}
|
||||
|
||||
void UTouchInputComponent::ResetTouchState()
|
||||
{
|
||||
for (FTrackedTouch& Touch : Touches)
|
||||
{
|
||||
Touch = FTrackedTouch();
|
||||
}
|
||||
|
||||
ResetGestureTracking();
|
||||
}
|
||||
|
||||
APlayerController* UTouchInputComponent::ResolvePlayerController() const
|
||||
{
|
||||
if (APlayerController* PlayerController = Cast<APlayerController>(GetOwner()))
|
||||
{
|
||||
return PlayerController;
|
||||
}
|
||||
|
||||
if (const APawn* Pawn = Cast<APawn>(GetOwner()))
|
||||
{
|
||||
return Cast<APlayerController>(Pawn->GetController());
|
||||
}
|
||||
|
||||
if (const UWorld* World = GetWorld())
|
||||
{
|
||||
return World->GetFirstPlayerController();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UTouchInputComponent::PollTouches(float DeltaTime)
|
||||
{
|
||||
APlayerController* PlayerController = ResolvePlayerController();
|
||||
if (!PlayerController)
|
||||
{
|
||||
ResetTouchState();
|
||||
return;
|
||||
}
|
||||
|
||||
const double CurrentTime = GetWorld() ? GetWorld()->GetTimeSeconds() : 0.0;
|
||||
for (int32 TouchIndex = 0; TouchIndex < MaxTouchCount; ++TouchIndex)
|
||||
{
|
||||
float LocationX = 0.0f;
|
||||
float LocationY = 0.0f;
|
||||
bool bPressed = false;
|
||||
PlayerController->GetInputTouchState(static_cast<ETouchIndex::Type>(TouchIndex), LocationX, LocationY, bPressed);
|
||||
|
||||
FTrackedTouch& Touch = Touches[TouchIndex];
|
||||
const FVector2D Location(LocationX, LocationY);
|
||||
|
||||
if (bPressed)
|
||||
{
|
||||
if (!Touch.bPressed)
|
||||
{
|
||||
Touch.StartLocation = Location;
|
||||
Touch.StartTime = CurrentTime;
|
||||
}
|
||||
|
||||
Touch.Location = Location;
|
||||
}
|
||||
|
||||
Touch.bPressed = bPressed;
|
||||
}
|
||||
|
||||
const int32 PressedCount = ReadPressedTouches();
|
||||
|
||||
if (PressedCount <= 0)
|
||||
{
|
||||
if (bGestureActive)
|
||||
{
|
||||
EndGesture(CurrentTime);
|
||||
}
|
||||
CurrentFingerCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const FVector2D Centroid = GetCentroid();
|
||||
|
||||
if (!bGestureActive)
|
||||
{
|
||||
BeginGesture(PressedCount, Centroid, CurrentTime);
|
||||
}
|
||||
else if (PressedCount != CurrentFingerCount)
|
||||
{
|
||||
HandleFingerCountChanged(PressedCount, Centroid, CurrentTime);
|
||||
}
|
||||
|
||||
HandleActiveGesture(PressedCount, Centroid, DeltaTime, CurrentTime);
|
||||
PreviousCentroid = Centroid;
|
||||
CurrentFingerCount = PressedCount;
|
||||
}
|
||||
|
||||
int32 UTouchInputComponent::ReadPressedTouches()
|
||||
{
|
||||
int32 PressedCount = 0;
|
||||
for (const FTrackedTouch& Touch : Touches)
|
||||
{
|
||||
if (Touch.bPressed)
|
||||
{
|
||||
++PressedCount;
|
||||
}
|
||||
}
|
||||
return PressedCount;
|
||||
}
|
||||
|
||||
void UTouchInputComponent::BeginGesture(int32 FingerCount, const FVector2D& Centroid, double CurrentTime)
|
||||
{
|
||||
bGestureActive = true;
|
||||
GestureState = ETouchGestureState::None;
|
||||
bMoveStarted = false;
|
||||
CurrentFingerCount = FingerCount;
|
||||
GestureFingerCount = FingerCount;
|
||||
GestureStartTime = CurrentTime;
|
||||
GestureStartCentroid = Centroid;
|
||||
PreviousCentroid = Centroid;
|
||||
InitialSpan = GetAverageSpan(Centroid);
|
||||
PreviousSpan = InitialSpan;
|
||||
MaxDistanceFromStart = 0.0f;
|
||||
}
|
||||
|
||||
void UTouchInputComponent::HandleFingerCountChanged(int32 FingerCount, const FVector2D& Centroid, double CurrentTime)
|
||||
{
|
||||
if (GestureState != ETouchGestureState::None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentFingerCount = FingerCount;
|
||||
GestureFingerCount = FMath::Max(GestureFingerCount, FingerCount);
|
||||
GestureStartTime = CurrentTime;
|
||||
GestureStartCentroid = Centroid;
|
||||
PreviousCentroid = Centroid;
|
||||
InitialSpan = GetAverageSpan(Centroid);
|
||||
PreviousSpan = InitialSpan;
|
||||
MaxDistanceFromStart = 0.0f;
|
||||
bMoveStarted = false;
|
||||
}
|
||||
|
||||
void UTouchInputComponent::HandleActiveGesture(int32 FingerCount, const FVector2D& Centroid, float DeltaTime, double CurrentTime)
|
||||
{
|
||||
const FVector2D Delta = Centroid - PreviousCentroid;
|
||||
const FVector2D TotalDelta = Centroid - GestureStartCentroid;
|
||||
MaxDistanceFromStart = FMath::Max(MaxDistanceFromStart, FVector2D::Distance(Centroid, GestureStartCentroid));
|
||||
const float Duration = static_cast<float>(CurrentTime - GestureStartTime);
|
||||
const float Distance = TotalDelta.Size();
|
||||
const bool bCanBeTap = Duration <= TapMaxDuration && MaxDistanceFromStart <= TapMaxMovement;
|
||||
|
||||
if ((FingerCount == 2 || FingerCount == 4) && InitialSpan > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
const float CurrentSpan = GetAverageSpan(Centroid);
|
||||
const float PreviousScale = PreviousSpan > KINDA_SMALL_NUMBER ? CurrentSpan / PreviousSpan : 1.0f;
|
||||
const float ScaleDelta = PreviousScale - 1.0f;
|
||||
|
||||
if (GestureState == ETouchGestureState::ZoomOrPinch)
|
||||
{
|
||||
BroadcastPinchOrZoom(BuildGestureData(FingerCount, Centroid, Delta, CurrentTime), ScaleDelta);
|
||||
}
|
||||
else if (GestureState == ETouchGestureState::None && FMath::Abs(ScaleDelta) >= PinchMinScaleDelta)
|
||||
{
|
||||
GestureState = ETouchGestureState::ZoomOrPinch;
|
||||
BroadcastPinchOrZoom(BuildGestureData(FingerCount, Centroid, Delta, CurrentTime), ScaleDelta);
|
||||
}
|
||||
|
||||
PreviousSpan = CurrentSpan;
|
||||
}
|
||||
|
||||
if (GestureState != ETouchGestureState::None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (GestureState == ETouchGestureState::None && FingerCount >= 1 && FingerCount <= 3 && Duration >= HoldDuration && MaxDistanceFromStart <= HoldMaxMovement)
|
||||
{
|
||||
GestureState = ETouchGestureState::Hold;
|
||||
BroadcastHold(BuildGestureData(FingerCount, Centroid, Delta, CurrentTime));
|
||||
return;
|
||||
}
|
||||
|
||||
const bool bShouldStartMove = !bCanBeTap && Duration >= MoveStartDelay;
|
||||
if (FingerCount == 1 && Delta.Size() >= MoveMinDistancePerFrame && (bMoveStarted || bShouldStartMove))
|
||||
{
|
||||
bMoveStarted = true;
|
||||
BroadcastMove(BuildGestureData(FingerCount, Centroid, Delta, CurrentTime));
|
||||
}
|
||||
}
|
||||
|
||||
void UTouchInputComponent::EndGesture(double CurrentTime)
|
||||
{
|
||||
const FVector2D EndCentroid = PreviousCentroid;
|
||||
const FVector2D TotalDelta = EndCentroid - GestureStartCentroid;
|
||||
const float Distance = TotalDelta.Size();
|
||||
const float Duration = static_cast<float>(CurrentTime - GestureStartTime);
|
||||
const int32 FinalFingerCount = FMath::Clamp(GestureFingerCount, 1, 5);
|
||||
const FTouchGestureData Gesture = BuildGestureData(FinalFingerCount, EndCentroid, FVector2D::ZeroVector, CurrentTime);
|
||||
|
||||
if (GestureState == ETouchGestureState::None && !bMoveStarted && FinalFingerCount >= 1 && FinalFingerCount <= 3)
|
||||
{
|
||||
if (Duration <= TapMaxDuration && Distance <= TapMaxMovement && MaxDistanceFromStart <= TapMaxMovement)
|
||||
{
|
||||
BroadcastTap(Gesture);
|
||||
}
|
||||
else if (FinalFingerCount == 1 && IsStableSwipe(TotalDelta, Duration))
|
||||
{
|
||||
GestureState = ETouchGestureState::Swipe;
|
||||
BroadcastSwipe(Gesture);
|
||||
}
|
||||
}
|
||||
|
||||
ResetGestureTracking();
|
||||
}
|
||||
|
||||
void UTouchInputComponent::ResetGestureTracking()
|
||||
{
|
||||
bGestureActive = false;
|
||||
GestureState = ETouchGestureState::None;
|
||||
bMoveStarted = false;
|
||||
CurrentFingerCount = 0;
|
||||
GestureFingerCount = 0;
|
||||
GestureStartTime = 0.0;
|
||||
GestureStartCentroid = FVector2D::ZeroVector;
|
||||
PreviousCentroid = FVector2D::ZeroVector;
|
||||
InitialSpan = 0.0f;
|
||||
PreviousSpan = 0.0f;
|
||||
MaxDistanceFromStart = 0.0f;
|
||||
}
|
||||
|
||||
FVector2D UTouchInputComponent::GetCentroid() const
|
||||
{
|
||||
FVector2D Sum = FVector2D::ZeroVector;
|
||||
int32 Count = 0;
|
||||
|
||||
for (const FTrackedTouch& Touch : Touches)
|
||||
{
|
||||
if (Touch.bPressed)
|
||||
{
|
||||
Sum += Touch.Location;
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
|
||||
return Count > 0 ? Sum / static_cast<float>(Count) : FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
float UTouchInputComponent::GetAverageSpan(const FVector2D& Centroid) const
|
||||
{
|
||||
float Sum = 0.0f;
|
||||
int32 Count = 0;
|
||||
|
||||
for (const FTrackedTouch& Touch : Touches)
|
||||
{
|
||||
if (Touch.bPressed)
|
||||
{
|
||||
Sum += FVector2D::Distance(Touch.Location, Centroid);
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
|
||||
return Count > 1 ? Sum / static_cast<float>(Count) : 0.0f;
|
||||
}
|
||||
|
||||
EFunnyGirlTouchSwipeDirection UTouchInputComponent::GetSwipeDirection(const FVector2D& TotalDelta) const
|
||||
{
|
||||
const float AbsX = FMath::Abs(TotalDelta.X);
|
||||
const float AbsY = FMath::Abs(TotalDelta.Y);
|
||||
|
||||
if (AbsX >= AbsY * SwipeDirectionBias)
|
||||
{
|
||||
return TotalDelta.X >= 0.0f ? EFunnyGirlTouchSwipeDirection::Right : EFunnyGirlTouchSwipeDirection::Left;
|
||||
}
|
||||
|
||||
if (AbsY >= AbsX * SwipeDirectionBias)
|
||||
{
|
||||
return TotalDelta.Y >= 0.0f ? EFunnyGirlTouchSwipeDirection::Down : EFunnyGirlTouchSwipeDirection::Up;
|
||||
}
|
||||
|
||||
return EFunnyGirlTouchSwipeDirection::None;
|
||||
}
|
||||
|
||||
bool UTouchInputComponent::IsStableSwipe(const FVector2D& TotalDelta, float Duration) const
|
||||
{
|
||||
return Duration > KINDA_SMALL_NUMBER
|
||||
&& Duration <= SwipeMaxDuration
|
||||
&& TotalDelta.Size() >= SwipeMinDistance
|
||||
&& GetSwipeDirection(TotalDelta) != EFunnyGirlTouchSwipeDirection::None;
|
||||
}
|
||||
|
||||
FTouchGestureData UTouchInputComponent::BuildGestureData(int32 FingerCount, const FVector2D& Location, const FVector2D& Delta, double CurrentTime) const
|
||||
{
|
||||
FTouchGestureData Gesture;
|
||||
Gesture.FingerCount = FingerCount;
|
||||
Gesture.StartLocation = GestureStartCentroid;
|
||||
Gesture.Location = Location;
|
||||
Gesture.Delta = Delta;
|
||||
Gesture.TotalDelta = Location - GestureStartCentroid;
|
||||
Gesture.SwipeDirection = GetSwipeDirection(Gesture.TotalDelta);
|
||||
Gesture.Distance = Gesture.TotalDelta.Size();
|
||||
Gesture.Duration = static_cast<float>(CurrentTime - GestureStartTime);
|
||||
return Gesture;
|
||||
}
|
||||
|
||||
void UTouchInputComponent::BroadcastTap(const FTouchGestureData& Gesture)
|
||||
{
|
||||
switch (Gesture.FingerCount)
|
||||
{
|
||||
case 1:
|
||||
OnOneFingerTap.Broadcast(Gesture);
|
||||
break;
|
||||
case 2:
|
||||
OnTwoFingerTap.Broadcast(Gesture);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UTouchInputComponent::BroadcastSwipe(const FTouchGestureData& Gesture)
|
||||
{
|
||||
switch (Gesture.FingerCount)
|
||||
{
|
||||
case 1:
|
||||
OnOneFingerSwipe.Broadcast(Gesture);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UTouchInputComponent::BroadcastMove(const FTouchGestureData& Gesture)
|
||||
{
|
||||
switch (Gesture.FingerCount)
|
||||
{
|
||||
case 1:
|
||||
OnOneFingerMove.Broadcast(Gesture);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UTouchInputComponent::BroadcastHold(const FTouchGestureData& Gesture)
|
||||
{
|
||||
switch (Gesture.FingerCount)
|
||||
{
|
||||
case 1:
|
||||
OnOneFingerHold.Broadcast(Gesture);
|
||||
break;
|
||||
case 2:
|
||||
OnTwoFingerHold.Broadcast(Gesture);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UTouchInputComponent::BroadcastPinchOrZoom(const FTouchGestureData& Gesture, float ZoomDelta)
|
||||
{
|
||||
switch (Gesture.FingerCount)
|
||||
{
|
||||
case 2:
|
||||
OnZoom.Broadcast(ZoomDelta);
|
||||
break;
|
||||
case 4:
|
||||
OnFourFingerPinch.Broadcast(Gesture);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "LLM/BrainComponentBase.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "Interfaces/IHttpRequest.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "LLM/BrainToolBase.h"
|
||||
#include "LLM/LLMProviderDataAsset.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "World/WorldContextActor.h"
|
||||
|
||||
UBrainComponentBase::UBrainComponentBase()
|
||||
: bAutoFindWorldContextActor(true)
|
||||
, bIsThinking(false)
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void UBrainComponentBase::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
AutoFindWorldContextActor();
|
||||
}
|
||||
|
||||
void UBrainComponentBase::CancelRequest()
|
||||
{
|
||||
if (ActiveRequest.IsValid())
|
||||
{
|
||||
ActiveRequest->CancelRequest();
|
||||
ActiveRequest.Reset();
|
||||
}
|
||||
|
||||
SetIsThinking(false);
|
||||
}
|
||||
|
||||
bool UBrainComponentBase::IsThinking() const
|
||||
{
|
||||
return bIsThinking;
|
||||
}
|
||||
|
||||
FString UBrainComponentBase::GetChatIdentityId() const
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("%s does not provide a chat identity id."), *GetBrainDebugName()));
|
||||
return FString();
|
||||
}
|
||||
|
||||
void UBrainComponentBase::BroadcastBrainError(const FString& ErrorMessage) const
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("%s error: %s"), *GetBrainDebugName(), *ErrorMessage);
|
||||
}
|
||||
|
||||
FString UBrainComponentBase::GetBrainDebugName() const
|
||||
{
|
||||
return GetNameSafe(GetClass());
|
||||
}
|
||||
|
||||
void UBrainComponentBase::SetIsThinking(bool bNewIsThinking)
|
||||
{
|
||||
bIsThinking = bNewIsThinking;
|
||||
}
|
||||
|
||||
void UBrainComponentBase::AutoFindWorldContextActor()
|
||||
{
|
||||
if (WorldContextActor || !bAutoFindWorldContextActor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WorldContextActor = Cast<AWorldContextActor>(UGameplayStatics::GetActorOfClass(this, AWorldContextActor::StaticClass()));
|
||||
}
|
||||
|
||||
bool UBrainComponentBase::ValidateProvider(const ULLMProviderDataAsset* EffectiveProvider) const
|
||||
{
|
||||
if (!EffectiveProvider)
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("%s LLM provider is not set."), *GetBrainDebugName()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (EffectiveProvider->ApiBaseUrl.IsEmpty() || EffectiveProvider->ChatCompletionsPath.IsEmpty())
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("%s LLM provider endpoint is not configured."), *GetBrainDebugName()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (EffectiveProvider->Model.IsEmpty())
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("%s LLM provider model is not configured."), *GetBrainDebugName()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UBrainComponentBase::ConfigureRequestWithBody(
|
||||
const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request,
|
||||
const ULLMProviderDataAsset* EffectiveProvider,
|
||||
const FString& RequestBody,
|
||||
const FString& AcceptHeader) const
|
||||
{
|
||||
if (!ValidateProvider(EffectiveProvider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Request->SetURL(EffectiveProvider->GetChatCompletionsUrl());
|
||||
Request->SetVerb(TEXT("POST"));
|
||||
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
|
||||
Request->SetHeader(TEXT("Accept"), AcceptHeader);
|
||||
|
||||
if (!EffectiveProvider->ApiKey.IsEmpty())
|
||||
{
|
||||
Request->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("Bearer %s"), *EffectiveProvider->ApiKey));
|
||||
}
|
||||
|
||||
for (const TPair<FString, FString>& Header : EffectiveProvider->ExtraHeaders)
|
||||
{
|
||||
Request->SetHeader(Header.Key, Header.Value);
|
||||
}
|
||||
|
||||
Request->SetTimeout(EffectiveProvider->TimeoutSeconds);
|
||||
Request->SetContentAsString(RequestBody);
|
||||
return true;
|
||||
}
|
||||
|
||||
void UBrainComponentBase::AddSystemMessage(TArray<TSharedPtr<FJsonValue>>& Messages, const FString& Content) const
|
||||
{
|
||||
const FString TrimmedContent = Content.TrimStartAndEnd();
|
||||
if (TrimmedContent.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Messages.Add(MakeShared<FJsonValueObject>(MakeMessageObject(TEXT("system"), TrimmedContent)));
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> UBrainComponentBase::MakeMessageObject(const FString& Role, const FString& Content) const
|
||||
{
|
||||
TSharedRef<FJsonObject> MessageObject = MakeShared<FJsonObject>();
|
||||
MessageObject->SetStringField(TEXT("role"), Role);
|
||||
MessageObject->SetStringField(TEXT("content"), Content);
|
||||
return MessageObject;
|
||||
}
|
||||
|
||||
FString UBrainComponentBase::SerializeJsonObject(const TSharedRef<FJsonObject>& RootObject) const
|
||||
{
|
||||
FString OutputJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputJson);
|
||||
FJsonSerializer::Serialize(RootObject, Writer);
|
||||
return OutputJson;
|
||||
}
|
||||
|
||||
void UBrainComponentBase::AppendAllowedTools(TSharedRef<FJsonObject> RootObject, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools) const
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Tools;
|
||||
const TArray<TSharedPtr<FJsonValue>>* ExistingTools = nullptr;
|
||||
if (RootObject->TryGetArrayField(TEXT("tools"), ExistingTools) && ExistingTools)
|
||||
{
|
||||
Tools = *ExistingTools;
|
||||
}
|
||||
|
||||
for (const TSubclassOf<UBrainToolBase>& ToolClass : AllowedTools)
|
||||
{
|
||||
if (!ToolClass)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
|
||||
TSharedPtr<FJsonObject> ToolDefinition = BuildToolDefinition(ToolDefaultObject);
|
||||
if (ToolDefinition.IsValid())
|
||||
{
|
||||
Tools.Add(MakeShared<FJsonValueObject>(ToolDefinition.ToSharedRef()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!Tools.IsEmpty())
|
||||
{
|
||||
RootObject->SetArrayField(TEXT("tools"), Tools);
|
||||
RootObject->SetStringField(TEXT("tool_choice"), TEXT("auto"));
|
||||
}
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> UBrainComponentBase::BuildToolDefinition(const UBrainToolBase* ToolDefaultObject) const
|
||||
{
|
||||
if (!ToolDefaultObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const FString ToolName = ToolDefaultObject->GetResolvedToolName();
|
||||
if (ToolName.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("Tool has empty ToolName: %s"), *GetNameSafe(ToolDefaultObject->GetClass())));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> ParametersObject = MakeShared<FJsonObject>();
|
||||
if (ToolDefaultObject->ParametersJsonSchema.IsEmpty())
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("Tool %s has empty ParametersJsonSchema."), *ToolName));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
{
|
||||
TSharedPtr<FJsonObject> ParsedParametersObject;
|
||||
TSharedRef<TJsonReader<>> ParametersReader = TJsonReaderFactory<>::Create(ToolDefaultObject->ParametersJsonSchema.ToString());
|
||||
if (FJsonSerializer::Deserialize(ParametersReader, ParsedParametersObject) && ParsedParametersObject.IsValid())
|
||||
{
|
||||
ParametersObject = ParsedParametersObject.ToSharedRef();
|
||||
}
|
||||
else
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("Tool %s has invalid ParametersJsonSchema."), *ToolName));
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (ParametersObject->Values.IsEmpty())
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("Tool %s has empty ParametersJsonSchema object."), *ToolName));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> FunctionObject = MakeShared<FJsonObject>();
|
||||
FunctionObject->SetStringField(TEXT("name"), ToolName);
|
||||
FunctionObject->SetStringField(TEXT("description"), ToolDefaultObject->Description.ToString());
|
||||
FunctionObject->SetObjectField(TEXT("parameters"), ParametersObject);
|
||||
|
||||
TSharedRef<FJsonObject> ToolObject = MakeShared<FJsonObject>();
|
||||
ToolObject->SetStringField(TEXT("type"), TEXT("function"));
|
||||
ToolObject->SetObjectField(TEXT("function"), FunctionObject);
|
||||
return ToolObject;
|
||||
}
|
||||
|
||||
const UBrainToolBase* UBrainComponentBase::FindAllowedToolDefaultObject(const FString& ToolName, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools) const
|
||||
{
|
||||
if (ToolName.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const TSubclassOf<UBrainToolBase>& ToolClass : AllowedTools)
|
||||
{
|
||||
if (!ToolClass)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
|
||||
if (ToolDefaultObject && ToolDefaultObject->GetResolvedToolName().Equals(ToolName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return ToolDefaultObject;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UBrainComponentBase::ExecuteBrainToolCalls(const TArray<TSharedPtr<FJsonValue>>& ToolCalls, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools)
|
||||
{
|
||||
if (ToolCalls.IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool bAllSucceeded = true;
|
||||
for (const TSharedPtr<FJsonValue>& ToolCallValue : ToolCalls)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* ToolCallObject = nullptr;
|
||||
if (!ToolCallValue.IsValid() || !ToolCallValue->TryGetObject(ToolCallObject) || !ToolCallObject || !ToolCallObject->IsValid())
|
||||
{
|
||||
bAllSucceeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* FunctionObject = nullptr;
|
||||
if (!(*ToolCallObject)->TryGetObjectField(TEXT("function"), FunctionObject) || !FunctionObject || !FunctionObject->IsValid())
|
||||
{
|
||||
BroadcastBrainError(TEXT("Native tool call is missing function object."));
|
||||
bAllSucceeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
FString ToolName;
|
||||
if (!(*FunctionObject)->TryGetStringField(TEXT("name"), ToolName))
|
||||
{
|
||||
BroadcastBrainError(TEXT("Native tool call is missing function name."));
|
||||
bAllSucceeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
FString ArgumentsJson;
|
||||
if (!(*FunctionObject)->TryGetStringField(TEXT("arguments"), ArgumentsJson))
|
||||
{
|
||||
ArgumentsJson = TEXT("{}");
|
||||
}
|
||||
|
||||
const UBrainToolBase* ToolDefaultObject = FindAllowedToolDefaultObject(ToolName, AllowedTools);
|
||||
if (!ToolDefaultObject)
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("Tried to call disallowed native tool: %s"), *ToolName));
|
||||
bAllSucceeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
UBrainToolBase* ToolInstance = NewObject<UBrainToolBase>(this, ToolDefaultObject->GetClass());
|
||||
if (!ToolInstance)
|
||||
{
|
||||
BroadcastBrainError(FString::Printf(TEXT("Could not create native tool: %s"), *ToolName));
|
||||
bAllSucceeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
FString ErrorMessage;
|
||||
if (!ToolInstance->ExecuteTool(this, ArgumentsJson, ErrorMessage))
|
||||
{
|
||||
BroadcastBrainError(ErrorMessage.IsEmpty() ? FString::Printf(TEXT("Native tool failed: %s"), *ToolName) : ErrorMessage);
|
||||
bAllSucceeded = false;
|
||||
}
|
||||
|
||||
ToolInstance->CleanupTool();
|
||||
}
|
||||
|
||||
return bAllSucceeded;
|
||||
}
|
||||
|
||||
FString UBrainComponentBase::GetChatDirectory() const
|
||||
{
|
||||
const FString Directory = FPaths::ProjectSavedDir() / TEXT("Chat") / GetChatIdentityId();
|
||||
IFileManager::Get().MakeDirectory(*Directory, true);
|
||||
return Directory;
|
||||
}
|
||||
|
||||
FString UBrainComponentBase::GetChatFilePath() const
|
||||
{
|
||||
return GetChatSiblingFilePath(TEXT("Chat.json"));
|
||||
}
|
||||
|
||||
FString UBrainComponentBase::GetChatSiblingFilePath(const FString& FileName) const
|
||||
{
|
||||
return GetChatDirectory() / FileName;
|
||||
}
|
||||
|
||||
void UBrainComponentBase::SaveRequestBodyToFile(const FString& RequestBody, const FString& FileName) const
|
||||
{
|
||||
FFileHelper::SaveStringToFile(RequestBody, *GetChatSiblingFilePath(FileName), FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "LLM/BrainPromptDataAsset.h"
|
||||
|
||||
#include "LLM/BrainToolBase.h"
|
||||
|
||||
FString UBrainPromptDataAsset::BuildBrainPrompt() const
|
||||
{
|
||||
TArray<FString> Sections;
|
||||
if (!SystemPrompt.IsEmpty())
|
||||
{
|
||||
Sections.Add(SystemPrompt.ToString());
|
||||
}
|
||||
|
||||
const FString ToolsPrompt = BuildToolsPrompt();
|
||||
if (!ToolsPrompt.IsEmpty())
|
||||
{
|
||||
Sections.Add(ToolsPrompt);
|
||||
}
|
||||
|
||||
return FString::Join(Sections, TEXT("\n\n"));
|
||||
}
|
||||
|
||||
FString UBrainPromptDataAsset::BuildToolsPrompt() const
|
||||
{
|
||||
TArray<FString> ToolPrompts;
|
||||
for (const TSubclassOf<UBrainToolBase>& ToolClass : AllowedTools)
|
||||
{
|
||||
if (!ToolClass)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
|
||||
if (ToolDefaultObject)
|
||||
{
|
||||
ToolPrompts.Add(ToolDefaultObject->BuildToolPrompt());
|
||||
}
|
||||
}
|
||||
|
||||
if (ToolPrompts.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
TArray<FString> Sections;
|
||||
if (!ToolsPromptHeader.IsEmpty())
|
||||
{
|
||||
Sections.Add(ToolsPromptHeader.ToString());
|
||||
}
|
||||
|
||||
Sections.Add(FString::Join(ToolPrompts, TEXT("\n")));
|
||||
|
||||
if (!ToolsPromptFooter.IsEmpty())
|
||||
{
|
||||
Sections.Add(ToolsPromptFooter.ToString());
|
||||
}
|
||||
|
||||
return FString::Join(Sections, TEXT("\n\n"));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "LLM/BrainToolBase.h"
|
||||
|
||||
FString UBrainToolBase::GetResolvedToolName() const
|
||||
{
|
||||
return ToolName.TrimStartAndEnd();
|
||||
}
|
||||
|
||||
FString UBrainToolBase::BuildToolPrompt() const
|
||||
{
|
||||
TArray<FString> Lines;
|
||||
Lines.Add(FString::Printf(TEXT("- tool: %s"), *GetResolvedToolName()));
|
||||
|
||||
if (!Description.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT(" description: %s"), *Description.ToString()));
|
||||
}
|
||||
|
||||
return FString::Join(Lines, TEXT("\n"));
|
||||
}
|
||||
|
||||
bool UBrainToolBase::ExecuteTool_Implementation(UBrainComponentBase* Brain, const FString& ArgumentsJson, FString& OutError)
|
||||
{
|
||||
OutError = TEXT("Brain tool execution is not implemented.");
|
||||
return false;
|
||||
}
|
||||
|
||||
void UBrainToolBase::CleanupTool_Implementation()
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "LLM/DirectorBrainComponent.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "HttpModule.h"
|
||||
#include "Interfaces/IHttpResponse.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "LLM/BrainToolBase.h"
|
||||
#include "LLM/DirectorPromptDataAsset.h"
|
||||
#include "LLM/IdentityDataAsset.h"
|
||||
#include "LLM/LLMProviderDataAsset.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "World/WorldContextActor.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogDirectorBrain, Log, All);
|
||||
|
||||
UDirectorBrainComponent::UDirectorBrainComponent()
|
||||
{
|
||||
ExistingPlayerFactsPromptTemplate = FText::FromString(TEXT(
|
||||
"EXISTING PLAYER FACTS:\n"
|
||||
"These facts are already saved in Player.json. Do not save duplicate facts. "
|
||||
"Only update a key when the new transcript clearly corrects it or adds more specific information.\n{facts}"));
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (!PersonaBrain)
|
||||
{
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
PersonaBrain = Owner->FindComponentByClass<UPersonaBrainComponent>();
|
||||
}
|
||||
}
|
||||
|
||||
if (PersonaBrain)
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("BeginPlay binding to PersonaBrain: %s"), *GetNameSafe(PersonaBrain));
|
||||
BindToPersonaBrain(PersonaBrain);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("BeginPlay has no PersonaBrain assigned."));
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
CancelRequest();
|
||||
|
||||
if (PersonaBrain)
|
||||
{
|
||||
PersonaBrain->OnLLMResponse.RemoveDynamic(this, &UDirectorBrainComponent::HandlePersonaBrainResponse);
|
||||
}
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::BindToPersonaBrain(UPersonaBrainComponent* Brain)
|
||||
{
|
||||
if (PersonaBrain)
|
||||
{
|
||||
PersonaBrain->OnLLMResponse.RemoveDynamic(this, &UDirectorBrainComponent::HandlePersonaBrainResponse);
|
||||
}
|
||||
|
||||
PersonaBrain = Brain;
|
||||
|
||||
if (PersonaBrain)
|
||||
{
|
||||
PersonaBrain->OnLLMResponse.AddUniqueDynamic(this, &UDirectorBrainComponent::HandlePersonaBrainResponse);
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Bound to PersonaBrain OnLLMResponse: %s"), *GetNameSafe(PersonaBrain));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("BindToPersonaBrain called with null Persona Brain."));
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::AnalyzeCurrentChat()
|
||||
{
|
||||
if (!ValidateSettings())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("AnalyzeCurrentChat aborted: invalid settings."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasValidAllowedTool())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("AnalyzeCurrentChat skipped: DirectorPrompt has no valid native tools."));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("AnalyzeCurrentChat requested. IdentityId=%s ChatFile=%s"), *GetChatIdentityId(), *GetChatFilePath());
|
||||
|
||||
CancelRequest();
|
||||
|
||||
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
|
||||
if (!ConfigureRequest(Request))
|
||||
{
|
||||
SetIsThinking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Request->OnProcessRequestComplete().BindUObject(this, &UDirectorBrainComponent::HandleRequestComplete);
|
||||
ActiveRequest = Request;
|
||||
SetIsThinking(true);
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Director request started."));
|
||||
Request->ProcessRequest();
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::HandlePersonaBrainResponse(const FLLMReplyResponse& Response)
|
||||
{
|
||||
if (Response.Reply.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("PersonaBrain response was empty. Director analysis skipped."));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("PersonaBrain response received. Starting Director analysis. ReplyLen=%d"), Response.Reply.Len());
|
||||
AnalyzeCurrentChat();
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::BroadcastBrainError(const FString& ErrorMessage) const
|
||||
{
|
||||
OnDirectorError.Broadcast(ErrorMessage);
|
||||
}
|
||||
|
||||
FString UDirectorBrainComponent::GetBrainDebugName() const
|
||||
{
|
||||
return TEXT("Director");
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::AppendSystemMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const
|
||||
{
|
||||
check(DirectorPrompt);
|
||||
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Building Director prompt with %d allowed tool(s)."), DirectorPrompt->AllowedTools.Num());
|
||||
for (const TSubclassOf<UBrainToolBase>& ToolClass : DirectorPrompt->AllowedTools)
|
||||
{
|
||||
if (!ToolClass)
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("DirectorPrompt has a null AllowedTools entry."));
|
||||
continue;
|
||||
}
|
||||
|
||||
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
|
||||
UE_LOG(
|
||||
LogDirectorBrain,
|
||||
Log,
|
||||
TEXT("Director allowed tool: %s Class=%s"),
|
||||
ToolDefaultObject ? *ToolDefaultObject->GetResolvedToolName() : TEXT("<no default object>"),
|
||||
*ToolClass->GetName());
|
||||
}
|
||||
|
||||
static const FString DirectorScopeGuard = TEXT(
|
||||
"Director scope rules:\n"
|
||||
"- Director is a hidden router for native tools. It does not roleplay and does not continue the dialogue.\n"
|
||||
"- Decide whether to call a tool by reading that tool's own description and parameter schema.\n"
|
||||
"- Tool-specific save/update rules belong to tool descriptions, not to the Director prompt.\n"
|
||||
"- If a situation does not clearly match any available tool description, do not call a tool.\n"
|
||||
"\n"
|
||||
"- Director does NOT control interactive scene objects.\n"
|
||||
"- Interactive objects and physical actions are handled by Persona interact_action and WorldContext.\n"
|
||||
"- Do NOT infer, track, or save physical object states from dialogue unless a tool explicitly says to do that.\n"
|
||||
"\n"
|
||||
"- The full transcript is context only. Use it to understand continuity, avoid duplicates, and detect corrections.\n"
|
||||
"- Call tools only for information introduced or clearly confirmed in LATEST TURN TO ANALYZE, unless a tool description explicitly says otherwise.\n"
|
||||
"- Do NOT mine older transcript messages for new tool calls during later analysis passes.\n"
|
||||
"- You may call multiple tools if multiple distinct tool-relevant updates are clearly present.\n"
|
||||
"\n"
|
||||
"- If the transcript only contains a command affecting a scene object and a character acknowledgement, do NOT call any tool.");
|
||||
|
||||
FString DirectorPromptText = DirectorPrompt->BuildDirectorPrompt();
|
||||
DirectorPromptText = DirectorPromptText.Replace(
|
||||
TEXT("Use at most one tool call per analysis."),
|
||||
TEXT("You may call multiple tools if multiple distinct tool-relevant updates are clearly present."));
|
||||
DirectorPromptText = DirectorPromptText.Replace(
|
||||
TEXT("Use one tool call per distinct durable fact. Multiple tool calls are allowed."),
|
||||
TEXT("You may call multiple tools if multiple distinct tool-relevant updates are clearly present."));
|
||||
|
||||
AddSystemMessage(Messages, FString::Printf(TEXT("%s\n\n%s"), *DirectorPromptText, *DirectorScopeGuard));
|
||||
|
||||
if (WorldContextActor)
|
||||
{
|
||||
AddSystemMessage(Messages, WorldContextActor->BuildBaseWorldContextPrompt());
|
||||
}
|
||||
}
|
||||
|
||||
FString UDirectorBrainComponent::BuildRequestBody(const ULLMProviderDataAsset* EffectiveProvider) const
|
||||
{
|
||||
TSharedRef<FJsonObject> RootObject = MakeShared<FJsonObject>();
|
||||
RootObject->SetStringField(TEXT("model"), EffectiveProvider->Model);
|
||||
RootObject->SetBoolField(TEXT("stream"), false);
|
||||
RootObject->SetNumberField(TEXT("temperature"), EffectiveProvider->Temperature);
|
||||
RootObject->SetNumberField(TEXT("max_tokens"), EffectiveProvider->MaxTokens);
|
||||
RootObject->SetNumberField(TEXT("top_p"), EffectiveProvider->TopP);
|
||||
check(DirectorPrompt);
|
||||
AppendAllowedTools(RootObject, DirectorPrompt->AllowedTools);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Messages;
|
||||
AppendSystemMessages(Messages);
|
||||
|
||||
AppendSavedSemanticMemoryMessages(Messages);
|
||||
AppendSavedPlayerFactsMessage(Messages);
|
||||
AppendSavedChatHistoryMessages(Messages);
|
||||
|
||||
RootObject->SetArrayField(TEXT("messages"), Messages);
|
||||
|
||||
FString RequestBody;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
|
||||
FJsonSerializer::Serialize(RootObject, Writer);
|
||||
return RequestBody;
|
||||
}
|
||||
|
||||
FString UDirectorBrainComponent::GetChatIdentityId() const
|
||||
{
|
||||
FString IdentityId = PersonaBrain && PersonaBrain->Identity ? PersonaBrain->Identity->ID : FString();
|
||||
if (IdentityId.IsEmpty())
|
||||
{
|
||||
BroadcastBrainError(TEXT("Director requires PersonaBrain with Identity.ID."));
|
||||
return FString();
|
||||
}
|
||||
|
||||
return FPaths::MakeValidFileName(IdentityId);
|
||||
}
|
||||
|
||||
FString UDirectorBrainComponent::GetSemanticsFilePath() const
|
||||
{
|
||||
return GetChatSiblingFilePath(TEXT("Semantics.json"));
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::AppendSavedSemanticMemoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const
|
||||
{
|
||||
const FString FilePath = GetSemanticsFilePath();
|
||||
FString SemanticsJson;
|
||||
if (!FFileHelper::LoadFileToString(SemanticsJson, *FilePath))
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("No saved semantic memories for Director yet: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(SemanticsJson);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("Could not parse semantic memories JSON for Director: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* SavedSemantics = nullptr;
|
||||
if (!RootObject->TryGetArrayField(TEXT("semantics"), SavedSemantics) || SavedSemantics->IsEmpty())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Semantic memory file has no memories for Director: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FString> SemanticLines;
|
||||
const int32 StartIndex = FMath::Max(0, SavedSemantics->Num() - 30);
|
||||
for (int32 Index = StartIndex; Index < SavedSemantics->Num(); ++Index)
|
||||
{
|
||||
const TSharedPtr<FJsonValue>& SemanticValue = (*SavedSemantics)[Index];
|
||||
const TSharedPtr<FJsonObject>* SemanticObject = nullptr;
|
||||
if (!SemanticValue.IsValid() || !SemanticValue->TryGetObject(SemanticObject) || !SemanticObject || !SemanticObject->IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Text;
|
||||
if (!(*SemanticObject)->TryGetStringField(TEXT("text"), Text) || Text.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Category;
|
||||
(*SemanticObject)->TryGetStringField(TEXT("category"), Category);
|
||||
if (Category.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
SemanticLines.Add(FString::Printf(TEXT("- %s"), *Text.TrimStartAndEnd()));
|
||||
}
|
||||
else
|
||||
{
|
||||
SemanticLines.Add(FString::Printf(TEXT("- [%s] %s"), *Category.TrimStartAndEnd(), *Text.TrimStartAndEnd()));
|
||||
}
|
||||
}
|
||||
|
||||
if (SemanticLines.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> SemanticMemoryMessage = MakeShared<FJsonObject>();
|
||||
SemanticMemoryMessage->SetStringField(TEXT("role"), TEXT("user"));
|
||||
check(DirectorPrompt);
|
||||
SemanticMemoryMessage->SetStringField(TEXT("content"), DirectorPrompt->BuildSavedSemanticsContext(SemanticLines));
|
||||
Messages.Add(MakeShared<FJsonValueObject>(SemanticMemoryMessage));
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Director appended %d saved semantic memories."), SemanticLines.Num());
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::AppendSavedPlayerFactsMessage(TArray<TSharedPtr<FJsonValue>>& Messages) const
|
||||
{
|
||||
const FString FilePath = GetChatSiblingFilePath(TEXT("Player.json"));
|
||||
FString PlayerJson;
|
||||
if (!FFileHelper::LoadFileToString(PlayerJson, *FilePath))
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("No saved player facts for Director yet: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(PlayerJson);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("Could not parse saved player facts JSON for Director: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* FactsObject = nullptr;
|
||||
if (!RootObject->TryGetObjectField(TEXT("facts"), FactsObject) || !FactsObject || !FactsObject->IsValid() || (*FactsObject)->Values.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Player facts file has no facts for Director: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FString> FactLines;
|
||||
for (const TPair<FString, TSharedPtr<FJsonValue>>& FactPair : (*FactsObject)->Values)
|
||||
{
|
||||
const TSharedPtr<FJsonValue>& FactValue = FactPair.Value;
|
||||
const TSharedPtr<FJsonObject>* FactObject = nullptr;
|
||||
if (!FactValue.IsValid() || !FactValue->TryGetObject(FactObject) || !FactObject || !FactObject->IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Value;
|
||||
if (!(*FactObject)->TryGetStringField(TEXT("value"), Value) || Value.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Category;
|
||||
(*FactObject)->TryGetStringField(TEXT("category"), Category);
|
||||
|
||||
const FString CleanKey = FactPair.Key.TrimStartAndEnd();
|
||||
const FString CleanValue = Value.TrimStartAndEnd();
|
||||
const FString CleanCategory = Category.TrimStartAndEnd();
|
||||
|
||||
if (CleanCategory.IsEmpty())
|
||||
{
|
||||
FactLines.Add(FString::Printf(TEXT("- %s: %s"), *CleanKey, *CleanValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
FactLines.Add(FString::Printf(TEXT("- [%s] %s: %s"), *CleanCategory, *CleanKey, *CleanValue));
|
||||
}
|
||||
}
|
||||
|
||||
if (FactLines.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FactLines.Sort();
|
||||
|
||||
TSharedRef<FJsonObject> PlayerFactsMessage = MakeShared<FJsonObject>();
|
||||
PlayerFactsMessage->SetStringField(TEXT("role"), TEXT("user"));
|
||||
|
||||
FString ExistingPlayerFactsPrompt = ExistingPlayerFactsPromptTemplate.IsEmptyOrWhitespace()
|
||||
? FString(TEXT("EXISTING PLAYER FACTS:\n{facts}"))
|
||||
: ExistingPlayerFactsPromptTemplate.ToString();
|
||||
ExistingPlayerFactsPrompt.ReplaceInline(TEXT("{facts}"), *FString::Join(FactLines, TEXT("\n")));
|
||||
|
||||
PlayerFactsMessage->SetStringField(
|
||||
TEXT("content"),
|
||||
ExistingPlayerFactsPrompt);
|
||||
|
||||
Messages.Add(MakeShared<FJsonValueObject>(PlayerFactsMessage));
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Director appended %d saved player facts."), FactLines.Num());
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::AppendSavedChatHistoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const
|
||||
{
|
||||
const FString FilePath = GetChatFilePath();
|
||||
FString ChatJson;
|
||||
if (!FFileHelper::LoadFileToString(ChatJson, *FilePath))
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("Could not load chat history for Director: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ChatJson);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("Could not parse chat history JSON for Director: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* SavedMessages = nullptr;
|
||||
if (!RootObject->TryGetArrayField(TEXT("messages"), SavedMessages) || SavedMessages->IsEmpty())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Warning, TEXT("Chat history has no messages for Director: %s"), *FilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
int32 AddedMessages = 0;
|
||||
int32 RelevantMessageCount = 0;
|
||||
TArray<FString> TranscriptLines;
|
||||
TArray<FString> LatestTurnLines;
|
||||
const UIdentityDataAsset* EffectiveIdentity = PersonaBrain ? PersonaBrain->Identity.Get() : nullptr;
|
||||
if (!EffectiveIdentity || EffectiveIdentity->Name.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Error, TEXT("Director requires PersonaBrain Identity.Name for transcript speaker labels."));
|
||||
OnDirectorError.Broadcast(TEXT("Director requires PersonaBrain Identity.Name for transcript speaker labels."));
|
||||
return;
|
||||
}
|
||||
|
||||
FString CharacterName = EffectiveIdentity->Name.ToString();
|
||||
if (CharacterName.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
UE_LOG(LogDirectorBrain, Error, TEXT("Director requires non-empty PersonaBrain Identity.Name for transcript speaker labels."));
|
||||
OnDirectorError.Broadcast(TEXT("Director requires non-empty PersonaBrain Identity.Name for transcript speaker labels."));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const TSharedPtr<FJsonValue>& MessageValue : *SavedMessages)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* MessageObject = nullptr;
|
||||
if (!MessageValue.IsValid() || !MessageValue->TryGetObject(MessageObject) || !MessageObject || !MessageObject->IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Role;
|
||||
FString Content;
|
||||
if ((*MessageObject)->TryGetStringField(TEXT("role"), Role) && (*MessageObject)->TryGetStringField(TEXT("content"), Content))
|
||||
{
|
||||
if ((Role != TEXT("user") && Role != TEXT("assistant")) || Content.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
++RelevantMessageCount;
|
||||
const FString Speaker = Role == TEXT("assistant") ? CharacterName : TEXT("Player");
|
||||
const FString TranscriptLine = FString::Printf(TEXT("%s: %s"), *Speaker, *Content.TrimStartAndEnd());
|
||||
TranscriptLines.Add(TranscriptLine);
|
||||
++AddedMessages;
|
||||
}
|
||||
}
|
||||
|
||||
const int32 LatestTurnStartIndex = FMath::Max(0, TranscriptLines.Num() - 2);
|
||||
for (int32 LineIndex = LatestTurnStartIndex; LineIndex < TranscriptLines.Num(); ++LineIndex)
|
||||
{
|
||||
LatestTurnLines.Add(TranscriptLines[LineIndex]);
|
||||
}
|
||||
|
||||
if (!TranscriptLines.IsEmpty())
|
||||
{
|
||||
TSharedRef<FJsonObject> TranscriptMessage = MakeShared<FJsonObject>();
|
||||
TranscriptMessage->SetStringField(TEXT("role"), TEXT("user"));
|
||||
check(DirectorPrompt);
|
||||
|
||||
TArray<FString> Sections;
|
||||
Sections.Add(TEXT("FULL TRANSCRIPT FOR CONTEXT ONLY.\nUse this section only to understand continuity, avoid duplicates, and detect corrections. Do not save facts that appear only in older messages here."));
|
||||
Sections.Add(DirectorPrompt->BuildTranscriptContext(TranscriptLines));
|
||||
|
||||
if (!LatestTurnLines.IsEmpty())
|
||||
{
|
||||
Sections.Add(FString::Printf(
|
||||
TEXT("LATEST TURN TO ANALYZE.\nOnly tool-relevant information introduced or clearly confirmed in this section may trigger tool calls.\n\n%s"),
|
||||
*FString::Join(LatestTurnLines, TEXT("\n"))));
|
||||
}
|
||||
|
||||
TranscriptMessage->SetStringField(TEXT("content"), FString::Join(Sections, TEXT("\n\n")));
|
||||
Messages.Add(MakeShared<FJsonValueObject>(TranscriptMessage));
|
||||
}
|
||||
|
||||
UE_LOG(
|
||||
LogDirectorBrain,
|
||||
Log,
|
||||
TEXT("Director appended %d saved chat messages as transcript. TotalRelevant=%d LatestTurn=%d"),
|
||||
AddedMessages,
|
||||
RelevantMessageCount,
|
||||
LatestTurnLines.Num());
|
||||
}
|
||||
|
||||
bool UDirectorBrainComponent::ValidateSettings() const
|
||||
{
|
||||
if (!ValidateProvider(Provider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PersonaBrain)
|
||||
{
|
||||
OnDirectorError.Broadcast(TEXT("Director persona brain is not set."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PersonaBrain->Identity)
|
||||
{
|
||||
OnDirectorError.Broadcast(TEXT("Director requires PersonaBrain identity data asset."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PersonaBrain->Identity->ID.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
OnDirectorError.Broadcast(TEXT("Director requires PersonaBrain identity ID."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PersonaBrain->Identity->Name.IsEmpty() || PersonaBrain->Identity->Name.ToString().TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
OnDirectorError.Broadcast(TEXT("Director requires PersonaBrain identity name."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DirectorPrompt)
|
||||
{
|
||||
OnDirectorError.Broadcast(TEXT("Director prompt data asset is not set."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDirectorBrainComponent::HasValidAllowedTool() const
|
||||
{
|
||||
if (!DirectorPrompt)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const TSubclassOf<UBrainToolBase>& ToolClass : DirectorPrompt->AllowedTools)
|
||||
{
|
||||
if (!ToolClass)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
|
||||
if (!ToolDefaultObject)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ToolDefaultObject->GetResolvedToolName().TrimStartAndEnd().IsEmpty() &&
|
||||
!ToolDefaultObject->ParametersJsonSchema.IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UDirectorBrainComponent::ConfigureRequest(const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request) const
|
||||
{
|
||||
if (!ValidateProvider(Provider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString RequestBody = BuildRequestBody(Provider);
|
||||
SaveRequestBodyToFile(RequestBody, TEXT("DirectorLastRequest.json"));
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Director request saved: %s"), *GetChatSiblingFilePath(TEXT("DirectorLastRequest.json")));
|
||||
return ConfigureRequestWithBody(Request, Provider, RequestBody, TEXT("application/json"));
|
||||
}
|
||||
|
||||
void UDirectorBrainComponent::HandleRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
|
||||
{
|
||||
if (Request != ActiveRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActiveRequest.Reset();
|
||||
SetIsThinking(false);
|
||||
|
||||
if (!bWasSuccessful || !Response.IsValid())
|
||||
{
|
||||
OnDirectorError.Broadcast(TEXT("Director LLM request failed."));
|
||||
UE_LOG(LogDirectorBrain, Error, TEXT("Director LLM request failed: no valid response."));
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 ResponseCode = Response->GetResponseCode();
|
||||
const FString ResponseBody = Response->GetContentAsString();
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Director response received. HTTP=%d BodyLen=%d"), ResponseCode, ResponseBody.Len());
|
||||
SaveRequestBodyToFile(ResponseBody, TEXT("DirectorLastResponse.json"));
|
||||
|
||||
if (ResponseCode < 200 || ResponseCode >= 300)
|
||||
{
|
||||
OnDirectorError.Broadcast(FString::Printf(TEXT("Director LLM request failed with HTTP %d: %s"), ResponseCode, *ResponseBody));
|
||||
UE_LOG(LogDirectorBrain, Error, TEXT("Director LLM request failed with HTTP %d: %s"), ResponseCode, *ResponseBody);
|
||||
return;
|
||||
}
|
||||
|
||||
FDirectorBrainResponse DirectorResponse;
|
||||
bool bNativeToolCallsFieldFound = false;
|
||||
bool bNativeToolCallsSucceeded = true;
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseBody);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
OnDirectorError.Broadcast(FString::Printf(TEXT("Could not parse Director LLM response JSON: %s"), *ResponseBody));
|
||||
return;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* Choices = nullptr;
|
||||
if (!RootObject->TryGetArrayField(TEXT("choices"), Choices) || Choices->Num() == 0 || !(*Choices)[0].IsValid())
|
||||
{
|
||||
OnDirectorError.Broadcast(FString::Printf(TEXT("Director LLM response is missing choices[0]: %s"), *ResponseBody));
|
||||
return;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* ChoiceObject = nullptr;
|
||||
if (!(*Choices)[0]->TryGetObject(ChoiceObject) || !ChoiceObject || !ChoiceObject->IsValid())
|
||||
{
|
||||
OnDirectorError.Broadcast(FString::Printf(TEXT("Director LLM response choices[0] is not an object: %s"), *ResponseBody));
|
||||
return;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* MessageObject = nullptr;
|
||||
if (!(*ChoiceObject)->TryGetObjectField(TEXT("message"), MessageObject) || !MessageObject || !MessageObject->IsValid())
|
||||
{
|
||||
OnDirectorError.Broadcast(FString::Printf(TEXT("Director LLM response is missing message object: %s"), *ResponseBody));
|
||||
return;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* NativeToolCalls = nullptr;
|
||||
if ((*MessageObject)->TryGetArrayField(TEXT("tool_calls"), NativeToolCalls) && NativeToolCalls)
|
||||
{
|
||||
bNativeToolCallsFieldFound = true;
|
||||
check(DirectorPrompt);
|
||||
bNativeToolCallsSucceeded = ExecuteBrainToolCalls(*NativeToolCalls, DirectorPrompt->AllowedTools);
|
||||
if (!bNativeToolCallsSucceeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FString Content;
|
||||
if (!(*MessageObject)->TryGetStringField(TEXT("content"), Content) || Content.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
OnDirectorError.Broadcast(bNativeToolCallsFieldFound
|
||||
? TEXT("Director LLM returned tool_calls without response content.")
|
||||
: TEXT("Director LLM response is missing content."));
|
||||
return;
|
||||
}
|
||||
|
||||
DirectorResponse.Content = Content;
|
||||
UE_LOG(LogDirectorBrain, Log, TEXT("Director content: %s"), *DirectorResponse.Content);
|
||||
OnDirectorResponse.Broadcast(DirectorResponse);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "LLM/DirectorPromptDataAsset.h"
|
||||
|
||||
FString UDirectorPromptDataAsset::BuildDirectorPrompt() const
|
||||
{
|
||||
return BuildBrainPrompt();
|
||||
}
|
||||
|
||||
FString UDirectorPromptDataAsset::BuildSavedSemanticsContext(const TArray<FString>& SemanticLines) const
|
||||
{
|
||||
if (SemanticLines.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
TArray<FString> Sections;
|
||||
if (!SavedSemanticsContextPrompt.IsEmpty())
|
||||
{
|
||||
Sections.Add(SavedSemanticsContextPrompt.ToString());
|
||||
}
|
||||
|
||||
Sections.Add(FString::Join(SemanticLines, TEXT("\n")));
|
||||
return FString::Join(Sections, TEXT("\n\n"));
|
||||
}
|
||||
|
||||
FString UDirectorPromptDataAsset::BuildTranscriptContext(const TArray<FString>& TranscriptLines) const
|
||||
{
|
||||
if (TranscriptLines.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
TArray<FString> Sections;
|
||||
if (!TranscriptContextPrompt.IsEmpty())
|
||||
{
|
||||
Sections.Add(TranscriptContextPrompt.ToString());
|
||||
}
|
||||
|
||||
Sections.Add(FString::Join(TranscriptLines, TEXT("\n")));
|
||||
return FString::Join(Sections, TEXT("\n\n"));
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "LLM/IdentityDataAsset.h"
|
||||
|
||||
#include "Identity/PersonalityTraitDataAsset.h"
|
||||
|
||||
FString UIdentityDataAsset::BuildIdentityPrompt() const
|
||||
{
|
||||
TArray<FString> Sections;
|
||||
const FString BrainPrompt = BuildBrainPrompt();
|
||||
if (!BrainPrompt.IsEmpty())
|
||||
{
|
||||
Sections.Add(BrainPrompt);
|
||||
}
|
||||
|
||||
TArray<FString> Lines;
|
||||
Lines.Add(TEXT("[IDENTITY]"));
|
||||
|
||||
if (!Name.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("- Name: %s"), *Name.ToString()));
|
||||
}
|
||||
|
||||
if (!Age.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("- Age: %s"), *Age.ToString()));
|
||||
}
|
||||
|
||||
if (!Appearance.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("- Appearance: %s"), *Appearance.ToString()));
|
||||
}
|
||||
|
||||
if (!Background.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("- Background: %s"), *Background.ToString()));
|
||||
}
|
||||
|
||||
if (!CoreInstruction.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("\n[CORE INSTRUCTION]\n%s"), *CoreInstruction.ToString()));
|
||||
}
|
||||
|
||||
if (!SpeechStyle.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("\n[SPEECH STYLE]\n%s"), *SpeechStyle.ToString()));
|
||||
}
|
||||
|
||||
if (!DialogueBehavior.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("\n[DIALOGUE BEHAVIOR]\n%s"), *DialogueBehavior.ToString()));
|
||||
}
|
||||
|
||||
TArray<FString> PersonalityPrompts;
|
||||
for (const TObjectPtr<UPersonalityTraitDataAsset>& Trait : PersonalityTraits)
|
||||
{
|
||||
if (Trait && !Trait->Prompt.IsEmpty())
|
||||
{
|
||||
PersonalityPrompts.Add(Trait->Prompt.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (!PersonalityPrompts.IsEmpty())
|
||||
{
|
||||
TArray<FString> PersonalitySectionLines;
|
||||
if (!PersonalityTraitsInstruction.IsEmpty())
|
||||
{
|
||||
PersonalitySectionLines.Add(PersonalityTraitsInstruction.ToString());
|
||||
}
|
||||
PersonalitySectionLines.Add(FString::Join(PersonalityPrompts, TEXT("\n")));
|
||||
|
||||
Lines.Add(FString::Printf(TEXT("\n[PERSONALITY TRAITS]\n%s"), *FString::Join(PersonalitySectionLines, TEXT("\n"))));
|
||||
}
|
||||
|
||||
Sections.Add(FString::Join(Lines, TEXT("\n")));
|
||||
return FString::Join(Sections, TEXT("\n\n"));
|
||||
}
|
||||
|
||||
FString UIdentityDataAsset::BuildFirstMeetingPrompt() const
|
||||
{
|
||||
if (FirstMeetingRule.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
return FString::Printf(TEXT("[FIRST MEETING]\n%s"), *FirstMeetingRule.ToString());
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "LLM/LLMProviderDataAsset.h"
|
||||
|
||||
ULLMProviderDataAsset::ULLMProviderDataAsset()
|
||||
: ProviderName(TEXT("DeepSeek"))
|
||||
, ApiBaseUrl(TEXT("https://api.deepseek.com"))
|
||||
, ChatCompletionsPath(TEXT("/chat/completions"))
|
||||
, Model(TEXT("deepseek-chat"))
|
||||
, Temperature(0.7f)
|
||||
, MaxTokens(1024)
|
||||
, TopP(1.0f)
|
||||
, TimeoutSeconds(60.0f)
|
||||
{
|
||||
}
|
||||
|
||||
FString ULLMProviderDataAsset::GetChatCompletionsUrl() const
|
||||
{
|
||||
FString NormalizedBaseUrl = ApiBaseUrl;
|
||||
NormalizedBaseUrl.RemoveFromEnd(TEXT("/"));
|
||||
|
||||
FString NormalizedPath = ChatCompletionsPath;
|
||||
if (!NormalizedPath.StartsWith(TEXT("/")))
|
||||
{
|
||||
NormalizedPath.InsertAt(0, TEXT("/"));
|
||||
}
|
||||
|
||||
return NormalizedBaseUrl + NormalizedPath;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "LLM/SystemEventDataAsset.h"
|
||||
|
||||
FString USystemEventDataAsset::BuildSystemEventPrompt() const
|
||||
{
|
||||
if (Prompt.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("System event happened right now.\n")
|
||||
TEXT("This is not a user message. Do not answer it as a question from the user.\n")
|
||||
TEXT("Your next assistant reply must be an immediate in-character visible reaction to this event.\n")
|
||||
TEXT("Do not continue the previous dialogue as if nothing happened.\n")
|
||||
TEXT("If the event requires an action, acknowledge the interruption and say what you are doing now.\n\n")
|
||||
TEXT("Event:\n%s"),
|
||||
*Prompt.ToString());
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/LipSyncComponent.h"
|
||||
#include "Morph/LipSyncMappingDataAsset.h"
|
||||
#include "Morph/MorphControllerComponent.h"
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
#include "Sound/SoundWave.h"
|
||||
|
||||
ULipSyncComponent::ULipSyncComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.TickInterval = 0.0f;
|
||||
|
||||
AudioDrivenVisemeSequence = {
|
||||
TEXT("A"),
|
||||
TEXT("I"),
|
||||
TEXT("U"),
|
||||
TEXT("E"),
|
||||
TEXT("O"),
|
||||
TEXT("aa"),
|
||||
TEXT("SS"),
|
||||
TEXT("MBP"),
|
||||
TEXT("oh")
|
||||
};
|
||||
}
|
||||
|
||||
void ULipSyncComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (!MorphController && bAutoResolveMorphController)
|
||||
{
|
||||
MorphController = ResolveMorphController();
|
||||
}
|
||||
}
|
||||
|
||||
void ULipSyncComponent::SetVisemeWeight(FName VisemeName, float Weight)
|
||||
{
|
||||
if (VisemeName.IsNone())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float ClampedWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
|
||||
if (ClampedWeight <= SilenceThreshold)
|
||||
{
|
||||
TargetVisemeWeights.Remove(VisemeName);
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetVisemeWeights.Add(VisemeName, ClampedWeight);
|
||||
}
|
||||
}
|
||||
|
||||
void ULipSyncComponent::SetVisemeWeights(const TMap<FName, float>& VisemeWeights)
|
||||
{
|
||||
TargetVisemeWeights.Empty();
|
||||
for (const auto& Pair : VisemeWeights)
|
||||
{
|
||||
SetVisemeWeight(Pair.Key, Pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
void ULipSyncComponent::ClearVisemes()
|
||||
{
|
||||
TargetVisemeWeights.Empty();
|
||||
CurrentVisemeWeights.Empty();
|
||||
LastNonSilentMouthWeight = 0.0f;
|
||||
SilenceHoldRemaining = 0.0f;
|
||||
|
||||
if (UMorphControllerComponent* ResolvedController = ResolveMorphController())
|
||||
{
|
||||
ResolvedController->ClearLipSyncMorphValues();
|
||||
}
|
||||
}
|
||||
|
||||
bool ULipSyncComponent::PlayAudioDrivenLipSync(USoundBase* Sound, float StartTime)
|
||||
{
|
||||
USoundBase* SoundToPlay = Sound ? Sound : AudioDrivenSound.Get();
|
||||
if (!SoundToPlay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UAudioComponent* AudioComponent = ResolveAudioDrivenComponent();
|
||||
if (!AudioComponent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return PlayAudioDrivenLipSyncWithComponent(AudioComponent, SoundToPlay, StartTime);
|
||||
}
|
||||
|
||||
bool ULipSyncComponent::PlayAudioDrivenLipSyncWithComponent(UAudioComponent* AudioComponent, USoundBase* Sound, float StartTime)
|
||||
{
|
||||
if (!AudioComponent || !Sound)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AudioComponent->Stop();
|
||||
AudioComponent->SetSound(Sound);
|
||||
AudioComponent->EnvelopeFollowerAttackTime = 8;
|
||||
AudioComponent->EnvelopeFollowerReleaseTime = 90;
|
||||
AudioComponent->OnAudioFinished.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenFinished);
|
||||
AudioComponent->OnAudioPlaybackPercent.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenPlaybackPercent);
|
||||
AudioComponent->OnAudioSingleEnvelopeValue.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenEnvelopeValue);
|
||||
AudioDrivenComponent = AudioComponent;
|
||||
|
||||
AudioDrivenPlaybackTime = FMath::Max(StartTime, 0.0f);
|
||||
AudioDrivenDuration = FMath::Max(Sound->GetDuration(), 0.0f);
|
||||
LastEnvelopeValue = 0.0f;
|
||||
LastNonSilentMouthWeight = 0.0f;
|
||||
SilenceHoldRemaining = 0.0f;
|
||||
bReceivedEnvelopeValue = false;
|
||||
bAudioDrivenControlsPlayback = true;
|
||||
bWaitingForExternalAudioPlayback = false;
|
||||
ExternalAudioPlaybackWaitTime = 0.0f;
|
||||
RebuildGeneratedTranscriptVisemes();
|
||||
ClearVisemes();
|
||||
bAudioDrivenLipSyncActive = true;
|
||||
|
||||
AudioComponent->Play(StartTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ULipSyncComponent::FollowExternalAudioComponent(UAudioComponent* AudioComponent, const FText& Transcript)
|
||||
{
|
||||
if (!AudioComponent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AudioDrivenComponent = AudioComponent;
|
||||
AudioDrivenTranscript = Transcript;
|
||||
AudioDrivenMode = ELipSyncAudioDrivenMode::TranscriptVisemes;
|
||||
AudioDrivenPlaybackTime = 0.0f;
|
||||
AudioDrivenDuration = 0.0f;
|
||||
LastEnvelopeValue = 0.0f;
|
||||
LastNonSilentMouthWeight = 0.0f;
|
||||
SilenceHoldRemaining = 0.0f;
|
||||
bReceivedEnvelopeValue = false;
|
||||
bAudioDrivenControlsPlayback = false;
|
||||
bWaitingForExternalAudioPlayback = !AudioComponent->IsPlaying();
|
||||
ExternalAudioPlaybackWaitTime = 0.0f;
|
||||
|
||||
AudioComponent->EnvelopeFollowerAttackTime = 8;
|
||||
AudioComponent->EnvelopeFollowerReleaseTime = 90;
|
||||
AudioComponent->OnAudioFinished.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenFinished);
|
||||
AudioComponent->OnAudioPlaybackPercent.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenPlaybackPercent);
|
||||
AudioComponent->OnAudioSingleEnvelopeValue.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenEnvelopeValue);
|
||||
|
||||
RebuildGeneratedTranscriptVisemes();
|
||||
ClearVisemes();
|
||||
bAudioDrivenLipSyncActive = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ULipSyncComponent::StopAudioDrivenLipSync()
|
||||
{
|
||||
bAudioDrivenLipSyncActive = false;
|
||||
|
||||
if (AudioDrivenComponent && bAudioDrivenControlsPlayback)
|
||||
{
|
||||
AudioDrivenComponent->Stop();
|
||||
}
|
||||
|
||||
bWaitingForExternalAudioPlayback = false;
|
||||
ExternalAudioPlaybackWaitTime = 0.0f;
|
||||
|
||||
ClearVisemes();
|
||||
}
|
||||
|
||||
bool ULipSyncComponent::IsAudioDrivenLipSyncPlaying() const
|
||||
{
|
||||
return bAudioDrivenLipSyncActive && AudioDrivenComponent && AudioDrivenComponent->IsPlaying();
|
||||
}
|
||||
|
||||
bool ULipSyncComponent::PlayLipSyncTestAudio(USoundBase* Sound, float StartTime)
|
||||
{
|
||||
return PlayAudioDrivenLipSync(Sound, StartTime);
|
||||
}
|
||||
|
||||
void ULipSyncComponent::StopLipSyncTestAudio()
|
||||
{
|
||||
StopAudioDrivenLipSync();
|
||||
}
|
||||
|
||||
bool ULipSyncComponent::IsLipSyncTestAudioPlaying() const
|
||||
{
|
||||
return IsAudioDrivenLipSyncPlaying();
|
||||
}
|
||||
|
||||
void ULipSyncComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
UpdateAudioDrivenLipSync(DeltaTime);
|
||||
|
||||
TSet<FName> VisemeNames;
|
||||
for (const auto& Pair : TargetVisemeWeights)
|
||||
{
|
||||
VisemeNames.Add(Pair.Key);
|
||||
}
|
||||
for (const auto& Pair : CurrentVisemeWeights)
|
||||
{
|
||||
VisemeNames.Add(Pair.Key);
|
||||
}
|
||||
|
||||
for (const FName VisemeName : VisemeNames)
|
||||
{
|
||||
const float TargetValue = TargetVisemeWeights.FindRef(VisemeName);
|
||||
const float CurrentValue = CurrentVisemeWeights.FindRef(VisemeName);
|
||||
const float SmoothSpeed = TargetValue >= CurrentValue ? VisemeAttackSpeed : VisemeReleaseSpeed;
|
||||
const float NewValue = FMath::FInterpTo(CurrentValue, TargetValue, DeltaTime, SmoothSpeed);
|
||||
|
||||
if (NewValue <= SilenceThreshold && TargetValue <= SilenceThreshold)
|
||||
{
|
||||
CurrentVisemeWeights.Remove(VisemeName);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentVisemeWeights.Add(VisemeName, NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
PushCurrentMorphValues();
|
||||
}
|
||||
|
||||
UMorphControllerComponent* ULipSyncComponent::ResolveMorphController() const
|
||||
{
|
||||
if (MorphController)
|
||||
{
|
||||
return MorphController;
|
||||
}
|
||||
|
||||
const AActor* Owner = GetOwner();
|
||||
return Owner ? Owner->FindComponentByClass<UMorphControllerComponent>() : nullptr;
|
||||
}
|
||||
|
||||
UAudioComponent* ULipSyncComponent::ResolveAudioDrivenComponent()
|
||||
{
|
||||
if (AudioDrivenComponent)
|
||||
{
|
||||
return AudioDrivenComponent;
|
||||
}
|
||||
|
||||
AActor* Owner = GetOwner();
|
||||
if (!Owner)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AudioDrivenComponent = NewObject<UAudioComponent>(Owner, TEXT("LipSyncAudioDrivenComponent"));
|
||||
if (!AudioDrivenComponent)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AudioDrivenComponent->bAutoActivate = false;
|
||||
AudioDrivenComponent->bAutoDestroy = false;
|
||||
AudioDrivenComponent->OnAudioFinished.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenFinished);
|
||||
AudioDrivenComponent->OnAudioPlaybackPercent.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenPlaybackPercent);
|
||||
AudioDrivenComponent->OnAudioSingleEnvelopeValue.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenEnvelopeValue);
|
||||
|
||||
if (USceneComponent* RootComponent = Owner->GetRootComponent())
|
||||
{
|
||||
AudioDrivenComponent->SetupAttachment(RootComponent);
|
||||
}
|
||||
|
||||
AudioDrivenComponent->RegisterComponent();
|
||||
Owner->AddInstanceComponent(AudioDrivenComponent);
|
||||
return AudioDrivenComponent;
|
||||
}
|
||||
|
||||
void ULipSyncComponent::UpdateAudioDrivenLipSync(float DeltaTime)
|
||||
{
|
||||
if (!bAudioDrivenLipSyncActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AudioDrivenComponent || !AudioDrivenComponent->IsPlaying())
|
||||
{
|
||||
if (AudioDrivenComponent && !bAudioDrivenControlsPlayback && bWaitingForExternalAudioPlayback)
|
||||
{
|
||||
ExternalAudioPlaybackWaitTime += DeltaTime;
|
||||
if (ExternalAudioPlaybackWaitTime < 10.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HandleAudioDrivenFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
bWaitingForExternalAudioPlayback = false;
|
||||
ExternalAudioPlaybackWaitTime = 0.0f;
|
||||
AudioDrivenPlaybackTime += DeltaTime;
|
||||
|
||||
const TArray<FName>& ActiveVisemeSequence = ResolveActiveAudioDrivenVisemeSequence();
|
||||
if (AudioDrivenMode == ELipSyncAudioDrivenMode::EnvelopeMouthOpen || ActiveVisemeSequence.Num() == 0)
|
||||
{
|
||||
TargetVisemeWeights.Empty();
|
||||
const float MouthWeight = GetAudioDrivenMouthWeight(DeltaTime);
|
||||
if (MouthWeight > SilenceThreshold && !AudioDrivenEnvelopeViseme.IsNone())
|
||||
{
|
||||
TargetVisemeWeights.Add(AudioDrivenEnvelopeViseme, MouthWeight);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SetAudioDrivenTargetVisemes(GetAudioDrivenMouthWeight(DeltaTime));
|
||||
}
|
||||
|
||||
float ULipSyncComponent::GetAudioDrivenMouthWeight(float DeltaTime)
|
||||
{
|
||||
float EnvelopeValue = 0.0f;
|
||||
bool bHasEnvelope = false;
|
||||
|
||||
if (AudioDrivenComponent)
|
||||
{
|
||||
bHasEnvelope = AudioDrivenComponent->GetCookedEnvelopeData(EnvelopeValue);
|
||||
}
|
||||
|
||||
if (!bHasEnvelope && bReceivedEnvelopeValue)
|
||||
{
|
||||
EnvelopeValue = LastEnvelopeValue;
|
||||
bHasEnvelope = true;
|
||||
}
|
||||
|
||||
if (!bHasEnvelope && bUseFallbackPulseWithoutEnvelope)
|
||||
{
|
||||
const float Pulse = 0.5f + 0.5f * FMath::Sin(AudioDrivenPlaybackTime * AudioDrivenVisemeRate * UE_TWO_PI);
|
||||
EnvelopeValue = FMath::Lerp(0.15f, 0.75f, Pulse);
|
||||
bHasEnvelope = true;
|
||||
}
|
||||
|
||||
if (!bHasEnvelope)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float ScaledEnvelope = FMath::Clamp(EnvelopeValue * AudioDrivenEnvelopeGain, 0.0f, 1.0f);
|
||||
float MouthWeight = FMath::Pow(ScaledEnvelope, FMath::Max(AudioDrivenEnvelopeGamma, 0.1f));
|
||||
if (MouthWeight > SilenceThreshold)
|
||||
{
|
||||
LastNonSilentMouthWeight = MouthWeight;
|
||||
SilenceHoldRemaining = SilenceHoldTime;
|
||||
return MouthWeight;
|
||||
}
|
||||
|
||||
if (SilenceHoldRemaining > 0.0f)
|
||||
{
|
||||
SilenceHoldRemaining = FMath::Max(0.0f, SilenceHoldRemaining - DeltaTime);
|
||||
return LastNonSilentMouthWeight * 0.65f;
|
||||
}
|
||||
|
||||
LastNonSilentMouthWeight = 0.0f;
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
void ULipSyncComponent::RebuildGeneratedTranscriptVisemes()
|
||||
{
|
||||
GeneratedTranscriptVisemes.Empty();
|
||||
GeneratedTranscriptVisemeUnits.Empty();
|
||||
|
||||
const FString Text = AudioDrivenTranscript.ToString().ToLower();
|
||||
auto AddCue = [this](FName Viseme, float Units)
|
||||
{
|
||||
if (Units <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (GeneratedTranscriptVisemes.Num() > 0 && GeneratedTranscriptVisemes.Last() == Viseme)
|
||||
{
|
||||
GeneratedTranscriptVisemeUnits.Last() += Units;
|
||||
return;
|
||||
}
|
||||
|
||||
GeneratedTranscriptVisemes.Add(Viseme);
|
||||
GeneratedTranscriptVisemeUnits.Add(Units);
|
||||
};
|
||||
|
||||
for (int32 Index = 0; Index < Text.Len(); ++Index)
|
||||
{
|
||||
const TCHAR Ch = Text[Index];
|
||||
FName Viseme = NAME_None;
|
||||
float Units = 0.45f;
|
||||
|
||||
const TCHAR NextCh = (Index + 1 < Text.Len()) ? Text[Index + 1] : 0;
|
||||
if ((Ch == TEXT('s') && Ch == NextCh) || (Ch == TEXT('s') && NextCh == TEXT('h')))
|
||||
{
|
||||
AddCue(TEXT("SS"), 0.55f);
|
||||
if (NextCh == TEXT('h')) ++Index;
|
||||
continue;
|
||||
}
|
||||
if (Ch == TEXT('c') && NextCh == TEXT('h'))
|
||||
{
|
||||
AddCue(TEXT("CH"), 0.55f);
|
||||
++Index;
|
||||
continue;
|
||||
}
|
||||
if (Ch == TEXT('t') && NextCh == TEXT('h'))
|
||||
{
|
||||
AddCue(TEXT("TH"), 0.5f);
|
||||
++Index;
|
||||
continue;
|
||||
}
|
||||
if (Ch == TEXT('o') && (NextCh == TEXT('o') || NextCh == TEXT('u')))
|
||||
{
|
||||
AddCue(TEXT("U"), 1.15f);
|
||||
++Index;
|
||||
continue;
|
||||
}
|
||||
if (Ch == TEXT('e') && NextCh == TEXT('e'))
|
||||
{
|
||||
AddCue(TEXT("I"), 1.15f);
|
||||
++Index;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (FChar::IsWhitespace(Ch))
|
||||
{
|
||||
AddCue(NAME_None, 0.35f);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Ch == TEXT('.') || Ch == TEXT(',') || Ch == TEXT('!') || Ch == TEXT('?') || Ch == TEXT(';') || Ch == TEXT(':') || Ch == TEXT('-'))
|
||||
{
|
||||
AddCue(NAME_None, 0.65f);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (Ch)
|
||||
{
|
||||
case TEXT('a'): case TEXT('а'): case TEXT('я'):
|
||||
Viseme = TEXT("A");
|
||||
Units = 1.2f;
|
||||
break;
|
||||
case TEXT('e'): case TEXT('э'): case TEXT('е'): case TEXT('ё'):
|
||||
Viseme = TEXT("E");
|
||||
Units = 1.05f;
|
||||
break;
|
||||
case TEXT('i'): case TEXT('y'): case TEXT('и'): case TEXT('ы'): case TEXT('й'):
|
||||
Viseme = TEXT("I");
|
||||
Units = 1.0f;
|
||||
break;
|
||||
case TEXT('o'): case TEXT('о'):
|
||||
Viseme = TEXT("O");
|
||||
Units = 1.1f;
|
||||
break;
|
||||
case TEXT('u'): case TEXT('w'): case TEXT('у'): case TEXT('ю'):
|
||||
Viseme = TEXT("U");
|
||||
Units = 1.05f;
|
||||
break;
|
||||
case TEXT('b'): case TEXT('m'): case TEXT('p'): case TEXT('б'): case TEXT('м'): case TEXT('п'):
|
||||
Viseme = TEXT("MBP");
|
||||
Units = 0.5f;
|
||||
break;
|
||||
case TEXT('f'): case TEXT('v'): case TEXT('ф'): case TEXT('в'):
|
||||
Viseme = TEXT("FV");
|
||||
Units = 0.5f;
|
||||
break;
|
||||
case TEXT('s'): case TEXT('z'): case TEXT('c'): case TEXT('с'): case TEXT('з'): case TEXT('ц'):
|
||||
Viseme = TEXT("SS");
|
||||
Units = 0.5f;
|
||||
break;
|
||||
case TEXT('t'): case TEXT('d'): case TEXT('n'): case TEXT('т'): case TEXT('д'): case TEXT('н'):
|
||||
Viseme = TEXT("DD");
|
||||
Units = 0.45f;
|
||||
break;
|
||||
case TEXT('l'): case TEXT('л'):
|
||||
Viseme = TEXT("L");
|
||||
Units = 0.5f;
|
||||
break;
|
||||
case TEXT('r'): case TEXT('р'):
|
||||
Viseme = TEXT("RR");
|
||||
Units = 0.5f;
|
||||
break;
|
||||
case TEXT('k'): case TEXT('g'): case TEXT('q'): case TEXT('x'): case TEXT('к'): case TEXT('г'): case TEXT('х'):
|
||||
Viseme = TEXT("kk");
|
||||
Units = 0.5f;
|
||||
break;
|
||||
case TEXT('h'): case TEXT('j'): case TEXT('ч'): case TEXT('ш'): case TEXT('щ'): case TEXT('ж'):
|
||||
Viseme = TEXT("CH");
|
||||
Units = 0.55f;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
AddCue(Viseme, Units);
|
||||
}
|
||||
}
|
||||
|
||||
const TArray<FName>& ULipSyncComponent::ResolveActiveAudioDrivenVisemeSequence() const
|
||||
{
|
||||
if (AudioDrivenMode == ELipSyncAudioDrivenMode::TranscriptVisemes)
|
||||
{
|
||||
return GeneratedTranscriptVisemes;
|
||||
}
|
||||
|
||||
return AudioDrivenVisemeSequence;
|
||||
}
|
||||
|
||||
float ULipSyncComponent::GetGeneratedTranscriptTotalUnits() const
|
||||
{
|
||||
float TotalUnits = 0.0f;
|
||||
for (float Units : GeneratedTranscriptVisemeUnits)
|
||||
{
|
||||
TotalUnits += FMath::Max(Units, 0.0f);
|
||||
}
|
||||
return TotalUnits;
|
||||
}
|
||||
|
||||
void ULipSyncComponent::SetAudioDrivenTargetVisemes(float MouthWeight)
|
||||
{
|
||||
TargetVisemeWeights.Empty();
|
||||
const TArray<FName>& ActiveVisemeSequence = ResolveActiveAudioDrivenVisemeSequence();
|
||||
if (MouthWeight <= SilenceThreshold || ActiveVisemeSequence.Num() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float SequencePosition = AudioDrivenPlaybackTime * FMath::Max(AudioDrivenVisemeRate, 0.1f);
|
||||
if (AudioDrivenMode == ELipSyncAudioDrivenMode::TranscriptVisemes && AudioDrivenDuration > 0.0f)
|
||||
{
|
||||
const float Alpha = FMath::Clamp(AudioDrivenPlaybackTime / AudioDrivenDuration, 0.0f, 0.999f);
|
||||
const float TotalUnits = GetGeneratedTranscriptTotalUnits();
|
||||
if (TotalUnits > 0.0f && GeneratedTranscriptVisemeUnits.Num() == ActiveVisemeSequence.Num())
|
||||
{
|
||||
const float TargetUnit = Alpha * TotalUnits;
|
||||
float UnitCursor = 0.0f;
|
||||
int32 CueIndex = 0;
|
||||
float CueFraction = 0.0f;
|
||||
for (; CueIndex < GeneratedTranscriptVisemeUnits.Num(); ++CueIndex)
|
||||
{
|
||||
const float CueUnits = FMath::Max(GeneratedTranscriptVisemeUnits[CueIndex], 0.001f);
|
||||
if (TargetUnit <= UnitCursor + CueUnits)
|
||||
{
|
||||
CueFraction = FMath::Clamp((TargetUnit - UnitCursor) / CueUnits, 0.0f, 1.0f);
|
||||
break;
|
||||
}
|
||||
UnitCursor += CueUnits;
|
||||
}
|
||||
|
||||
const int32 CurrentIndex = FMath::Clamp(CueIndex, 0, ActiveVisemeSequence.Num() - 1);
|
||||
const int32 NextIndex = FMath::Min(CurrentIndex + 1, ActiveVisemeSequence.Num() - 1);
|
||||
const float Crossfade = FMath::Clamp(AudioDrivenVisemeCrossfade, 0.001f, 0.45f);
|
||||
const float NextBlend = FMath::Clamp((CueFraction - (1.0f - Crossfade)) / Crossfade, 0.0f, 1.0f);
|
||||
const FName CurrentViseme = ActiveVisemeSequence[CurrentIndex];
|
||||
const FName NextViseme = ActiveVisemeSequence[NextIndex];
|
||||
|
||||
if (!CurrentViseme.IsNone())
|
||||
{
|
||||
TargetVisemeWeights.Add(CurrentViseme, MouthWeight * (1.0f - NextBlend));
|
||||
}
|
||||
if (!NextViseme.IsNone() && NextBlend > 0.001f)
|
||||
{
|
||||
float& ExistingValue = TargetVisemeWeights.FindOrAdd(NextViseme);
|
||||
ExistingValue = FMath::Max(ExistingValue, MouthWeight * NextBlend);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SequencePosition = Alpha * ActiveVisemeSequence.Num();
|
||||
}
|
||||
|
||||
const int32 CurrentIndex = FMath::FloorToInt(SequencePosition) % ActiveVisemeSequence.Num();
|
||||
const int32 NextIndex = (CurrentIndex + 1) % ActiveVisemeSequence.Num();
|
||||
const float Fraction = FMath::Frac(SequencePosition);
|
||||
|
||||
float NextBlend = 0.0f;
|
||||
if (AudioDrivenVisemeCrossfade > 0.0f)
|
||||
{
|
||||
const float Crossfade = FMath::Clamp(AudioDrivenVisemeCrossfade, 0.001f, 0.45f);
|
||||
NextBlend = FMath::Clamp((Fraction - (1.0f - Crossfade)) / Crossfade, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
const FName CurrentViseme = ActiveVisemeSequence[CurrentIndex];
|
||||
const FName NextViseme = ActiveVisemeSequence[NextIndex];
|
||||
|
||||
if (!CurrentViseme.IsNone())
|
||||
{
|
||||
TargetVisemeWeights.Add(CurrentViseme, MouthWeight * (1.0f - NextBlend));
|
||||
}
|
||||
|
||||
if (!NextViseme.IsNone() && NextBlend > 0.001f)
|
||||
{
|
||||
float& ExistingValue = TargetVisemeWeights.FindOrAdd(NextViseme);
|
||||
ExistingValue = FMath::Max(ExistingValue, MouthWeight * NextBlend);
|
||||
}
|
||||
}
|
||||
|
||||
void ULipSyncComponent::PushCurrentMorphValues()
|
||||
{
|
||||
UMorphControllerComponent* ResolvedController = ResolveMorphController();
|
||||
if (!ResolvedController)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Mapping || CurrentVisemeWeights.Num() == 0)
|
||||
{
|
||||
ResolvedController->ClearLipSyncMorphValues();
|
||||
return;
|
||||
}
|
||||
|
||||
TMap<FName, float> MorphValues;
|
||||
Mapping->AccumulateMorphValues(CurrentVisemeWeights, MorphValues);
|
||||
ResolvedController->SetLipSyncMorphValues(MorphValues);
|
||||
}
|
||||
|
||||
void ULipSyncComponent::HandleAudioDrivenFinished()
|
||||
{
|
||||
if (!bAudioDrivenLipSyncActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bAudioDrivenLipSyncActive = false;
|
||||
bWaitingForExternalAudioPlayback = false;
|
||||
ExternalAudioPlaybackWaitTime = 0.0f;
|
||||
ClearVisemes();
|
||||
}
|
||||
|
||||
void ULipSyncComponent::HandleAudioDrivenPlaybackPercent(const USoundWave* PlayingSoundWave, const float PlaybackPercent)
|
||||
{
|
||||
if (!bAudioDrivenControlsPlayback && AudioDrivenDuration <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayingSoundWave)
|
||||
{
|
||||
AudioDrivenPlaybackTime = FMath::Max(PlayingSoundWave->Duration * PlaybackPercent, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void ULipSyncComponent::HandleAudioDrivenEnvelopeValue(const USoundWave* PlayingSoundWave, const float EnvelopeValue)
|
||||
{
|
||||
LastEnvelopeValue = FMath::Max(EnvelopeValue, 0.0f);
|
||||
bReceivedEnvelopeValue = true;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/LipSyncMappingDataAsset.h"
|
||||
|
||||
ULipSyncMappingDataAsset::ULipSyncMappingDataAsset()
|
||||
{
|
||||
auto AddMapping = [this](FName VisemeName, TArray<FLipSyncMorphTargetValue>&& MorphTargets)
|
||||
{
|
||||
FLipSyncVisemeMapping Mapping;
|
||||
Mapping.VisemeName = VisemeName;
|
||||
Mapping.MorphTargets = MoveTemp(MorphTargets);
|
||||
VisemeMappings.Add(MoveTemp(Mapping));
|
||||
};
|
||||
|
||||
auto Morph = [](const TCHAR* MorphTargetName, float Weight)
|
||||
{
|
||||
FLipSyncMorphTargetValue Value;
|
||||
Value.MorphTargetName = FName(MorphTargetName);
|
||||
Value.Weight = Weight;
|
||||
return Value;
|
||||
};
|
||||
|
||||
AddMapping(TEXT("sil"), {});
|
||||
AddMapping(TEXT("rest"), {});
|
||||
|
||||
AddMapping(TEXT("A"), { Morph(TEXT("Fcl_MTH_A"), 1.0f), Morph(TEXT("Fcl_MTH_Large"), 0.22f) });
|
||||
AddMapping(TEXT("I"), { Morph(TEXT("Fcl_MTH_I"), 1.0f), Morph(TEXT("Fcl_MTH_Small"), 0.12f) });
|
||||
AddMapping(TEXT("U"), { Morph(TEXT("Fcl_MTH_U"), 1.0f), Morph(TEXT("Fcl_MTH_Small"), 0.25f) });
|
||||
AddMapping(TEXT("E"), { Morph(TEXT("Fcl_MTH_E"), 1.0f), Morph(TEXT("Fcl_MTH_I"), 0.28f) });
|
||||
AddMapping(TEXT("O"), { Morph(TEXT("Fcl_MTH_O"), 1.0f), Morph(TEXT("Fcl_MTH_U"), 0.18f) });
|
||||
|
||||
AddMapping(TEXT("aa"), { Morph(TEXT("Fcl_MTH_A"), 1.0f), Morph(TEXT("Fcl_MTH_Large"), 0.25f) });
|
||||
AddMapping(TEXT("ih"), { Morph(TEXT("Fcl_MTH_I"), 1.0f), Morph(TEXT("Fcl_MTH_Small"), 0.12f) });
|
||||
AddMapping(TEXT("ee"), { Morph(TEXT("Fcl_MTH_E"), 0.85f), Morph(TEXT("Fcl_MTH_I"), 0.35f) });
|
||||
AddMapping(TEXT("oh"), { Morph(TEXT("Fcl_MTH_O"), 1.0f) });
|
||||
AddMapping(TEXT("ou"), { Morph(TEXT("Fcl_MTH_U"), 0.95f), Morph(TEXT("Fcl_MTH_O"), 0.35f), Morph(TEXT("Fcl_MTH_Small"), 0.2f) });
|
||||
|
||||
AddMapping(TEXT("PP"), { Morph(TEXT("Fcl_MTH_Close"), 1.0f), Morph(TEXT("Fcl_MTH_Small"), 0.18f) });
|
||||
AddMapping(TEXT("MBP"), { Morph(TEXT("Fcl_MTH_Close"), 1.0f) });
|
||||
AddMapping(TEXT("FF"), { Morph(TEXT("Fcl_MTH_Close"), 0.45f), Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_Small"), 0.2f) });
|
||||
AddMapping(TEXT("FV"), { Morph(TEXT("Fcl_MTH_Close"), 0.45f), Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_Small"), 0.2f) });
|
||||
AddMapping(TEXT("TH"), { Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_A"), 0.18f) });
|
||||
AddMapping(TEXT("DD"), { Morph(TEXT("Fcl_MTH_I"), 0.45f), Morph(TEXT("Fcl_MTH_A"), 0.18f) });
|
||||
AddMapping(TEXT("kk"), { Morph(TEXT("Fcl_MTH_A"), 0.5f), Morph(TEXT("Fcl_MTH_Small"), 0.15f) });
|
||||
AddMapping(TEXT("CH"), { Morph(TEXT("Fcl_MTH_U"), 0.5f), Morph(TEXT("Fcl_MTH_Small"), 0.45f) });
|
||||
AddMapping(TEXT("SS"), { Morph(TEXT("Fcl_MTH_I"), 0.55f), Morph(TEXT("Fcl_MTH_Small"), 0.45f) });
|
||||
AddMapping(TEXT("nn"), { Morph(TEXT("Fcl_MTH_I"), 0.45f), Morph(TEXT("Fcl_MTH_Close"), 0.25f) });
|
||||
AddMapping(TEXT("RR"), { Morph(TEXT("Fcl_MTH_U"), 0.45f), Morph(TEXT("Fcl_MTH_O"), 0.25f) });
|
||||
AddMapping(TEXT("L"), { Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_A"), 0.22f) });
|
||||
AddMapping(TEXT("WQ"), { Morph(TEXT("Fcl_MTH_U"), 0.85f), Morph(TEXT("Fcl_MTH_Small"), 0.32f) });
|
||||
|
||||
AddMapping(TEXT("Rhubarb_A"), { Morph(TEXT("Fcl_MTH_A"), 0.75f), Morph(TEXT("Fcl_MTH_I"), 0.2f) });
|
||||
AddMapping(TEXT("Rhubarb_B"), { Morph(TEXT("Fcl_MTH_Close"), 1.0f) });
|
||||
AddMapping(TEXT("Rhubarb_C"), { Morph(TEXT("Fcl_MTH_E"), 0.85f), Morph(TEXT("Fcl_MTH_I"), 0.28f) });
|
||||
AddMapping(TEXT("Rhubarb_D"), { Morph(TEXT("Fcl_MTH_A"), 1.0f), Morph(TEXT("Fcl_MTH_Large"), 0.25f) });
|
||||
AddMapping(TEXT("Rhubarb_E"), { Morph(TEXT("Fcl_MTH_O"), 0.95f), Morph(TEXT("Fcl_MTH_U"), 0.18f) });
|
||||
AddMapping(TEXT("Rhubarb_F"), { Morph(TEXT("Fcl_MTH_U"), 0.65f), Morph(TEXT("Fcl_MTH_O"), 0.25f) });
|
||||
AddMapping(TEXT("Rhubarb_G"), { Morph(TEXT("Fcl_MTH_Close"), 0.45f), Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_Small"), 0.2f) });
|
||||
AddMapping(TEXT("Rhubarb_H"), { Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_A"), 0.22f) });
|
||||
AddMapping(TEXT("Rhubarb_X"), {});
|
||||
}
|
||||
|
||||
bool ULipSyncMappingDataAsset::GetMorphValuesForViseme(FName VisemeName, float VisemeWeight, TMap<FName, float>& OutMorphValues) const
|
||||
{
|
||||
OutMorphValues.Empty();
|
||||
if (VisemeName.IsNone() || VisemeWeight <= 0.001f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const FLipSyncVisemeMapping& Mapping : VisemeMappings)
|
||||
{
|
||||
if (Mapping.VisemeName != VisemeName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const FLipSyncMorphTargetValue& MorphTarget : Mapping.MorphTargets)
|
||||
{
|
||||
if (MorphTarget.IsValid())
|
||||
{
|
||||
OutMorphValues.Add(MorphTarget.MorphTargetName, FMath::Clamp(MorphTarget.Weight * VisemeWeight, 0.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
return OutMorphValues.Num() > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ULipSyncMappingDataAsset::AccumulateMorphValues(const TMap<FName, float>& VisemeWeights, TMap<FName, float>& InOutMorphValues) const
|
||||
{
|
||||
for (const auto& VisemePair : VisemeWeights)
|
||||
{
|
||||
if (VisemePair.Key.IsNone() || VisemePair.Value <= 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const FLipSyncVisemeMapping& Mapping : VisemeMappings)
|
||||
{
|
||||
if (Mapping.VisemeName != VisemePair.Key)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const FLipSyncMorphTargetValue& MorphTarget : Mapping.MorphTargets)
|
||||
{
|
||||
if (!MorphTarget.IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Contribution = FMath::Clamp(MorphTarget.Weight * VisemePair.Value, 0.0f, 1.0f);
|
||||
float& ExistingValue = InOutMorphValues.FindOrAdd(MorphTarget.MorphTargetName);
|
||||
ExistingValue = FMath::Max(ExistingValue, Contribution);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/MorphControllerComponent.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
#include "Animation/AnimMontage.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogMorphController, Log, All);
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float MinMorphTargetValue = -1.0f;
|
||||
constexpr float MaxMorphTargetValue = 1.0f;
|
||||
|
||||
float ClampMorphTargetValue(float Value)
|
||||
{
|
||||
return FMath::Clamp(Value, MinMorphTargetValue, MaxMorphTargetValue);
|
||||
}
|
||||
}
|
||||
|
||||
UMorphControllerComponent::UMorphControllerComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.TickInterval = 0.0f; // Tick every frame
|
||||
EmotionPresetsPromptHeader = FText::FromString(TEXT(
|
||||
"[EMOTION MORPH PRESETS]\n"
|
||||
"These presets are available facial/emotional expressions for the character. "
|
||||
"Use the exact preset name when you need to request or describe an emotion preset. "
|
||||
"Choose only from this list; if no preset fits, do not invent one."));
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
if (bAutoUpdate)
|
||||
{
|
||||
UpdateMorphTargets(DeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
USkeletalMeshComponent* UMorphControllerComponent::ResolveMeshComponent() const
|
||||
{
|
||||
if (TargetMeshComponent)
|
||||
{
|
||||
return TargetMeshComponent;
|
||||
}
|
||||
|
||||
if (ACharacter* Character = Cast<ACharacter>(GetOwner()))
|
||||
{
|
||||
return Character->GetMesh();
|
||||
}
|
||||
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
return Owner->FindComponentByClass<USkeletalMeshComponent>();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UMorphControllerComponent::PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const
|
||||
{
|
||||
for (const FMorphTimelineClip& Clip : Preset.MorphTargets)
|
||||
{
|
||||
if (!Clip.IsValid() || Clip.Weight <= 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FRichCurve* Curve = Clip.WeightCurve.GetRichCurveConst();
|
||||
if (!Curve)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto It = Curve->GetKeyHandleIterator(); It; ++It)
|
||||
{
|
||||
const FKeyHandle KeyHandle = *It;
|
||||
if (Curve->IsKeyHandleValid(KeyHandle) && Curve->GetKeyValue(KeyHandle) > 0.001f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
FString UMorphControllerComponent::BuildEmotionPresetsPrompt() const
|
||||
{
|
||||
TArray<FString> PromptSections;
|
||||
const FString Header = EmotionPresetsPromptHeader.ToString().TrimStartAndEnd();
|
||||
if (!Header.IsEmpty())
|
||||
{
|
||||
PromptSections.Add(Header);
|
||||
}
|
||||
|
||||
TArray<FString> PresetLines;
|
||||
for (const TObjectPtr<UMorphPresetDataAsset>& PresetRef : EmotionPresets)
|
||||
{
|
||||
const UMorphPresetDataAsset* Preset = PresetRef.Get();
|
||||
if (!Preset || !Preset->HasPlayableContent())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FString PresetName = Preset->DisplayName.ToString().TrimStartAndEnd();
|
||||
const FString Description = Preset->Description.ToString().TrimStartAndEnd();
|
||||
if (PresetName.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PresetLines.Add(Description.IsEmpty()
|
||||
? FString::Printf(TEXT("- name: %s"), *PresetName)
|
||||
: FString::Printf(TEXT("- name: %s | description: %s"), *PresetName, *Description));
|
||||
}
|
||||
|
||||
if (PresetLines.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
PromptSections.Add(TEXT("Available presets:"));
|
||||
PromptSections.Add(FString::Join(PresetLines, TEXT("\n")));
|
||||
return FString::Join(PromptSections, TEXT("\n\n"));
|
||||
}
|
||||
|
||||
UMorphPresetDataAsset* UMorphControllerComponent::FindEmotionPresetByName(const FString& PresetName) const
|
||||
{
|
||||
const FString CleanPresetName = PresetName.TrimStartAndEnd();
|
||||
if (CleanPresetName.IsEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const TObjectPtr<UMorphPresetDataAsset>& PresetRef : EmotionPresets)
|
||||
{
|
||||
UMorphPresetDataAsset* Preset = PresetRef.Get();
|
||||
if (!Preset || !Preset->HasPlayableContent())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FString DisplayName = Preset->DisplayName.ToString().TrimStartAndEnd();
|
||||
if (DisplayName.Equals(CleanPresetName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return Preset;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UMorphControllerComponent::PlayEmotionPresetByName(const FString& PresetName, float Weight)
|
||||
{
|
||||
UMorphPresetDataAsset* Preset = FindEmotionPresetByName(PresetName);
|
||||
if (!Preset)
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("PlayEmotionPresetByName ignored on %s: no emotion preset named '%s'."), *GetNameSafe(GetOwner()), *PresetName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return PlayPreset(Preset, Weight);
|
||||
}
|
||||
|
||||
bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float Weight)
|
||||
{
|
||||
if (!Preset || !Preset->IsValidPreset())
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("AddPreset ignored on %s: preset is null or has no valid clips."), *GetNameSafe(GetOwner()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const float TimelineDuration = Preset->GetTimelineLength();
|
||||
if (TimelineDuration <= 0.0f)
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("AddPreset ignored on %s: preset %s has no timeline duration."), *GetNameSafe(GetOwner()), *GetNameSafe(Preset));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ResolveMeshComponent() && bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("AddPreset accepted on %s, but no SkeletalMeshComponent was found. Set TargetMeshComponent or place the component on an actor with a skeletal mesh."), *GetNameSafe(GetOwner()));
|
||||
}
|
||||
|
||||
if (!PresetHasAnyPositiveCurveValue(*Preset) && bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("AddPreset accepted on %s, but preset %s has no positive curve key values. Default curve keys are 0, so raise keys in the clip curve editor to see morph output."), *GetNameSafe(GetOwner()), *GetNameSafe(Preset));
|
||||
}
|
||||
|
||||
for (FActiveMorphPresetSlot& Slot : ActivePresets)
|
||||
{
|
||||
if (Slot.Preset == Preset)
|
||||
{
|
||||
Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
|
||||
Slot.CurrentWeight = Slot.TargetWeight;
|
||||
Slot.PlaybackTime = 0.0f;
|
||||
Slot.LoopDelayRemaining = 0.0f;
|
||||
Slot.TimelineDuration = TimelineDuration;
|
||||
UpdateMorphTargets(0.0f);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
FActiveMorphPresetSlot NewSlot;
|
||||
NewSlot.Preset = Preset;
|
||||
NewSlot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
|
||||
NewSlot.CurrentWeight = NewSlot.TargetWeight;
|
||||
NewSlot.PlaybackTime = 0.0f;
|
||||
NewSlot.LoopDelayRemaining = 0.0f;
|
||||
NewSlot.TimelineDuration = TimelineDuration;
|
||||
ActivePresets.Add(NewSlot);
|
||||
UpdateMorphTargets(0.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UMorphControllerComponent::PlayPreset(UMorphPresetDataAsset* Preset, float Weight)
|
||||
{
|
||||
if (!Preset || !Preset->HasPlayableContent())
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("PlayPreset ignored on %s: preset is null or has no morph clips or animation montage."), *GetNameSafe(GetOwner()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bPlayedMorphs = Preset->IsValidPreset() ? AddPreset(Preset, Weight) : false;
|
||||
const bool bPlayedAnimation = PlayPresetAnimation(Preset);
|
||||
return bPlayedMorphs || bPlayedAnimation;
|
||||
}
|
||||
|
||||
bool UMorphControllerComponent::PlayPresetAnimation(UMorphPresetDataAsset* Preset)
|
||||
{
|
||||
if (!Preset || !Preset->AnimationMontage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
|
||||
if (!Mesh)
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: no SkeletalMeshComponent was found."), *GetNameSafe(GetOwner()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UAnimInstance* AnimInstance = Mesh->GetAnimInstance();
|
||||
if (!AnimInstance)
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: mesh %s has no AnimInstance."), *GetNameSafe(GetOwner()), *GetNameSafe(Mesh));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UAnimMontage* Montage = Preset->AnimationMontage;
|
||||
if (Preset->bRestartAnimationOnReplay && AnimInstance->Montage_IsPlaying(Montage))
|
||||
{
|
||||
AnimInstance->Montage_Stop(0.0f, Montage);
|
||||
}
|
||||
|
||||
const float MontageLength = AnimInstance->Montage_Play(Montage, FMath::Max(Preset->AnimationPlayRate, 0.01f));
|
||||
if (MontageLength <= 0.0f)
|
||||
{
|
||||
if (bLogWarnings)
|
||||
{
|
||||
UE_LOG(LogMorphController, Warning, TEXT("Preset animation failed on %s: montage %s could not play. Check the Anim Blueprint slot setup."), *GetNameSafe(GetOwner()), *GetNameSafe(Montage));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Preset->AnimationStartSection.IsNone())
|
||||
{
|
||||
AnimInstance->Montage_JumpToSection(Preset->AnimationStartSection, Montage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::RemovePreset(UMorphPresetDataAsset* Preset)
|
||||
{
|
||||
if (!Preset) return;
|
||||
|
||||
for (int32 i = ActivePresets.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
if (ActivePresets[i].Preset == Preset)
|
||||
{
|
||||
ActivePresets.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight)
|
||||
{
|
||||
if (!Preset) return;
|
||||
|
||||
for (FActiveMorphPresetSlot& Slot : ActivePresets)
|
||||
{
|
||||
if (Slot.Preset == Preset)
|
||||
{
|
||||
Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPresetSlot>& Presets)
|
||||
{
|
||||
ActivePresets.Empty();
|
||||
|
||||
for (const FActiveMorphPresetSlot& Slot : Presets)
|
||||
{
|
||||
if (Slot.Preset && Slot.Preset->IsValidPreset())
|
||||
{
|
||||
FActiveMorphPresetSlot NewSlot = Slot;
|
||||
NewSlot.CurrentWeight = 0.0f;
|
||||
NewSlot.PlaybackTime = 0.0f;
|
||||
NewSlot.LoopDelayRemaining = 0.0f;
|
||||
NewSlot.TimelineDuration = NewSlot.Preset->GetTimelineLength();
|
||||
if (NewSlot.TimelineDuration > 0.0f)
|
||||
{
|
||||
ActivePresets.Add(NewSlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::ClearAllPresets()
|
||||
{
|
||||
ActivePresets.Empty();
|
||||
}
|
||||
|
||||
bool UMorphControllerComponent::IsPresetActive(UMorphPresetDataAsset* Preset) const
|
||||
{
|
||||
if (!Preset) return false;
|
||||
|
||||
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
|
||||
{
|
||||
if (Slot.Preset == Preset && Slot.TargetWeight > 0.0f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<FActiveMorphPresetSlot> UMorphControllerComponent::GetActivePresets() const
|
||||
{
|
||||
TArray<FActiveMorphPresetSlot> Result;
|
||||
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
|
||||
{
|
||||
if (Slot.Preset && Slot.TargetWeight > 0.0f)
|
||||
{
|
||||
Result.Add(Slot);
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::SetMorphTarget(FName MorphTargetName, float Value)
|
||||
{
|
||||
DirectMorphOverrides.Add(MorphTargetName, ClampMorphTargetValue(Value));
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::RemoveMorphTargetOverride(FName MorphTargetName)
|
||||
{
|
||||
DirectMorphOverrides.Remove(MorphTargetName);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::ClearDirectMorphOverrides()
|
||||
{
|
||||
DirectMorphOverrides.Empty();
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>& MorphValues)
|
||||
{
|
||||
LipSyncMorphValues.Empty();
|
||||
for (const auto& Pair : MorphValues)
|
||||
{
|
||||
if (!Pair.Key.IsNone() && FMath::Abs(Pair.Value) > 0.001f)
|
||||
{
|
||||
LipSyncMorphValues.Add(Pair.Key, ClampMorphTargetValue(Pair.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::ClearLipSyncMorphValues()
|
||||
{
|
||||
LipSyncMorphValues.Empty();
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::UpdateMorphTargetsNow(float DeltaTime)
|
||||
{
|
||||
UpdateMorphTargets(FMath::Max(DeltaTime, 0.0f));
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::ClearMorphsImmediately()
|
||||
{
|
||||
ActivePresets.Empty();
|
||||
DirectMorphOverrides.Empty();
|
||||
LipSyncMorphValues.Empty();
|
||||
|
||||
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
|
||||
if (Mesh)
|
||||
{
|
||||
for (const FName MorphName : LastAppliedMorphNames)
|
||||
{
|
||||
Mesh->SetMorphTarget(MorphName, 0.0f, false);
|
||||
}
|
||||
}
|
||||
LastAppliedMorphNames.Empty();
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
|
||||
{
|
||||
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
|
||||
if (!Mesh)
|
||||
{
|
||||
if (bLogWarnings && !bMissingMeshWarningIssued && ActivePresets.Num() > 0)
|
||||
{
|
||||
bMissingMeshWarningIssued = true;
|
||||
UE_LOG(LogMorphController, Warning, TEXT("Morph update skipped on %s: no SkeletalMeshComponent was found."), *GetNameSafe(GetOwner()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
bMissingMeshWarningIssued = false;
|
||||
|
||||
// 1. Advance each active preset. Looping presets wrap forever; non-looping presets remove themselves at the end.
|
||||
for (int32 i = ActivePresets.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
FActiveMorphPresetSlot& Slot = ActivePresets[i];
|
||||
UMorphPresetDataAsset* Preset = Slot.Preset;
|
||||
if (!Preset || !Preset->IsValidPreset())
|
||||
{
|
||||
ActivePresets.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Slot.TimelineDuration <= 0.0f)
|
||||
{
|
||||
Slot.TimelineDuration = Preset->GetTimelineLength();
|
||||
}
|
||||
if (Slot.TimelineDuration <= 0.0f)
|
||||
{
|
||||
ActivePresets.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
const float EffectiveBlendSpeed = (Preset->BlendSpeedOverride > 0.0f) ? Preset->BlendSpeedOverride : BlendSpeed;
|
||||
Slot.CurrentWeight = FMath::FInterpTo(Slot.CurrentWeight, Slot.TargetWeight, DeltaTime, EffectiveBlendSpeed);
|
||||
|
||||
if (Preset->bLoop)
|
||||
{
|
||||
if (Slot.LoopDelayRemaining > 0.0f)
|
||||
{
|
||||
Slot.LoopDelayRemaining = FMath::Max(0.0f, Slot.LoopDelayRemaining - DeltaTime);
|
||||
Slot.PlaybackTime = Slot.TimelineDuration;
|
||||
}
|
||||
else
|
||||
{
|
||||
Slot.PlaybackTime += DeltaTime;
|
||||
if (Slot.PlaybackTime >= Slot.TimelineDuration)
|
||||
{
|
||||
Slot.PlaybackTime = 0.0f;
|
||||
Slot.LoopDelayRemaining = FMath::Max(Preset->LoopDelay, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Slot.PlaybackTime += DeltaTime;
|
||||
if (Slot.PlaybackTime >= Slot.TimelineDuration)
|
||||
{
|
||||
ActivePresets.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Gather final morph values. Overlapping clips use the strongest contribution.
|
||||
TMap<FName, float> FinalValues;
|
||||
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
|
||||
{
|
||||
if (!Slot.Preset || Slot.CurrentWeight < 0.001f || Slot.LoopDelayRemaining > 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Slot.Preset, Slot.PlaybackTime, Slot.CurrentWeight, FinalValues);
|
||||
}
|
||||
|
||||
// 3. Apply direct overrides with the same max-based priority rule.
|
||||
for (const auto& Pair : DirectMorphOverrides)
|
||||
{
|
||||
float* Existing = FinalValues.Find(Pair.Key);
|
||||
if (Existing)
|
||||
{
|
||||
*Existing = FMath::Max(*Existing, Pair.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
FinalValues.Add(Pair.Key, Pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Lip sync is a transient top layer, so emotion presets can keep driving smile/frown/eyes while speech drives mouth shapes.
|
||||
if (bEnableLipSyncLayer)
|
||||
{
|
||||
BlendMorphLayer(LipSyncMorphValues, LipSyncLayerWeight, LipSyncBlendMode, FinalValues);
|
||||
}
|
||||
|
||||
// 5. Push the final values to the mesh.
|
||||
ApplyFinalMorphValues(FinalValues);
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const
|
||||
{
|
||||
if (LayerWeight <= 0.001f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& Pair : LayerValues)
|
||||
{
|
||||
if (Pair.Key.IsNone())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float LayerValue = ClampMorphTargetValue(Pair.Value * LayerWeight);
|
||||
if (FMath::Abs(LayerValue) <= 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float& ExistingValue = InOutFinalValues.FindOrAdd(Pair.Key);
|
||||
switch (BlendMode)
|
||||
{
|
||||
case EMorphLayerBlendMode::Additive:
|
||||
ExistingValue = ClampMorphTargetValue(ExistingValue + LayerValue);
|
||||
break;
|
||||
case EMorphLayerBlendMode::Override:
|
||||
ExistingValue = LayerValue;
|
||||
break;
|
||||
case EMorphLayerBlendMode::Max:
|
||||
default:
|
||||
ExistingValue = FMath::Max(ExistingValue, LayerValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UMorphControllerComponent::ApplyFinalMorphValues(const TMap<FName, float>& FinalValues)
|
||||
{
|
||||
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
|
||||
if (!Mesh) return;
|
||||
|
||||
for (const FName MorphName : LastAppliedMorphNames)
|
||||
{
|
||||
if (!FinalValues.Contains(MorphName))
|
||||
{
|
||||
Mesh->SetMorphTarget(MorphName, 0.0f, false);
|
||||
}
|
||||
}
|
||||
|
||||
LastAppliedMorphNames.Empty();
|
||||
for (const auto& Pair : FinalValues)
|
||||
{
|
||||
LastAppliedMorphNames.Add(Pair.Key);
|
||||
const float ClampedValue = ClampMorphTargetValue(Pair.Value);
|
||||
if (FMath::Abs(ClampedValue) > 0.001f)
|
||||
{
|
||||
Mesh->SetMorphTarget(Pair.Key, ClampedValue, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Mesh->SetMorphTarget(Pair.Key, 0.0f, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float MinMorphTargetValue = -1.0f;
|
||||
constexpr float MaxMorphTargetValue = 1.0f;
|
||||
|
||||
float EvaluateTimelineClipCurve(const FMorphTimelineClip& Clip, float Time)
|
||||
{
|
||||
const float ClipStart = Clip.StartTime;
|
||||
const float ClipEnd = Clip.StartTime + Clip.Duration;
|
||||
if (Time < ClipStart || Time >= ClipEnd)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float LocalTime = Time - ClipStart;
|
||||
const FRichCurve* Curve = Clip.WeightCurve.GetRichCurveConst();
|
||||
if (!Curve || Curve->GetNumKeys() == 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return FMath::Clamp(Curve->Eval(LocalTime, 0.0f), 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void FMorphTimelineClip::EnsureDefaultCurve()
|
||||
{
|
||||
FRichCurve* Curve = WeightCurve.GetRichCurve();
|
||||
if (!Curve || Curve->GetNumKeys() > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Curve->UpdateOrAddKey(0.0f, 0.0f);
|
||||
Curve->UpdateOrAddKey(FMath::Max(Duration, 0.001f), 0.0f);
|
||||
for (auto It = Curve->GetKeyHandleIterator(); It; ++It)
|
||||
{
|
||||
Curve->SetKeyInterpMode(*It, RCIM_Linear);
|
||||
}
|
||||
}
|
||||
|
||||
int32 UMorphPresetDataAsset::GetMorphCount() const
|
||||
{
|
||||
int32 Count = 0;
|
||||
for (const FMorphTimelineClip& Clip : MorphTargets)
|
||||
{
|
||||
if (Clip.IsValid()) Count++;
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
float UMorphPresetDataAsset::GetTimelineLength() const
|
||||
{
|
||||
if (Duration > 0.0f) return Duration;
|
||||
float MaxEnd = 0.0f;
|
||||
for (const FMorphTimelineClip& Clip : MorphTargets)
|
||||
{
|
||||
if (Clip.IsValid())
|
||||
{
|
||||
MaxEnd = FMath::Max(MaxEnd, Clip.StartTime + Clip.Duration);
|
||||
}
|
||||
}
|
||||
return MaxEnd;
|
||||
}
|
||||
|
||||
float MorphPresetRuntime::EvaluateClipWeight(const FMorphTimelineClip& Clip, float Time)
|
||||
{
|
||||
return EvaluateTimelineClipCurve(Clip, Time) * Clip.Weight;
|
||||
}
|
||||
|
||||
void MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(
|
||||
const UMorphPresetDataAsset& Preset,
|
||||
float Time,
|
||||
float PresetWeight,
|
||||
TMap<FName, float>& InOutValues)
|
||||
{
|
||||
if (PresetWeight < 0.001f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const FMorphTimelineClip& Clip : Preset.MorphTargets)
|
||||
{
|
||||
if (!Clip.IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Contribution = FMath::Clamp(EvaluateClipWeight(Clip, Time) * PresetWeight, MinMorphTargetValue, MaxMorphTargetValue);
|
||||
if (FMath::Abs(Contribution) < 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float& ExistingValue = InOutValues.FindOrAdd(Clip.Name);
|
||||
ExistingValue = FMath::Max(ExistingValue, Contribution);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/MorphTargetBlueprintLibrary.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "Animation/Skeleton.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Animation/MorphTarget.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float MinMorphTargetValue = -1.0f;
|
||||
constexpr float MaxMorphTargetValue = 1.0f;
|
||||
}
|
||||
|
||||
TArray<FString> UMorphTargetBlueprintLibrary::GetMorphTargetNames(USkeletalMeshComponent* SkeletalMeshComponent)
|
||||
{
|
||||
TArray<FString> Result;
|
||||
|
||||
if (!SkeletalMeshComponent)
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
|
||||
USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
|
||||
if (!SkeletalMesh)
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
|
||||
const TArray<UMorphTarget*>& MorphTargets = SkeletalMesh->GetMorphTargets();
|
||||
for (UMorphTarget* MorphTarget : MorphTargets)
|
||||
{
|
||||
if (MorphTarget)
|
||||
{
|
||||
Result.Add(MorphTarget->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<FString> UMorphTargetBlueprintLibrary::GetMorphTargetNamesFromSkeleton(USkeleton* Skeleton)
|
||||
{
|
||||
TArray<FString> Result;
|
||||
if (!Skeleton)
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSet<FString> UniqueMorphNames;
|
||||
const FString SkeletonPath = Skeleton->GetPathName();
|
||||
static const FName SkeletonTagName(TEXT("Skeleton"));
|
||||
|
||||
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
||||
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
|
||||
|
||||
FARFilter Filter;
|
||||
Filter.ClassPaths.Add(USkeletalMesh::StaticClass()->GetClassPathName());
|
||||
Filter.bRecursiveClasses = true;
|
||||
|
||||
TArray<FAssetData> SkeletalMeshAssets;
|
||||
AssetRegistry.GetAssets(Filter, SkeletalMeshAssets);
|
||||
|
||||
for (const FAssetData& AssetData : SkeletalMeshAssets)
|
||||
{
|
||||
FString MeshSkeletonPath;
|
||||
if (AssetData.GetTagValue(SkeletonTagName, MeshSkeletonPath) && MeshSkeletonPath != SkeletonPath)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(AssetData.GetAsset());
|
||||
if (!SkeletalMesh || SkeletalMesh->GetSkeleton() != Skeleton)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
|
||||
{
|
||||
if (MorphTarget)
|
||||
{
|
||||
UniqueMorphNames.Add(MorphTarget->GetName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const FString& MorphName : UniqueMorphNames)
|
||||
{
|
||||
Result.Add(MorphName);
|
||||
}
|
||||
Result.Sort();
|
||||
return Result;
|
||||
}
|
||||
|
||||
void UMorphTargetBlueprintLibrary::ApplyPresetToMeshAtTime(USkeletalMeshComponent* SkeletalMeshComponent, UMorphPresetDataAsset* Preset, float Time, float Weight)
|
||||
{
|
||||
if (!SkeletalMeshComponent || !Preset || !Preset->IsValidPreset())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TMap<FName, float> FinalValues;
|
||||
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Preset, FMath::Max(Time, 0.0f), FMath::Clamp(Weight, 0.0f, 1.0f), FinalValues);
|
||||
|
||||
for (const FMorphTimelineClip& Clip : Preset->MorphTargets)
|
||||
{
|
||||
if (Clip.IsValid())
|
||||
{
|
||||
SkeletalMeshComponent->SetMorphTarget(Clip.Name, 0.0f, false);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& Pair : FinalValues)
|
||||
{
|
||||
SkeletalMeshComponent->SetMorphTarget(Pair.Key, FMath::Clamp(Pair.Value, MinMorphTargetValue, MaxMorphTargetValue), false);
|
||||
}
|
||||
}
|
||||
|
||||
FVector2D UMorphTargetBlueprintLibrary::GetGyroCameraOffset2D(
|
||||
APlayerController* PlayerController,
|
||||
float HorizontalAmplitude,
|
||||
float VerticalAmplitude,
|
||||
float DeadZone,
|
||||
float MaxTiltInput,
|
||||
bool bInvertHorizontal,
|
||||
bool bInvertVertical)
|
||||
{
|
||||
if (!PlayerController)
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
FVector Tilt = FVector::ZeroVector;
|
||||
FVector RotationRate = FVector::ZeroVector;
|
||||
FVector Gravity = FVector::ZeroVector;
|
||||
FVector Acceleration = FVector::ZeroVector;
|
||||
PlayerController->GetInputMotionState(Tilt, RotationRate, Gravity, Acceleration);
|
||||
|
||||
const float SafeDeadZone = FMath::Max(0.0f, DeadZone);
|
||||
const float SafeMaxTiltInput = FMath::Max(SafeDeadZone + KINDA_SMALL_NUMBER, MaxTiltInput);
|
||||
|
||||
const auto RemapTilt = [SafeDeadZone, SafeMaxTiltInput](float AxisValue) -> float
|
||||
{
|
||||
const float AbsValue = FMath::Abs(AxisValue);
|
||||
if (AbsValue <= SafeDeadZone)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float Alpha = (AbsValue - SafeDeadZone) / (SafeMaxTiltInput - SafeDeadZone);
|
||||
const float Normalized = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||
return FMath::Sign(AxisValue) * Normalized;
|
||||
};
|
||||
|
||||
// Common UE mapping for tilt vectors:
|
||||
// Tilt.Y -> left/right, Tilt.X -> up/down.
|
||||
float Horizontal = RemapTilt(Tilt.Y) * HorizontalAmplitude;
|
||||
float Vertical = RemapTilt(Tilt.X) * VerticalAmplitude;
|
||||
|
||||
if (bInvertHorizontal)
|
||||
{
|
||||
Horizontal *= -1.0f;
|
||||
}
|
||||
|
||||
if (bInvertVertical)
|
||||
{
|
||||
Vertical *= -1.0f;
|
||||
}
|
||||
|
||||
return FVector2D(Horizontal, Vertical);
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Narrative/NarrativeBlueprintLibrary.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
FString NormalizeNarrativeKey(FString Key)
|
||||
{
|
||||
Key = Key.TrimStartAndEnd().ToLower();
|
||||
Key.ReplaceInline(TEXT(" "), TEXT("_"));
|
||||
return Key;
|
||||
}
|
||||
|
||||
bool SavePlayerFactInternal(
|
||||
const FString& IdentityId,
|
||||
const FString& Key,
|
||||
const FString& Value,
|
||||
const FString& Category,
|
||||
const FString& SourceText,
|
||||
double Confidence,
|
||||
const TArray<FString>& Tags,
|
||||
FString& OutError)
|
||||
{
|
||||
FString CleanKey = NormalizeNarrativeKey(Key);
|
||||
if (CleanKey.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Player data key is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString CleanValue = Value.TrimStartAndEnd();
|
||||
if (CleanValue.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Player data value is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString CleanIdentityId = IdentityId.TrimStartAndEnd();
|
||||
if (CleanIdentityId.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Identity ID is required.");
|
||||
return false;
|
||||
}
|
||||
|
||||
CleanIdentityId = FPaths::MakeValidFileName(CleanIdentityId);
|
||||
const FString Directory = FPaths::ProjectSavedDir() / TEXT("Chat") / CleanIdentityId;
|
||||
IFileManager::Get().MakeDirectory(*Directory, true);
|
||||
|
||||
const FString FullFilePath = Directory / TEXT("Player.json");
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
FString ExistingJson;
|
||||
if (FFileHelper::LoadFileToString(ExistingJson, *FullFilePath))
|
||||
{
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ExistingJson);
|
||||
FJsonSerializer::Deserialize(Reader, RootObject);
|
||||
}
|
||||
|
||||
if (!RootObject.IsValid())
|
||||
{
|
||||
RootObject = MakeShared<FJsonObject>();
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* ExistingFactsObject = nullptr;
|
||||
TSharedPtr<FJsonObject> FactsObject;
|
||||
if (RootObject->TryGetObjectField(TEXT("facts"), ExistingFactsObject) && ExistingFactsObject)
|
||||
{
|
||||
FactsObject = *ExistingFactsObject;
|
||||
}
|
||||
|
||||
if (!FactsObject.IsValid())
|
||||
{
|
||||
FactsObject = MakeShared<FJsonObject>();
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> FactObject = MakeShared<FJsonObject>();
|
||||
FactObject->SetStringField(TEXT("value"), CleanValue);
|
||||
FactObject->SetStringField(TEXT("updated_at"), FDateTime::Now().ToIso8601());
|
||||
|
||||
const FString CleanCategory = Category.TrimStartAndEnd();
|
||||
if (!CleanCategory.IsEmpty())
|
||||
{
|
||||
FactObject->SetStringField(TEXT("category"), CleanCategory);
|
||||
}
|
||||
|
||||
const FString CleanSourceText = SourceText.TrimStartAndEnd();
|
||||
if (!CleanSourceText.IsEmpty())
|
||||
{
|
||||
FactObject->SetStringField(TEXT("source_text"), CleanSourceText);
|
||||
}
|
||||
|
||||
FactObject->SetNumberField(TEXT("confidence"), FMath::Clamp(Confidence, 0.0, 1.0));
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> TagValues;
|
||||
for (const FString& Tag : Tags)
|
||||
{
|
||||
const FString CleanTag = Tag.TrimStartAndEnd();
|
||||
if (!CleanTag.IsEmpty())
|
||||
{
|
||||
TagValues.Add(MakeShared<FJsonValueString>(CleanTag));
|
||||
}
|
||||
}
|
||||
FactObject->SetArrayField(TEXT("tags"), TagValues);
|
||||
|
||||
FactsObject->SetObjectField(CleanKey, FactObject);
|
||||
RootObject->SetStringField(TEXT("updated_at"), FDateTime::Now().ToIso8601());
|
||||
RootObject->SetObjectField(TEXT("facts"), FactsObject);
|
||||
|
||||
FString OutputJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputJson);
|
||||
if (!FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer))
|
||||
{
|
||||
OutError = TEXT("Could not serialize player data JSON.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FFileHelper::SaveStringToFile(OutputJson, *FullFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not save player data file: %s"), *FullFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool UNarrativeBlueprintLibrary::AppendSemanticMemoryFromArgumentsJson(
|
||||
const FString& IdentityId,
|
||||
const FString& ArgumentsJson,
|
||||
FString& OutError)
|
||||
{
|
||||
OutError.Reset();
|
||||
|
||||
TSharedPtr<FJsonObject> ArgumentsObject;
|
||||
TSharedRef<TJsonReader<>> ArgumentsReader = TJsonReaderFactory<>::Create(ArgumentsJson);
|
||||
if (!FJsonSerializer::Deserialize(ArgumentsReader, ArgumentsObject) || !ArgumentsObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Semantic memory arguments JSON is invalid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString SemanticText;
|
||||
if (!ArgumentsObject->TryGetStringField(TEXT("text"), SemanticText))
|
||||
{
|
||||
OutError = TEXT("Semantic memory arguments must contain a text field.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString CleanSemanticText = SemanticText.TrimStartAndEnd();
|
||||
if (CleanSemanticText.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Semantic text is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString CleanIdentityId = IdentityId.TrimStartAndEnd();
|
||||
if (CleanIdentityId.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Identity ID is required.");
|
||||
return false;
|
||||
}
|
||||
|
||||
CleanIdentityId = FPaths::MakeValidFileName(CleanIdentityId);
|
||||
const FString Directory = FPaths::ProjectSavedDir() / TEXT("Chat") / CleanIdentityId;
|
||||
IFileManager::Get().MakeDirectory(*Directory, true);
|
||||
|
||||
const FString FullFilePath = Directory / TEXT("Semantics.json");
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
FString ExistingJson;
|
||||
if (FFileHelper::LoadFileToString(ExistingJson, *FullFilePath))
|
||||
{
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ExistingJson);
|
||||
FJsonSerializer::Deserialize(Reader, RootObject);
|
||||
}
|
||||
|
||||
if (!RootObject.IsValid())
|
||||
{
|
||||
RootObject = MakeShared<FJsonObject>();
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> SemanticValues;
|
||||
const TArray<TSharedPtr<FJsonValue>>* ExistingSemanticValues = nullptr;
|
||||
if (RootObject->TryGetArrayField(TEXT("semantics"), ExistingSemanticValues))
|
||||
{
|
||||
SemanticValues = *ExistingSemanticValues;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> SemanticObject = MakeShared<FJsonObject>();
|
||||
SemanticObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601());
|
||||
SemanticObject->SetStringField(TEXT("text"), CleanSemanticText);
|
||||
|
||||
FString Category;
|
||||
ArgumentsObject->TryGetStringField(TEXT("category"), Category);
|
||||
if (!Category.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
SemanticObject->SetStringField(TEXT("category"), Category.TrimStartAndEnd());
|
||||
}
|
||||
|
||||
int32 Importance = 1;
|
||||
double ImportanceValue = 1.0;
|
||||
if (ArgumentsObject->TryGetNumberField(TEXT("importance"), ImportanceValue))
|
||||
{
|
||||
Importance = FMath::RoundToInt(ImportanceValue);
|
||||
}
|
||||
SemanticObject->SetNumberField(TEXT("importance"), FMath::Clamp(Importance, 1, 5));
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> TagValues;
|
||||
const TArray<TSharedPtr<FJsonValue>>* Tags = nullptr;
|
||||
if (ArgumentsObject->TryGetArrayField(TEXT("tags"), Tags))
|
||||
{
|
||||
for (const TSharedPtr<FJsonValue>& TagValue : *Tags)
|
||||
{
|
||||
FString Tag;
|
||||
if (TagValue.IsValid() && TagValue->TryGetString(Tag))
|
||||
{
|
||||
const FString CleanTag = Tag.TrimStartAndEnd();
|
||||
if (!CleanTag.IsEmpty())
|
||||
{
|
||||
TagValues.Add(MakeShared<FJsonValueString>(CleanTag));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SemanticObject->SetArrayField(TEXT("tags"), TagValues);
|
||||
|
||||
SemanticValues.Add(MakeShared<FJsonValueObject>(SemanticObject));
|
||||
RootObject->SetArrayField(TEXT("semantics"), SemanticValues);
|
||||
|
||||
FString OutputJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputJson);
|
||||
if (!FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer))
|
||||
{
|
||||
OutError = TEXT("Could not serialize semantic memory JSON.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FFileHelper::SaveStringToFile(OutputJson, *FullFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not save semantic memory file: %s"), *FullFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UNarrativeBlueprintLibrary::SavePlayerDataFromArgumentsJson(
|
||||
const FString& IdentityId,
|
||||
const FString& ArgumentsJson,
|
||||
FString& OutError)
|
||||
{
|
||||
OutError.Reset();
|
||||
|
||||
TSharedPtr<FJsonObject> ArgumentsObject;
|
||||
TSharedRef<TJsonReader<>> ArgumentsReader = TJsonReaderFactory<>::Create(ArgumentsJson);
|
||||
if (!FJsonSerializer::Deserialize(ArgumentsReader, ArgumentsObject) || !ArgumentsObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Player data arguments JSON is invalid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString Key;
|
||||
if (!ArgumentsObject->TryGetStringField(TEXT("key"), Key))
|
||||
{
|
||||
OutError = TEXT("Player data arguments must contain a key field.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString Value;
|
||||
if (!ArgumentsObject->TryGetStringField(TEXT("value"), Value))
|
||||
{
|
||||
OutError = TEXT("Player data arguments must contain a value field.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString Category;
|
||||
ArgumentsObject->TryGetStringField(TEXT("category"), Category);
|
||||
|
||||
FString SourceText;
|
||||
ArgumentsObject->TryGetStringField(TEXT("source_text"), SourceText);
|
||||
|
||||
double Confidence = 1.0;
|
||||
ArgumentsObject->TryGetNumberField(TEXT("confidence"), Confidence);
|
||||
|
||||
TArray<FString> CleanTags;
|
||||
const TArray<TSharedPtr<FJsonValue>>* Tags = nullptr;
|
||||
if (ArgumentsObject->TryGetArrayField(TEXT("tags"), Tags))
|
||||
{
|
||||
for (const TSharedPtr<FJsonValue>& TagValue : *Tags)
|
||||
{
|
||||
FString Tag;
|
||||
if (TagValue.IsValid() && TagValue->TryGetString(Tag))
|
||||
{
|
||||
const FString CleanTag = Tag.TrimStartAndEnd();
|
||||
if (!CleanTag.IsEmpty())
|
||||
{
|
||||
CleanTags.Add(CleanTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SavePlayerFactInternal(IdentityId, Key, Value, Category, SourceText, Confidence, CleanTags, OutError);
|
||||
}
|
||||
|
||||
bool UNarrativeBlueprintLibrary::SavePlayerFact(
|
||||
const FString& IdentityId,
|
||||
const FString& Key,
|
||||
const FString& Value,
|
||||
const FString& Category,
|
||||
const FString& SourceText,
|
||||
float Confidence,
|
||||
const TArray<FString>& Tags,
|
||||
FString& OutError)
|
||||
{
|
||||
OutError.Reset();
|
||||
return SavePlayerFactInternal(IdentityId, Key, Value, Category, SourceText, Confidence, Tags, OutError);
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "UI/AudioPlaybackThrobber.h"
|
||||
|
||||
#include "Animation/CurveSequence.h"
|
||||
#include "Brushes/SlateRoundedBoxBrush.h"
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "Rendering/DrawElements.h"
|
||||
#include "Sound/SoundWave.h"
|
||||
#include "Widgets/SLeafWidget.h"
|
||||
|
||||
class SAudioPlaybackThrobber : public SLeafWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SAudioPlaybackThrobber)
|
||||
: _BarCount(7)
|
||||
, _BarWidth(3.0f)
|
||||
, _BarGap(2.5f)
|
||||
, _MinHeightFraction(0.18f)
|
||||
, _AnimationSpeed(4.8f)
|
||||
, _GlowStrength(0.35f)
|
||||
, _CenterBoost(0.35f)
|
||||
, _BarColor(FLinearColor(0.18f, 0.82f, 1.0f, 1.0f))
|
||||
, _IdleBarColor(FLinearColor(0.18f, 0.82f, 1.0f, 0.18f))
|
||||
, _IsPlaying(false)
|
||||
, _AudioComponent(nullptr)
|
||||
, _UseAudioEnvelope(false)
|
||||
, _AudioEnvelopeGain(5.0f)
|
||||
, _AudioEnvelopeGamma(0.65f)
|
||||
, _AudioEnvelopeSmoothingSpeed(16.0f)
|
||||
, _ManualAudioLevel(0.0f)
|
||||
, _AudioSilenceThreshold(0.03f)
|
||||
, _AudioSilenceOpacity(0.08f)
|
||||
{
|
||||
}
|
||||
SLATE_ATTRIBUTE(int32, BarCount)
|
||||
SLATE_ATTRIBUTE(float, BarWidth)
|
||||
SLATE_ATTRIBUTE(float, BarGap)
|
||||
SLATE_ATTRIBUTE(float, MinHeightFraction)
|
||||
SLATE_ATTRIBUTE(float, AnimationSpeed)
|
||||
SLATE_ATTRIBUTE(float, GlowStrength)
|
||||
SLATE_ATTRIBUTE(float, CenterBoost)
|
||||
SLATE_ATTRIBUTE(FLinearColor, BarColor)
|
||||
SLATE_ATTRIBUTE(FLinearColor, IdleBarColor)
|
||||
SLATE_ATTRIBUTE(bool, IsPlaying)
|
||||
SLATE_ATTRIBUTE(UAudioComponent*, AudioComponent)
|
||||
SLATE_ATTRIBUTE(bool, UseAudioEnvelope)
|
||||
SLATE_ATTRIBUTE(float, AudioEnvelopeGain)
|
||||
SLATE_ATTRIBUTE(float, AudioEnvelopeGamma)
|
||||
SLATE_ATTRIBUTE(float, AudioEnvelopeSmoothingSpeed)
|
||||
SLATE_ATTRIBUTE(float, ManualAudioLevel)
|
||||
SLATE_ATTRIBUTE(float, AudioSilenceThreshold)
|
||||
SLATE_ATTRIBUTE(float, AudioSilenceOpacity)
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments& InArgs)
|
||||
{
|
||||
BarCount = InArgs._BarCount;
|
||||
BarWidth = InArgs._BarWidth;
|
||||
BarGap = InArgs._BarGap;
|
||||
MinHeightFraction = InArgs._MinHeightFraction;
|
||||
AnimationSpeed = InArgs._AnimationSpeed;
|
||||
GlowStrength = InArgs._GlowStrength;
|
||||
CenterBoost = InArgs._CenterBoost;
|
||||
BarColor = InArgs._BarColor;
|
||||
IdleBarColor = InArgs._IdleBarColor;
|
||||
bIsPlaying = InArgs._IsPlaying;
|
||||
AudioComponent = InArgs._AudioComponent;
|
||||
bUseAudioEnvelope = InArgs._UseAudioEnvelope;
|
||||
AudioEnvelopeGain = InArgs._AudioEnvelopeGain;
|
||||
AudioEnvelopeGamma = InArgs._AudioEnvelopeGamma;
|
||||
AudioEnvelopeSmoothingSpeed = InArgs._AudioEnvelopeSmoothingSpeed;
|
||||
ManualAudioLevel = InArgs._ManualAudioLevel;
|
||||
AudioSilenceThreshold = InArgs._AudioSilenceThreshold;
|
||||
AudioSilenceOpacity = InArgs._AudioSilenceOpacity;
|
||||
|
||||
ConstructSequence();
|
||||
}
|
||||
|
||||
void SetBarCount(int32 InBarCount)
|
||||
{
|
||||
BarCount.Set(FMath::Clamp(InBarCount, 1, 16));
|
||||
Invalidate(EInvalidateWidgetReason::LayoutAndVolatility);
|
||||
}
|
||||
|
||||
void SetBarWidth(float InBarWidth)
|
||||
{
|
||||
BarWidth.Set(FMath::Max(0.5f, InBarWidth));
|
||||
Invalidate(EInvalidateWidgetReason::LayoutAndVolatility);
|
||||
}
|
||||
|
||||
void SetBarGap(float InBarGap)
|
||||
{
|
||||
BarGap.Set(FMath::Max(0.0f, InBarGap));
|
||||
Invalidate(EInvalidateWidgetReason::LayoutAndVolatility);
|
||||
}
|
||||
|
||||
void SetMinHeightFraction(float InMinHeightFraction)
|
||||
{
|
||||
MinHeightFraction.Set(FMath::Clamp(InMinHeightFraction, 0.0f, 1.0f));
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetAnimationSpeed(float InAnimationSpeed)
|
||||
{
|
||||
AnimationSpeed.Set(FMath::Max(0.0f, InAnimationSpeed));
|
||||
ConstructSequence();
|
||||
}
|
||||
|
||||
void SetGlowStrength(float InGlowStrength)
|
||||
{
|
||||
GlowStrength.Set(FMath::Clamp(InGlowStrength, 0.0f, 1.0f));
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetCenterBoost(float InCenterBoost)
|
||||
{
|
||||
CenterBoost.Set(FMath::Clamp(InCenterBoost, 0.0f, 1.0f));
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetBarColor(const FLinearColor& InBarColor)
|
||||
{
|
||||
BarColor.Set(InBarColor);
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetIdleBarColor(const FLinearColor& InIdleBarColor)
|
||||
{
|
||||
IdleBarColor.Set(InIdleBarColor);
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetIsPlaying(bool bInIsPlaying)
|
||||
{
|
||||
bIsPlaying.Set(bInIsPlaying);
|
||||
if (bInIsPlaying && !Sequence.IsPlaying())
|
||||
{
|
||||
Sequence.Play(AsShared(), true);
|
||||
}
|
||||
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
|
||||
}
|
||||
|
||||
void SetAudioComponent(UAudioComponent* InAudioComponent)
|
||||
{
|
||||
AudioComponent.Set(InAudioComponent);
|
||||
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
|
||||
}
|
||||
|
||||
void SetUseAudioEnvelope(bool bInUseAudioEnvelope)
|
||||
{
|
||||
bUseAudioEnvelope.Set(bInUseAudioEnvelope);
|
||||
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
|
||||
}
|
||||
|
||||
void SetAudioEnvelopeGain(float InAudioEnvelopeGain)
|
||||
{
|
||||
AudioEnvelopeGain.Set(FMath::Max(0.0f, InAudioEnvelopeGain));
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetAudioEnvelopeGamma(float InAudioEnvelopeGamma)
|
||||
{
|
||||
AudioEnvelopeGamma.Set(FMath::Max(0.1f, InAudioEnvelopeGamma));
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetAudioEnvelopeSmoothingSpeed(float InSmoothingSpeed)
|
||||
{
|
||||
AudioEnvelopeSmoothingSpeed.Set(FMath::Max(0.0f, InSmoothingSpeed));
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetManualAudioLevel(float InAudioLevel)
|
||||
{
|
||||
ManualAudioLevel.Set(FMath::Clamp(InAudioLevel, 0.0f, 1.0f));
|
||||
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
|
||||
}
|
||||
|
||||
void SetAudioSilenceThreshold(float InSilenceThreshold)
|
||||
{
|
||||
AudioSilenceThreshold.Set(FMath::Clamp(InSilenceThreshold, 0.0f, 1.0f));
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetAudioSilenceOpacity(float InSilenceOpacity)
|
||||
{
|
||||
AudioSilenceOpacity.Set(FMath::Clamp(InSilenceOpacity, 0.0f, 1.0f));
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
virtual bool ComputeVolatility() const override
|
||||
{
|
||||
return (bIsPlaying.Get(false) || bUseAudioEnvelope.Get(false)) && Sequence.IsPlaying();
|
||||
}
|
||||
|
||||
virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override
|
||||
{
|
||||
const int32 Count = FMath::Max(1, BarCount.Get(5));
|
||||
const float Width = FMath::Max(0.5f, BarWidth.Get(4.0f));
|
||||
const float Gap = FMath::Max(0.0f, BarGap.Get(3.0f));
|
||||
return FVector2D(Count * Width + (Count - 1) * Gap, 26.0f);
|
||||
}
|
||||
|
||||
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override
|
||||
{
|
||||
const float TargetLevel = ResolveAudioLevel();
|
||||
const float SmoothingSpeed = FMath::Max(0.0f, AudioEnvelopeSmoothingSpeed.Get(16.0f));
|
||||
if (SmoothingSpeed <= 0.0f)
|
||||
{
|
||||
SmoothedAudioLevel = TargetLevel;
|
||||
}
|
||||
else
|
||||
{
|
||||
SmoothedAudioLevel = FMath::FInterpTo(SmoothedAudioLevel, TargetLevel, InDeltaTime, SmoothingSpeed);
|
||||
}
|
||||
|
||||
if (bIsPlaying.Get(false) || bUseAudioEnvelope.Get(false) || SmoothedAudioLevel > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
}
|
||||
|
||||
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect,
|
||||
FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override
|
||||
{
|
||||
const int32 Count = FMath::Max(1, BarCount.Get(5));
|
||||
const float Width = FMath::Max(0.5f, BarWidth.Get(4.0f));
|
||||
const float Gap = FMath::Max(0.0f, BarGap.Get(3.0f));
|
||||
const float MinFraction = FMath::Clamp(MinHeightFraction.Get(0.25f), 0.0f, 1.0f);
|
||||
const bool bPlaying = bIsPlaying.Get(false);
|
||||
const bool bEnvelopeDriven = bUseAudioEnvelope.Get(false);
|
||||
const float SilenceThreshold = FMath::Clamp(AudioSilenceThreshold.Get(0.03f), 0.0f, 1.0f);
|
||||
const float SilenceOpacity = FMath::Clamp(AudioSilenceOpacity.Get(0.08f), 0.0f, 1.0f);
|
||||
const float AudioPresence = bEnvelopeDriven
|
||||
? FMath::GetMappedRangeValueClamped(FVector2f(SilenceThreshold, 1.0f), FVector2f(0.0f, 1.0f), SmoothedAudioLevel)
|
||||
: 1.0f;
|
||||
const float VisualPresence = bEnvelopeDriven ? FMath::Clamp(SmoothedAudioLevel, 0.0f, 1.0f) : 1.0f;
|
||||
const bool bActive = bEnvelopeDriven ? AudioPresence > KINDA_SMALL_NUMBER : bPlaying;
|
||||
const FLinearColor ActiveColor = BarColor.Get(FLinearColor::White);
|
||||
const FLinearColor IdleColor = IdleBarColor.Get(FLinearColor::White);
|
||||
const FVector2f LocalSize = AllottedGeometry.GetLocalSize();
|
||||
const float TotalWidth = Count * Width + (Count - 1) * Gap;
|
||||
const float StartX = (LocalSize.X - TotalWidth) * 0.5f;
|
||||
const float Time = Curve.GetLerp() * TWO_PI;
|
||||
const float CenterIndex = static_cast<float>(Count - 1) * 0.5f;
|
||||
const float MaxDistance = FMath::Max(1.0f, CenterIndex);
|
||||
const float Glow = FMath::Clamp(GlowStrength.Get(0.35f), 0.0f, 1.0f);
|
||||
const float CenterAccent = FMath::Clamp(CenterBoost.Get(0.35f), 0.0f, 1.0f);
|
||||
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
const float DistanceFromCenter = FMath::Abs(static_cast<float>(Index) - CenterIndex) / MaxDistance;
|
||||
const float CenterWeight = 1.0f - DistanceFromCenter;
|
||||
float HeightFraction = MinFraction;
|
||||
float Energy = 0.0f;
|
||||
if (bActive)
|
||||
{
|
||||
const float SymmetricIndex = FMath::Abs(static_cast<float>(Index) - CenterIndex);
|
||||
const float Phase = Time - SymmetricIndex * 0.72f;
|
||||
const float PrimaryWave = 0.5f + 0.5f * FMath::Sin(Phase);
|
||||
const float SecondaryWave = 0.5f + 0.5f * FMath::Sin(Phase * 0.73f + 1.85f);
|
||||
const float MixedWave = PrimaryWave * 0.72f + SecondaryWave * 0.28f;
|
||||
const float WaveEnergy = MixedWave * MixedWave * (3.0f - 2.0f * MixedWave);
|
||||
const float EnvelopeEnergy = bEnvelopeDriven
|
||||
? FMath::Clamp(AudioPresence * FMath::Lerp(0.50f, 1.20f, WaveEnergy), 0.0f, 1.0f)
|
||||
: WaveEnergy;
|
||||
Energy = bEnvelopeDriven ? EnvelopeEnergy : WaveEnergy;
|
||||
const float BoostScale = bEnvelopeDriven ? AudioPresence : 1.0f;
|
||||
Energy = FMath::Clamp(Energy + CenterWeight * CenterAccent * BoostScale, 0.0f, 1.0f);
|
||||
HeightFraction = FMath::Lerp(MinFraction, 1.0f, Energy);
|
||||
}
|
||||
else if (bEnvelopeDriven)
|
||||
{
|
||||
HeightFraction = MinFraction;
|
||||
}
|
||||
else
|
||||
{
|
||||
HeightFraction = FMath::Lerp(MinFraction, 0.42f, CenterWeight);
|
||||
}
|
||||
|
||||
const float BarHeight = FMath::Max(1.0f, LocalSize.Y * HeightFraction);
|
||||
const FVector2f Position(StartX + Index * (Width + Gap), (LocalSize.Y - BarHeight) * 0.5f);
|
||||
const FVector2f Size(Width, BarHeight);
|
||||
const float Radius = Width * 0.5f;
|
||||
const FLinearColor BarTint = bActive
|
||||
? ActiveColor.CopyWithNewOpacity(VisualPresence * FMath::Lerp(0.55f, ActiveColor.A, FMath::Max(Energy, CenterWeight * 0.45f)))
|
||||
: (bEnvelopeDriven
|
||||
? IdleColor.CopyWithNewOpacity(IdleColor.A * FMath::Lerp(SilenceOpacity, 1.0f, VisualPresence))
|
||||
: IdleColor);
|
||||
|
||||
if (bEnvelopeDriven && !bActive && BarTint.A <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bActive && Glow > 0.0f)
|
||||
{
|
||||
const float GlowPadX = Width * FMath::Lerp(0.65f, 1.25f, Energy);
|
||||
const float GlowPadY = FMath::Lerp(2.0f, 5.5f, Energy);
|
||||
const FVector2f GlowPosition(Position.X - GlowPadX * 0.5f, Position.Y - GlowPadY * 0.5f);
|
||||
const FVector2f GlowSize(Size.X + GlowPadX, Size.Y + GlowPadY);
|
||||
const FSlateRoundedBoxBrush GlowBrush(FLinearColor::White, Radius + GlowPadX);
|
||||
const float GlowScale = bEnvelopeDriven ? VisualPresence : 1.0f;
|
||||
const FLinearColor GlowTint = ActiveColor.CopyWithNewOpacity(Glow * GlowScale * FMath::Lerp(0.10f, 0.28f, Energy));
|
||||
FSlateDrawElement::MakeBox(
|
||||
OutDrawElements,
|
||||
LayerId,
|
||||
AllottedGeometry.ToPaintGeometry(GlowSize, FSlateLayoutTransform(GlowPosition)),
|
||||
&GlowBrush,
|
||||
ESlateDrawEffect::None,
|
||||
GlowTint * InWidgetStyle.GetColorAndOpacityTint());
|
||||
}
|
||||
|
||||
const FSlateRoundedBoxBrush BarBrush(FLinearColor::White, Radius);
|
||||
|
||||
FSlateDrawElement::MakeBox(
|
||||
OutDrawElements,
|
||||
LayerId + 1,
|
||||
AllottedGeometry.ToPaintGeometry(Size, FSlateLayoutTransform(Position)),
|
||||
&BarBrush,
|
||||
ESlateDrawEffect::None,
|
||||
BarTint * InWidgetStyle.GetColorAndOpacityTint());
|
||||
}
|
||||
|
||||
return LayerId + 1;
|
||||
}
|
||||
|
||||
private:
|
||||
float ResolveAudioLevel() const
|
||||
{
|
||||
if (!bUseAudioEnvelope.Get(false))
|
||||
{
|
||||
return FMath::Clamp(ManualAudioLevel.Get(0.0f), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float EnvelopeValue = 0.0f;
|
||||
if (UAudioComponent* Component = AudioComponent.Get(nullptr))
|
||||
{
|
||||
const bool bHasCookedEnvelope = Component->GetCookedEnvelopeData(EnvelopeValue);
|
||||
if (!bHasCookedEnvelope)
|
||||
{
|
||||
EnvelopeValue = ManualAudioLevel.Get(0.0f);
|
||||
}
|
||||
if (!Component->IsPlaying() && EnvelopeValue <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EnvelopeValue = ManualAudioLevel.Get(0.0f);
|
||||
}
|
||||
|
||||
const float ScaledEnvelope = FMath::Clamp(EnvelopeValue * AudioEnvelopeGain.Get(5.0f), 0.0f, 1.0f);
|
||||
return FMath::Pow(ScaledEnvelope, FMath::Max(AudioEnvelopeGamma.Get(0.65f), 0.1f));
|
||||
}
|
||||
|
||||
void ConstructSequence()
|
||||
{
|
||||
const float Speed = FMath::Max(0.01f, AnimationSpeed.Get(4.8f));
|
||||
const float Period = TWO_PI / Speed;
|
||||
|
||||
Sequence = FCurveSequence();
|
||||
Curve = Sequence.AddCurve(0.0f, Period);
|
||||
Sequence.Play(AsShared(), true);
|
||||
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
|
||||
}
|
||||
|
||||
TAttribute<int32> BarCount;
|
||||
TAttribute<float> BarWidth;
|
||||
TAttribute<float> BarGap;
|
||||
TAttribute<float> MinHeightFraction;
|
||||
TAttribute<float> AnimationSpeed;
|
||||
TAttribute<float> GlowStrength;
|
||||
TAttribute<float> CenterBoost;
|
||||
TAttribute<FLinearColor> BarColor;
|
||||
TAttribute<FLinearColor> IdleBarColor;
|
||||
TAttribute<bool> bIsPlaying;
|
||||
TAttribute<UAudioComponent*> AudioComponent;
|
||||
TAttribute<bool> bUseAudioEnvelope;
|
||||
TAttribute<float> AudioEnvelopeGain;
|
||||
TAttribute<float> AudioEnvelopeGamma;
|
||||
TAttribute<float> AudioEnvelopeSmoothingSpeed;
|
||||
TAttribute<float> ManualAudioLevel;
|
||||
TAttribute<float> AudioSilenceThreshold;
|
||||
TAttribute<float> AudioSilenceOpacity;
|
||||
FCurveSequence Sequence;
|
||||
FCurveHandle Curve;
|
||||
float SmoothedAudioLevel = 0.0f;
|
||||
};
|
||||
|
||||
UAudioPlaybackThrobber::UAudioPlaybackThrobber()
|
||||
{
|
||||
SetVisibility(ESlateVisibility::SelfHitTestInvisible);
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::SetIsPlaying(bool bInIsPlaying)
|
||||
{
|
||||
bIsPlaying = bInIsPlaying;
|
||||
if (MyThrobber.IsValid())
|
||||
{
|
||||
MyThrobber->SetIsPlaying(bIsPlaying);
|
||||
}
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::SetAudioComponent(UAudioComponent* InAudioComponent)
|
||||
{
|
||||
if (AudioComponent == InAudioComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnbindAudioComponent();
|
||||
AudioComponent = InAudioComponent;
|
||||
BindAudioComponent();
|
||||
|
||||
if (MyThrobber.IsValid())
|
||||
{
|
||||
MyThrobber->SetAudioComponent(AudioComponent);
|
||||
}
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::SetUseAudioEnvelope(bool bInUseAudioEnvelope)
|
||||
{
|
||||
bUseAudioEnvelope = bInUseAudioEnvelope;
|
||||
if (MyThrobber.IsValid())
|
||||
{
|
||||
MyThrobber->SetUseAudioEnvelope(bUseAudioEnvelope);
|
||||
}
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::SetManualAudioLevel(float InAudioLevel)
|
||||
{
|
||||
ManualAudioLevel = FMath::Clamp(InAudioLevel, 0.0f, 1.0f);
|
||||
if (MyThrobber.IsValid())
|
||||
{
|
||||
MyThrobber->SetManualAudioLevel(ManualAudioLevel);
|
||||
}
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::BeginDestroy()
|
||||
{
|
||||
UnbindAudioComponent();
|
||||
Super::BeginDestroy();
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UAudioPlaybackThrobber::RebuildWidget()
|
||||
{
|
||||
SAssignNew(MyThrobber, SAudioPlaybackThrobber);
|
||||
SynchronizeProperties();
|
||||
return MyThrobber.ToSharedRef();
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::SynchronizeProperties()
|
||||
{
|
||||
Super::SynchronizeProperties();
|
||||
|
||||
if (!MyThrobber.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MyThrobber->SetBarCount(BarCount);
|
||||
MyThrobber->SetBarWidth(BarWidth);
|
||||
MyThrobber->SetBarGap(BarGap);
|
||||
MyThrobber->SetMinHeightFraction(MinHeightFraction);
|
||||
MyThrobber->SetAnimationSpeed(AnimationSpeed);
|
||||
MyThrobber->SetGlowStrength(GlowStrength);
|
||||
MyThrobber->SetCenterBoost(CenterBoost);
|
||||
MyThrobber->SetBarColor(BarColor);
|
||||
MyThrobber->SetIdleBarColor(IdleBarColor);
|
||||
MyThrobber->SetIsPlaying(bIsPlaying);
|
||||
BindAudioComponent();
|
||||
MyThrobber->SetAudioComponent(AudioComponent);
|
||||
MyThrobber->SetUseAudioEnvelope(bUseAudioEnvelope);
|
||||
MyThrobber->SetAudioEnvelopeGain(AudioEnvelopeGain);
|
||||
MyThrobber->SetAudioEnvelopeGamma(AudioEnvelopeGamma);
|
||||
MyThrobber->SetAudioEnvelopeSmoothingSpeed(AudioEnvelopeSmoothingSpeed);
|
||||
MyThrobber->SetManualAudioLevel(ManualAudioLevel);
|
||||
MyThrobber->SetAudioSilenceThreshold(AudioSilenceThreshold);
|
||||
MyThrobber->SetAudioSilenceOpacity(AudioSilenceOpacity);
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::ReleaseSlateResources(bool bReleaseChildren)
|
||||
{
|
||||
Super::ReleaseSlateResources(bReleaseChildren);
|
||||
MyThrobber.Reset();
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::HandleAudioEnvelopeValue(const USoundWave* PlayingSoundWave, const float EnvelopeValue)
|
||||
{
|
||||
SetManualAudioLevel(FMath::Clamp(EnvelopeValue, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::UnbindAudioComponent()
|
||||
{
|
||||
if (AudioComponent && bAudioEnvelopeDelegateBound)
|
||||
{
|
||||
AudioComponent->OnAudioSingleEnvelopeValue.RemoveDynamic(this, &UAudioPlaybackThrobber::HandleAudioEnvelopeValue);
|
||||
}
|
||||
bAudioEnvelopeDelegateBound = false;
|
||||
}
|
||||
|
||||
void UAudioPlaybackThrobber::BindAudioComponent()
|
||||
{
|
||||
if (!AudioComponent || bAudioEnvelopeDelegateBound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AudioComponent->EnvelopeFollowerAttackTime = 8;
|
||||
AudioComponent->EnvelopeFollowerReleaseTime = 90;
|
||||
AudioComponent->OnAudioSingleEnvelopeValue.AddUniqueDynamic(this, &UAudioPlaybackThrobber::HandleAudioEnvelopeValue);
|
||||
bAudioEnvelopeDelegateBound = true;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "UI/FunnyManagedUserWidget.h"
|
||||
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "UI/FunnyUILayerManagerSubsystem.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
UFunnyUILayerManagerSubsystem* GetLayerManager(const UFunnyManagedUserWidget* Widget)
|
||||
{
|
||||
if (!Widget)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ULocalPlayer* LocalPlayer = const_cast<UFunnyManagedUserWidget*>(Widget)->GetOwningLocalPlayer();
|
||||
return LocalPlayer ? LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>() : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyManagedUserWidget::SetManagedVisibility(ESlateVisibility InVisibility)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
|
||||
{
|
||||
LayerManager->SetWidgetDesiredVisibility(this, InVisibility);
|
||||
return;
|
||||
}
|
||||
|
||||
SetVisibility(InVisibility);
|
||||
}
|
||||
|
||||
ESlateVisibility UFunnyManagedUserWidget::GetManagedVisibility() const
|
||||
{
|
||||
if (const UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
|
||||
{
|
||||
return LayerManager->GetWidgetDesiredVisibility(const_cast<UFunnyManagedUserWidget*>(this));
|
||||
}
|
||||
|
||||
return GetVisibility();
|
||||
}
|
||||
|
||||
void UFunnyManagedUserWidget::SetManagedEnabled(bool bEnabled)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
|
||||
{
|
||||
LayerManager->SetWidgetDesiredEnabled(this, bEnabled);
|
||||
return;
|
||||
}
|
||||
|
||||
SetIsEnabled(bEnabled);
|
||||
}
|
||||
|
||||
bool UFunnyManagedUserWidget::GetManagedEnabled() const
|
||||
{
|
||||
if (const UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
|
||||
{
|
||||
return LayerManager->GetWidgetDesiredEnabled(const_cast<UFunnyManagedUserWidget*>(this));
|
||||
}
|
||||
|
||||
return GetIsEnabled();
|
||||
}
|
||||
|
||||
void UFunnyManagedUserWidget::RegisterWithUILayer()
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
|
||||
{
|
||||
LayerManager->RegisterWidget(UILayerName, this, bAutoShowWithLayer);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyManagedUserWidget::UnregisterFromUILayer()
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
|
||||
{
|
||||
LayerManager->UnregisterWidget(this);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyManagedUserWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
if (bAutoRegisterWithUILayer)
|
||||
{
|
||||
RegisterWithUILayer();
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyManagedUserWidget::NativeDestruct()
|
||||
{
|
||||
UnregisterFromUILayer();
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "UI/FunnyUIBlueprintLibrary.h"
|
||||
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "UI/FunnyUILayerManagerSubsystem.h"
|
||||
|
||||
UFunnyUILayerManagerSubsystem* UFunnyUIBlueprintLibrary::GetUILayerManager(const UObject* WorldContextObject, int32 PlayerIndex)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (UUserWidget* Widget = const_cast<UUserWidget*>(Cast<UUserWidget>(WorldContextObject)))
|
||||
{
|
||||
if (ULocalPlayer* LocalPlayer = Widget->GetOwningLocalPlayer())
|
||||
{
|
||||
return LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>();
|
||||
}
|
||||
}
|
||||
|
||||
if (APlayerController* PlayerController = const_cast<APlayerController*>(Cast<APlayerController>(WorldContextObject)))
|
||||
{
|
||||
if (ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer())
|
||||
{
|
||||
return LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>();
|
||||
}
|
||||
}
|
||||
|
||||
if (ULocalPlayer* LocalPlayer = const_cast<ULocalPlayer*>(Cast<ULocalPlayer>(WorldContextObject)))
|
||||
{
|
||||
return LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>();
|
||||
}
|
||||
|
||||
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(WorldContextObject, PlayerIndex);
|
||||
if (!PlayerController)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer();
|
||||
return LocalPlayer ? LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::RegisterWidgetToLayer(const UObject* WorldContextObject, FName LayerName, UUserWidget* Widget, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->RegisterWidget(LayerName, Widget);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::UnregisterWidgetFromLayer(const UObject* WorldContextObject, UUserWidget* Widget, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->UnregisterWidget(Widget);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::SetManagedWidgetVisibility(const UObject* WorldContextObject, UUserWidget* Widget, ESlateVisibility Visibility, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->SetWidgetDesiredVisibility(Widget, Visibility);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::SetManagedWidgetEnabled(const UObject* WorldContextObject, UUserWidget* Widget, bool bEnabled, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->SetWidgetDesiredEnabled(Widget, bEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::SetUILayerVisibility(const UObject* WorldContextObject, FName LayerName, ESlateVisibility Visibility, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->SetLayerVisibility(LayerName, Visibility);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::ShowUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->ShowLayer(LayerName);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::HideUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->HideLayer(LayerName);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::SetUILayerEnabled(const UObject* WorldContextObject, FName LayerName, bool bEnabled, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->SetLayerEnabled(LayerName, bEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::ShowOnlyUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->ShowOnlyLayer(LayerName);
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUIBlueprintLibrary::PushUILayerState(const UObject* WorldContextObject, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
LayerManager->PushLayerState();
|
||||
}
|
||||
}
|
||||
|
||||
bool UFunnyUIBlueprintLibrary::PopUILayerState(const UObject* WorldContextObject, int32 PlayerIndex)
|
||||
{
|
||||
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
|
||||
{
|
||||
return LayerManager->PopLayerState();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "UI/FunnyUILayerManagerSubsystem.h"
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::RegisterWidget(FName LayerName, UUserWidget* Widget)
|
||||
{
|
||||
RegisterWidget(LayerName, Widget, false);
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::RegisterWidget(FName LayerName, UUserWidget* Widget, bool bAutoShowWithLayer)
|
||||
{
|
||||
if (LayerName.IsNone() || !Widget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnregisterWidget(Widget);
|
||||
|
||||
FFunnyUILayerWidgetEntry Entry;
|
||||
Entry.Widget = Widget;
|
||||
Entry.DesiredVisibility = Widget->GetVisibility();
|
||||
Entry.bDesiredEnabled = Widget->GetIsEnabled();
|
||||
Entry.bAutoShowWithLayer = bAutoShowWithLayer;
|
||||
|
||||
LayerWidgets.FindOrAdd(LayerName).Add(Entry);
|
||||
ApplyWidgetState(LayerWidgets[LayerName].Last(), FindOrAddLayerState(LayerName));
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::UnregisterWidget(UUserWidget* Widget)
|
||||
{
|
||||
if (!Widget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
|
||||
{
|
||||
LayerPair.Value.RemoveAll([Widget](const FFunnyUILayerWidgetEntry& Entry)
|
||||
{
|
||||
return !Entry.Widget.IsValid() || Entry.Widget.Get() == Widget;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::SetWidgetDesiredVisibility(UUserWidget* Widget, ESlateVisibility Visibility)
|
||||
{
|
||||
if (FFunnyUILayerWidgetEntry* Entry = FindWidgetEntry(Widget))
|
||||
{
|
||||
Entry->DesiredVisibility = Visibility;
|
||||
|
||||
for (const TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
|
||||
{
|
||||
if (LayerPair.Value.ContainsByPredicate([Widget](const FFunnyUILayerWidgetEntry& Candidate)
|
||||
{
|
||||
return Candidate.Widget.Get() == Widget;
|
||||
}))
|
||||
{
|
||||
ApplyWidgetState(*Entry, FindOrAddLayerState(LayerPair.Key));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Widget)
|
||||
{
|
||||
Widget->SetVisibility(Visibility);
|
||||
}
|
||||
}
|
||||
|
||||
ESlateVisibility UFunnyUILayerManagerSubsystem::GetWidgetDesiredVisibility(UUserWidget* Widget) const
|
||||
{
|
||||
if (const FFunnyUILayerWidgetEntry* Entry = FindWidgetEntry(Widget))
|
||||
{
|
||||
return Entry->DesiredVisibility;
|
||||
}
|
||||
|
||||
return Widget ? Widget->GetVisibility() : ESlateVisibility::Collapsed;
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::SetWidgetDesiredEnabled(UUserWidget* Widget, bool bEnabled)
|
||||
{
|
||||
if (FFunnyUILayerWidgetEntry* Entry = FindWidgetEntry(Widget))
|
||||
{
|
||||
Entry->bDesiredEnabled = bEnabled;
|
||||
|
||||
for (const TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
|
||||
{
|
||||
if (LayerPair.Value.ContainsByPredicate([Widget](const FFunnyUILayerWidgetEntry& Candidate)
|
||||
{
|
||||
return Candidate.Widget.Get() == Widget;
|
||||
}))
|
||||
{
|
||||
ApplyWidgetState(*Entry, FindOrAddLayerState(LayerPair.Key));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Widget)
|
||||
{
|
||||
Widget->SetIsEnabled(bEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
bool UFunnyUILayerManagerSubsystem::GetWidgetDesiredEnabled(UUserWidget* Widget) const
|
||||
{
|
||||
if (const FFunnyUILayerWidgetEntry* Entry = FindWidgetEntry(Widget))
|
||||
{
|
||||
return Entry->bDesiredEnabled;
|
||||
}
|
||||
|
||||
return Widget ? Widget->GetIsEnabled() : false;
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::SetLayerVisibility(FName LayerName, ESlateVisibility Visibility)
|
||||
{
|
||||
if (LayerName.IsNone())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FindOrAddLayerState(LayerName).Visibility = Visibility;
|
||||
if (IsVisibleLayerState(Visibility))
|
||||
{
|
||||
AutoShowLayerWidgets(LayerName);
|
||||
}
|
||||
|
||||
ApplyLayerState(LayerName);
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::ShowLayer(FName LayerName)
|
||||
{
|
||||
SetLayerVisibility(LayerName, ESlateVisibility::Visible);
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::HideLayer(FName LayerName)
|
||||
{
|
||||
SetLayerVisibility(LayerName, ESlateVisibility::Collapsed);
|
||||
}
|
||||
|
||||
ESlateVisibility UFunnyUILayerManagerSubsystem::GetLayerVisibility(FName LayerName) const
|
||||
{
|
||||
if (const FFunnyUILayerState* State = FindLayerState(LayerName))
|
||||
{
|
||||
return State->Visibility;
|
||||
}
|
||||
|
||||
return ESlateVisibility::Visible;
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::SetLayerEnabled(FName LayerName, bool bEnabled)
|
||||
{
|
||||
if (LayerName.IsNone())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FindOrAddLayerState(LayerName).bEnabled = bEnabled;
|
||||
ApplyLayerState(LayerName);
|
||||
}
|
||||
|
||||
bool UFunnyUILayerManagerSubsystem::IsLayerEnabled(FName LayerName) const
|
||||
{
|
||||
if (const FFunnyUILayerState* State = FindLayerState(LayerName))
|
||||
{
|
||||
return State->bEnabled;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::ShowOnlyLayer(FName LayerName)
|
||||
{
|
||||
if (LayerName.IsNone())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (TPair<FName, FFunnyUILayerState>& LayerPair : LayerStates)
|
||||
{
|
||||
LayerPair.Value.Visibility = LayerPair.Key == LayerName ? ESlateVisibility::Visible : ESlateVisibility::Collapsed;
|
||||
}
|
||||
|
||||
FindOrAddLayerState(LayerName).Visibility = ESlateVisibility::Visible;
|
||||
AutoShowLayerWidgets(LayerName);
|
||||
ApplyAllLayers();
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::PushLayerState()
|
||||
{
|
||||
LayerStateStack.Push(LayerStates);
|
||||
}
|
||||
|
||||
bool UFunnyUILayerManagerSubsystem::PopLayerState()
|
||||
{
|
||||
if (LayerStateStack.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
LayerStates = LayerStateStack.Pop(EAllowShrinking::No);
|
||||
ApplyAllLayers();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::ApplyAllLayers()
|
||||
{
|
||||
for (const TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
|
||||
{
|
||||
ApplyLayerState(LayerPair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
FFunnyUILayerState& UFunnyUILayerManagerSubsystem::FindOrAddLayerState(FName LayerName)
|
||||
{
|
||||
return LayerStates.FindOrAdd(LayerName);
|
||||
}
|
||||
|
||||
const FFunnyUILayerState* UFunnyUILayerManagerSubsystem::FindLayerState(FName LayerName) const
|
||||
{
|
||||
return LayerStates.Find(LayerName);
|
||||
}
|
||||
|
||||
FFunnyUILayerWidgetEntry* UFunnyUILayerManagerSubsystem::FindWidgetEntry(UUserWidget* Widget)
|
||||
{
|
||||
if (!Widget)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
|
||||
{
|
||||
for (FFunnyUILayerWidgetEntry& Entry : LayerPair.Value)
|
||||
{
|
||||
if (Entry.Widget.Get() == Widget)
|
||||
{
|
||||
return &Entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const FFunnyUILayerWidgetEntry* UFunnyUILayerManagerSubsystem::FindWidgetEntry(UUserWidget* Widget) const
|
||||
{
|
||||
if (!Widget)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
|
||||
{
|
||||
for (const FFunnyUILayerWidgetEntry& Entry : LayerPair.Value)
|
||||
{
|
||||
if (Entry.Widget.Get() == Widget)
|
||||
{
|
||||
return &Entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::PruneInvalidWidgets(FName LayerName)
|
||||
{
|
||||
if (TArray<FFunnyUILayerWidgetEntry>* Widgets = LayerWidgets.Find(LayerName))
|
||||
{
|
||||
Widgets->RemoveAll([](const FFunnyUILayerWidgetEntry& Entry)
|
||||
{
|
||||
return !Entry.Widget.IsValid();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::AutoShowLayerWidgets(FName LayerName)
|
||||
{
|
||||
if (TArray<FFunnyUILayerWidgetEntry>* Widgets = LayerWidgets.Find(LayerName))
|
||||
{
|
||||
for (FFunnyUILayerWidgetEntry& Entry : *Widgets)
|
||||
{
|
||||
if (Entry.bAutoShowWithLayer)
|
||||
{
|
||||
Entry.DesiredVisibility = ESlateVisibility::Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::ApplyLayerState(FName LayerName)
|
||||
{
|
||||
PruneInvalidWidgets(LayerName);
|
||||
|
||||
const FFunnyUILayerState& LayerState = FindOrAddLayerState(LayerName);
|
||||
if (const TArray<FFunnyUILayerWidgetEntry>* Widgets = LayerWidgets.Find(LayerName))
|
||||
{
|
||||
for (const FFunnyUILayerWidgetEntry& Entry : *Widgets)
|
||||
{
|
||||
ApplyWidgetState(Entry, LayerState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UFunnyUILayerManagerSubsystem::ApplyWidgetState(const FFunnyUILayerWidgetEntry& Entry, const FFunnyUILayerState& LayerState) const
|
||||
{
|
||||
UUserWidget* Widget = Entry.Widget.Get();
|
||||
if (!Widget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Widget->SetVisibility(CombineVisibility(LayerState.Visibility, Entry.DesiredVisibility));
|
||||
Widget->SetIsEnabled(LayerState.bEnabled && Entry.bDesiredEnabled);
|
||||
}
|
||||
|
||||
bool UFunnyUILayerManagerSubsystem::IsVisibleLayerState(ESlateVisibility Visibility)
|
||||
{
|
||||
return Visibility != ESlateVisibility::Collapsed && Visibility != ESlateVisibility::Hidden;
|
||||
}
|
||||
|
||||
ESlateVisibility UFunnyUILayerManagerSubsystem::CombineVisibility(ESlateVisibility LayerVisibility, ESlateVisibility DesiredVisibility)
|
||||
{
|
||||
if (LayerVisibility == ESlateVisibility::Collapsed || LayerVisibility == ESlateVisibility::Hidden)
|
||||
{
|
||||
return LayerVisibility;
|
||||
}
|
||||
|
||||
if (DesiredVisibility == ESlateVisibility::Collapsed || DesiredVisibility == ESlateVisibility::Hidden)
|
||||
{
|
||||
return DesiredVisibility;
|
||||
}
|
||||
|
||||
if (LayerVisibility == ESlateVisibility::HitTestInvisible)
|
||||
{
|
||||
return ESlateVisibility::HitTestInvisible;
|
||||
}
|
||||
|
||||
if (LayerVisibility == ESlateVisibility::SelfHitTestInvisible && DesiredVisibility == ESlateVisibility::Visible)
|
||||
{
|
||||
return ESlateVisibility::SelfHitTestInvisible;
|
||||
}
|
||||
|
||||
return DesiredVisibility;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "UI/TypingTextBlock.h"
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
UTypingTextBlock::UTypingTextBlock()
|
||||
: CharacterInterval(0.025f)
|
||||
, bAutoStartTyping(true)
|
||||
{
|
||||
SetText(FText::GetEmpty());
|
||||
}
|
||||
|
||||
void UTypingTextBlock::AppendTextChunk(const FString& TextChunk)
|
||||
{
|
||||
if (TextChunk.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PendingText += TextChunk;
|
||||
|
||||
if (bAutoStartTyping)
|
||||
{
|
||||
StartTyping();
|
||||
}
|
||||
}
|
||||
|
||||
void UTypingTextBlock::ClearTypedText()
|
||||
{
|
||||
StopTyping();
|
||||
DisplayedText.Reset();
|
||||
PendingText.Reset();
|
||||
SetText(FText::GetEmpty());
|
||||
}
|
||||
|
||||
void UTypingTextBlock::FlushTyping()
|
||||
{
|
||||
if (PendingText.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisplayedText += PendingText;
|
||||
PendingText.Reset();
|
||||
SetText(FText::FromString(DisplayedText));
|
||||
StopTyping();
|
||||
}
|
||||
|
||||
bool UTypingTextBlock::IsTyping() const
|
||||
{
|
||||
return !PendingText.IsEmpty();
|
||||
}
|
||||
|
||||
void UTypingTextBlock::ReleaseSlateResources(bool bReleaseChildren)
|
||||
{
|
||||
StopTyping();
|
||||
Super::ReleaseSlateResources(bReleaseChildren);
|
||||
}
|
||||
|
||||
void UTypingTextBlock::StartTyping()
|
||||
{
|
||||
if (PendingText.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UWorld* World = GetWorld();
|
||||
if (!World)
|
||||
{
|
||||
FlushTyping();
|
||||
return;
|
||||
}
|
||||
|
||||
if (World->GetTimerManager().IsTimerActive(TypingTimerHandle))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
World->GetTimerManager().SetTimer(TypingTimerHandle, this, &UTypingTextBlock::TypeNextCharacter, CharacterInterval, true);
|
||||
}
|
||||
|
||||
void UTypingTextBlock::StopTyping()
|
||||
{
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(TypingTimerHandle);
|
||||
}
|
||||
}
|
||||
|
||||
void UTypingTextBlock::TypeNextCharacter()
|
||||
{
|
||||
if (PendingText.IsEmpty())
|
||||
{
|
||||
StopTyping();
|
||||
return;
|
||||
}
|
||||
|
||||
DisplayedText.AppendChars(*PendingText, 1);
|
||||
PendingText.RightChopInline(1, EAllowShrinking::No);
|
||||
SetText(FText::FromString(DisplayedText));
|
||||
|
||||
if (PendingText.IsEmpty())
|
||||
{
|
||||
StopTyping();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Utility/FunnyGirlUtilityBlueprintLibrary.h"
|
||||
|
||||
#include "Animation/MorphTarget.h"
|
||||
#include "Components/Button.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Misc/Base64.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
bool TryParseJsonObject(const FString& Json, TSharedPtr<FJsonObject>& OutObject)
|
||||
{
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Json);
|
||||
return FJsonSerializer::Deserialize(Reader, OutObject) && OutObject.IsValid();
|
||||
}
|
||||
|
||||
FString GetLightSaveFilePath()
|
||||
{
|
||||
return FPaths::ProjectSavedDir() / TEXT("LightSave") / TEXT("Strings.json");
|
||||
}
|
||||
|
||||
FString EncodeStringBase64(const FString& Value)
|
||||
{
|
||||
FTCHARToUTF8 Utf8Value(*Value);
|
||||
return FBase64::Encode(reinterpret_cast<const uint8*>(Utf8Value.Get()), Utf8Value.Length());
|
||||
}
|
||||
|
||||
bool DecodeStringBase64(const FString& EncodedValue, FString& OutValue)
|
||||
{
|
||||
TArray<uint8> DecodedBytes;
|
||||
if (!FBase64::Decode(EncodedValue, DecodedBytes))
|
||||
{
|
||||
OutValue.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
DecodedBytes.Add(0);
|
||||
OutValue = FString(UTF8_TO_TCHAR(reinterpret_cast<const char*>(DecodedBytes.GetData())));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadLightSaveObject(TSharedPtr<FJsonObject>& OutObject, FString& OutError)
|
||||
{
|
||||
OutObject = MakeShared<FJsonObject>();
|
||||
OutError.Reset();
|
||||
|
||||
const FString SaveFilePath = GetLightSaveFilePath();
|
||||
if (!FPaths::FileExists(SaveFilePath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
FString SaveJson;
|
||||
if (!FFileHelper::LoadFileToString(SaveJson, *SaveFilePath))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not read light save file: %s"), *SaveFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SaveJson.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryParseJsonObject(SaveJson, OutObject))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not parse light save file: %s"), *SaveFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SaveLightSaveObject(const TSharedRef<FJsonObject>& SaveObject, FString& OutError)
|
||||
{
|
||||
OutError.Reset();
|
||||
|
||||
const FString SaveFilePath = GetLightSaveFilePath();
|
||||
IFileManager::Get().MakeDirectory(*FPaths::GetPath(SaveFilePath), true);
|
||||
|
||||
FString SaveJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&SaveJson);
|
||||
if (!FJsonSerializer::Serialize(SaveObject, Writer))
|
||||
{
|
||||
OutError = TEXT("Could not serialize light save JSON.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FFileHelper::SaveStringToFile(SaveJson, *SaveFilePath))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not write light save file: %s"), *SaveFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::SaveMorphTargetNamesToFile(
|
||||
USkeletalMesh* SkeletalMesh,
|
||||
FString& OutFilePath,
|
||||
FString& OutError)
|
||||
{
|
||||
OutFilePath.Reset();
|
||||
OutError.Reset();
|
||||
|
||||
if (!SkeletalMesh)
|
||||
{
|
||||
OutError = TEXT("Skeletal mesh is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString MeshName = SkeletalMesh->GetName().TrimStartAndEnd();
|
||||
if (MeshName.IsEmpty())
|
||||
{
|
||||
MeshName = TEXT("SkeletalMesh");
|
||||
}
|
||||
|
||||
MeshName = FPaths::MakeValidFileName(MeshName);
|
||||
|
||||
const FString Directory = FPaths::ProjectSavedDir() / TEXT("MorphTarget");
|
||||
IFileManager::Get().MakeDirectory(*Directory, true);
|
||||
|
||||
OutFilePath = Directory / FString::Printf(TEXT("%s.json"), *MeshName);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> MorphTargetValues;
|
||||
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
|
||||
{
|
||||
if (!MorphTarget)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FString MorphTargetName = MorphTarget->GetName().TrimStartAndEnd();
|
||||
if (!MorphTargetName.IsEmpty())
|
||||
{
|
||||
MorphTargetValues.Add(MakeShared<FJsonValueString>(MorphTargetName));
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> RootObject = MakeShared<FJsonObject>();
|
||||
RootObject->SetStringField(TEXT("mesh_name"), SkeletalMesh->GetName());
|
||||
RootObject->SetStringField(TEXT("mesh_path"), SkeletalMesh->GetPathName());
|
||||
RootObject->SetStringField(TEXT("saved_at"), FDateTime::Now().ToIso8601());
|
||||
RootObject->SetNumberField(TEXT("morph_target_count"), MorphTargetValues.Num());
|
||||
RootObject->SetArrayField(TEXT("morph_targets"), MorphTargetValues);
|
||||
|
||||
FString OutputJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputJson);
|
||||
if (!FJsonSerializer::Serialize(RootObject, Writer))
|
||||
{
|
||||
OutError = TEXT("Could not serialize morph target JSON.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FFileHelper::SaveStringToFile(OutputJson, *OutFilePath))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not save morph target file: %s"), *OutFilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::GetJsonStringField(const FString& Json, const FString& FieldName, FString& OutValue)
|
||||
{
|
||||
OutValue.Reset();
|
||||
|
||||
FString CleanFieldName = FieldName;
|
||||
CleanFieldName.TrimStartAndEndInline();
|
||||
if (CleanFieldName.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
if (!TryParseJsonObject(Json, RootObject))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return RootObject->TryGetStringField(CleanFieldName, OutValue);
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::GetJsonNumberField(const FString& Json, const FString& FieldName, float& OutValue)
|
||||
{
|
||||
OutValue = 0.0f;
|
||||
|
||||
FString CleanFieldName = FieldName;
|
||||
CleanFieldName.TrimStartAndEndInline();
|
||||
if (CleanFieldName.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
if (!TryParseJsonObject(Json, RootObject))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double NumberValue = 0.0;
|
||||
if (!RootObject->TryGetNumberField(CleanFieldName, NumberValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutValue = static_cast<float>(NumberValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::GetJsonBoolField(const FString& Json, const FString& FieldName, bool& OutValue)
|
||||
{
|
||||
OutValue = false;
|
||||
|
||||
FString CleanFieldName = FieldName;
|
||||
CleanFieldName.TrimStartAndEndInline();
|
||||
if (CleanFieldName.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
if (!TryParseJsonObject(Json, RootObject))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return RootObject->TryGetBoolField(CleanFieldName, OutValue);
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::SetButtonStyleTexture(
|
||||
UButton* Button,
|
||||
UTexture2D* Texture,
|
||||
EFunnyGirlButtonBrushState State)
|
||||
{
|
||||
if (!Button || !Texture)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FButtonStyle ButtonStyle = Button->GetStyle();
|
||||
FSlateBrush* TargetBrush = nullptr;
|
||||
|
||||
switch (State)
|
||||
{
|
||||
case EFunnyGirlButtonBrushState::Normal:
|
||||
TargetBrush = &ButtonStyle.Normal;
|
||||
break;
|
||||
case EFunnyGirlButtonBrushState::Hovered:
|
||||
TargetBrush = &ButtonStyle.Hovered;
|
||||
break;
|
||||
case EFunnyGirlButtonBrushState::Pressed:
|
||||
TargetBrush = &ButtonStyle.Pressed;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
TargetBrush->SetResourceObject(Texture);
|
||||
if (TargetBrush->ImageSize.IsNearlyZero())
|
||||
{
|
||||
TargetBrush->ImageSize = FVector2D(Texture->GetSizeX(), Texture->GetSizeY());
|
||||
}
|
||||
|
||||
Button->SetStyle(ButtonStyle);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::SaveLightString(const FString& Field, const FString& Value, FString& OutError)
|
||||
{
|
||||
OutError.Reset();
|
||||
|
||||
const FString CleanField = Field.TrimStartAndEnd();
|
||||
if (CleanField.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Field is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> SaveObject;
|
||||
if (!LoadLightSaveObject(SaveObject, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SaveObject->SetStringField(EncodeStringBase64(CleanField), EncodeStringBase64(Value));
|
||||
return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError);
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::LoadLightString(const FString& Field, FString& OutValue, FString& OutError)
|
||||
{
|
||||
OutValue.Reset();
|
||||
OutError.Reset();
|
||||
|
||||
const FString CleanField = Field.TrimStartAndEnd();
|
||||
if (CleanField.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Field is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> SaveObject;
|
||||
if (!LoadLightSaveObject(SaveObject, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FString EncodedValue;
|
||||
if (!SaveObject->TryGetStringField(EncodeStringBase64(CleanField), EncodedValue))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Field not found: %s"), *CleanField);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DecodeStringBase64(EncodedValue, OutValue))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not decode value for field: %s"), *CleanField);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UFunnyGirlUtilityBlueprintLibrary::DeleteLightString(const FString& Field, FString& OutError)
|
||||
{
|
||||
OutError.Reset();
|
||||
|
||||
const FString CleanField = Field.TrimStartAndEnd();
|
||||
if (CleanField.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Field is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> SaveObject;
|
||||
if (!LoadLightSaveObject(SaveObject, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString EncodedField = EncodeStringBase64(CleanField);
|
||||
if (!SaveObject->HasField(EncodedField))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Field not found: %s"), *CleanField);
|
||||
return false;
|
||||
}
|
||||
|
||||
SaveObject->RemoveField(EncodedField);
|
||||
return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "World/InteractableObjectComponent.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
|
||||
UInteractableObjectComponent::UInteractableObjectComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
bool UInteractableObjectComponent::HasAction(FName Action) const
|
||||
{
|
||||
FInteractableObjectAction FoundAction;
|
||||
return FindAction(Action, FoundAction);
|
||||
}
|
||||
|
||||
bool UInteractableObjectComponent::FindAction(FName Action, FInteractableObjectAction& OutAction) const
|
||||
{
|
||||
for (const FInteractableObjectAction& Candidate : Actions)
|
||||
{
|
||||
if (Candidate.Action == Action)
|
||||
{
|
||||
OutAction = Candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UInteractableObjectComponent::ExecuteAction(const FString& ActionJson)
|
||||
{
|
||||
FName Action;
|
||||
if (!TryExtractActionFromJson(ActionJson, Action))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FInteractableObjectAction ActionData;
|
||||
if (!FindAction(Action, ActionData))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnActionRequested.Broadcast(Action, ActionJson);
|
||||
return OnExecuteAction(ActionJson);
|
||||
}
|
||||
|
||||
bool UInteractableObjectComponent::OnExecuteAction_Implementation(const FString& ActionJson)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInteractableObjectComponent::TryExtractActionFromJson(const FString& ActionJson, FName& OutAction) const
|
||||
{
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ActionJson);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FString ActionString;
|
||||
if (!RootObject->TryGetStringField(TEXT("action"), ActionString) && !RootObject->TryGetStringField(TEXT("Action"), ActionString))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ActionString.TrimStartAndEndInline();
|
||||
if (ActionString.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutAction = FName(*ActionString);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "World/WorldContextActor.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "World/InteractableObjectComponent.h"
|
||||
#include "World/WorldContextDataAsset.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
bool TryExtractInteractableObjectID(const FString& ActionJson, FString& OutObjectID)
|
||||
{
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ActionJson);
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RootObject->TryGetStringField(TEXT("object_id"), OutObjectID) &&
|
||||
!RootObject->TryGetStringField(TEXT("objectId"), OutObjectID) &&
|
||||
!RootObject->TryGetStringField(TEXT("ObjectID"), OutObjectID))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutObjectID.TrimStartAndEndInline();
|
||||
return !OutObjectID.IsEmpty();
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> BuildInteractableObjectStateJsonObject(const TMap<FString, FString>& State)
|
||||
{
|
||||
TSharedRef<FJsonObject> StateObject = MakeShared<FJsonObject>();
|
||||
|
||||
for (const TPair<FString, FString>& StatePair : State)
|
||||
{
|
||||
FString Key = StatePair.Key;
|
||||
Key.TrimStartAndEndInline();
|
||||
if (Key.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Value = StatePair.Value;
|
||||
Value.TrimStartAndEndInline();
|
||||
if (Value.IsEmpty())
|
||||
{
|
||||
StateObject->SetField(Key, MakeShared<FJsonValueNull>());
|
||||
}
|
||||
else
|
||||
{
|
||||
StateObject->SetStringField(Key, Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (StateObject->Values.IsEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return StateObject;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonValue> BuildActionFormatJsonValue(const FText& ActionFormat)
|
||||
{
|
||||
const FString FormatText = ActionFormat.ToString().TrimStartAndEnd();
|
||||
if (FormatText.IsEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> FormatObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(FormatText);
|
||||
if (FJsonSerializer::Deserialize(Reader, FormatObject) && FormatObject.IsValid())
|
||||
{
|
||||
return MakeShared<FJsonValueObject>(FormatObject);
|
||||
}
|
||||
|
||||
return MakeShared<FJsonValueString>(FormatText);
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> BuildInteractableObjectJsonObject(const UInteractableObjectComponent* InteractableComponent)
|
||||
{
|
||||
if (!InteractableComponent)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> ObjectJson = MakeShared<FJsonObject>();
|
||||
|
||||
if (!InteractableComponent->ID.IsEmpty())
|
||||
{
|
||||
ObjectJson->SetStringField(TEXT("id"), InteractableComponent->ID);
|
||||
}
|
||||
|
||||
if (!InteractableComponent->Name.IsEmpty())
|
||||
{
|
||||
ObjectJson->SetStringField(TEXT("name"), InteractableComponent->Name.ToString());
|
||||
}
|
||||
|
||||
if (!InteractableComponent->Description.IsEmpty())
|
||||
{
|
||||
ObjectJson->SetStringField(TEXT("description"), InteractableComponent->Description.ToString());
|
||||
}
|
||||
|
||||
if (!InteractableComponent->State.IsEmpty())
|
||||
{
|
||||
const TSharedPtr<FJsonObject> StateJson = BuildInteractableObjectStateJsonObject(InteractableComponent->State);
|
||||
if (StateJson.IsValid())
|
||||
{
|
||||
ObjectJson->SetObjectField(TEXT("state"), StateJson);
|
||||
}
|
||||
}
|
||||
|
||||
if (!InteractableComponent->CustomData.IsEmpty())
|
||||
{
|
||||
ObjectJson->SetStringField(TEXT("custom_data"), InteractableComponent->CustomData.ToString());
|
||||
}
|
||||
|
||||
if (!InteractableComponent->Actions.IsEmpty())
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> ActionsJson;
|
||||
for (const FInteractableObjectAction& Action : InteractableComponent->Actions)
|
||||
{
|
||||
if (Action.Action.IsNone() && Action.Description.IsEmpty() && Action.ActionFormat.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> ActionJson = MakeShared<FJsonObject>();
|
||||
|
||||
if (!Action.Action.IsNone())
|
||||
{
|
||||
ActionJson->SetStringField(TEXT("action"), Action.Action.ToString());
|
||||
}
|
||||
|
||||
if (!Action.Description.IsEmpty())
|
||||
{
|
||||
ActionJson->SetStringField(TEXT("description"), Action.Description.ToString());
|
||||
}
|
||||
|
||||
if (const TSharedPtr<FJsonValue> FormatJson = BuildActionFormatJsonValue(Action.ActionFormat))
|
||||
{
|
||||
ActionJson->SetField(TEXT("format"), FormatJson);
|
||||
}
|
||||
|
||||
ActionsJson.Add(MakeShared<FJsonValueObject>(ActionJson));
|
||||
}
|
||||
|
||||
if (!ActionsJson.IsEmpty())
|
||||
{
|
||||
ObjectJson->SetArrayField(TEXT("actions"), ActionsJson);
|
||||
}
|
||||
}
|
||||
|
||||
if (ObjectJson->Values.IsEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return ObjectJson;
|
||||
}
|
||||
}
|
||||
|
||||
AWorldContextActor::AWorldContextActor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
FString AWorldContextActor::BuildWorldContextPrompt() const
|
||||
{
|
||||
TArray<FString> PromptParts;
|
||||
|
||||
const FString BaseWorldContextPrompt = BuildBaseWorldContextPrompt();
|
||||
if (!BaseWorldContextPrompt.IsEmpty())
|
||||
{
|
||||
PromptParts.Add(BaseWorldContextPrompt);
|
||||
}
|
||||
|
||||
const FString InteractableObjectsPrompt = BuildInteractableObjectsPrompt();
|
||||
if (!InteractableObjectsPrompt.IsEmpty())
|
||||
{
|
||||
PromptParts.Add(InteractableObjectsPrompt);
|
||||
}
|
||||
|
||||
return FString::Join(PromptParts, TEXT("\n\n"));
|
||||
}
|
||||
|
||||
FString AWorldContextActor::BuildBaseWorldContextPrompt() const
|
||||
{
|
||||
return WorldContext ? WorldContext->BuildWorldContextPrompt() : FString();
|
||||
}
|
||||
|
||||
FString AWorldContextActor::BuildInteractableObjectsPrompt() const
|
||||
{
|
||||
EnsureInteractableObjectsCached();
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> ObjectsJson;
|
||||
for (const TWeakObjectPtr<UInteractableObjectComponent>& InteractableObject : InteractableObjects)
|
||||
{
|
||||
const UInteractableObjectComponent* InteractableComponent = InteractableObject.Get();
|
||||
const TSharedPtr<FJsonObject> ObjectJson = BuildInteractableObjectJsonObject(InteractableComponent);
|
||||
if (ObjectJson.IsValid())
|
||||
{
|
||||
ObjectsJson.Add(MakeShared<FJsonValueObject>(ObjectJson));
|
||||
}
|
||||
}
|
||||
|
||||
if (ObjectsJson.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
FString ObjectsJsonString;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ObjectsJsonString);
|
||||
FJsonSerializer::Serialize(ObjectsJson, Writer);
|
||||
|
||||
return FString::Printf(TEXT("INTERACTABLE OBJECTS JSON:\n%s"), *ObjectsJsonString);
|
||||
}
|
||||
|
||||
void AWorldContextActor::RefreshInteractableObjects() const
|
||||
{
|
||||
InteractableObjects.Empty();
|
||||
InteractableObjectsByID.Empty();
|
||||
|
||||
const UWorld* CurrentWorld = GetWorld();
|
||||
if (!CurrentWorld)
|
||||
{
|
||||
bInteractableObjectsCached = false;
|
||||
return;
|
||||
}
|
||||
|
||||
bInteractableObjectsCached = true;
|
||||
|
||||
for (TActorIterator<AActor> ActorIterator(CurrentWorld); ActorIterator; ++ActorIterator)
|
||||
{
|
||||
AActor* Actor = *ActorIterator;
|
||||
if (!Actor || Actor->IsPendingKillPending())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TArray<UInteractableObjectComponent*> InteractableComponents;
|
||||
Actor->GetComponents<UInteractableObjectComponent>(InteractableComponents);
|
||||
|
||||
for (UInteractableObjectComponent* InteractableComponent : InteractableComponents)
|
||||
{
|
||||
if (!InteractableComponent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString ObjectID = InteractableComponent->ID;
|
||||
ObjectID.TrimStartAndEndInline();
|
||||
if (ObjectID.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (InteractableObjectsByID.Contains(ObjectID))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Duplicate interactable object ID '%s' on %s. Keeping the first object."), *ObjectID, *GetNameSafe(Actor));
|
||||
continue;
|
||||
}
|
||||
|
||||
InteractableObjects.Add(InteractableComponent);
|
||||
InteractableObjectsByID.Add(ObjectID, InteractableComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AWorldContextActor::EnsureInteractableObjectsCached() const
|
||||
{
|
||||
if (!bInteractableObjectsCached)
|
||||
{
|
||||
RefreshInteractableObjects();
|
||||
}
|
||||
}
|
||||
|
||||
UInteractableObjectComponent* AWorldContextActor::FindInteractableObjectByID(const FString& ObjectID) const
|
||||
{
|
||||
EnsureInteractableObjectsCached();
|
||||
|
||||
FString CleanObjectID = ObjectID;
|
||||
CleanObjectID.TrimStartAndEndInline();
|
||||
if (CleanObjectID.IsEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (const TWeakObjectPtr<UInteractableObjectComponent>* FoundComponent = InteractableObjectsByID.Find(CleanObjectID))
|
||||
{
|
||||
return FoundComponent->Get();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool AWorldContextActor::ExecuteInteractableObjectAction(const FString& ActionJson, FString& OutError)
|
||||
{
|
||||
FString ObjectID;
|
||||
if (!TryExtractInteractableObjectID(ActionJson, ObjectID))
|
||||
{
|
||||
OutError = TEXT("Interactable action JSON must contain a non-empty object_id.");
|
||||
return false;
|
||||
}
|
||||
|
||||
UInteractableObjectComponent* InteractableObject = FindInteractableObjectByID(ObjectID);
|
||||
if (!InteractableObject)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Interactable object not found: %s"), *ObjectID);
|
||||
UE_LOG(LogTemp, Warning, TEXT("Interactable action failed: %s"), *OutError);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InteractableObject->ExecuteAction(ActionJson))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Interactable object '%s' rejected the requested action."), *ObjectID);
|
||||
UE_LOG(LogTemp, Warning, TEXT("Interactable action failed: %s ActionJson=%s"), *OutError, *ActionJson);
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("Interactable action executed on '%s': %s"), *ObjectID, *ActionJson);
|
||||
OutError.Reset();
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "World/WorldContextDataAsset.h"
|
||||
|
||||
FString UWorldContextDataAsset::BuildWorldContextPrompt() const
|
||||
{
|
||||
TArray<FString> Lines;
|
||||
|
||||
if (!LocationName.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("- Location: %s"), *LocationName.ToString()));
|
||||
}
|
||||
|
||||
if (!SceneDescription.IsEmpty())
|
||||
{
|
||||
Lines.Add(FString::Printf(TEXT("\nScene description:\n%s"), *SceneDescription.ToString()));
|
||||
}
|
||||
|
||||
if (Lines.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
return FString::Printf(TEXT("[WORLD CONTEXT]\n%s"), *FString::Join(Lines, TEXT("\n")));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "PersonalityTraitDataAsset.generated.h"
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API UPersonalityTraitDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality Trait")
|
||||
FString ID;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality Trait")
|
||||
FText Name;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality Trait", meta = (MultiLine = "true"))
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true"))
|
||||
FText Prompt;
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "TouchInputComponent.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EFunnyGirlTouchSwipeDirection : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "None"),
|
||||
Up UMETA(DisplayName = "Up"),
|
||||
Down UMETA(DisplayName = "Down"),
|
||||
Left UMETA(DisplayName = "Left"),
|
||||
Right UMETA(DisplayName = "Right")
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FTouchGestureData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
|
||||
int32 FingerCount = 0;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
|
||||
FVector2D StartLocation = FVector2D::ZeroVector;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
|
||||
FVector2D Location = FVector2D::ZeroVector;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
|
||||
FVector2D Delta = FVector2D::ZeroVector;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
|
||||
FVector2D TotalDelta = FVector2D::ZeroVector;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
|
||||
EFunnyGirlTouchSwipeDirection SwipeDirection = EFunnyGirlTouchSwipeDirection::None;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
|
||||
float Distance = 0.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
|
||||
float Duration = 0.0f;
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTouchGestureEvent, FTouchGestureData, Gesture);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTouchZoomEvent, float, ZoomDelta);
|
||||
|
||||
UCLASS(ClassGroup = (FunnyGirl), meta = (BlueprintSpawnableComponent))
|
||||
class FUNNYGIRL_API UTouchInputComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UTouchInputComponent();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input")
|
||||
bool bEnabled = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.01"))
|
||||
float TapMaxDuration = 0.25f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
|
||||
float TapMaxMovement = 24.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
|
||||
float SwipeMinDistance = 80.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.01"))
|
||||
float SwipeMaxDuration = 0.75f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "1.0"))
|
||||
float SwipeDirectionBias = 1.35f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
|
||||
float MoveMinDistancePerFrame = 1.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
|
||||
float MoveStartDelay = 0.12f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.01"))
|
||||
float HoldDuration = 0.45f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
|
||||
float HoldMaxMovement = 20.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
|
||||
float PinchMinScaleDelta = 0.01f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input")
|
||||
bool bLogWarnings = true;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Tap")
|
||||
FTouchGestureEvent OnOneFingerTap;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Tap")
|
||||
FTouchGestureEvent OnTwoFingerTap;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Swipe")
|
||||
FTouchGestureEvent OnOneFingerSwipe;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Move")
|
||||
FTouchGestureEvent OnOneFingerMove;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Hold")
|
||||
FTouchGestureEvent OnOneFingerHold;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Hold")
|
||||
FTouchGestureEvent OnTwoFingerHold;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Zoom")
|
||||
FTouchZoomEvent OnZoom;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Pinch")
|
||||
FTouchGestureEvent OnFourFingerPinch;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Touch Input")
|
||||
void ResetTouchState();
|
||||
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
enum class ETouchGestureState : uint8
|
||||
{
|
||||
None,
|
||||
Move,
|
||||
Hold,
|
||||
Swipe,
|
||||
ZoomOrPinch
|
||||
};
|
||||
|
||||
struct FTrackedTouch
|
||||
{
|
||||
bool bPressed = false;
|
||||
FVector2D StartLocation = FVector2D::ZeroVector;
|
||||
FVector2D Location = FVector2D::ZeroVector;
|
||||
double StartTime = 0.0;
|
||||
};
|
||||
|
||||
static constexpr int32 MaxTouchCount = 10;
|
||||
|
||||
FTrackedTouch Touches[MaxTouchCount];
|
||||
bool bGestureActive = false;
|
||||
ETouchGestureState GestureState = ETouchGestureState::None;
|
||||
bool bMoveStarted = false;
|
||||
int32 CurrentFingerCount = 0;
|
||||
int32 GestureFingerCount = 0;
|
||||
double GestureStartTime = 0.0;
|
||||
FVector2D GestureStartCentroid = FVector2D::ZeroVector;
|
||||
FVector2D PreviousCentroid = FVector2D::ZeroVector;
|
||||
float InitialSpan = 0.0f;
|
||||
float PreviousSpan = 0.0f;
|
||||
float MaxDistanceFromStart = 0.0f;
|
||||
|
||||
void PollTouches(float DeltaTime);
|
||||
void BeginGesture(int32 FingerCount, const FVector2D& Centroid, double CurrentTime);
|
||||
void EndGesture(double CurrentTime);
|
||||
void HandleFingerCountChanged(int32 FingerCount, const FVector2D& Centroid, double CurrentTime);
|
||||
void HandleActiveGesture(int32 FingerCount, const FVector2D& Centroid, float DeltaTime, double CurrentTime);
|
||||
void ResetGestureTracking();
|
||||
|
||||
class APlayerController* ResolvePlayerController() const;
|
||||
int32 ReadPressedTouches();
|
||||
FVector2D GetCentroid() const;
|
||||
float GetAverageSpan(const FVector2D& Centroid) const;
|
||||
EFunnyGirlTouchSwipeDirection GetSwipeDirection(const FVector2D& TotalDelta) const;
|
||||
bool IsStableSwipe(const FVector2D& TotalDelta, float Duration) const;
|
||||
FTouchGestureData BuildGestureData(int32 FingerCount, const FVector2D& Location, const FVector2D& Delta, double CurrentTime) const;
|
||||
|
||||
void BroadcastTap(const FTouchGestureData& Gesture);
|
||||
void BroadcastSwipe(const FTouchGestureData& Gesture);
|
||||
void BroadcastMove(const FTouchGestureData& Gesture);
|
||||
void BroadcastHold(const FTouchGestureData& Gesture);
|
||||
void BroadcastPinchOrZoom(const FTouchGestureData& Gesture, float ZoomDelta);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Interfaces/IHttpRequest.h"
|
||||
#include "BrainComponentBase.generated.h"
|
||||
|
||||
class AWorldContextActor;
|
||||
class FJsonObject;
|
||||
class FJsonValue;
|
||||
class UBrainToolBase;
|
||||
class ULLMProviderDataAsset;
|
||||
|
||||
UCLASS(Abstract, ClassGroup = (AI))
|
||||
class FUNNYGIRL_API UBrainComponentBase : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBrainComponentBase();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM")
|
||||
TObjectPtr<ULLMProviderDataAsset> Provider;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context")
|
||||
TObjectPtr<AWorldContextActor> WorldContextActor;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context")
|
||||
bool bAutoFindWorldContextActor;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Brain")
|
||||
bool bIsThinking;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Brain")
|
||||
virtual void CancelRequest();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Brain")
|
||||
bool IsThinking() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Brain|History")
|
||||
virtual FString GetChatIdentityId() const;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
TSharedPtr<IHttpRequest, ESPMode::ThreadSafe> ActiveRequest;
|
||||
|
||||
virtual void BroadcastBrainError(const FString& ErrorMessage) const;
|
||||
virtual FString GetBrainDebugName() const;
|
||||
|
||||
void SetIsThinking(bool bNewIsThinking);
|
||||
void AutoFindWorldContextActor();
|
||||
bool ValidateProvider(const ULLMProviderDataAsset* EffectiveProvider) const;
|
||||
bool ConfigureRequestWithBody(
|
||||
const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request,
|
||||
const ULLMProviderDataAsset* EffectiveProvider,
|
||||
const FString& RequestBody,
|
||||
const FString& AcceptHeader) const;
|
||||
|
||||
void AddSystemMessage(TArray<TSharedPtr<FJsonValue>>& Messages, const FString& Content) const;
|
||||
TSharedRef<FJsonObject> MakeMessageObject(const FString& Role, const FString& Content) const;
|
||||
FString SerializeJsonObject(const TSharedRef<FJsonObject>& RootObject) const;
|
||||
void AppendAllowedTools(TSharedRef<FJsonObject> RootObject, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools) const;
|
||||
TSharedPtr<FJsonObject> BuildToolDefinition(const UBrainToolBase* ToolDefaultObject) const;
|
||||
const UBrainToolBase* FindAllowedToolDefaultObject(const FString& ToolName, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools) const;
|
||||
bool ExecuteBrainToolCalls(const TArray<TSharedPtr<FJsonValue>>& ToolCalls, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools);
|
||||
|
||||
FString GetChatDirectory() const;
|
||||
FString GetChatFilePath() const;
|
||||
FString GetChatSiblingFilePath(const FString& FileName) const;
|
||||
void SaveRequestBodyToFile(const FString& RequestBody, const FString& FileName) const;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "Templates/SubclassOf.h"
|
||||
#include "BrainPromptDataAsset.generated.h"
|
||||
|
||||
class UBrainToolBase;
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API UBrainPromptDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Brain", meta = (DisplayPriority = "0"))
|
||||
FString ID;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (DisplayPriority = "20"))
|
||||
FText Name;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "10"))
|
||||
FText SystemPrompt;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Tools", meta = (MultiLine = "true", DisplayPriority = "10"))
|
||||
FText ToolsPromptHeader;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Tools", meta = (MultiLine = "true", DisplayPriority = "30"))
|
||||
FText ToolsPromptFooter;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Tools", meta = (DisplayPriority = "20"))
|
||||
TArray<TSubclassOf<UBrainToolBase>> AllowedTools;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Brain")
|
||||
virtual FString BuildBrainPrompt() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Tools")
|
||||
FString BuildToolsPrompt() const;
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "BrainToolBase.generated.h"
|
||||
|
||||
class UBrainComponentBase;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable, Abstract)
|
||||
class FUNNYGIRL_API UBrainToolBase : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Brain|Tool")
|
||||
FString ToolName;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Brain|Tool", meta = (MultiLine = "true"))
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Brain|Tool", meta = (MultiLine = "true"))
|
||||
FText ParametersJsonSchema;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Brain|Tool")
|
||||
FString GetResolvedToolName() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Brain|Tool")
|
||||
FString BuildToolPrompt() const;
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Brain|Tool")
|
||||
bool ExecuteTool(UBrainComponentBase* Brain, const FString& ArgumentsJson, UPARAM(ref) FString& OutError);
|
||||
virtual bool ExecuteTool_Implementation(UBrainComponentBase* Brain, const FString& ArgumentsJson, FString& OutError);
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Brain|Tool")
|
||||
void CleanupTool();
|
||||
virtual void CleanupTool_Implementation();
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "LLM/BrainComponentBase.h"
|
||||
#include "LLM/PersonaBrainComponent.h"
|
||||
#include "DirectorBrainComponent.generated.h"
|
||||
|
||||
class UDirectorPromptDataAsset;
|
||||
class ULLMProviderDataAsset;
|
||||
class FJsonObject;
|
||||
class FJsonValue;
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FDirectorBrainResponse
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Director")
|
||||
FString Content;
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDirectorBrainResponseDelegate, const FDirectorBrainResponse&, Response);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDirectorBrainErrorDelegate, const FString&, ErrorMessage);
|
||||
|
||||
UCLASS(ClassGroup = (AI), meta = (BlueprintSpawnableComponent, DisplayName = "Director Brain"))
|
||||
class FUNNYGIRL_API UDirectorBrainComponent : public UBrainComponentBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDirectorBrainComponent();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Director")
|
||||
TObjectPtr<UPersonaBrainComponent> PersonaBrain;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Director")
|
||||
TObjectPtr<UDirectorPromptDataAsset> DirectorPrompt;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Director|Memory", meta = (MultiLine = "true"))
|
||||
FText ExistingPlayerFactsPromptTemplate;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Director|Events")
|
||||
FDirectorBrainResponseDelegate OnDirectorResponse;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Director|Events")
|
||||
FDirectorBrainErrorDelegate OnDirectorError;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Director")
|
||||
void BindToPersonaBrain(UPersonaBrainComponent* Brain);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Director")
|
||||
void AnalyzeCurrentChat();
|
||||
|
||||
virtual FString GetChatIdentityId() const override;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
virtual void BroadcastBrainError(const FString& ErrorMessage) const override;
|
||||
virtual FString GetBrainDebugName() const override;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandlePersonaBrainResponse(const FLLMReplyResponse& Response);
|
||||
|
||||
void AppendSystemMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
|
||||
FString BuildRequestBody(const ULLMProviderDataAsset* EffectiveProvider) const;
|
||||
FString GetSemanticsFilePath() const;
|
||||
void AppendSavedSemanticMemoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
|
||||
void AppendSavedPlayerFactsMessage(TArray<TSharedPtr<FJsonValue>>& Messages) const;
|
||||
void AppendSavedChatHistoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
|
||||
bool ValidateSettings() const;
|
||||
bool HasValidAllowedTool() const;
|
||||
bool ConfigureRequest(const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request) const;
|
||||
void HandleRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "LLM/BrainPromptDataAsset.h"
|
||||
#include "DirectorPromptDataAsset.generated.h"
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API UDirectorPromptDataAsset : public UBrainPromptDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Context", meta = (MultiLine = "true"))
|
||||
FText SavedSemanticsContextPrompt;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Context", meta = (MultiLine = "true"))
|
||||
FText TranscriptContextPrompt;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Director")
|
||||
FString BuildDirectorPrompt() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Director")
|
||||
FString BuildSavedSemanticsContext(const TArray<FString>& SemanticLines) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Director")
|
||||
FString BuildTranscriptContext(const TArray<FString>& TranscriptLines) const;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "LLM/BrainPromptDataAsset.h"
|
||||
#include "IdentityDataAsset.generated.h"
|
||||
|
||||
class UPersonalityTraitDataAsset;
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API UIdentityDataAsset : public UBrainPromptDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (DisplayPriority = "30"))
|
||||
FText Age;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "40"))
|
||||
FText Appearance;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "50"))
|
||||
FText Background;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "60"))
|
||||
FText CoreInstruction;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "70"))
|
||||
FText SpeechStyle;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "80"))
|
||||
FText DialogueBehavior;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "90"))
|
||||
FText PersonalityTraitsInstruction;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (DisplayPriority = "100"))
|
||||
TArray<TObjectPtr<UPersonalityTraitDataAsset>> PersonalityTraits;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "110"))
|
||||
FText FirstMeetingRule;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Identity")
|
||||
FString BuildIdentityPrompt() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Identity")
|
||||
FString BuildFirstMeetingPrompt() const;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "LLMProviderDataAsset.generated.h"
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API ULLMProviderDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULLMProviderDataAsset();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
|
||||
FString ProviderName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
|
||||
FString ApiBaseUrl;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
|
||||
FString ChatCompletionsPath;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
|
||||
FString ApiKey;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
|
||||
FString Model;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Generation", meta = (ClampMin = "0.0", ClampMax = "2.0"))
|
||||
float Temperature;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Generation", meta = (ClampMin = "1"))
|
||||
int32 MaxTokens;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Generation", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float TopP;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Network", meta = (ClampMin = "1.0"))
|
||||
float TimeoutSeconds;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
|
||||
TMap<FString, FString> ExtraHeaders;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "LLM|Provider")
|
||||
FString GetChatCompletionsUrl() const;
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "LLM/BrainComponentBase.h"
|
||||
#include "Interfaces/IHttpRequest.h"
|
||||
#include "PersonaBrainComponent.generated.h"
|
||||
|
||||
class USystemEventDataAsset;
|
||||
class UIdentityDataAsset;
|
||||
class UMorphControllerComponent;
|
||||
class FJsonObject;
|
||||
class FJsonValue;
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FLLMReplyResponse
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM")
|
||||
FString Reply;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Interaction")
|
||||
FString InteractActionJson;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Emotion")
|
||||
FString Emotion;
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMResponseDelegate, const FLLMReplyResponse&, Response);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMReplyStreamChunkDelegate, const FString&, ReplyChunk);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMErrorDelegate, const FString&, ErrorMessage);
|
||||
|
||||
UCLASS(ClassGroup = (AI), meta = (BlueprintSpawnableComponent, DisplayName = "Persona Brain"))
|
||||
class FUNNYGIRL_API UPersonaBrainComponent : public UBrainComponentBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPersonaBrainComponent();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM")
|
||||
FString SystemPrompt;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Interaction", meta = (MultiLine = "true"))
|
||||
FText InteractionRulesPrompt;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|World Context", meta = (MultiLine = "true"))
|
||||
FText WorldUsageRulesPrompt;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Memory", meta = (MultiLine = "true"))
|
||||
FText PlayerFactsPromptTemplate;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Identity")
|
||||
TObjectPtr<UIdentityDataAsset> Identity;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|First Meeting", meta = (ClampMin = "0"))
|
||||
int32 FirstMeetingPromptAssistantMessages = 20;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM")
|
||||
bool bUseStreaming;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
|
||||
TObjectPtr<UMorphControllerComponent> MorphController;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
|
||||
bool bAutoFindMorphController = true;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "LLM|Events")
|
||||
FLLMResponseDelegate OnLLMResponse;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "LLM|Events")
|
||||
FLLMReplyStreamChunkDelegate OnLLMReplyStreamChunk;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "LLM|Events")
|
||||
FLLMErrorDelegate OnLLMError;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "LLM")
|
||||
void SendMessage(const FString& Prompt);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "LLM|Events")
|
||||
void SendSystemEvent(USystemEventDataAsset* SystemEvent);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "LLM|History")
|
||||
bool GetLastSavedLLMMessage(FString& OutMessage) const;
|
||||
|
||||
virtual void CancelRequest() override;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual FString GetChatIdentityId() const override;
|
||||
virtual void BroadcastBrainError(const FString& ErrorMessage) const override;
|
||||
virtual FString GetBrainDebugName() const override;
|
||||
|
||||
private:
|
||||
int32 StreamBytesProcessed;
|
||||
FString PendingStreamBuffer;
|
||||
FString StreamResponseContent;
|
||||
FString LastStreamReply;
|
||||
FString CurrentSystemEventPrompt;
|
||||
|
||||
void AppendSystemMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
|
||||
FString BuildWorldUsageRulesPrompt() const;
|
||||
FString BuildInteractionRulesPrompt() const;
|
||||
FString BuildResponseContractPrompt() const;
|
||||
void AppendSavedPlayerFactsMessage(TArray<TSharedPtr<FJsonValue>>& Messages) const;
|
||||
int32 CountSavedChatMessagesByRole(const FString& RoleToCount) const;
|
||||
void AppendSavedChatHistoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
|
||||
FString BuildRequestBody(const FString& MessageContent, const FString& MessageRole, bool bStream) const;
|
||||
FString BuildResponseExampleJson() const;
|
||||
void AppendChatMessageToFile(const FString& Role, const FString& Content, const FString& InteractActionJson = FString(), const FString& Emotion = FString()) const;
|
||||
FString BuildSavedAssistantMessageContent(const TSharedPtr<FJsonObject>& MessageObject) const;
|
||||
void BroadcastLLMResponse(const FLLMReplyResponse& Response);
|
||||
bool TryParseReplyResponse(const FString& Json, FLLMReplyResponse& OutResponse) const;
|
||||
bool TryExtractPartialReply(const FString& Json, FString& OutReply) const;
|
||||
void BroadcastReplyStreamContent(const FString& ContentDelta);
|
||||
void BroadcastFinalStreamResponse();
|
||||
void SendMessageInternal(const FString& MessageContent, const FString& MessageRole, bool bStream, bool bSaveMessageToHistory, bool bSaveResponseToHistory);
|
||||
bool ValidateSettings() const;
|
||||
bool ConfigureRequest(const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request, const FString& MessageContent, const FString& MessageRole, bool bStream) const;
|
||||
void HandleRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
|
||||
void BroadcastProtocolError(const FString& ErrorMessage, const FString& ResponseBody = FString());
|
||||
void HandleStreamProgress(FHttpRequestPtr Request, uint64 BytesSent, uint64 BytesReceived);
|
||||
void ParseStreamBuffer(const FString& Buffer, bool bFlushPending);
|
||||
void BroadcastStreamLine(const FString& Line);
|
||||
|
||||
bool bSaveCurrentResponseToHistory;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "SystemEventDataAsset.generated.h"
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API USystemEventDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "System Event")
|
||||
FString ID;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "System Event")
|
||||
FText Name;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "System Event", meta = (MultiLine = "true"))
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true"))
|
||||
FText Prompt;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "System Event")
|
||||
FString BuildSystemEventPrompt() const;
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "LipSyncComponent.generated.h"
|
||||
|
||||
class ULipSyncMappingDataAsset;
|
||||
class UMorphControllerComponent;
|
||||
class UAudioComponent;
|
||||
class USoundBase;
|
||||
class USoundWave;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ELipSyncAudioDrivenMode : uint8
|
||||
{
|
||||
EnvelopeMouthOpen UMETA(DisplayName = "Envelope Mouth Open"),
|
||||
TranscriptVisemes UMETA(DisplayName = "Transcript Visemes"),
|
||||
VisemeSequence UMETA(DisplayName = "Viseme Sequence")
|
||||
};
|
||||
|
||||
/**
|
||||
* Runtime lip sync layer. Feed it viseme weights from TTS/audio analysis; it maps them to morph targets
|
||||
* and sends the result to MorphControllerComponent without interrupting emotion preset playback.
|
||||
*/
|
||||
UCLASS(ClassGroup = (FunnyGirl), meta = (BlueprintSpawnableComponent))
|
||||
class FUNNYGIRL_API ULipSyncComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULipSyncComponent();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
|
||||
TObjectPtr<ULipSyncMappingDataAsset> Mapping;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
|
||||
TObjectPtr<UMorphControllerComponent> MorphController;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
|
||||
bool bAutoResolveMorphController = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0.1", DeprecatedProperty, DeprecationMessage = "Use VisemeAttackSpeed and VisemeReleaseSpeed instead."))
|
||||
float InterpolationSpeed = 18.0f;
|
||||
|
||||
/** How quickly viseme weights rise toward the target. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0.1"))
|
||||
float VisemeAttackSpeed = 24.0f;
|
||||
|
||||
/** How quickly viseme weights fall toward silence. Lower values make speech softer and less choppy. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0.1"))
|
||||
float VisemeReleaseSpeed = 10.0f;
|
||||
|
||||
/** Keeps the previous mouth shape briefly during tiny envelope dips. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0"))
|
||||
float SilenceHoldTime = 0.045f;
|
||||
|
||||
/** Crossfades to the next audio-driven viseme instead of hard switching. 0 disables crossfade. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0", ClampMax = "0.45"))
|
||||
float AudioDrivenVisemeCrossfade = 0.12f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync", meta = (ClampMin = "0"))
|
||||
float SilenceThreshold = 0.001f;
|
||||
|
||||
// --- Audio-driven lip sync ---
|
||||
|
||||
/** Default audio asset used by Play Audio Driven Lip Sync when no sound is passed explicitly. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
|
||||
TObjectPtr<USoundBase> AudioDrivenSound;
|
||||
|
||||
/** Audio-only mode cannot know phonemes. Envelope mode opens/closes the mouth naturally; sequence mode cycles fake visemes for debugging. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
|
||||
ELipSyncAudioDrivenMode AudioDrivenMode = ELipSyncAudioDrivenMode::EnvelopeMouthOpen;
|
||||
|
||||
/** Main viseme used by Envelope Mouth Open mode. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
|
||||
FName AudioDrivenEnvelopeViseme = TEXT("A");
|
||||
|
||||
/** Optional transcript for Transcript Visemes mode. The component builds an approximate viseme sequence from this text. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven", meta = (MultiLine = "true"))
|
||||
FText AudioDrivenTranscript;
|
||||
|
||||
/** Viseme sequence used while audio-driven lip sync is playing. The audio envelope controls intensity. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
|
||||
TArray<FName> AudioDrivenVisemeSequence;
|
||||
|
||||
/** How many viseme steps per second to advance while test audio is playing. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven", meta = (ClampMin = "0.1"))
|
||||
float AudioDrivenVisemeRate = 9.0f;
|
||||
|
||||
/** Multiplies the audio envelope before it becomes mouth intensity. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven", meta = (ClampMin = "0"))
|
||||
float AudioDrivenEnvelopeGain = 5.0f;
|
||||
|
||||
/** Smooths low audio values into a more visible mouth movement. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven", meta = (ClampMin = "0.1"))
|
||||
float AudioDrivenEnvelopeGamma = 0.65f;
|
||||
|
||||
/** Used when neither cooked envelope data nor realtime envelope callbacks are available. Disable for production if you prefer silence over synthetic movement. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
|
||||
bool bUseFallbackPulseWithoutEnvelope = false;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync")
|
||||
void SetVisemeWeight(FName VisemeName, float Weight);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync")
|
||||
void SetVisemeWeights(const TMap<FName, float>& VisemeWeights);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync")
|
||||
void ClearVisemes();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Play Audio Driven Lip Sync"))
|
||||
bool PlayAudioDrivenLipSync(USoundBase* Sound = nullptr, float StartTime = 0.0f);
|
||||
|
||||
bool PlayAudioDrivenLipSyncWithComponent(UAudioComponent* AudioComponent, USoundBase* Sound, float StartTime = 0.0f);
|
||||
|
||||
/** Follow an audio component that is controlled elsewhere, such as a streaming TTS component. Does not stop, replace, or start audio playback. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Follow External Audio Component"))
|
||||
bool FollowExternalAudioComponent(UAudioComponent* AudioComponent, const FText& Transcript);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Stop Audio Driven Lip Sync"))
|
||||
void StopAudioDrivenLipSync();
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Lip Sync|Audio Driven")
|
||||
bool IsAudioDrivenLipSyncPlaying() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Play Lip Sync Test Audio", DeprecatedFunction, DeprecationMessage = "Use Play Audio Driven Lip Sync instead."))
|
||||
bool PlayLipSyncTestAudio(USoundBase* Sound = nullptr, float StartTime = 0.0f);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Stop Lip Sync Test Audio", DeprecatedFunction, DeprecationMessage = "Use Stop Audio Driven Lip Sync instead."))
|
||||
void StopLipSyncTestAudio();
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DeprecatedFunction, DeprecationMessage = "Use Is Audio Driven Lip Sync Playing instead."))
|
||||
bool IsLipSyncTestAudioPlaying() const;
|
||||
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TMap<FName, float> TargetVisemeWeights;
|
||||
|
||||
UPROPERTY()
|
||||
TMap<FName, float> CurrentVisemeWeights;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAudioComponent> AudioDrivenComponent;
|
||||
|
||||
bool bAudioDrivenLipSyncActive = false;
|
||||
float AudioDrivenPlaybackTime = 0.0f;
|
||||
float LastEnvelopeValue = 0.0f;
|
||||
float LastNonSilentMouthWeight = 0.0f;
|
||||
float SilenceHoldRemaining = 0.0f;
|
||||
float AudioDrivenDuration = 0.0f;
|
||||
bool bReceivedEnvelopeValue = false;
|
||||
bool bAudioDrivenControlsPlayback = true;
|
||||
bool bWaitingForExternalAudioPlayback = false;
|
||||
float ExternalAudioPlaybackWaitTime = 0.0f;
|
||||
|
||||
TArray<FName> GeneratedTranscriptVisemes;
|
||||
TArray<float> GeneratedTranscriptVisemeUnits;
|
||||
|
||||
UMorphControllerComponent* ResolveMorphController() const;
|
||||
UAudioComponent* ResolveAudioDrivenComponent();
|
||||
void UpdateAudioDrivenLipSync(float DeltaTime);
|
||||
float GetAudioDrivenMouthWeight(float DeltaTime);
|
||||
void RebuildGeneratedTranscriptVisemes();
|
||||
const TArray<FName>& ResolveActiveAudioDrivenVisemeSequence() const;
|
||||
float GetGeneratedTranscriptTotalUnits() const;
|
||||
void SetAudioDrivenTargetVisemes(float MouthWeight);
|
||||
void PushCurrentMorphValues();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleAudioDrivenFinished();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleAudioDrivenPlaybackPercent(const USoundWave* PlayingSoundWave, const float PlaybackPercent);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleAudioDrivenEnvelopeValue(const USoundWave* PlayingSoundWave, const float EnvelopeValue);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "LipSyncMappingDataAsset.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FLipSyncMorphTargetValue
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
|
||||
FName MorphTargetName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync", meta = (ClampMin = "0", ClampMax = "2"))
|
||||
float Weight = 1.0f;
|
||||
|
||||
bool IsValid() const { return !MorphTargetName.IsNone() && Weight > 0.0f; }
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FLipSyncVisemeMapping
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
|
||||
FName VisemeName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
|
||||
TArray<FLipSyncMorphTargetValue> MorphTargets;
|
||||
|
||||
bool IsValid() const { return !VisemeName.IsNone() && MorphTargets.Num() > 0; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps phoneme/viseme names coming from TTS or audio analysis to actual morph targets on the character mesh.
|
||||
* One viseme can drive several morphs, so speech can combine with smile, frown, and other emotion presets.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API ULipSyncMappingDataAsset : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULipSyncMappingDataAsset();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lip Sync")
|
||||
TArray<FLipSyncVisemeMapping> VisemeMappings;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Lip Sync")
|
||||
bool GetMorphValuesForViseme(FName VisemeName, float VisemeWeight, TMap<FName, float>& OutMorphValues) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync")
|
||||
void AccumulateMorphValues(const TMap<FName, float>& VisemeWeights, TMap<FName, float>& InOutMorphValues) const;
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "MorphControllerComponent.generated.h"
|
||||
|
||||
class USkeletalMeshComponent;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EMorphLayerBlendMode : uint8
|
||||
{
|
||||
Max UMETA(DisplayName = "Max"),
|
||||
Additive UMETA(DisplayName = "Additive"),
|
||||
Override UMETA(DisplayName = "Override")
|
||||
};
|
||||
|
||||
/** Active preset playback state. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FActiveMorphPresetSlot
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
TObjectPtr<UMorphPresetDataAsset> Preset = nullptr;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", ClampMax = "1"))
|
||||
float TargetWeight = 1.0f;
|
||||
|
||||
/** Current blended-in preset weight. */
|
||||
float CurrentWeight = 0.0f;
|
||||
|
||||
/** Current time inside this playback instance. */
|
||||
float PlaybackTime = 0.0f;
|
||||
|
||||
/** Remaining delay before the next loop iteration. */
|
||||
float LoopDelayRemaining = 0.0f;
|
||||
|
||||
/** Resolved timeline length for this playback instance. */
|
||||
float TimelineDuration = 0.0f;
|
||||
|
||||
bool IsValid() const { return Preset != nullptr && Preset->IsValidPreset(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Компонент управления морф таргетами персонажа.
|
||||
* Поддерживает несколько пресетов одновременно, плавные переходы, блендинг.
|
||||
*/
|
||||
UCLASS(ClassGroup = (FunnyGirl), meta = (BlueprintSpawnableComponent))
|
||||
class FUNNYGIRL_API UMorphControllerComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UMorphControllerComponent();
|
||||
|
||||
// --- Конфигурация ---
|
||||
|
||||
/** Компонент скелетного меша. Если не задан — берётся GetMesh() у Character. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
|
||||
TObjectPtr<USkeletalMeshComponent> TargetMeshComponent;
|
||||
|
||||
/** Speed used when blending preset weight toward the requested weight. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller", meta = (ClampMin = "0.1"))
|
||||
float BlendSpeed = 4.0f;
|
||||
|
||||
/** Обновлять морфы каждый кадр (выключить для экономии, если управляете вручную) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
|
||||
bool bAutoUpdate = true;
|
||||
|
||||
/** Print warnings when a preset cannot play because of missing mesh, empty curves, or invalid data. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
|
||||
bool bLogWarnings = true;
|
||||
|
||||
/** Emotion presets available for AI/director prompting. Use these as the character's facial expression vocabulary. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions")
|
||||
TArray<TObjectPtr<UMorphPresetDataAsset>> EmotionPresets;
|
||||
|
||||
/** Prompt text placed before the generated emotion preset list. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions", meta = (MultiLine = "true"))
|
||||
FText EmotionPresetsPromptHeader;
|
||||
|
||||
/** Build a system prompt that explains available emotion morph presets to an LLM. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller|Emotions")
|
||||
FString BuildEmotionPresetsPrompt() const;
|
||||
|
||||
/** Find an emotion preset by its Display Name. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller|Emotions", meta = (DisplayName = "Find Emotion Preset By Name"))
|
||||
UMorphPresetDataAsset* FindEmotionPresetByName(const FString& PresetName) const;
|
||||
|
||||
/** Play an emotion preset from Emotion Presets by its Display Name. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller|Emotions", meta = (DisplayName = "Play Emotion Preset By Name"))
|
||||
bool PlayEmotionPresetByName(const FString& PresetName, float Weight = 1.0f);
|
||||
|
||||
// --- API: Пресеты ---
|
||||
|
||||
/** Play or restart a preset. Looping presets stay active forever until removed. Non-looping presets are removed when playback reaches the end. Returns false if the preset cannot be played. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Add Preset"))
|
||||
bool AddPreset(UMorphPresetDataAsset* Preset, float Weight = 1.0f);
|
||||
|
||||
/** Play or restart a preset. Alias for Add Preset with a clearer Blueprint name. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Play Morph Preset"))
|
||||
bool PlayPreset(UMorphPresetDataAsset* Preset, float Weight = 1.0f);
|
||||
|
||||
/** Remove a preset from active playback immediately. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Remove Preset"))
|
||||
void RemovePreset(UMorphPresetDataAsset* Preset);
|
||||
|
||||
/** Установить вес пресета. Пресет должен быть уже добавлен. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Set Preset Weight"))
|
||||
void SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight);
|
||||
|
||||
/** Применить несколько пресетов сразу (заменяет текущие активные) */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Set Active Presets"))
|
||||
void SetActivePresets(const TArray<FActiveMorphPresetSlot>& Presets);
|
||||
|
||||
/** Clear all active preset playback immediately. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Clear All Presets"))
|
||||
void ClearAllPresets();
|
||||
|
||||
/** Проверить, активен ли пресет */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller")
|
||||
bool IsPresetActive(UMorphPresetDataAsset* Preset) const;
|
||||
|
||||
/** Получить текущие активные пресеты */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller")
|
||||
TArray<FActiveMorphPresetSlot> GetActivePresets() const;
|
||||
|
||||
// --- API: Прямое управление морфами ---
|
||||
|
||||
/** Установить морф таргет напрямую (добавляется к пресетам) */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Set Morph Target"))
|
||||
void SetMorphTarget(FName MorphTargetName, float Value);
|
||||
|
||||
/** Remove one direct morph override. Preset playback can still drive this morph afterwards. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Remove Morph Target Override"))
|
||||
void RemoveMorphTargetOverride(FName MorphTargetName);
|
||||
|
||||
/** Clear only direct morph overrides, without stopping active presets. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Clear Direct Morph Overrides"))
|
||||
void ClearDirectMorphOverrides();
|
||||
|
||||
// --- API: Lip sync layer ---
|
||||
|
||||
/** Enable applying transient lip sync morph values on top of active emotion presets. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Lip Sync")
|
||||
bool bEnableLipSyncLayer = true;
|
||||
|
||||
/** How lip sync morphs are mixed with emotion presets and direct overrides. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Lip Sync")
|
||||
EMorphLayerBlendMode LipSyncBlendMode = EMorphLayerBlendMode::Max;
|
||||
|
||||
/** Overall multiplier for lip sync values before they are mixed into the final morph set. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Lip Sync", meta = (ClampMin = "0", ClampMax = "2"))
|
||||
float LipSyncLayerWeight = 1.0f;
|
||||
|
||||
/** Set transient lip sync morph values. These values are blended during the next morph update. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller|Lip Sync", meta = (DisplayName = "Set Lip Sync Morph Values"))
|
||||
void SetLipSyncMorphValues(const TMap<FName, float>& MorphValues);
|
||||
|
||||
/** Clear transient lip sync morph values without stopping emotion presets. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller|Lip Sync", meta = (DisplayName = "Clear Lip Sync Morph Values"))
|
||||
void ClearLipSyncMorphValues();
|
||||
|
||||
/** Update morph targets immediately. Useful when Auto Update is disabled. */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Update Morph Targets Now"))
|
||||
void UpdateMorphTargetsNow(float DeltaTime = 0.0f);
|
||||
|
||||
/** Очистить все морфы мгновенно */
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Clear Morphs Immediately"))
|
||||
void ClearMorphsImmediately();
|
||||
|
||||
// --- Внутреннее ---
|
||||
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TArray<FActiveMorphPresetSlot> ActivePresets;
|
||||
|
||||
/** Прямые значения морфов (дополнительно к пресетам) */
|
||||
UPROPERTY()
|
||||
TMap<FName, float> DirectMorphOverrides;
|
||||
|
||||
UPROPERTY()
|
||||
TMap<FName, float> LipSyncMorphValues;
|
||||
|
||||
UPROPERTY()
|
||||
TSet<FName> LastAppliedMorphNames;
|
||||
|
||||
bool bMissingMeshWarningIssued = false;
|
||||
|
||||
USkeletalMeshComponent* ResolveMeshComponent() const;
|
||||
bool PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const;
|
||||
bool PlayPresetAnimation(UMorphPresetDataAsset* Preset);
|
||||
void UpdateMorphTargets(float DeltaTime);
|
||||
void BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const;
|
||||
void ApplyFinalMorphValues(const TMap<FName, float>& FinalValues);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Curves/CurveFloat.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "MorphPresetDataAsset.generated.h"
|
||||
|
||||
class UAnimMontage;
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FMorphTimelineClip
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (DisplayName = "Morph Name"))
|
||||
FName Name;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (ClampMin = "0", DisplayName = "Start Time"))
|
||||
float StartTime = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (ClampMin = "0.001", DisplayName = "Duration"))
|
||||
float Duration = 0.2f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (ClampMin = "0", ClampMax = "5"))
|
||||
float Weight = 1.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (DisplayName = "Weight Curve"))
|
||||
FRuntimeFloatCurve WeightCurve;
|
||||
|
||||
bool IsValid() const { return !Name.IsNone() && Duration > 0.0f; }
|
||||
void EnsureDefaultCurve();
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API UMorphPresetDataAsset : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Preview", meta = (DisplayName = "Preview Mesh"))
|
||||
TSoftObjectPtr<USkeletalMesh> PreviewMesh;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (DisplayName = "Display Name"))
|
||||
FText DisplayName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset")
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
|
||||
DisplayName = "Timeline Clips",
|
||||
ToolTip = "Morph clips placed on the preset timeline."))
|
||||
TArray<FMorphTimelineClip> MorphTargets;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
|
||||
DisplayName = "Duration",
|
||||
ToolTip = "Preset duration in seconds. 0 means auto-calculate from clips.",
|
||||
ClampMin = "0"))
|
||||
float Duration = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
|
||||
DisplayName = "Loop",
|
||||
ToolTip = "Loop the preset playback."))
|
||||
bool bLoop = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
|
||||
DisplayName = "Loop Delay",
|
||||
ToolTip = "Delay in seconds between loop iterations. Only used when Loop is enabled.",
|
||||
ClampMin = "0"))
|
||||
float LoopDelay = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
|
||||
DisplayName = "Blend Speed Override",
|
||||
ToolTip = "0 uses the component default.",
|
||||
ClampMin = "0"))
|
||||
float BlendSpeedOverride = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
|
||||
DisplayName = "Animation Montage",
|
||||
ToolTip = "Optional animation montage played when this preset starts. The montage must use a slot that is present in the character Anim Blueprint."))
|
||||
TObjectPtr<UAnimMontage> AnimationMontage = nullptr;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
|
||||
DisplayName = "Animation Play Rate",
|
||||
ClampMin = "0.01"))
|
||||
float AnimationPlayRate = 1.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
|
||||
DisplayName = "Start Section",
|
||||
ToolTip = "Optional montage section to jump to after playback starts."))
|
||||
FName AnimationStartSection;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
|
||||
DisplayName = "Restart Animation On Replay",
|
||||
ToolTip = "If enabled, an already playing montage is stopped and restarted when this preset is triggered again."))
|
||||
bool bRestartAnimationOnReplay = true;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
int32 GetMorphCount() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
float GetTimelineLength() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
bool HasAnimation() const { return AnimationMontage != nullptr; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
bool HasPlayableContent() const { return IsValidPreset() || HasAnimation(); }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
|
||||
bool IsValidPreset() const { return GetMorphCount() > 0; }
|
||||
};
|
||||
|
||||
namespace MorphPresetRuntime
|
||||
{
|
||||
FUNNYGIRL_API float EvaluateClipWeight(const FMorphTimelineClip& Clip, float Time);
|
||||
FUNNYGIRL_API void AccumulatePresetMorphValuesAtTime(const UMorphPresetDataAsset& Preset, float Time, float PresetWeight, TMap<FName, float>& InOutValues);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "MorphTargetBlueprintLibrary.generated.h"
|
||||
|
||||
class USkeletalMeshComponent;
|
||||
class USkeleton;
|
||||
class UMorphPresetDataAsset;
|
||||
class APlayerController;
|
||||
|
||||
/**
|
||||
* Blueprint function library for morph target helpers and camera motion utilities.
|
||||
*/
|
||||
UCLASS()
|
||||
class FUNNYGIRL_API UMorphTargetBlueprintLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Returns all morph target names from the skeletal mesh component.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Targets", meta = (
|
||||
DisplayName = "Get Morph Target Names",
|
||||
ToolTip = "Get all morph target names from the character mesh."))
|
||||
static TArray<FString> GetMorphTargetNames(USkeletalMeshComponent* SkeletalMeshComponent);
|
||||
|
||||
/**
|
||||
* Returns unique morph target names from all skeletal meshes that use this skeleton.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Targets", meta = (
|
||||
DisplayName = "Get Morph Target Names From Skeleton",
|
||||
ToolTip = "Collects unique morph target names from all skeletal meshes linked to the skeleton."))
|
||||
static TArray<FString> GetMorphTargetNamesFromSkeleton(USkeleton* Skeleton);
|
||||
|
||||
/**
|
||||
* Applies a morph preset directly to the mesh at a specific timeline time.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Targets", meta = (
|
||||
DisplayName = "Apply Preset To Mesh At Time",
|
||||
ToolTip = "Apply a morph preset to a skeletal mesh using the preset timeline and curves."))
|
||||
static void ApplyPresetToMeshAtTime(USkeletalMeshComponent* SkeletalMeshComponent, UMorphPresetDataAsset* Preset, float Time, float Weight = 1.0f);
|
||||
|
||||
/**
|
||||
* Converts device tilt to a small 2D camera offset.
|
||||
* X: left/right, Y: up/down.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Camera|Gyroscope", meta = (
|
||||
DisplayName = "Get Gyro Camera Offset 2D",
|
||||
ToolTip = "Converts device tilt to a small camera offset for left/right and up/down movement."))
|
||||
static FVector2D GetGyroCameraOffset2D(
|
||||
APlayerController* PlayerController,
|
||||
float HorizontalAmplitude = 4.0f,
|
||||
float VerticalAmplitude = 3.0f,
|
||||
float DeadZone = 0.05f,
|
||||
float MaxTiltInput = 0.35f,
|
||||
bool bInvertHorizontal = false,
|
||||
bool bInvertVertical = false);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "NarrativeBlueprintLibrary.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class FUNNYGIRL_API UNarrativeBlueprintLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Narrative|Semantic Memory")
|
||||
static bool AppendSemanticMemoryFromArgumentsJson(
|
||||
const FString& IdentityId,
|
||||
const FString& ArgumentsJson,
|
||||
FString& OutError);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Narrative|Player Data")
|
||||
static bool SavePlayerDataFromArgumentsJson(
|
||||
const FString& IdentityId,
|
||||
const FString& ArgumentsJson,
|
||||
FString& OutError);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Narrative|Player Data")
|
||||
static bool SavePlayerFact(
|
||||
const FString& IdentityId,
|
||||
const FString& Key,
|
||||
const FString& Value,
|
||||
const FString& Category,
|
||||
const FString& SourceText,
|
||||
float Confidence,
|
||||
const TArray<FString>& Tags,
|
||||
FString& OutError);
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/Widget.h"
|
||||
#include "AudioPlaybackThrobber.generated.h"
|
||||
|
||||
class SAudioPlaybackThrobber;
|
||||
class UAudioComponent;
|
||||
class USoundWave;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class FUNNYGIRL_API UAudioPlaybackThrobber : public UWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UAudioPlaybackThrobber();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "1", ClampMax = "16"))
|
||||
int32 BarCount = 7;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.5"))
|
||||
float BarWidth = 3.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0"))
|
||||
float BarGap = 2.5f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float MinHeightFraction = 0.18f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0"))
|
||||
float AnimationSpeed = 4.8f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float GlowStrength = 0.35f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float CenterBoost = 0.35f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback")
|
||||
FLinearColor BarColor = FLinearColor(0.18f, 0.82f, 1.0f, 1.0f);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback")
|
||||
FLinearColor IdleBarColor = FLinearColor(0.18f, 0.82f, 1.0f, 0.18f);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback")
|
||||
bool bIsPlaying = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio")
|
||||
TObjectPtr<UAudioComponent> AudioComponent;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio")
|
||||
bool bUseAudioEnvelope = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0"))
|
||||
float AudioEnvelopeGain = 5.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.1"))
|
||||
float AudioEnvelopeGamma = 0.65f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0"))
|
||||
float AudioEnvelopeSmoothingSpeed = 16.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float ManualAudioLevel = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float AudioSilenceThreshold = 0.03f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float AudioSilenceOpacity = 0.08f;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Audio Playback")
|
||||
void SetIsPlaying(bool bInIsPlaying);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Audio Playback")
|
||||
bool IsPlaying() const { return bIsPlaying; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Audio Playback|Audio")
|
||||
void SetAudioComponent(UAudioComponent* InAudioComponent);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Audio Playback|Audio")
|
||||
void SetUseAudioEnvelope(bool bInUseAudioEnvelope);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Audio Playback|Audio")
|
||||
void SetManualAudioLevel(float InAudioLevel);
|
||||
|
||||
protected:
|
||||
virtual void BeginDestroy() override;
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
virtual void SynchronizeProperties() override;
|
||||
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleAudioEnvelopeValue(const USoundWave* PlayingSoundWave, const float EnvelopeValue);
|
||||
|
||||
void UnbindAudioComponent();
|
||||
void BindAudioComponent();
|
||||
|
||||
TSharedPtr<SAudioPlaybackThrobber> MyThrobber;
|
||||
bool bAudioEnvelopeDelegateBound = false;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "FunnyManagedUserWidget.generated.h"
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class FUNNYGIRL_API UFunnyManagedUserWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Layer")
|
||||
FName UILayerName = TEXT("HUD");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Layer")
|
||||
bool bAutoRegisterWithUILayer = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Layer")
|
||||
bool bAutoShowWithLayer = false;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void SetManagedVisibility(ESlateVisibility InVisibility);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
|
||||
ESlateVisibility GetManagedVisibility() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void SetManagedEnabled(bool bEnabled);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
|
||||
bool GetManagedEnabled() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void RegisterWithUILayer();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void UnregisterFromUILayer();
|
||||
|
||||
protected:
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "FunnyUIBlueprintLibrary.generated.h"
|
||||
|
||||
class UFunnyUILayerManagerSubsystem;
|
||||
class UUserWidget;
|
||||
|
||||
UCLASS()
|
||||
class FUNNYGIRL_API UFunnyUIBlueprintLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static UFunnyUILayerManagerSubsystem* GetUILayerManager(const UObject* WorldContextObject, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void RegisterWidgetToLayer(const UObject* WorldContextObject, FName LayerName, UUserWidget* Widget, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void UnregisterWidgetFromLayer(const UObject* WorldContextObject, UUserWidget* Widget, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void SetManagedWidgetVisibility(const UObject* WorldContextObject, UUserWidget* Widget, ESlateVisibility Visibility, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void SetManagedWidgetEnabled(const UObject* WorldContextObject, UUserWidget* Widget, bool bEnabled, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void SetUILayerVisibility(const UObject* WorldContextObject, FName LayerName, ESlateVisibility Visibility, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void ShowUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void HideUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void SetUILayerEnabled(const UObject* WorldContextObject, FName LayerName, bool bEnabled, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void ShowOnlyUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static void PushUILayerState(const UObject* WorldContextObject, int32 PlayerIndex = 0);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
|
||||
static bool PopUILayerState(const UObject* WorldContextObject, int32 PlayerIndex = 0);
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Subsystems/LocalPlayerSubsystem.h"
|
||||
#include "FunnyUILayerManagerSubsystem.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FFunnyUILayerState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Layer")
|
||||
ESlateVisibility Visibility = ESlateVisibility::Visible;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Layer")
|
||||
bool bEnabled = true;
|
||||
};
|
||||
|
||||
USTRUCT()
|
||||
struct FUNNYGIRL_API FFunnyUILayerWidgetEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<UUserWidget> Widget;
|
||||
|
||||
UPROPERTY()
|
||||
ESlateVisibility DesiredVisibility = ESlateVisibility::Visible;
|
||||
|
||||
UPROPERTY()
|
||||
bool bDesiredEnabled = true;
|
||||
|
||||
UPROPERTY()
|
||||
bool bAutoShowWithLayer = false;
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class FUNNYGIRL_API UFunnyUILayerManagerSubsystem : public ULocalPlayerSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void RegisterWidget(FName LayerName, UUserWidget* Widget);
|
||||
|
||||
void RegisterWidget(FName LayerName, UUserWidget* Widget, bool bAutoShowWithLayer);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void UnregisterWidget(UUserWidget* Widget);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void SetWidgetDesiredVisibility(UUserWidget* Widget, ESlateVisibility Visibility);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
|
||||
ESlateVisibility GetWidgetDesiredVisibility(UUserWidget* Widget) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void SetWidgetDesiredEnabled(UUserWidget* Widget, bool bEnabled);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
|
||||
bool GetWidgetDesiredEnabled(UUserWidget* Widget) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void SetLayerVisibility(FName LayerName, ESlateVisibility Visibility);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void ShowLayer(FName LayerName);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void HideLayer(FName LayerName);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
|
||||
ESlateVisibility GetLayerVisibility(FName LayerName) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void SetLayerEnabled(FName LayerName, bool bEnabled);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
|
||||
bool IsLayerEnabled(FName LayerName) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void ShowOnlyLayer(FName LayerName);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void PushLayerState();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
bool PopLayerState();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
|
||||
void ApplyAllLayers();
|
||||
|
||||
private:
|
||||
TMap<FName, FFunnyUILayerState> LayerStates;
|
||||
|
||||
TMap<FName, TArray<FFunnyUILayerWidgetEntry>> LayerWidgets;
|
||||
|
||||
TArray<TMap<FName, FFunnyUILayerState>> LayerStateStack;
|
||||
|
||||
FFunnyUILayerState& FindOrAddLayerState(FName LayerName);
|
||||
const FFunnyUILayerState* FindLayerState(FName LayerName) const;
|
||||
FFunnyUILayerWidgetEntry* FindWidgetEntry(UUserWidget* Widget);
|
||||
const FFunnyUILayerWidgetEntry* FindWidgetEntry(UUserWidget* Widget) const;
|
||||
void PruneInvalidWidgets(FName LayerName);
|
||||
void AutoShowLayerWidgets(FName LayerName);
|
||||
void ApplyLayerState(FName LayerName);
|
||||
void ApplyWidgetState(const FFunnyUILayerWidgetEntry& Entry, const FFunnyUILayerState& LayerState) const;
|
||||
static bool IsVisibleLayerState(ESlateVisibility Visibility);
|
||||
static ESlateVisibility CombineVisibility(ESlateVisibility LayerVisibility, ESlateVisibility DesiredVisibility);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "TypingTextBlock.generated.h"
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class FUNNYGIRL_API UTypingTextBlock : public UTextBlock
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UTypingTextBlock();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typing", meta = (ClampMin = "0.001"))
|
||||
float CharacterInterval;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typing")
|
||||
bool bAutoStartTyping;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Typing")
|
||||
void AppendTextChunk(const FString& TextChunk);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Typing")
|
||||
void ClearTypedText();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Typing")
|
||||
void FlushTyping();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Typing")
|
||||
bool IsTyping() const;
|
||||
|
||||
protected:
|
||||
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||
|
||||
private:
|
||||
FTimerHandle TypingTimerHandle;
|
||||
FString DisplayedText;
|
||||
FString PendingText;
|
||||
|
||||
void StartTyping();
|
||||
void StopTyping();
|
||||
void TypeNextCharacter();
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "FunnyGirlUtilityBlueprintLibrary.generated.h"
|
||||
|
||||
class USkeletalMesh;
|
||||
class UButton;
|
||||
class UTexture2D;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EFunnyGirlButtonBrushState : uint8
|
||||
{
|
||||
Normal UMETA(DisplayName = "Normal"),
|
||||
Hovered UMETA(DisplayName = "Hovered"),
|
||||
Pressed UMETA(DisplayName = "Pressed")
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class FUNNYGIRL_API UFunnyGirlUtilityBlueprintLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Targets")
|
||||
static bool SaveMorphTargetNamesToFile(
|
||||
USkeletalMesh* SkeletalMesh,
|
||||
FString& OutFilePath,
|
||||
FString& OutError);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|Json")
|
||||
static bool GetJsonStringField(const FString& Json, const FString& FieldName, FString& OutValue);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|Json")
|
||||
static bool GetJsonNumberField(const FString& Json, const FString& FieldName, float& OutValue);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "FunnyGirl|Json")
|
||||
static bool GetJsonBoolField(const FString& Json, const FString& FieldName, bool& OutValue);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Button")
|
||||
static bool SetButtonStyleTexture(
|
||||
UButton* Button,
|
||||
UTexture2D* Texture,
|
||||
EFunnyGirlButtonBrushState State);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
|
||||
static bool SaveLightString(const FString& Field, const FString& Value, FString& OutError);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
|
||||
static bool LoadLightString(const FString& Field, FString& OutValue, FString& OutError);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
|
||||
static bool DeleteLightString(const FString& Field, FString& OutError);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "InteractableObjectComponent.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FUNNYGIRL_API FInteractableObjectAction
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Action")
|
||||
FName Action;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Action", meta = (MultiLine = "true"))
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Action", meta = (MultiLine = "true"))
|
||||
FText ActionFormat;
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FInteractableObjectActionRequestedDelegate, FName, Action, const FString&, ActionJson);
|
||||
|
||||
UCLASS(ClassGroup = (FunnyGirl), meta = (BlueprintSpawnableComponent, DisplayName = "Interactable Object"))
|
||||
class FUNNYGIRL_API UInteractableObjectComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UInteractableObjectComponent();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object")
|
||||
FString ID;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object")
|
||||
FText Name;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object", meta = (MultiLine = "true"))
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object")
|
||||
TMap<FString, FString> State;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object", meta = (MultiLine = "true"))
|
||||
FText CustomData;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object")
|
||||
TArray<FInteractableObjectAction> Actions;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Interactable Object")
|
||||
FInteractableObjectActionRequestedDelegate OnActionRequested;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Interactable Object")
|
||||
bool HasAction(FName Action) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Interactable Object")
|
||||
bool FindAction(FName Action, FInteractableObjectAction& OutAction) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Interactable Object")
|
||||
bool ExecuteAction(const FString& ActionJson);
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "FunnyGirl|Interactable Object")
|
||||
bool OnExecuteAction(const FString& ActionJson);
|
||||
|
||||
private:
|
||||
bool TryExtractActionFromJson(const FString& ActionJson, FName& OutAction) const;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "WorldContextActor.generated.h"
|
||||
|
||||
class UWorldContextDataAsset;
|
||||
class UInteractableObjectComponent;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class FUNNYGIRL_API AWorldContextActor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AWorldContextActor();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context")
|
||||
TObjectPtr<UWorldContextDataAsset> WorldContext;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "World Context")
|
||||
FString BuildWorldContextPrompt() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "World Context")
|
||||
FString BuildBaseWorldContextPrompt() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "World Context")
|
||||
FString BuildInteractableObjectsPrompt() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "World Context|Interactable Objects")
|
||||
void RefreshInteractableObjects() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "World Context|Interactable Objects")
|
||||
UInteractableObjectComponent* FindInteractableObjectByID(const FString& ObjectID) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "World Context|Interactable Objects")
|
||||
bool ExecuteInteractableObjectAction(const FString& ActionJson, UPARAM(ref) FString& OutError);
|
||||
|
||||
private:
|
||||
void EnsureInteractableObjectsCached() const;
|
||||
|
||||
mutable bool bInteractableObjectsCached = false;
|
||||
mutable TArray<TWeakObjectPtr<UInteractableObjectComponent>> InteractableObjects;
|
||||
mutable TMap<FString, TWeakObjectPtr<UInteractableObjectComponent>> InteractableObjectsByID;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "WorldContextDataAsset.generated.h"
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FUNNYGIRL_API UWorldContextDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context")
|
||||
FText LocationName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context", meta = (MultiLine = "true"))
|
||||
FText SceneDescription;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "World Context")
|
||||
FString BuildWorldContextPrompt() const;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class FunnyGirlEditorTarget : TargetRules
|
||||
{
|
||||
public FunnyGirlEditorTarget( TargetInfo Target) : base(Target)
|
||||
{
|
||||
Type = TargetType.Editor;
|
||||
DefaultBuildSettings = BuildSettingsVersion.V6;
|
||||
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7;
|
||||
ExtraModuleNames.AddRange(new string[] { "FunnyGirl", "FunnyGirlEditor" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class FunnyGirlEditor : ModuleRules
|
||||
{
|
||||
public FunnyGirlEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "FunnyGirl" });
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "AdvancedPreviewScene", "AnimationDataController", "AnimGraph", "AssetTools", "AssetRegistry", "ContentBrowser", "EditorWidgets", "InputCore", "Json", "JsonUtilities", "KismetWidgets", "LevelEditor", "Persona", "PropertyEditor", "SequencerWidgets", "Slate", "SlateCore", "ToolMenus", "UnrealEd" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "AssetTypeActions/AssetTypeActions_DirectorPrompt.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "LLM/DirectorPromptDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "AssetTypeActions_DirectorPrompt"
|
||||
|
||||
FText FAssetTypeActions_DirectorPrompt::GetName() const
|
||||
{
|
||||
return LOCTEXT("Name", "Director Prompt");
|
||||
}
|
||||
|
||||
FColor FAssetTypeActions_DirectorPrompt::GetTypeColor() const
|
||||
{
|
||||
return FColor(120, 90, 210);
|
||||
}
|
||||
|
||||
UClass* FAssetTypeActions_DirectorPrompt::GetSupportedClass() const
|
||||
{
|
||||
return UDirectorPromptDataAsset::StaticClass();
|
||||
}
|
||||
|
||||
uint32 FAssetTypeActions_DirectorPrompt::GetCategories()
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AssetTypeActions_Base.h"
|
||||
|
||||
class FAssetTypeActions_DirectorPrompt : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "AssetTypeActions/AssetTypeActions_Identity.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "LLM/IdentityDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "AssetTypeActions_Identity"
|
||||
|
||||
FText FAssetTypeActions_Identity::GetName() const
|
||||
{
|
||||
return LOCTEXT("Name", "Identity");
|
||||
}
|
||||
|
||||
FColor FAssetTypeActions_Identity::GetTypeColor() const
|
||||
{
|
||||
return FColor(48, 190, 150);
|
||||
}
|
||||
|
||||
UClass* FAssetTypeActions_Identity::GetSupportedClass() const
|
||||
{
|
||||
return UIdentityDataAsset::StaticClass();
|
||||
}
|
||||
|
||||
uint32 FAssetTypeActions_Identity::GetCategories()
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AssetTypeActions_Base.h"
|
||||
|
||||
class FAssetTypeActions_Identity : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "AssetTypeActions/AssetTypeActions_LLMProvider.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "LLM/LLMProviderDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "AssetTypeActions_LLMProvider"
|
||||
|
||||
FText FAssetTypeActions_LLMProvider::GetName() const
|
||||
{
|
||||
return LOCTEXT("Name", "LLM Provider");
|
||||
}
|
||||
|
||||
FColor FAssetTypeActions_LLMProvider::GetTypeColor() const
|
||||
{
|
||||
return FColor(72, 128, 255);
|
||||
}
|
||||
|
||||
UClass* FAssetTypeActions_LLMProvider::GetSupportedClass() const
|
||||
{
|
||||
return ULLMProviderDataAsset::StaticClass();
|
||||
}
|
||||
|
||||
uint32 FAssetTypeActions_LLMProvider::GetCategories()
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetLLMAssetCategory();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AssetTypeActions_Base.h"
|
||||
|
||||
class FAssetTypeActions_LLMProvider : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "AssetTypeActions/AssetTypeActions_PersonalityTrait.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "Identity/PersonalityTraitDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "AssetTypeActions_PersonalityTrait"
|
||||
|
||||
FText FAssetTypeActions_PersonalityTrait::GetName() const
|
||||
{
|
||||
return LOCTEXT("Name", "Personality Trait");
|
||||
}
|
||||
|
||||
FColor FAssetTypeActions_PersonalityTrait::GetTypeColor() const
|
||||
{
|
||||
return FColor(225, 150, 65);
|
||||
}
|
||||
|
||||
UClass* FAssetTypeActions_PersonalityTrait::GetSupportedClass() const
|
||||
{
|
||||
return UPersonalityTraitDataAsset::StaticClass();
|
||||
}
|
||||
|
||||
uint32 FAssetTypeActions_PersonalityTrait::GetCategories()
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AssetTypeActions_Base.h"
|
||||
|
||||
class FAssetTypeActions_PersonalityTrait : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "AssetTypeActions/AssetTypeActions_SystemEvent.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "LLM/SystemEventDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "AssetTypeActions_SystemEvent"
|
||||
|
||||
FText FAssetTypeActions_SystemEvent::GetName() const
|
||||
{
|
||||
return LOCTEXT("Name", "System Event");
|
||||
}
|
||||
|
||||
FColor FAssetTypeActions_SystemEvent::GetTypeColor() const
|
||||
{
|
||||
return FColor(205, 95, 205);
|
||||
}
|
||||
|
||||
UClass* FAssetTypeActions_SystemEvent::GetSupportedClass() const
|
||||
{
|
||||
return USystemEventDataAsset::StaticClass();
|
||||
}
|
||||
|
||||
uint32 FAssetTypeActions_SystemEvent::GetCategories()
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AssetTypeActions_Base.h"
|
||||
|
||||
class FAssetTypeActions_SystemEvent : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "AssetTypeActions/AssetTypeActions_WorldContext.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "World/WorldContextDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "AssetTypeActions_WorldContext"
|
||||
|
||||
FText FAssetTypeActions_WorldContext::GetName() const
|
||||
{
|
||||
return LOCTEXT("Name", "World Context");
|
||||
}
|
||||
|
||||
FColor FAssetTypeActions_WorldContext::GetTypeColor() const
|
||||
{
|
||||
return FColor(80, 170, 220);
|
||||
}
|
||||
|
||||
UClass* FAssetTypeActions_WorldContext::GetSupportedClass() const
|
||||
{
|
||||
return UWorldContextDataAsset::StaticClass();
|
||||
}
|
||||
|
||||
uint32 FAssetTypeActions_WorldContext::GetCategories()
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AssetTypeActions_Base.h"
|
||||
|
||||
class FAssetTypeActions_WorldContext : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Factories/DirectorPromptDataAssetFactory.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "LLM/DirectorPromptDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "DirectorPromptDataAssetFactory"
|
||||
|
||||
UDirectorPromptDataAssetFactory::UDirectorPromptDataAssetFactory()
|
||||
{
|
||||
SupportedClass = UDirectorPromptDataAsset::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* UDirectorPromptDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<UDirectorPromptDataAsset>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FText UDirectorPromptDataAssetFactory::GetDisplayName() const
|
||||
{
|
||||
return LOCTEXT("DisplayName", "Director Prompt");
|
||||
}
|
||||
|
||||
uint32 UDirectorPromptDataAssetFactory::GetMenuCategories() const
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
bool UDirectorPromptDataAssetFactory::ShouldShowInNewMenu() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Factories/Factory.h"
|
||||
#include "DirectorPromptDataAssetFactory.generated.h"
|
||||
|
||||
UCLASS(hidecategories = Object)
|
||||
class UDirectorPromptDataAssetFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDirectorPromptDataAssetFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual FText GetDisplayName() const override;
|
||||
virtual uint32 GetMenuCategories() const override;
|
||||
virtual bool ShouldShowInNewMenu() const override;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Factories/DirectorToolBlueprintFactory.h"
|
||||
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/BlueprintGeneratedClass.h"
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "LLM/BrainToolBase.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "DirectorToolBlueprintFactory"
|
||||
|
||||
UDirectorToolBlueprintFactory::UDirectorToolBlueprintFactory()
|
||||
{
|
||||
SupportedClass = UBlueprint::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* UDirectorToolBlueprintFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return FKismetEditorUtilities::CreateBlueprint(UBrainToolBase::StaticClass(), InParent, InName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass());
|
||||
}
|
||||
|
||||
FText UDirectorToolBlueprintFactory::GetDisplayName() const
|
||||
{
|
||||
return LOCTEXT("DisplayName", "Brain Tool");
|
||||
}
|
||||
|
||||
uint32 UDirectorToolBlueprintFactory::GetMenuCategories() const
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
bool UDirectorToolBlueprintFactory::ShouldShowInNewMenu() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Factories/Factory.h"
|
||||
#include "DirectorToolBlueprintFactory.generated.h"
|
||||
|
||||
UCLASS(hidecategories = Object)
|
||||
class UDirectorToolBlueprintFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDirectorToolBlueprintFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual FText GetDisplayName() const override;
|
||||
virtual uint32 GetMenuCategories() const override;
|
||||
virtual bool ShouldShowInNewMenu() const override;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Factories/IdentityDataAssetFactory.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "LLM/IdentityDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "IdentityDataAssetFactory"
|
||||
|
||||
UIdentityDataAssetFactory::UIdentityDataAssetFactory()
|
||||
{
|
||||
SupportedClass = UIdentityDataAsset::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* UIdentityDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<UIdentityDataAsset>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FText UIdentityDataAssetFactory::GetDisplayName() const
|
||||
{
|
||||
return LOCTEXT("DisplayName", "Identity");
|
||||
}
|
||||
|
||||
uint32 UIdentityDataAssetFactory::GetMenuCategories() const
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
bool UIdentityDataAssetFactory::ShouldShowInNewMenu() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Factories/Factory.h"
|
||||
#include "IdentityDataAssetFactory.generated.h"
|
||||
|
||||
UCLASS(hidecategories = Object)
|
||||
class UIdentityDataAssetFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UIdentityDataAssetFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual FText GetDisplayName() const override;
|
||||
virtual uint32 GetMenuCategories() const override;
|
||||
virtual bool ShouldShowInNewMenu() const override;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Factories/LLMProviderDataAssetFactory.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "LLM/LLMProviderDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "LLMProviderDataAssetFactory"
|
||||
|
||||
ULLMProviderDataAssetFactory::ULLMProviderDataAssetFactory()
|
||||
{
|
||||
SupportedClass = ULLMProviderDataAsset::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* ULLMProviderDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<ULLMProviderDataAsset>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FText ULLMProviderDataAssetFactory::GetDisplayName() const
|
||||
{
|
||||
return LOCTEXT("DisplayName", "LLM Provider");
|
||||
}
|
||||
|
||||
uint32 ULLMProviderDataAssetFactory::GetMenuCategories() const
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetLLMAssetCategory();
|
||||
}
|
||||
|
||||
bool ULLMProviderDataAssetFactory::ShouldShowInNewMenu() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Factories/Factory.h"
|
||||
#include "LLMProviderDataAssetFactory.generated.h"
|
||||
|
||||
UCLASS(hidecategories = Object)
|
||||
class ULLMProviderDataAssetFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULLMProviderDataAssetFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual FText GetDisplayName() const override;
|
||||
virtual uint32 GetMenuCategories() const override;
|
||||
virtual bool ShouldShowInNewMenu() const override;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Factories/PersonalityTraitDataAssetFactory.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "Identity/PersonalityTraitDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "PersonalityTraitDataAssetFactory"
|
||||
|
||||
UPersonalityTraitDataAssetFactory::UPersonalityTraitDataAssetFactory()
|
||||
{
|
||||
SupportedClass = UPersonalityTraitDataAsset::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* UPersonalityTraitDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<UPersonalityTraitDataAsset>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FText UPersonalityTraitDataAssetFactory::GetDisplayName() const
|
||||
{
|
||||
return LOCTEXT("DisplayName", "Personality Trait");
|
||||
}
|
||||
|
||||
uint32 UPersonalityTraitDataAssetFactory::GetMenuCategories() const
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
bool UPersonalityTraitDataAssetFactory::ShouldShowInNewMenu() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Factories/Factory.h"
|
||||
#include "PersonalityTraitDataAssetFactory.generated.h"
|
||||
|
||||
UCLASS(hidecategories = Object)
|
||||
class UPersonalityTraitDataAssetFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPersonalityTraitDataAssetFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual FText GetDisplayName() const override;
|
||||
virtual uint32 GetMenuCategories() const override;
|
||||
virtual bool ShouldShowInNewMenu() const override;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Factories/SystemEventDataAssetFactory.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "LLM/SystemEventDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "SystemEventDataAssetFactory"
|
||||
|
||||
USystemEventDataAssetFactory::USystemEventDataAssetFactory()
|
||||
{
|
||||
SupportedClass = USystemEventDataAsset::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* USystemEventDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<USystemEventDataAsset>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FText USystemEventDataAssetFactory::GetDisplayName() const
|
||||
{
|
||||
return LOCTEXT("DisplayName", "System Event");
|
||||
}
|
||||
|
||||
uint32 USystemEventDataAssetFactory::GetMenuCategories() const
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
bool USystemEventDataAssetFactory::ShouldShowInNewMenu() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Factories/Factory.h"
|
||||
#include "SystemEventDataAssetFactory.generated.h"
|
||||
|
||||
UCLASS(hidecategories = Object)
|
||||
class USystemEventDataAssetFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USystemEventDataAssetFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual FText GetDisplayName() const override;
|
||||
virtual uint32 GetMenuCategories() const override;
|
||||
virtual bool ShouldShowInNewMenu() const override;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Factories/WorldContextDataAssetFactory.h"
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
#include "World/WorldContextDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "WorldContextDataAssetFactory"
|
||||
|
||||
UWorldContextDataAssetFactory::UWorldContextDataAssetFactory()
|
||||
{
|
||||
SupportedClass = UWorldContextDataAsset::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* UWorldContextDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<UWorldContextDataAsset>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FText UWorldContextDataAssetFactory::GetDisplayName() const
|
||||
{
|
||||
return LOCTEXT("DisplayName", "World Context");
|
||||
}
|
||||
|
||||
uint32 UWorldContextDataAssetFactory::GetMenuCategories() const
|
||||
{
|
||||
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
|
||||
}
|
||||
|
||||
bool UWorldContextDataAssetFactory::ShouldShowInNewMenu() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Factories/Factory.h"
|
||||
#include "WorldContextDataAssetFactory.generated.h"
|
||||
|
||||
UCLASS(hidecategories = Object)
|
||||
class UWorldContextDataAssetFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UWorldContextDataAssetFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual FText GetDisplayName() const override;
|
||||
virtual uint32 GetMenuCategories() const override;
|
||||
virtual bool ShouldShowInNewMenu() const override;
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "FunnyGirlEditorModule.h"
|
||||
|
||||
#include "AssetTypeActions/AssetTypeActions_DirectorPrompt.h"
|
||||
#include "AssetTypeActions/AssetTypeActions_Identity.h"
|
||||
#include "AssetTypeActions/AssetTypeActions_LLMProvider.h"
|
||||
#include "AssetTypeActions/AssetTypeActions_PersonalityTrait.h"
|
||||
#include "AssetTypeActions/AssetTypeActions_SystemEvent.h"
|
||||
#include "AssetTypeActions/AssetTypeActions_WorldContext.h"
|
||||
#include "AssetToolsModule.h"
|
||||
#include "IAssetTools.h"
|
||||
#include "IAssetTypeActions.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "Morph/Editor/LipSyncMappingAssetTypeActions.h"
|
||||
#include "Morph/Editor/MorphPresetAssetTypeActions.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "Morph/Editor/MorphPresetDetailsCustomization.h"
|
||||
#include "PropertyEditorModule.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FunnyGirlEditor"
|
||||
|
||||
EAssetTypeCategories::Type FFunnyGirlEditorModule::LLMAssetCategory = EAssetTypeCategories::Misc;
|
||||
EAssetTypeCategories::Type FFunnyGirlEditorModule::NarrativeAssetCategory = EAssetTypeCategories::Misc;
|
||||
|
||||
void FFunnyGirlEditorModule::StartupModule()
|
||||
{
|
||||
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||
LLMAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("LLM")), LOCTEXT("LLMAssetCategory", "LLM"));
|
||||
NarrativeAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Narrative")), LOCTEXT("NarrativeAssetCategory", "Narrative"));
|
||||
|
||||
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_LLMProvider>());
|
||||
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_DirectorPrompt>());
|
||||
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_Identity>());
|
||||
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_PersonalityTrait>());
|
||||
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_SystemEvent>());
|
||||
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_WorldContext>());
|
||||
RegisterAssetTypeAction(MakeShared<FMorphPresetAssetTypeActions>());
|
||||
RegisterAssetTypeAction(MakeShared<FLipSyncMappingAssetTypeActions>());
|
||||
|
||||
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
|
||||
PropertyEditorModule.RegisterCustomClassLayout(
|
||||
UMorphPresetDataAsset::StaticClass()->GetFName(),
|
||||
FOnGetDetailCustomizationInstance::CreateStatic(&FMorphPresetDetailsCustomization::MakeInstance));
|
||||
PropertyEditorModule.NotifyCustomizationModuleChanged();
|
||||
bMorphPresetCustomizationRegistered = true;
|
||||
}
|
||||
|
||||
void FFunnyGirlEditorModule::ShutdownModule()
|
||||
{
|
||||
if (FModuleManager::Get().IsModuleLoaded(TEXT("AssetTools")))
|
||||
{
|
||||
IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||
for (const TSharedPtr<IAssetTypeActions>& AssetTypeAction : RegisteredAssetTypeActions)
|
||||
{
|
||||
if (AssetTypeAction.IsValid())
|
||||
{
|
||||
AssetTools.UnregisterAssetTypeActions(AssetTypeAction.ToSharedRef());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RegisteredAssetTypeActions.Empty();
|
||||
|
||||
if (bMorphPresetCustomizationRegistered && FModuleManager::Get().IsModuleLoaded(TEXT("PropertyEditor")))
|
||||
{
|
||||
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
|
||||
PropertyEditorModule.UnregisterCustomClassLayout(UMorphPresetDataAsset::StaticClass()->GetFName());
|
||||
PropertyEditorModule.NotifyCustomizationModuleChanged();
|
||||
bMorphPresetCustomizationRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
EAssetTypeCategories::Type FFunnyGirlEditorModule::GetLLMAssetCategory()
|
||||
{
|
||||
return LLMAssetCategory;
|
||||
}
|
||||
|
||||
EAssetTypeCategories::Type FFunnyGirlEditorModule::GetNarrativeAssetCategory()
|
||||
{
|
||||
return NarrativeAssetCategory;
|
||||
}
|
||||
|
||||
void FFunnyGirlEditorModule::RegisterAssetTypeAction(const TSharedRef<IAssetTypeActions>& AssetTypeAction)
|
||||
{
|
||||
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||
AssetTools.RegisterAssetTypeActions(AssetTypeAction);
|
||||
RegisteredAssetTypeActions.Add(AssetTypeAction);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FFunnyGirlEditorModule, FunnyGirlEditor)
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AssetTypeCategories.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
class IAssetTypeActions;
|
||||
|
||||
class FFunnyGirlEditorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
static EAssetTypeCategories::Type GetLLMAssetCategory();
|
||||
static EAssetTypeCategories::Type GetNarrativeAssetCategory();
|
||||
|
||||
private:
|
||||
static EAssetTypeCategories::Type LLMAssetCategory;
|
||||
static EAssetTypeCategories::Type NarrativeAssetCategory;
|
||||
|
||||
TArray<TSharedPtr<IAssetTypeActions>> RegisteredAssetTypeActions;
|
||||
bool bMorphPresetCustomizationRegistered = false;
|
||||
|
||||
void RegisterAssetTypeAction(const TSharedRef<IAssetTypeActions>& AssetTypeAction);
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/AnimTimeline/SAnimEditorBase.h"
|
||||
|
||||
#include "AnimPreviewInstance.h"
|
||||
#include "Animation/AnimSequenceBase.h"
|
||||
#include "Animation/BlendSpace.h"
|
||||
#include "Animation/DebugSkelMeshComponent.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
|
||||
void SAnimEditorBase::Construct(const FArguments& InArgs, const TSharedPtr<IPersonaPreviewScene>& InPreviewScene)
|
||||
{
|
||||
PreviewScenePtr = InPreviewScene;
|
||||
OnObjectsSelectedDelegate = InArgs._OnObjectsSelected;
|
||||
SetInputViewRange(0.0f, GetSequenceLength());
|
||||
|
||||
const TSharedRef<SWidget> TimelineWidget = InArgs._DisplayAnimTimeline
|
||||
? CreateAnimTimelineWidget()
|
||||
: SNullWidget::NullWidget;
|
||||
|
||||
TSharedPtr<SVerticalBox> RootBox;
|
||||
ChildSlot
|
||||
[
|
||||
SAssignNew(RootBox, SVerticalBox)
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.0f)
|
||||
[
|
||||
SNew(SOverlay)
|
||||
+ SOverlay::Slot()
|
||||
[
|
||||
SAssignNew(NonScrollEditorPanels, SVerticalBox)
|
||||
]
|
||||
+ SOverlay::Slot()
|
||||
[
|
||||
TimelineWidget
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if (InArgs._DisplayAnimScrubBar)
|
||||
{
|
||||
RootBox->AddSlot()
|
||||
.AutoHeight()
|
||||
[
|
||||
ConstructAnimScrubPanel(InArgs._DisplayAnimScrubBarEditing)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
void SAnimEditorBase::SetInputViewRange(float InViewMinInput, float InViewMaxInput)
|
||||
{
|
||||
ViewMaxInput = FMath::Min(InViewMaxInput, GetSequenceLength());
|
||||
ViewMinInput = FMath::Max(InViewMinInput, 0.0f);
|
||||
}
|
||||
|
||||
float SAnimEditorBase::GetScrubValue() const
|
||||
{
|
||||
if (PreviewScenePtr.IsValid())
|
||||
{
|
||||
if (UDebugSkelMeshComponent* PreviewMesh = PreviewScenePtr.Pin()->GetPreviewMeshComponent())
|
||||
{
|
||||
if (PreviewMesh->PreviewInstance)
|
||||
{
|
||||
return PreviewMesh->PreviewInstance->GetCurrentTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
void SAnimEditorBase::OnSelectionChanged(const TArray<UObject*>& SelectedItems)
|
||||
{
|
||||
OnObjectsSelectedDelegate.ExecuteIfBound(SelectedItems);
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> SAnimEditorBase::CreateDocumentAnchor()
|
||||
{
|
||||
return SNullWidget::NullWidget;
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> SAnimEditorBase::ConstructAnimScrubPanel(bool bDisplayAnimScrubBarEditing)
|
||||
{
|
||||
return SNullWidget::NullWidget;
|
||||
}
|
||||
|
||||
float SAnimEditorBase::GetSequenceLength() const
|
||||
{
|
||||
if (const UAnimSequenceBase* AnimSequence = Cast<UAnimSequenceBase>(GetEditorObject()))
|
||||
{
|
||||
return AnimSequence->GetPlayLength();
|
||||
}
|
||||
|
||||
if (const UBlendSpace* BlendSpace = Cast<UBlendSpace>(GetEditorObject()))
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/LipSyncMappingAssetFactory.h"
|
||||
|
||||
#include "Morph/LipSyncMappingDataAsset.h"
|
||||
|
||||
ULipSyncMappingAssetFactory::ULipSyncMappingAssetFactory()
|
||||
{
|
||||
SupportedClass = ULipSyncMappingDataAsset::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* ULipSyncMappingAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<ULipSyncMappingDataAsset>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FString ULipSyncMappingAssetFactory::GetDefaultNewAssetName() const
|
||||
{
|
||||
return TEXT("LipSyncMapping");
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/LipSyncMappingAssetTypeActions.h"
|
||||
|
||||
#include "Morph/LipSyncMappingDataAsset.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "LipSyncMapping"
|
||||
|
||||
FText FLipSyncMappingAssetTypeActions::GetName() const
|
||||
{
|
||||
return LOCTEXT("AssetTypeActions_LipSyncMapping", "Lip Sync Mapping");
|
||||
}
|
||||
|
||||
FColor FLipSyncMappingAssetTypeActions::GetTypeColor() const
|
||||
{
|
||||
return FColor(120, 210, 255);
|
||||
}
|
||||
|
||||
UClass* FLipSyncMappingAssetTypeActions::GetSupportedClass() const
|
||||
{
|
||||
return ULipSyncMappingDataAsset::StaticClass();
|
||||
}
|
||||
|
||||
uint32 FLipSyncMappingAssetTypeActions::GetCategories()
|
||||
{
|
||||
return EAssetTypeCategories::Animation;
|
||||
}
|
||||
|
||||
FText FLipSyncMappingAssetTypeActions::GetAssetDescription(const FAssetData& AssetData) const
|
||||
{
|
||||
return LOCTEXT("LipSyncMappingDescription", "Maps lip sync viseme names to character morph targets.");
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/MorphPresetAssetEditor.h"
|
||||
|
||||
#include "IDetailsView.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "Morph/Editor/MorphPresetEditorController.h"
|
||||
#include "Morph/Editor/MorphPresetTimelinePanel.h"
|
||||
#include "Morph/Editor/MorphPresetViewportPanel.h"
|
||||
#include "PropertyEditorModule.h"
|
||||
#include "Widgets/Docking/SDockTab.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "MorphPresetAssetEditor"
|
||||
|
||||
const FName FMorphPresetAssetEditor::EditorName(TEXT("MorphPresetAssetEditor"));
|
||||
const FName FMorphPresetAssetEditor::ViewportTabId(TEXT("MorphPresetEditor_Viewport"));
|
||||
const FName FMorphPresetAssetEditor::TimelineTabId(TEXT("MorphPresetEditor_Timeline"));
|
||||
const FName FMorphPresetAssetEditor::DetailsTabId(TEXT("MorphPresetEditor_Details"));
|
||||
|
||||
void FMorphPresetAssetEditor::InitMorphPresetAssetEditor(
|
||||
const EToolkitMode::Type Mode,
|
||||
const TSharedPtr<IToolkitHost>& InitToolkitHost,
|
||||
UMorphPresetDataAsset* InAsset)
|
||||
{
|
||||
Preset = InAsset;
|
||||
EditorController = MakeShared<FMorphPresetEditorController>(InAsset);
|
||||
EditorController->OnPresetChanged().AddSP(this, &FMorphPresetAssetEditor::HandlePresetChanged);
|
||||
EditorController->Initialize();
|
||||
|
||||
TSharedRef<FTabManager::FLayout> Layout = FTabManager::NewLayout("MorphPresetEditorLayout_V2")
|
||||
->AddArea(
|
||||
FTabManager::NewPrimaryArea()
|
||||
->SetOrientation(Orient_Horizontal)
|
||||
->Split(
|
||||
FTabManager::NewSplitter()
|
||||
->SetOrientation(Orient_Vertical)
|
||||
->Split(
|
||||
FTabManager::NewStack()
|
||||
->SetSizeCoefficient(0.68f)
|
||||
->AddTab(ViewportTabId, ETabState::OpenedTab)
|
||||
)
|
||||
->Split(
|
||||
FTabManager::NewStack()
|
||||
->SetSizeCoefficient(0.32f)
|
||||
->AddTab(TimelineTabId, ETabState::OpenedTab)
|
||||
)
|
||||
)
|
||||
->Split(
|
||||
FTabManager::NewStack()
|
||||
->SetSizeCoefficient(0.25f)
|
||||
->AddTab(DetailsTabId, ETabState::OpenedTab)
|
||||
)
|
||||
);
|
||||
|
||||
FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, EditorName, Layout, true, true, InAsset);
|
||||
RegenerateMenusAndToolbars();
|
||||
}
|
||||
|
||||
void FMorphPresetAssetEditor::RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
|
||||
{
|
||||
FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
|
||||
|
||||
InTabManager->RegisterTabSpawner(ViewportTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Viewport))
|
||||
.SetDisplayName(LOCTEXT("ViewportTab", "Viewport"));
|
||||
|
||||
InTabManager->RegisterTabSpawner(TimelineTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Timeline))
|
||||
.SetDisplayName(LOCTEXT("TimelineTab", "Timeline"));
|
||||
|
||||
InTabManager->RegisterTabSpawner(DetailsTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Details))
|
||||
.SetDisplayName(LOCTEXT("DetailsTab", "Details"));
|
||||
}
|
||||
|
||||
void FMorphPresetAssetEditor::UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
|
||||
{
|
||||
FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);
|
||||
InTabManager->UnregisterTabSpawner(ViewportTabId);
|
||||
InTabManager->UnregisterTabSpawner(TimelineTabId);
|
||||
InTabManager->UnregisterTabSpawner(DetailsTabId);
|
||||
}
|
||||
|
||||
FName FMorphPresetAssetEditor::GetToolkitFName() const
|
||||
{
|
||||
return EditorName;
|
||||
}
|
||||
|
||||
FText FMorphPresetAssetEditor::GetBaseToolkitName() const
|
||||
{
|
||||
return LOCTEXT("ToolkitName", "Morph Target Preset Editor");
|
||||
}
|
||||
|
||||
FString FMorphPresetAssetEditor::GetWorldCentricTabPrefix() const
|
||||
{
|
||||
return TEXT("Morph Preset ");
|
||||
}
|
||||
|
||||
FLinearColor FMorphPresetAssetEditor::GetWorldCentricTabColorScale() const
|
||||
{
|
||||
return FLinearColor(0.18f, 0.47f, 0.83f);
|
||||
}
|
||||
|
||||
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Viewport(const FSpawnTabArgs& Args)
|
||||
{
|
||||
ViewportWidget = SNew(SMorphPresetViewportPanel)
|
||||
.Controller(EditorController)
|
||||
.AssetEditorToolkit(SharedThis(this));
|
||||
|
||||
return SNew(SDockTab)
|
||||
.Label(LOCTEXT("ViewportTabLabel", "Viewport"))
|
||||
[
|
||||
ViewportWidget.ToSharedRef()
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Timeline(const FSpawnTabArgs& Args)
|
||||
{
|
||||
TimelineWidget = SNew(SMorphPresetTimelinePanel)
|
||||
.Controller(EditorController);
|
||||
|
||||
return SNew(SDockTab)
|
||||
.Label(LOCTEXT("TimelineTabLabel", "Timeline"))
|
||||
[
|
||||
TimelineWidget.ToSharedRef()
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Args)
|
||||
{
|
||||
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||
FDetailsViewArgs DetailsArgs;
|
||||
DetailsArgs.bAllowSearch = true;
|
||||
DetailsArgs.bHideSelectionTip = true;
|
||||
DetailsView = PropertyModule.CreateDetailView(DetailsArgs);
|
||||
DetailsView->OnFinishedChangingProperties().AddSP(this, &FMorphPresetAssetEditor::HandleDetailsChanged);
|
||||
DetailsView->SetObjects(TArray<UObject*>{ Preset.Get() });
|
||||
|
||||
return SNew(SDockTab)
|
||||
.Label(LOCTEXT("DetailsTabLabel", "Details"))
|
||||
[
|
||||
DetailsView.ToSharedRef()
|
||||
];
|
||||
}
|
||||
|
||||
void FMorphPresetAssetEditor::HandleDetailsChanged(const FPropertyChangedEvent&)
|
||||
{
|
||||
if (EditorController.IsValid())
|
||||
{
|
||||
EditorController->NotifyPresetEdited();
|
||||
}
|
||||
|
||||
if (TimelineWidget.IsValid())
|
||||
{
|
||||
TimelineWidget->Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void FMorphPresetAssetEditor::HandlePresetChanged()
|
||||
{
|
||||
if (DetailsView.IsValid())
|
||||
{
|
||||
DetailsView->ForceRefresh();
|
||||
}
|
||||
|
||||
if (TimelineWidget.IsValid())
|
||||
{
|
||||
TimelineWidget->Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/MorphPresetAssetFactory.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
|
||||
UMorphPresetAssetFactory::UMorphPresetAssetFactory()
|
||||
{
|
||||
SupportedClass = UMorphPresetDataAsset::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* UMorphPresetAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<UMorphPresetDataAsset>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FString UMorphPresetAssetFactory::GetDefaultNewAssetName() const
|
||||
{
|
||||
return TEXT("MorphPreset");
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/MorphPresetAssetTypeActions.h"
|
||||
#include "Morph/Editor/MorphPresetAssetEditor.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "Morph/Editor/MorphPresetAssetFactory.h"
|
||||
#include "Toolkits/IToolkitHost.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "MorphPreset"
|
||||
|
||||
FText FMorphPresetAssetTypeActions::GetName() const
|
||||
{
|
||||
return LOCTEXT("AssetTypeActions_MorphPreset", "Morph Preset");
|
||||
}
|
||||
|
||||
FColor FMorphPresetAssetTypeActions::GetTypeColor() const
|
||||
{
|
||||
return FColor(200, 150, 255); // Светло-фиолетовый
|
||||
}
|
||||
|
||||
UClass* FMorphPresetAssetTypeActions::GetSupportedClass() const
|
||||
{
|
||||
return UMorphPresetDataAsset::StaticClass();
|
||||
}
|
||||
|
||||
uint32 FMorphPresetAssetTypeActions::GetCategories()
|
||||
{
|
||||
return EAssetTypeCategories::Animation;
|
||||
}
|
||||
|
||||
FText FMorphPresetAssetTypeActions::GetAssetDescription(const FAssetData& AssetData) const
|
||||
{
|
||||
return LOCTEXT("MorphPresetDescription", "Пресет морф таргетов для персонажа. Улыбка, грусть, удивление и т.д.");
|
||||
}
|
||||
|
||||
void FMorphPresetAssetTypeActions::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor)
|
||||
{
|
||||
if (InObjects.Num() == 0) return;
|
||||
UMorphPresetDataAsset* Preset = Cast<UMorphPresetDataAsset>(InObjects[0]);
|
||||
if (!Preset) return;
|
||||
|
||||
TSharedRef<FMorphPresetAssetEditor> Editor = MakeShared<FMorphPresetAssetEditor>();
|
||||
Editor->InitMorphPresetAssetEditor(EToolkitMode::Standalone, EditWithinLevelEditor, Preset);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,245 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/MorphPresetDetailsCustomization.h"
|
||||
|
||||
#include "DetailCategoryBuilder.h"
|
||||
#include "DetailLayoutBuilder.h"
|
||||
#include "DetailWidgetRow.h"
|
||||
#include "IPropertyUtilities.h"
|
||||
#include "JsonObjectConverter.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Policies/PrettyJsonPrintPolicy.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "Widgets/Input/SMultiLineEditableTextBox.h"
|
||||
#include "Widgets/Layout/SBox.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "MorphPresetDetailsCustomization"
|
||||
|
||||
namespace
|
||||
{
|
||||
FString SerializePresetToJson(const UMorphPresetDataAsset* Preset)
|
||||
{
|
||||
if (!Preset)
|
||||
{
|
||||
return TEXT("{}");
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> JsonObject = MakeShared<FJsonObject>();
|
||||
if (!FJsonObjectConverter::UStructToJsonObject(Preset->GetClass(), Preset, JsonObject, 0, 0))
|
||||
{
|
||||
return TEXT("{}");
|
||||
}
|
||||
|
||||
JsonObject->RemoveField(TEXT("PreviewMesh"));
|
||||
|
||||
FString JsonString;
|
||||
const TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
|
||||
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonString);
|
||||
FJsonSerializer::Serialize(JsonObject, Writer);
|
||||
return JsonString;
|
||||
}
|
||||
|
||||
bool DeserializePresetFromJson(const FString& JsonString, UMorphPresetDataAsset& OutPreset)
|
||||
{
|
||||
TSharedPtr<FJsonObject> JsonObject;
|
||||
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
|
||||
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonObject->RemoveField(TEXT("PreviewMesh"));
|
||||
return FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutPreset, 0, 0);
|
||||
}
|
||||
|
||||
void CopyPresetValues(const UMorphPresetDataAsset& SourcePreset, UMorphPresetDataAsset& TargetPreset)
|
||||
{
|
||||
TargetPreset.DisplayName = SourcePreset.DisplayName;
|
||||
TargetPreset.Description = SourcePreset.Description;
|
||||
TargetPreset.MorphTargets = SourcePreset.MorphTargets;
|
||||
TargetPreset.Duration = SourcePreset.Duration;
|
||||
TargetPreset.bLoop = SourcePreset.bLoop;
|
||||
TargetPreset.LoopDelay = SourcePreset.LoopDelay;
|
||||
TargetPreset.BlendSpeedOverride = SourcePreset.BlendSpeedOverride;
|
||||
TargetPreset.AnimationMontage = SourcePreset.AnimationMontage;
|
||||
TargetPreset.AnimationPlayRate = SourcePreset.AnimationPlayRate;
|
||||
TargetPreset.AnimationStartSection = SourcePreset.AnimationStartSection;
|
||||
TargetPreset.bRestartAnimationOnReplay = SourcePreset.bRestartAnimationOnReplay;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<IDetailCustomization> FMorphPresetDetailsCustomization::MakeInstance()
|
||||
{
|
||||
return MakeShared<FMorphPresetDetailsCustomization>();
|
||||
}
|
||||
|
||||
void FMorphPresetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
|
||||
{
|
||||
const TArray<TWeakObjectPtr<UObject>>& CustomizedObjects = DetailBuilder.GetSelectedObjects();
|
||||
for (const TWeakObjectPtr<UObject>& Object : CustomizedObjects)
|
||||
{
|
||||
if (UMorphPresetDataAsset* MorphPreset = Cast<UMorphPresetDataAsset>(Object.Get()))
|
||||
{
|
||||
Preset = MorphPreset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PropertyUtilities = DetailBuilder.GetPropertyUtilities();
|
||||
RefreshJsonFromPreset();
|
||||
|
||||
IDetailCategoryBuilder& JsonCategory = DetailBuilder.EditCategory("JSON", LOCTEXT("JsonCategory", "JSON"), ECategoryPriority::Uncommon);
|
||||
JsonCategory.AddCustomRow(LOCTEXT("PresetJsonFilter", "Preset JSON"))
|
||||
.WholeRowContent()
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 0.0f, 0.0f, 6.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("JsonHelpText", "JSON representation of the current preset. Pasted valid JSON is applied immediately."))
|
||||
.AutoWrapText(true)
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.0f)
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredHeight(260.0f)
|
||||
.MaxDesiredHeight(420.0f)
|
||||
[
|
||||
SAssignNew(JsonTextBox, SMultiLineEditableTextBox)
|
||||
.Text(FText::FromString(CachedJsonText))
|
||||
.OnTextChanged(this, &FMorphPresetDetailsCustomization::HandleJsonTextChanged)
|
||||
.AutoWrapText(false)
|
||||
]
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 6.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("ApplyJsonButton", "Apply JSON"))
|
||||
.OnClicked(this, &FMorphPresetDetailsCustomization::OnApplyJsonClicked)
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("ResetJsonButton", "Reset From Preset"))
|
||||
.OnClicked(this, &FMorphPresetDetailsCustomization::OnResetJsonClicked)
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.0f)
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(this, &FMorphPresetDetailsCustomization::GetStatusText)
|
||||
.ColorAndOpacity(this, &FMorphPresetDetailsCustomization::GetStatusColor)
|
||||
.AutoWrapText(true)
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
void FMorphPresetDetailsCustomization::RefreshJsonFromPreset()
|
||||
{
|
||||
CachedJsonText = SerializePresetToJson(Preset.Get());
|
||||
bIsUpdatingJsonText = true;
|
||||
if (JsonTextBox.IsValid())
|
||||
{
|
||||
JsonTextBox->SetText(FText::FromString(CachedJsonText));
|
||||
}
|
||||
bIsUpdatingJsonText = false;
|
||||
}
|
||||
|
||||
void FMorphPresetDetailsCustomization::ApplyJsonToPreset(const FString& JsonString)
|
||||
{
|
||||
UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
if (!PresetAsset)
|
||||
{
|
||||
bLastApplySucceeded = false;
|
||||
StatusText = LOCTEXT("NoPresetStatus", "Preset is no longer available.");
|
||||
return;
|
||||
}
|
||||
|
||||
UMorphPresetDataAsset* TempPreset = NewObject<UMorphPresetDataAsset>(GetTransientPackage(), UMorphPresetDataAsset::StaticClass(), NAME_None, RF_Transient);
|
||||
if (!DeserializePresetFromJson(JsonString, *TempPreset))
|
||||
{
|
||||
bLastApplySucceeded = false;
|
||||
StatusText = LOCTEXT("InvalidJsonStatus", "JSON is invalid or does not match the preset structure.");
|
||||
return;
|
||||
}
|
||||
|
||||
PresetAsset->Modify();
|
||||
CopyPresetValues(*TempPreset, *PresetAsset);
|
||||
PresetAsset->MarkPackageDirty();
|
||||
PresetAsset->PostEditChange();
|
||||
|
||||
bLastApplySucceeded = true;
|
||||
StatusText = LOCTEXT("AppliedJsonStatus", "JSON applied to the preset.");
|
||||
RefreshJsonFromPreset();
|
||||
|
||||
if (PropertyUtilities.IsValid())
|
||||
{
|
||||
const FPropertyChangedEvent ChangeEvent(nullptr, EPropertyChangeType::ValueSet);
|
||||
PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent);
|
||||
PropertyUtilities->ForceRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
FText FMorphPresetDetailsCustomization::GetStatusText() const
|
||||
{
|
||||
return StatusText;
|
||||
}
|
||||
|
||||
FSlateColor FMorphPresetDetailsCustomization::GetStatusColor() const
|
||||
{
|
||||
return bLastApplySucceeded ? FSlateColor(FLinearColor(0.35f, 0.8f, 0.45f, 1.0f)) : FSlateColor(FLinearColor(0.95f, 0.4f, 0.35f, 1.0f));
|
||||
}
|
||||
|
||||
FReply FMorphPresetDetailsCustomization::OnApplyJsonClicked()
|
||||
{
|
||||
if (JsonTextBox.IsValid())
|
||||
{
|
||||
ApplyJsonToPreset(JsonTextBox->GetText().ToString());
|
||||
}
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply FMorphPresetDetailsCustomization::OnResetJsonClicked()
|
||||
{
|
||||
RefreshJsonFromPreset();
|
||||
bLastApplySucceeded = true;
|
||||
StatusText = LOCTEXT("ResetJsonStatus", "JSON reset to the current preset state.");
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
void FMorphPresetDetailsCustomization::HandleJsonTextChanged(const FText& NewText)
|
||||
{
|
||||
if (bIsUpdatingJsonText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FString NewJsonText = NewText.ToString();
|
||||
if (NewJsonText == CachedJsonText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CachedJsonText = NewJsonText;
|
||||
ApplyJsonToPreset(NewJsonText);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,733 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/MorphPresetEditorController.h"
|
||||
|
||||
#include "Animation/AnimData/IAnimationDataController.h"
|
||||
#include "AnimPreviewInstance.h"
|
||||
#include "Animation/AnimMontage.h"
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Animation/AnimSequenceBase.h"
|
||||
#include "Animation/AnimationAsset.h"
|
||||
#include "Animation/DebugSkelMeshComponent.h"
|
||||
#include "Animation/Skeleton.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Editor.h"
|
||||
#include "Framework/Commands/UICommandList.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "IPersonaPreviewScene.h"
|
||||
#include "IPersonaToolkit.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "Morph/LipSyncComponent.h"
|
||||
#include "Morph/LipSyncMappingDataAsset.h"
|
||||
#include "Morph/MorphControllerComponent.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "PersonaModule.h"
|
||||
#include "ReferenceSkeleton.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogMorphPresetEditorPreview, Log, All);
|
||||
|
||||
namespace
|
||||
{
|
||||
void SetAnimSequencePlayLength(UAnimSequence& AnimSequence, float PlayLength, bool bShouldTransact)
|
||||
{
|
||||
IAnimationDataController& Controller = AnimSequence.GetController();
|
||||
Controller.SetNumberOfFrames(FMath::Max(Controller.ConvertSecondsToFrameNumber(PlayLength).Value, 1), bShouldTransact);
|
||||
}
|
||||
}
|
||||
|
||||
FMorphPresetEditorController::FMorphPresetEditorController(UMorphPresetDataAsset* InPreset)
|
||||
: Preset(InPreset)
|
||||
{
|
||||
CommandList = MakeShared<FUICommandList>();
|
||||
DefaultLipSyncPreviewMapping = TStrongObjectPtr<ULipSyncMappingDataAsset>(
|
||||
NewObject<ULipSyncMappingDataAsset>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
}
|
||||
|
||||
FMorphPresetEditorController::~FMorphPresetEditorController()
|
||||
{
|
||||
StopLipSyncPreviewAudio();
|
||||
DestroyLipSyncPreviewComponents();
|
||||
|
||||
if (PersonaToolkit.IsValid())
|
||||
{
|
||||
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = PersonaToolkit->GetPreviewScene())
|
||||
{
|
||||
PreviewScene->ClearSelectedBone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::Initialize()
|
||||
{
|
||||
RebuildPreviewScene();
|
||||
SetCurrentTime(0.0f);
|
||||
}
|
||||
|
||||
UMorphPresetDataAsset* FMorphPresetEditorController::GetPreset() const
|
||||
{
|
||||
return Preset.Get();
|
||||
}
|
||||
|
||||
USkeletalMesh* FMorphPresetEditorController::GetPreviewMesh() const
|
||||
{
|
||||
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
|
||||
{
|
||||
return PresetAsset->PreviewMesh.LoadSynchronous();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::SetPreviewMesh(USkeletalMesh* InPreviewMesh)
|
||||
{
|
||||
UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
if (!PresetAsset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PresetAsset->PreviewMesh.Get() == InPreviewMesh)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PresetAsset->Modify();
|
||||
PresetAsset->PreviewMesh = InPreviewMesh;
|
||||
PresetAsset->MarkPackageDirty();
|
||||
|
||||
RebuildPreviewScene();
|
||||
return true;
|
||||
}
|
||||
|
||||
TSharedPtr<IPersonaPreviewScene> FMorphPresetEditorController::GetPreviewScene() const
|
||||
{
|
||||
TSharedPtr<IPersonaPreviewScene> PreviewScene;
|
||||
if (PersonaToolkit.IsValid())
|
||||
{
|
||||
PreviewScene = PersonaToolkit->GetPreviewScene();
|
||||
}
|
||||
|
||||
return PreviewScene;
|
||||
}
|
||||
|
||||
float FMorphPresetEditorController::GetTimelineLength() const
|
||||
{
|
||||
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
|
||||
{
|
||||
float TimelineLength = PresetAsset->GetTimelineLength();
|
||||
if (PresetAsset->AnimationMontage)
|
||||
{
|
||||
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
|
||||
const UAnimSequenceBase* PreviewSequence = PresetAsset->AnimationMontage->GetFirstAnimReference();
|
||||
const float AnimationLength = PreviewSequence ? PreviewSequence->GetPlayLength() : PresetAsset->AnimationMontage->GetPlayLength();
|
||||
TimelineLength = FMath::Max(TimelineLength, AnimationLength / PlayRate);
|
||||
}
|
||||
return FMath::Max(TimelineLength, 0.1f);
|
||||
}
|
||||
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
float FMorphPresetEditorController::GetCurrentTime() const
|
||||
{
|
||||
return CurrentTime;
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::SetCurrentTime(float InTime)
|
||||
{
|
||||
CurrentTime = FMath::Clamp(InTime, 0.0f, GetTimelineLength());
|
||||
|
||||
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
|
||||
{
|
||||
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
|
||||
{
|
||||
if (PreviewMeshComponent->PreviewInstance)
|
||||
{
|
||||
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
|
||||
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
|
||||
RefreshPreviewAnimationPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ApplyPresetToPreview();
|
||||
PlaybackStateChanged.Broadcast();
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::TryGetHeadLocation(FVector& OutHeadLocation) const
|
||||
{
|
||||
USkeletalMesh* PreviewMesh = GetPreviewMesh();
|
||||
if (!PreviewMesh)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FReferenceSkeleton& RefSkeleton = PreviewMesh->GetRefSkeleton();
|
||||
int32 HeadBoneIndex = RefSkeleton.FindBoneIndex(TEXT("head"));
|
||||
if (HeadBoneIndex == INDEX_NONE)
|
||||
{
|
||||
HeadBoneIndex = RefSkeleton.FindBoneIndex(TEXT("Head"));
|
||||
}
|
||||
|
||||
if (HeadBoneIndex == INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<FTransform>& RefPose = RefSkeleton.GetRefBonePose();
|
||||
FTransform HeadTransform = FTransform::Identity;
|
||||
int32 BoneIndex = HeadBoneIndex;
|
||||
|
||||
while (BoneIndex != INDEX_NONE)
|
||||
{
|
||||
HeadTransform = RefPose[BoneIndex] * HeadTransform;
|
||||
BoneIndex = RefSkeleton.GetParentIndex(BoneIndex);
|
||||
}
|
||||
|
||||
OutHeadLocation = HeadTransform.GetLocation();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::ResolvePreviewBoneName(FName InBoneName, FName& OutBoneName) const
|
||||
{
|
||||
USkeletalMesh* PreviewMesh = GetPreviewMesh();
|
||||
if (!PreviewMesh || InBoneName.IsNone())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FReferenceSkeleton& RefSkeleton = PreviewMesh->GetRefSkeleton();
|
||||
if (RefSkeleton.FindBoneIndex(InBoneName) != INDEX_NONE)
|
||||
{
|
||||
OutBoneName = InBoneName;
|
||||
return true;
|
||||
}
|
||||
|
||||
const FString RequestedBoneName = InBoneName.ToString();
|
||||
for (int32 BoneIndex = 0; BoneIndex < RefSkeleton.GetNum(); ++BoneIndex)
|
||||
{
|
||||
const FName CandidateBoneName = RefSkeleton.GetBoneName(BoneIndex);
|
||||
if (CandidateBoneName.ToString().Equals(RequestedBoneName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
OutBoneName = CandidateBoneName;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::NotifyPresetEdited(bool bBroadcastPresetChanged)
|
||||
{
|
||||
if (UMorphPresetDataAsset* PresetAsset = Preset.Get())
|
||||
{
|
||||
PresetAsset->MarkPackageDirty();
|
||||
}
|
||||
|
||||
UAnimationAsset* DesiredPreviewAsset = ResolvePreviewAnimationAsset();
|
||||
UAnimationAsset* CurrentPreviewAsset = nullptr;
|
||||
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
|
||||
{
|
||||
CurrentPreviewAsset = PreviewScene->GetPreviewAnimationAsset();
|
||||
}
|
||||
|
||||
if (DesiredPreviewAsset != CurrentPreviewAsset ||
|
||||
(DesiredPreviewAsset && PreviewAnimation.IsValid()) ||
|
||||
(!DesiredPreviewAsset && !PreviewAnimation.IsValid()))
|
||||
{
|
||||
RebuildPreviewAnimation();
|
||||
}
|
||||
else if (PreviewAnimation.IsValid())
|
||||
{
|
||||
SetAnimSequencePlayLength(*PreviewAnimation, GetTimelineLength(), false);
|
||||
}
|
||||
|
||||
CurrentTime = FMath::Clamp(CurrentTime, 0.0f, GetTimelineLength());
|
||||
|
||||
// Re-apply morph weights immediately so viewport reflects drag/resize edits without stalling.
|
||||
ApplyPresetToPreview();
|
||||
if (bBroadcastPresetChanged)
|
||||
{
|
||||
PresetChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::SetPlaying(bool bInPlaying)
|
||||
{
|
||||
if (bIsPlaying == bInPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bIsPlaying = bInPlaying;
|
||||
PlaybackStateChanged.Broadcast();
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::TogglePlaying()
|
||||
{
|
||||
SetPlaying(!bIsPlaying);
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::Tick(float DeltaTime)
|
||||
{
|
||||
if (bIsPlaying)
|
||||
{
|
||||
float NewTime = CurrentTime + DeltaTime;
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
const float Length = GetTimelineLength();
|
||||
|
||||
if (PresetAsset && PresetAsset->bLoop && Length > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
NewTime = FMath::Fmod(NewTime, Length);
|
||||
if (NewTime < 0.0f)
|
||||
{
|
||||
NewTime += Length;
|
||||
}
|
||||
}
|
||||
else if (NewTime >= Length)
|
||||
{
|
||||
NewTime = Length;
|
||||
bIsPlaying = false;
|
||||
}
|
||||
|
||||
SetCurrentTime(NewTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyPresetToPreview();
|
||||
}
|
||||
|
||||
TickLipSyncPreviewComponents(DeltaTime);
|
||||
if (bLipSyncPreviewAudioActive && !IsLipSyncPreviewAudioPlaying())
|
||||
{
|
||||
bLipSyncPreviewAudioActive = false;
|
||||
PlaybackStateChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
USoundBase* FMorphPresetEditorController::GetLipSyncPreviewSound() const
|
||||
{
|
||||
return LipSyncPreviewSound.Get();
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::SetLipSyncPreviewSound(USoundBase* InSound)
|
||||
{
|
||||
if (LipSyncPreviewSound.Get() == InSound)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StopLipSyncPreviewAudio();
|
||||
LipSyncPreviewSound = InSound;
|
||||
PlaybackStateChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::PlayLipSyncPreviewAudio(float StartTime)
|
||||
{
|
||||
USoundBase* Sound = LipSyncPreviewSound.Get();
|
||||
if (!Sound)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RebuildLipSyncPreviewComponents();
|
||||
ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get();
|
||||
if (!PreviewLipSync || !GEditor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UAudioComponent* PreviewAudioComponent = GEditor->ResetPreviewAudioComponent(Sound);
|
||||
if (!PreviewAudioComponent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PreviewLipSync->Mapping = ResolveLipSyncPreviewMapping();
|
||||
PreviewLipSync->AudioDrivenSound = Sound;
|
||||
PreviewLipSync->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
|
||||
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
|
||||
: ELipSyncAudioDrivenMode::TranscriptVisemes;
|
||||
PreviewLipSync->AudioDrivenTranscript = LipSyncPreviewTranscript;
|
||||
PreviewLipSync->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
|
||||
PreviewLipSync->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
|
||||
PreviewLipSync->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
|
||||
PreviewLipSync->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
|
||||
PreviewLipSync->bUseFallbackPulseWithoutEnvelope = false;
|
||||
|
||||
bLipSyncPreviewAudioActive = PreviewLipSync->PlayAudioDrivenLipSyncWithComponent(PreviewAudioComponent, Sound, StartTime);
|
||||
PlaybackStateChanged.Broadcast();
|
||||
return bLipSyncPreviewAudioActive;
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::StopLipSyncPreviewAudio()
|
||||
{
|
||||
bLipSyncPreviewAudioActive = false;
|
||||
|
||||
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
|
||||
{
|
||||
PreviewLipSync->StopAudioDrivenLipSync();
|
||||
}
|
||||
|
||||
ApplyPresetToPreview();
|
||||
PlaybackStateChanged.Broadcast();
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::IsLipSyncPreviewAudioPlaying() const
|
||||
{
|
||||
return bLipSyncPreviewAudioActive && LipSyncPreviewComponent.IsValid() && LipSyncPreviewComponent->IsAudioDrivenLipSyncPlaying();
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::SetLipSyncPreviewSmoothness(float InSmoothness)
|
||||
{
|
||||
LipSyncPreviewSmoothness = FMath::Clamp(InSmoothness, 0.0f, 1.0f);
|
||||
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
|
||||
{
|
||||
PreviewLipSync->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
|
||||
PreviewLipSync->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
|
||||
PreviewLipSync->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
|
||||
PreviewLipSync->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
|
||||
}
|
||||
PlaybackStateChanged.Broadcast();
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::SetLipSyncPreviewTranscript(const FText& InTranscript)
|
||||
{
|
||||
LipSyncPreviewTranscript = InTranscript;
|
||||
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
|
||||
{
|
||||
PreviewLipSync->AudioDrivenTranscript = LipSyncPreviewTranscript;
|
||||
PreviewLipSync->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
|
||||
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
|
||||
: ELipSyncAudioDrivenMode::TranscriptVisemes;
|
||||
}
|
||||
PlaybackStateChanged.Broadcast();
|
||||
}
|
||||
|
||||
ULipSyncMappingDataAsset* FMorphPresetEditorController::GetLipSyncPreviewMapping() const
|
||||
{
|
||||
return LipSyncPreviewMapping.Get();
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::SetLipSyncPreviewMapping(ULipSyncMappingDataAsset* InMapping)
|
||||
{
|
||||
if (LipSyncPreviewMapping.Get() == InMapping)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
LipSyncPreviewMapping = InMapping;
|
||||
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
|
||||
{
|
||||
PreviewLipSync->Mapping = ResolveLipSyncPreviewMapping();
|
||||
}
|
||||
ApplyPresetToPreview();
|
||||
PlaybackStateChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::RebuildPreviewScene()
|
||||
{
|
||||
StopLipSyncPreviewAudio();
|
||||
DestroyLipSyncPreviewComponents();
|
||||
PersonaToolkit.Reset();
|
||||
PreviewAnimation.Reset();
|
||||
|
||||
USkeletalMesh* PreviewMesh = GetPreviewMesh();
|
||||
if (PreviewMesh == nullptr)
|
||||
{
|
||||
PreviewSceneChanged.Broadcast();
|
||||
return;
|
||||
}
|
||||
|
||||
FPersonaModule& PersonaModule = FModuleManager::LoadModuleChecked<FPersonaModule>("Persona");
|
||||
FPersonaToolkitArgs ToolkitArgs;
|
||||
PersonaToolkit = PersonaModule.CreatePersonaToolkit(PreviewMesh, ToolkitArgs);
|
||||
|
||||
RebuildPreviewAnimation();
|
||||
RebuildLipSyncPreviewComponents();
|
||||
SetCurrentTime(CurrentTime);
|
||||
PreviewSceneChanged.Broadcast();
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::RebuildPreviewAnimation()
|
||||
{
|
||||
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
|
||||
if (!PreviewScene.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UAnimSequence* AnimSequence = NewObject<UAnimSequence>(GetTransientPackage(), NAME_None, RF_Transient);
|
||||
PreviewAnimation.Reset();
|
||||
PreviewStatusText = FText::GetEmpty();
|
||||
|
||||
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
|
||||
if (PreviewAsset && !IsPreviewAnimationCompatible(PreviewAsset))
|
||||
{
|
||||
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
|
||||
const FString PreviewMeshSkeletonName = PreviewMesh && PreviewMesh->GetSkeleton()
|
||||
? PreviewMesh->GetSkeleton()->GetPathName()
|
||||
: TEXT("None");
|
||||
const FString AnimationSkeletonName = PreviewAsset->GetSkeleton()
|
||||
? PreviewAsset->GetSkeleton()->GetPathName()
|
||||
: TEXT("None");
|
||||
|
||||
PreviewStatusText = FText::FromString(FString::Printf(
|
||||
TEXT("Animation preview is disabled: montage sequence '%s' uses skeleton '%s', but Preview Mesh uses '%s'. Retarget the animation to the Preview Mesh skeleton."),
|
||||
*GetNameSafe(PreviewAsset),
|
||||
*AnimationSkeletonName,
|
||||
*PreviewMeshSkeletonName));
|
||||
UE_LOG(LogMorphPresetEditorPreview, Warning, TEXT("%s"), *PreviewStatusText.ToString());
|
||||
PreviewAsset = nullptr;
|
||||
}
|
||||
|
||||
if (!PreviewAsset)
|
||||
{
|
||||
PreviewAnimation = TStrongObjectPtr<UAnimSequence>(AnimSequence);
|
||||
PreviewAsset = AnimSequence;
|
||||
|
||||
SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
|
||||
if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
|
||||
{
|
||||
if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
|
||||
{
|
||||
AnimSequence->SetSkeleton(Skeleton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PreviewScene->SetPreviewAnimationAsset(PreviewAsset, true);
|
||||
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
|
||||
{
|
||||
PreviewMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);
|
||||
if (PreviewMeshComponent->PreviewInstance)
|
||||
{
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
const float PlayRate = PresetAsset ? FMath::Max(PresetAsset->AnimationPlayRate, 0.01f) : 1.0f;
|
||||
PreviewMeshComponent->PreviewInstance->SetAnimationAsset(PreviewAsset, false, PlayRate);
|
||||
PreviewMeshComponent->PreviewInstance->EnableControllers(true);
|
||||
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
|
||||
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
|
||||
PreviewMeshComponent->PreviewInstance->SetLooping(false);
|
||||
|
||||
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
|
||||
RefreshPreviewAnimationPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::RefreshPreviewAnimationPose()
|
||||
{
|
||||
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
|
||||
if (!PreviewScene.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
|
||||
if (!PreviewMeshComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewMeshComponent->TickAnimation(0.0f, false);
|
||||
PreviewMeshComponent->RefreshBoneTransforms();
|
||||
PreviewMeshComponent->MarkRenderStateDirty();
|
||||
PreviewScene->InvalidateViews();
|
||||
}
|
||||
|
||||
UAnimationAsset* FMorphPresetEditorController::ResolvePreviewAnimationAsset() const
|
||||
{
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
if (PresetAsset && PresetAsset->AnimationMontage)
|
||||
{
|
||||
if (UAnimSequenceBase* FirstAnimReference = PresetAsset->AnimationMontage->GetFirstAnimReference())
|
||||
{
|
||||
return FirstAnimReference;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float FMorphPresetEditorController::GetPreviewAnimationPosition() const
|
||||
{
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
|
||||
if (!PresetAsset || !PreviewAsset)
|
||||
{
|
||||
return CurrentTime;
|
||||
}
|
||||
|
||||
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
|
||||
float PreviewPosition = CurrentTime * PlayRate;
|
||||
if (const UAnimSequenceBase* PreviewSequence = Cast<UAnimSequenceBase>(PreviewAsset))
|
||||
{
|
||||
return FMath::Clamp(PreviewPosition, 0.0f, PreviewSequence->GetPlayLength());
|
||||
}
|
||||
|
||||
return PreviewPosition;
|
||||
}
|
||||
|
||||
bool FMorphPresetEditorController::IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const
|
||||
{
|
||||
if (!PreviewAsset)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
|
||||
const USkeleton* PreviewSkeleton = PreviewMesh ? PreviewMesh->GetSkeleton() : nullptr;
|
||||
const USkeleton* AnimationSkeleton = PreviewAsset->GetSkeleton();
|
||||
return PreviewSkeleton && AnimationSkeleton && PreviewSkeleton->IsCompatibleForEditor(AnimationSkeleton);
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::ApplyPresetToPreview()
|
||||
{
|
||||
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
|
||||
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
|
||||
if (!PresetAsset || !PreviewScene.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
|
||||
if (!PreviewMeshComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewMeshComponent->ClearMorphTargets();
|
||||
if (PreviewMeshComponent->PreviewInstance)
|
||||
{
|
||||
PreviewMeshComponent->PreviewInstance->EnableControllers(true);
|
||||
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
|
||||
}
|
||||
|
||||
TMap<FName, float> FinalValues;
|
||||
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*PresetAsset, CurrentTime, 1.0f, FinalValues);
|
||||
|
||||
for (const TPair<FName, float>& Pair : FinalValues)
|
||||
{
|
||||
if (Pair.Value <= 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PreviewMeshComponent->SetMorphTarget(Pair.Key, Pair.Value, false);
|
||||
}
|
||||
|
||||
RefreshPreviewAnimationPose();
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::RebuildLipSyncPreviewComponents()
|
||||
{
|
||||
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
|
||||
if (!PreviewScene.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
|
||||
UWorld* PreviewWorld = PreviewScene->GetWorld();
|
||||
if (!PreviewMeshComponent || !PreviewWorld)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (LipSyncPreviewActor.IsValid() && LipSyncPreviewMorphController.IsValid() && LipSyncPreviewComponent.IsValid())
|
||||
{
|
||||
LipSyncPreviewMorphController->TargetMeshComponent = PreviewMeshComponent;
|
||||
LipSyncPreviewComponent->Mapping = ResolveLipSyncPreviewMapping();
|
||||
return;
|
||||
}
|
||||
|
||||
AActor* PreviewActor = PreviewWorld->SpawnActor<AActor>();
|
||||
if (!PreviewActor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewActor->SetFlags(RF_Transient);
|
||||
|
||||
UMorphControllerComponent* MorphController = NewObject<UMorphControllerComponent>(PreviewActor, TEXT("MorphEditorPreviewMorphController"), RF_Transient);
|
||||
ULipSyncComponent* LipSyncComponent = NewObject<ULipSyncComponent>(PreviewActor, TEXT("MorphEditorPreviewLipSyncComponent"), RF_Transient);
|
||||
if (!MorphController || !LipSyncComponent)
|
||||
{
|
||||
PreviewActor->Destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewActor->AddInstanceComponent(MorphController);
|
||||
PreviewActor->AddInstanceComponent(LipSyncComponent);
|
||||
|
||||
MorphController->TargetMeshComponent = PreviewMeshComponent;
|
||||
MorphController->bAutoUpdate = false;
|
||||
MorphController->bLogWarnings = false;
|
||||
MorphController->RegisterComponent();
|
||||
|
||||
LipSyncComponent->MorphController = MorphController;
|
||||
LipSyncComponent->Mapping = ResolveLipSyncPreviewMapping();
|
||||
LipSyncComponent->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
|
||||
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
|
||||
: ELipSyncAudioDrivenMode::TranscriptVisemes;
|
||||
LipSyncComponent->AudioDrivenTranscript = LipSyncPreviewTranscript;
|
||||
LipSyncComponent->bAutoResolveMorphController = false;
|
||||
LipSyncComponent->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
|
||||
LipSyncComponent->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
|
||||
LipSyncComponent->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
|
||||
LipSyncComponent->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
|
||||
LipSyncComponent->bUseFallbackPulseWithoutEnvelope = false;
|
||||
LipSyncComponent->RegisterComponent();
|
||||
|
||||
LipSyncPreviewActor = PreviewActor;
|
||||
LipSyncPreviewMorphController = MorphController;
|
||||
LipSyncPreviewComponent = LipSyncComponent;
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::DestroyLipSyncPreviewComponents()
|
||||
{
|
||||
if (ULipSyncComponent* LipSyncComponent = LipSyncPreviewComponent.Get())
|
||||
{
|
||||
LipSyncComponent->StopAudioDrivenLipSync();
|
||||
LipSyncComponent->DestroyComponent();
|
||||
}
|
||||
|
||||
if (UMorphControllerComponent* MorphController = LipSyncPreviewMorphController.Get())
|
||||
{
|
||||
MorphController->DestroyComponent();
|
||||
}
|
||||
|
||||
if (AActor* PreviewActor = LipSyncPreviewActor.Get())
|
||||
{
|
||||
PreviewActor->Destroy();
|
||||
}
|
||||
|
||||
LipSyncPreviewComponent.Reset();
|
||||
LipSyncPreviewMorphController.Reset();
|
||||
LipSyncPreviewActor.Reset();
|
||||
}
|
||||
|
||||
void FMorphPresetEditorController::TickLipSyncPreviewComponents(float DeltaTime)
|
||||
{
|
||||
if (!LipSyncPreviewComponent.IsValid() || !LipSyncPreviewMorphController.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LipSyncPreviewComponent->TickComponent(DeltaTime, LEVELTICK_All, nullptr);
|
||||
LipSyncPreviewMorphController->UpdateMorphTargetsNow(DeltaTime);
|
||||
}
|
||||
|
||||
ULipSyncMappingDataAsset* FMorphPresetEditorController::ResolveLipSyncPreviewMapping() const
|
||||
{
|
||||
if (ULipSyncMappingDataAsset* SelectedMapping = LipSyncPreviewMapping.Get())
|
||||
{
|
||||
return SelectedMapping;
|
||||
}
|
||||
|
||||
return DefaultLipSyncPreviewMapping.Get();
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/MorphPresetEditorWindow.h"
|
||||
#include "Morph/MorphPresetDataAsset.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/SWindow.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Input/SMultiLineEditableTextBox.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "PropertyCustomizationHelpers.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Curves/RichCurve.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "MorphPresetEditor"
|
||||
|
||||
namespace
|
||||
{
|
||||
FString CurveInterpToString(ERichCurveInterpMode InterpMode)
|
||||
{
|
||||
switch (InterpMode)
|
||||
{
|
||||
case RCIM_Constant:
|
||||
return TEXT("constant");
|
||||
case RCIM_Cubic:
|
||||
return TEXT("ease");
|
||||
case RCIM_Linear:
|
||||
default:
|
||||
return TEXT("linear");
|
||||
}
|
||||
}
|
||||
|
||||
FString CurveInterpToUnrealString(ERichCurveInterpMode InterpMode)
|
||||
{
|
||||
switch (InterpMode)
|
||||
{
|
||||
case RCIM_Constant:
|
||||
return TEXT("RCIM_Constant");
|
||||
case RCIM_Cubic:
|
||||
return TEXT("RCIM_Cubic");
|
||||
case RCIM_Linear:
|
||||
default:
|
||||
return TEXT("RCIM_Linear");
|
||||
}
|
||||
}
|
||||
|
||||
ERichCurveInterpMode ParseCurveInterp(const TSharedPtr<FJsonValue>& Value)
|
||||
{
|
||||
FString InterpName;
|
||||
if (!Value.IsValid() || !Value->TryGetString(InterpName))
|
||||
{
|
||||
return RCIM_Linear;
|
||||
}
|
||||
|
||||
InterpName = InterpName.ToLower();
|
||||
if (InterpName == TEXT("constant") || InterpName == TEXT("step"))
|
||||
{
|
||||
return RCIM_Constant;
|
||||
}
|
||||
if (InterpName == TEXT("ease") || InterpName == TEXT("cubic") || InterpName == TEXT("auto") || InterpName == TEXT("rcim_cubic"))
|
||||
{
|
||||
return RCIM_Cubic;
|
||||
}
|
||||
return RCIM_Linear;
|
||||
}
|
||||
|
||||
FKeyHandle FindAdjacentKeyHandle(const FRichCurve& Curve, FKeyHandle KeyHandle, bool bFindNext)
|
||||
{
|
||||
const float KeyTime = Curve.GetKeyTime(KeyHandle);
|
||||
FKeyHandle BestHandle;
|
||||
float BestDistance = TNumericLimits<float>::Max();
|
||||
for (auto It = Curve.GetKeyHandleIterator(); It; ++It)
|
||||
{
|
||||
const FKeyHandle CandidateHandle = *It;
|
||||
if (CandidateHandle == KeyHandle)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float CandidateTime = Curve.GetKeyTime(CandidateHandle);
|
||||
const float Distance = bFindNext ? CandidateTime - KeyTime : KeyTime - CandidateTime;
|
||||
if (Distance > 0.0f && Distance < BestDistance)
|
||||
{
|
||||
BestDistance = Distance;
|
||||
BestHandle = CandidateHandle;
|
||||
}
|
||||
}
|
||||
|
||||
return BestHandle;
|
||||
}
|
||||
|
||||
void ApplyVisibleEaseTangents(FRichCurve& Curve, FKeyHandle KeyHandle)
|
||||
{
|
||||
FKeyHandle SegmentStartHandle = KeyHandle;
|
||||
FKeyHandle SegmentEndHandle = FindAdjacentKeyHandle(Curve, SegmentStartHandle, true);
|
||||
if (!SegmentEndHandle.IsValid())
|
||||
{
|
||||
SegmentEndHandle = SegmentStartHandle;
|
||||
SegmentStartHandle = FindAdjacentKeyHandle(Curve, SegmentEndHandle, false);
|
||||
}
|
||||
if (!SegmentStartHandle.IsValid() || !SegmentEndHandle.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Curve.SetKeyTangentMode(SegmentStartHandle, RCTM_Break, false);
|
||||
Curve.SetKeyTangentMode(SegmentEndHandle, RCTM_Break, false);
|
||||
Curve.GetKey(SegmentStartHandle).LeaveTangent = 0.0f;
|
||||
Curve.GetKey(SegmentEndHandle).ArriveTangent = 0.0f;
|
||||
}
|
||||
|
||||
void WriteCurveKeys(const FMorphTimelineClip& Clip, const TSharedPtr<FJsonObject>& Obj)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> KeyArray;
|
||||
if (const FRichCurve* Curve = Clip.WeightCurve.GetRichCurveConst())
|
||||
{
|
||||
for (auto It = Curve->GetKeyHandleIterator(); It; ++It)
|
||||
{
|
||||
const FKeyHandle KeyHandle = *It;
|
||||
TSharedPtr<FJsonObject> KeyObj = MakeShared<FJsonObject>();
|
||||
KeyObj->SetNumberField(TEXT("time"), Curve->GetKeyTime(KeyHandle));
|
||||
KeyObj->SetNumberField(TEXT("value"), Curve->GetKeyValue(KeyHandle));
|
||||
KeyObj->SetStringField(TEXT("interp"), CurveInterpToString(Curve->GetKeyInterpMode(KeyHandle)));
|
||||
KeyObj->SetStringField(TEXT("interpMode"), CurveInterpToUnrealString(Curve->GetKeyInterpMode(KeyHandle)));
|
||||
KeyObj->SetStringField(TEXT("tangentMode"), TEXT("RCTM_Auto"));
|
||||
KeyObj->SetStringField(TEXT("tangentWeightMode"), TEXT("RCTWM_WeightedNone"));
|
||||
KeyObj->SetNumberField(TEXT("arriveTangent"), 0.0);
|
||||
KeyObj->SetNumberField(TEXT("arriveTangentWeight"), 0.0);
|
||||
KeyObj->SetNumberField(TEXT("leaveTangent"), 0.0);
|
||||
KeyObj->SetNumberField(TEXT("leaveTangentWeight"), 0.0);
|
||||
KeyArray.Add(MakeShared<FJsonValueObject>(KeyObj));
|
||||
}
|
||||
}
|
||||
|
||||
Obj->SetArrayField(TEXT("curve_keys"), KeyArray);
|
||||
Obj->SetArrayField(TEXT("curveKeys"), KeyArray);
|
||||
|
||||
TSharedPtr<FJsonObject> EditorCurveDataObj = MakeShared<FJsonObject>();
|
||||
EditorCurveDataObj->SetArrayField(TEXT("keys"), KeyArray);
|
||||
EditorCurveDataObj->SetNumberField(TEXT("defaultValue"), TNumericLimits<float>::Max());
|
||||
EditorCurveDataObj->SetStringField(TEXT("preInfinityExtrap"), TEXT("RCCE_Constant"));
|
||||
EditorCurveDataObj->SetStringField(TEXT("postInfinityExtrap"), TEXT("RCCE_Constant"));
|
||||
|
||||
TSharedPtr<FJsonObject> WeightCurveObj = MakeShared<FJsonObject>();
|
||||
WeightCurveObj->SetObjectField(TEXT("editorCurveData"), EditorCurveDataObj);
|
||||
WeightCurveObj->SetStringField(TEXT("externalCurve"), TEXT("None"));
|
||||
Obj->SetObjectField(TEXT("weightCurve"), WeightCurveObj);
|
||||
}
|
||||
|
||||
float ParseCurveNumber(const TSharedPtr<FJsonValue>& Value, float DefaultValue)
|
||||
{
|
||||
double Number = 0.0;
|
||||
return Value.IsValid() && Value->TryGetNumber(Number) ? static_cast<float>(Number) : DefaultValue;
|
||||
}
|
||||
|
||||
void ReadCurveKeys(const TSharedPtr<FJsonObject>& Obj, FMorphTimelineClip& Clip)
|
||||
{
|
||||
if (!Obj.IsValid())
|
||||
{
|
||||
Clip.EnsureDefaultCurve();
|
||||
return;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* KeyArray = nullptr;
|
||||
if ((!Obj->TryGetArrayField(TEXT("curve_keys"), KeyArray) || !KeyArray) &&
|
||||
(!Obj->TryGetArrayField(TEXT("curveKeys"), KeyArray) || !KeyArray))
|
||||
{
|
||||
if (const TSharedPtr<FJsonObject>* WeightCurveObj = nullptr; Obj->TryGetObjectField(TEXT("weightCurve"), WeightCurveObj) && WeightCurveObj->IsValid())
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* EditorCurveDataObj = nullptr;
|
||||
if (!(*WeightCurveObj)->TryGetObjectField(TEXT("editorCurveData"), EditorCurveDataObj) || !EditorCurveDataObj || !EditorCurveDataObj->IsValid() ||
|
||||
!(*EditorCurveDataObj)->TryGetArrayField(TEXT("keys"), KeyArray) || !KeyArray)
|
||||
{
|
||||
Clip.EnsureDefaultCurve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Clip.EnsureDefaultCurve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FRichCurve* Curve = Clip.WeightCurve.GetRichCurve();
|
||||
if (!Curve)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Curve->Reset();
|
||||
TArray<FKeyHandle> EaseKeyHandles;
|
||||
for (const TSharedPtr<FJsonValue>& KeyValue : *KeyArray)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* KeyObj = nullptr;
|
||||
if (!KeyValue.IsValid() || !KeyValue->TryGetObject(KeyObj) || !KeyObj || !KeyObj->IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Time = FMath::Clamp(ParseCurveNumber((*KeyObj)->TryGetField(TEXT("time")), 0.0f), 0.0f, FMath::Max(Clip.Duration, 0.001f));
|
||||
const float Value = FMath::Clamp(ParseCurveNumber((*KeyObj)->TryGetField(TEXT("value")), 0.0f), 0.0f, 1.0f);
|
||||
const ERichCurveInterpMode InterpMode = ParseCurveInterp((*KeyObj)->TryGetField(TEXT("interp")).IsValid()
|
||||
? (*KeyObj)->TryGetField(TEXT("interp"))
|
||||
: (*KeyObj)->TryGetField(TEXT("interpMode")));
|
||||
const FKeyHandle KeyHandle = Curve->UpdateOrAddKey(Time, Value);
|
||||
Curve->SetKeyInterpMode(KeyHandle, InterpMode, false);
|
||||
if (InterpMode == RCIM_Cubic)
|
||||
{
|
||||
EaseKeyHandles.Add(KeyHandle);
|
||||
}
|
||||
}
|
||||
|
||||
for (const FKeyHandle KeyHandle : EaseKeyHandles)
|
||||
{
|
||||
ApplyVisibleEaseTangents(*Curve, KeyHandle);
|
||||
}
|
||||
|
||||
Clip.EnsureDefaultCurve();
|
||||
}
|
||||
}
|
||||
|
||||
void SMorphPresetEditorWindow::Construct(const FArguments& InArgs)
|
||||
{
|
||||
if (UMorphPresetDataAsset* PresetArg = InArgs._Preset)
|
||||
{
|
||||
SelectedPreset = PresetArg;
|
||||
bEmbeddedMode = true;
|
||||
}
|
||||
ChildSlot
|
||||
[
|
||||
BuildContent()
|
||||
];
|
||||
if (bEmbeddedMode && SelectedPreset.IsValid())
|
||||
{
|
||||
RefreshFromPreset();
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> SMorphPresetEditorWindow::BuildContent()
|
||||
{
|
||||
TSharedRef<SVerticalBox> Root = SNew(SVerticalBox);
|
||||
|
||||
if (!bEmbeddedMode)
|
||||
{
|
||||
Root->AddSlot()
|
||||
.AutoHeight()
|
||||
.Padding(4.0f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(0, 0, 8, 0)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("PresetLabel", "Morph Preset:"))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.0f)
|
||||
[
|
||||
SAssignNew(PresetPicker, SObjectPropertyEntryBox)
|
||||
.AllowedClass(UMorphPresetDataAsset::StaticClass())
|
||||
.ObjectPath(this, &SMorphPresetEditorWindow::GetSelectedPresetPath)
|
||||
.OnObjectChanged(this, &SMorphPresetEditorWindow::OnPresetChanged)
|
||||
.DisplayUseSelected(true)
|
||||
.DisplayBrowse(true)
|
||||
.AllowClear(true)
|
||||
.AllowCreate(false)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
Root->AddSlot()
|
||||
.FillHeight(1.0f)
|
||||
.Padding(4.0f)
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("JsonLabel", "JSON (структурированный формат):"))
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.0f)
|
||||
.Padding(0, 4)
|
||||
[
|
||||
SAssignNew(JsonTextBox, SMultiLineEditableTextBox)
|
||||
.HintText(LOCTEXT("JsonHint",
|
||||
"{\n"
|
||||
" \"metadata\": { \"display_name\": \"Моргание\", \"description\": \"Полное моргание\" },\n"
|
||||
" \"settings\": { \"duration\": 0.15, \"loop\": true, \"loop_delay\": 2.5, \"blend_speed\": 18 },\n"
|
||||
" \"morph_targets\": [\n"
|
||||
" { \"name\": \"eyeBlinkLeft\", \"start_time\": 0, \"duration\": 0.15, \"weight\": 1.5, \"curve_keys\": [{ \"time\": 0, \"value\": 0, \"interp\": \"ease\" }, { \"time\": 0.15, \"value\": 0, \"interp\": \"ease\" }] },\n"
|
||||
" { \"name\": \"eyeBlinkRight\", \"start_time\": 0, \"duration\": 0.15, \"weight\": 1.5, \"curve_keys\": [{ \"time\": 0, \"value\": 0, \"interp\": \"ease\" }, { \"time\": 0.15, \"value\": 0, \"interp\": \"ease\" }] }\n"
|
||||
" ]\n"
|
||||
"}"))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Mono", 10))
|
||||
]
|
||||
];
|
||||
|
||||
Root->AddSlot()
|
||||
.AutoHeight()
|
||||
.Padding(4.0f)
|
||||
.HAlign(HAlign_Right)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0, 0, 8, 0)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("ExportButton", "Export"))
|
||||
.OnClicked(this, &SMorphPresetEditorWindow::OnExportClicked)
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("ApplyButton", "Apply"))
|
||||
.OnClicked(this, &SMorphPresetEditorWindow::OnApplyClicked)
|
||||
]
|
||||
];
|
||||
|
||||
return Root;
|
||||
}
|
||||
|
||||
void SMorphPresetEditorWindow::RefreshFromPreset()
|
||||
{
|
||||
if (!SelectedPreset.IsValid() || !JsonTextBox.IsValid()) return;
|
||||
TSharedPtr<FJsonObject> RootObj = MakeShared<FJsonObject>();
|
||||
RootObj->SetStringField(TEXT("displayName"), SelectedPreset->DisplayName.ToString());
|
||||
RootObj->SetStringField(TEXT("description"), SelectedPreset->Description.ToString());
|
||||
TArray<TSharedPtr<FJsonValue>> MorphArr;
|
||||
for (const FMorphTimelineClip& Clip : SelectedPreset->MorphTargets)
|
||||
{
|
||||
if (!Clip.IsValid()) continue;
|
||||
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
||||
Obj->SetStringField(TEXT("name"), Clip.Name.ToString());
|
||||
Obj->SetNumberField(TEXT("startTime"), Clip.StartTime);
|
||||
Obj->SetNumberField(TEXT("duration"), Clip.Duration);
|
||||
Obj->SetNumberField(TEXT("weight"), Clip.Weight);
|
||||
WriteCurveKeys(Clip, Obj);
|
||||
MorphArr.Add(MakeShared<FJsonValueObject>(Obj));
|
||||
}
|
||||
RootObj->SetArrayField(TEXT("morphTargets"), MorphArr);
|
||||
RootObj->SetNumberField(TEXT("duration"), SelectedPreset->Duration);
|
||||
RootObj->SetBoolField(TEXT("bLoop"), SelectedPreset->bLoop);
|
||||
RootObj->SetNumberField(TEXT("loopDelay"), SelectedPreset->LoopDelay);
|
||||
RootObj->SetNumberField(TEXT("blendSpeedOverride"), SelectedPreset->BlendSpeedOverride);
|
||||
RootObj->SetStringField(TEXT("nativeClass"), TEXT("/Script/CoreUObject.Class'/Script/FunnyGirl.MorphPresetDataAsset'"));
|
||||
FString OutputString;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString, 0);
|
||||
FJsonSerializer::Serialize(RootObj.ToSharedRef(), Writer);
|
||||
JsonTextBox->SetText(FText::FromString(OutputString));
|
||||
}
|
||||
|
||||
FString SMorphPresetEditorWindow::GetSelectedPresetPath() const
|
||||
{
|
||||
if (UMorphPresetDataAsset* Preset = SelectedPreset.Get())
|
||||
{
|
||||
return Preset->GetPathName();
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
void SMorphPresetEditorWindow::OnPresetChanged(const FAssetData& AssetData)
|
||||
{
|
||||
SelectedPreset = Cast<UMorphPresetDataAsset>(AssetData.GetAsset());
|
||||
}
|
||||
|
||||
FReply SMorphPresetEditorWindow::OnApplyClicked()
|
||||
{
|
||||
UMorphPresetDataAsset* Preset = SelectedPreset.Get();
|
||||
if (!Preset)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: Select a Morph Preset first."));
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FString JsonString = JsonTextBox->GetText().ToString();
|
||||
if (JsonString.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: JSON is empty."));
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
TSharedPtr<FJsonObject> JsonObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
|
||||
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("Morph Preset Editor: Invalid JSON: %s"), *Reader->GetErrorMessage());
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
// Структурированный парсинг: metadata, settings, morph_targets
|
||||
// Поддержка обратной совместимости: плоский формат (все поля в корне)
|
||||
auto ParseFloat = [](const TSharedPtr<FJsonValue>& Val, float Default) -> float
|
||||
{
|
||||
double Num = 0.0;
|
||||
return Val.IsValid() && Val->TryGetNumber(Num) ? static_cast<float>(FMath::Max(0.0, Num)) : Default;
|
||||
};
|
||||
auto ParseBool = [](const TSharedPtr<FJsonValue>& Val, bool Default) -> bool
|
||||
{
|
||||
bool b = false;
|
||||
if (Val.IsValid() && Val->TryGetBool(b)) return b;
|
||||
double Num = 0.0;
|
||||
if (Val.IsValid() && Val->TryGetNumber(Num)) return Num != 0.0;
|
||||
return Default;
|
||||
};
|
||||
auto ParseInt = [](const TSharedPtr<FJsonValue>& Val, int32 Default) -> int32
|
||||
{
|
||||
double Num = 0.0;
|
||||
return Val.IsValid() && Val->TryGetNumber(Num) ? FMath::Max(0, static_cast<int32>(Num)) : Default;
|
||||
};
|
||||
auto ParseString = [](const TSharedPtr<FJsonValue>& Val) -> FString
|
||||
{
|
||||
FString S;
|
||||
return Val.IsValid() && Val->TryGetString(S) ? S : FString();
|
||||
};
|
||||
|
||||
TArray<FMorphTimelineClip> NewMorphTargets;
|
||||
|
||||
// metadata
|
||||
if (const TSharedPtr<FJsonObject>* MetaObj = nullptr; JsonObject->TryGetObjectField(TEXT("metadata"), MetaObj) && MetaObj->IsValid())
|
||||
{
|
||||
Preset->DisplayName = FText::FromString(ParseString((*MetaObj)->TryGetField(TEXT("display_name"))));
|
||||
Preset->Description = FText::FromString(ParseString((*MetaObj)->TryGetField(TEXT("description"))));
|
||||
}
|
||||
else
|
||||
{
|
||||
FString DisplayNameStr = ParseString(JsonObject->TryGetField(TEXT("displayName")));
|
||||
if (!DisplayNameStr.IsEmpty()) Preset->DisplayName = FText::FromString(DisplayNameStr);
|
||||
FString DescStr = ParseString(JsonObject->TryGetField(TEXT("description")));
|
||||
if (!DescStr.IsEmpty()) Preset->Description = FText::FromString(DescStr);
|
||||
}
|
||||
|
||||
// settings
|
||||
if (const TSharedPtr<FJsonObject>* SettingsObj = nullptr; JsonObject->TryGetObjectField(TEXT("settings"), SettingsObj) && SettingsObj->IsValid())
|
||||
{
|
||||
Preset->Duration = ParseFloat((*SettingsObj)->TryGetField(TEXT("duration")), Preset->Duration);
|
||||
Preset->bLoop = ParseBool((*SettingsObj)->TryGetField(TEXT("loop")), Preset->bLoop);
|
||||
Preset->LoopDelay = ParseFloat((*SettingsObj)->TryGetField(TEXT("loop_delay")), Preset->LoopDelay);
|
||||
Preset->BlendSpeedOverride = ParseFloat((*SettingsObj)->TryGetField(TEXT("blend_speed")), Preset->BlendSpeedOverride);
|
||||
}
|
||||
else
|
||||
{
|
||||
Preset->Duration = ParseFloat(JsonObject->TryGetField(TEXT("duration")), Preset->Duration);
|
||||
Preset->bLoop = ParseBool(JsonObject->TryGetField(TEXT("bLoop")), Preset->bLoop);
|
||||
Preset->LoopDelay = ParseFloat(JsonObject->TryGetField(TEXT("loopDelay")), Preset->LoopDelay);
|
||||
Preset->BlendSpeedOverride = ParseFloat(JsonObject->TryGetField(TEXT("blendSpeedOverride")), Preset->BlendSpeedOverride);
|
||||
}
|
||||
|
||||
// morph_targets: массив объектов { name, start_time, duration, weight, curve_keys? }
|
||||
const TArray<TSharedPtr<FJsonValue>>* MorphArr = nullptr;
|
||||
if ((JsonObject->TryGetArrayField(TEXT("morph_targets"), MorphArr) || JsonObject->TryGetArrayField(TEXT("morphTargets"), MorphArr)) && MorphArr)
|
||||
{
|
||||
for (const TSharedPtr<FJsonValue>& Val : *MorphArr)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* Obj = nullptr;
|
||||
if (Val->TryGetObject(Obj) && Obj->IsValid())
|
||||
{
|
||||
FString NameStr = ParseString((*Obj)->TryGetField(TEXT("name")));
|
||||
if (NameStr.IsEmpty()) continue;
|
||||
|
||||
FMorphTimelineClip Clip;
|
||||
Clip.Name = FName(*NameStr);
|
||||
Clip.StartTime = ParseFloat((*Obj)->TryGetField(TEXT("start_time")),
|
||||
ParseFloat((*Obj)->TryGetField(TEXT("startTime")), ParseFloat((*Obj)->TryGetField(TEXT("delay")), 0.0f)));
|
||||
Clip.Duration = ParseFloat((*Obj)->TryGetField(TEXT("duration")), 0.2f);
|
||||
if (Clip.Duration < 0.001f) Clip.Duration = 0.2f;
|
||||
Clip.Weight = ParseFloat((*Obj)->TryGetField(TEXT("weight")), 1.0f);
|
||||
Clip.Weight = FMath::Clamp(Clip.Weight, 0.0f, 5.0f);
|
||||
ReadCurveKeys(*Obj, Clip);
|
||||
NewMorphTargets.Add(Clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обратная совместимость: старый формат morph_targets как объект { "name": weight }
|
||||
if (NewMorphTargets.Num() == 0)
|
||||
{
|
||||
if (const TSharedPtr<FJsonObject>* MorphObj = nullptr; JsonObject->TryGetObjectField(TEXT("morph_targets"), MorphObj) && MorphObj->IsValid())
|
||||
{
|
||||
for (const auto& Pair : (*MorphObj)->Values)
|
||||
{
|
||||
double NumValue = 0.0;
|
||||
if (Pair.Value->TryGetNumber(NumValue))
|
||||
{
|
||||
FMorphTimelineClip Clip;
|
||||
Clip.Name = FName(*Pair.Key);
|
||||
Clip.StartTime = 0.0f;
|
||||
Clip.Duration = 0.2f;
|
||||
Clip.Weight = static_cast<float>(FMath::Clamp(NumValue, 0.0, 5.0));
|
||||
Clip.EnsureDefaultCurve();
|
||||
NewMorphTargets.Add(Clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Старый morph_target_settings (если morph_targets был объектом)
|
||||
if (const TSharedPtr<FJsonObject>* SettingsMap = nullptr; JsonObject->TryGetObjectField(TEXT("morph_target_settings"), SettingsMap) && SettingsMap->IsValid())
|
||||
{
|
||||
for (const auto& Pair : (*SettingsMap)->Values)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* TimingObj = nullptr;
|
||||
if (Pair.Value->TryGetObject(TimingObj) && TimingObj->IsValid())
|
||||
{
|
||||
FName MorphName(*Pair.Key);
|
||||
for (FMorphTimelineClip& Clip : NewMorphTargets)
|
||||
{
|
||||
if (Clip.Name == MorphName)
|
||||
{
|
||||
Clip.StartTime = ParseFloat((*TimingObj)->TryGetField(TEXT("delay")), 0.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Плоский формат (поля в корне) — если morph_targets не найден
|
||||
if (NewMorphTargets.Num() == 0)
|
||||
{
|
||||
for (const auto& Pair : JsonObject->Values)
|
||||
{
|
||||
const FString& Key = Pair.Key;
|
||||
const TSharedPtr<FJsonValue>& Value = Pair.Value;
|
||||
if (Key.Equals(TEXT("metadata"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("settings"), ESearchCase::IgnoreCase) ||
|
||||
Key.Equals(TEXT("morph_targets"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("morph_target_settings"), ESearchCase::IgnoreCase) ||
|
||||
Key.Equals(TEXT("duration"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("loop"), ESearchCase::IgnoreCase) ||
|
||||
Key.Equals(TEXT("loop_delay"), ESearchCase::IgnoreCase) ||
|
||||
Key.Equals(TEXT("blend_speed"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("display_name"), ESearchCase::IgnoreCase) ||
|
||||
Key.Equals(TEXT("description"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("morphTargets"), ESearchCase::IgnoreCase) ||
|
||||
Key.Equals(TEXT("bLoop"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("loopDelay"), ESearchCase::IgnoreCase) ||
|
||||
Key.Equals(TEXT("blendSpeedOverride"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("displayName"), ESearchCase::IgnoreCase) ||
|
||||
Key.Equals(TEXT("nativeClass"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double NumValue = 0.0;
|
||||
if (Value->TryGetNumber(NumValue))
|
||||
{
|
||||
FMorphTimelineClip Clip;
|
||||
Clip.Name = FName(*Key);
|
||||
Clip.StartTime = 0.0f;
|
||||
Clip.Duration = 0.2f;
|
||||
Clip.Weight = static_cast<float>(FMath::Clamp(NumValue, 0.0, 5.0));
|
||||
Clip.EnsureDefaultCurve();
|
||||
NewMorphTargets.Add(Clip);
|
||||
}
|
||||
}
|
||||
Preset->Duration = ParseFloat(JsonObject->TryGetField(TEXT("duration")), Preset->Duration);
|
||||
Preset->bLoop = ParseBool(JsonObject->TryGetField(TEXT("loop")), Preset->bLoop);
|
||||
Preset->LoopDelay = ParseFloat(JsonObject->TryGetField(TEXT("loop_delay")), Preset->LoopDelay);
|
||||
Preset->BlendSpeedOverride = ParseFloat(JsonObject->TryGetField(TEXT("blend_speed")), Preset->BlendSpeedOverride);
|
||||
FString DisplayNameStr = ParseString(JsonObject->TryGetField(TEXT("display_name")));
|
||||
if (!DisplayNameStr.IsEmpty()) Preset->DisplayName = FText::FromString(DisplayNameStr);
|
||||
FString DescStr = ParseString(JsonObject->TryGetField(TEXT("description")));
|
||||
if (!DescStr.IsEmpty()) Preset->Description = FText::FromString(DescStr);
|
||||
}
|
||||
|
||||
Preset->MorphTargets = MoveTemp(NewMorphTargets);
|
||||
Preset->MarkPackageDirty();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("Morph Preset Editor: Applied %d morph targets to %s"), Preset->GetMorphCount(), *Preset->GetName());
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SMorphPresetEditorWindow::OnExportClicked()
|
||||
{
|
||||
UMorphPresetDataAsset* Preset = SelectedPreset.Get();
|
||||
if (!Preset)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: Select a Morph Preset first."));
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObj = MakeShared<FJsonObject>();
|
||||
RootObj->SetStringField(TEXT("displayName"), Preset->DisplayName.ToString());
|
||||
RootObj->SetStringField(TEXT("description"), Preset->Description.ToString());
|
||||
|
||||
// morph_targets — массив объектов (timeline clips)
|
||||
TArray<TSharedPtr<FJsonValue>> MorphArr;
|
||||
for (const FMorphTimelineClip& Clip : Preset->MorphTargets)
|
||||
{
|
||||
if (!Clip.IsValid()) continue;
|
||||
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
||||
Obj->SetStringField(TEXT("name"), Clip.Name.ToString());
|
||||
Obj->SetNumberField(TEXT("startTime"), Clip.StartTime);
|
||||
Obj->SetNumberField(TEXT("duration"), Clip.Duration);
|
||||
Obj->SetNumberField(TEXT("weight"), Clip.Weight);
|
||||
WriteCurveKeys(Clip, Obj);
|
||||
MorphArr.Add(MakeShared<FJsonValueObject>(Obj));
|
||||
}
|
||||
RootObj->SetArrayField(TEXT("morphTargets"), MorphArr);
|
||||
RootObj->SetNumberField(TEXT("duration"), Preset->Duration);
|
||||
RootObj->SetBoolField(TEXT("bLoop"), Preset->bLoop);
|
||||
RootObj->SetNumberField(TEXT("loopDelay"), Preset->LoopDelay);
|
||||
RootObj->SetNumberField(TEXT("blendSpeedOverride"), Preset->BlendSpeedOverride);
|
||||
RootObj->SetStringField(TEXT("nativeClass"), TEXT("/Script/CoreUObject.Class'/Script/FunnyGirl.MorphPresetDataAsset'"));
|
||||
|
||||
FString OutputString;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString, 0);
|
||||
FJsonSerializer::Serialize(RootObj.ToSharedRef(), Writer);
|
||||
|
||||
JsonTextBox->SetText(FText::FromString(OutputString));
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "Morph/Editor/MorphPresetViewportPanel.h"
|
||||
|
||||
#include "AssetRegistry/AssetData.h"
|
||||
#include "AdvancedPreviewScene.h"
|
||||
#include "Animation/DebugSkelMeshComponent.h"
|
||||
#include "EditorViewportClient.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "IPersonaPreviewScene.h"
|
||||
#include "Morph/LipSyncMappingDataAsset.h"
|
||||
#include "Morph/Editor/MorphPresetEditorController.h"
|
||||
#include "PropertyCustomizationHelpers.h"
|
||||
#include "SEditorViewport.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
#include "Toolkits/AssetEditorToolkit.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Input/SEditableTextBox.h"
|
||||
#include "Widgets/Input/SSlider.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "MorphPresetViewportPanel"
|
||||
|
||||
namespace
|
||||
{
|
||||
static constexpr float MinFaceCameraDistance = 90.0f;
|
||||
static constexpr float MaxFaceCameraDistance = 220.0f;
|
||||
|
||||
FName FindHeadBoneName(const UDebugSkelMeshComponent* PreviewMeshComponent)
|
||||
{
|
||||
if (!PreviewMeshComponent)
|
||||
{
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
static const FName CandidateBoneNames[] =
|
||||
{
|
||||
TEXT("head"),
|
||||
TEXT("Head"),
|
||||
TEXT("head_jnt"),
|
||||
TEXT("Head_jnt")
|
||||
};
|
||||
|
||||
for (const FName CandidateBoneName : CandidateBoneNames)
|
||||
{
|
||||
if (PreviewMeshComponent->GetBoneIndex(CandidateBoneName) != INDEX_NONE)
|
||||
{
|
||||
return CandidateBoneName;
|
||||
}
|
||||
}
|
||||
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
FVector ResolveFaceForwardDirection(const FTransform& HeadTransform)
|
||||
{
|
||||
const FVector PreferredDirection = FVector::YAxisVector;
|
||||
const FVector CandidateDirections[] =
|
||||
{
|
||||
HeadTransform.GetUnitAxis(EAxis::X),
|
||||
-HeadTransform.GetUnitAxis(EAxis::X),
|
||||
HeadTransform.GetUnitAxis(EAxis::Y),
|
||||
-HeadTransform.GetUnitAxis(EAxis::Y)
|
||||
};
|
||||
|
||||
float BestScore = -FLT_MAX;
|
||||
FVector BestDirection = PreferredDirection;
|
||||
for (const FVector& CandidateDirection : CandidateDirections)
|
||||
{
|
||||
FVector FlattenedDirection = FVector(CandidateDirection.X, CandidateDirection.Y, 0.0f);
|
||||
if (!FlattenedDirection.Normalize())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Score = FVector::DotProduct(FlattenedDirection, PreferredDirection);
|
||||
if (Score > BestScore)
|
||||
{
|
||||
BestScore = Score;
|
||||
BestDirection = FlattenedDirection;
|
||||
}
|
||||
}
|
||||
|
||||
return BestDirection;
|
||||
}
|
||||
|
||||
void ResolveDefaultCamera(const TSharedPtr<IPersonaPreviewScene>& PreviewScene, FVector& OutCameraLocation, FVector& OutCameraTarget)
|
||||
{
|
||||
OutCameraTarget = FVector(0.0f, 0.0f, 80.0f);
|
||||
FVector FaceForwardDirection = FVector::YAxisVector;
|
||||
float CameraDistance = 165.0f;
|
||||
|
||||
if (PreviewScene.IsValid())
|
||||
{
|
||||
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
|
||||
{
|
||||
const FBoxSphereBounds Bounds = PreviewMeshComponent->Bounds;
|
||||
CameraDistance = FMath::Clamp(FMath::Max(Bounds.BoxExtent.X, Bounds.BoxExtent.Y) * 1.35f, MinFaceCameraDistance, MaxFaceCameraDistance);
|
||||
OutCameraTarget = Bounds.Origin + FVector(0.0f, 0.0f, Bounds.BoxExtent.Z * 0.62f);
|
||||
|
||||
const FName HeadBoneName = FindHeadBoneName(PreviewMeshComponent);
|
||||
if (HeadBoneName != NAME_None)
|
||||
{
|
||||
const FTransform HeadTransform = PreviewMeshComponent->GetSocketTransform(HeadBoneName, RTS_World);
|
||||
OutCameraTarget = HeadTransform.GetLocation() + FVector(0.0f, 0.0f, 4.0f);
|
||||
FaceForwardDirection = ResolveFaceForwardDirection(HeadTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutCameraLocation = OutCameraTarget + FaceForwardDirection * CameraDistance + FVector(0.0f, 0.0f, 8.0f);
|
||||
}
|
||||
|
||||
class SMorphPresetPreviewViewport : public SEditorViewport
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SMorphPresetPreviewViewport) {}
|
||||
SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller)
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments& InArgs)
|
||||
{
|
||||
Controller = InArgs._Controller;
|
||||
SEditorViewport::Construct(SEditorViewport::FArguments());
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<FEditorViewportClient> MakeEditorViewportClient() override
|
||||
{
|
||||
TSharedPtr<IPersonaPreviewScene> PreviewScene = Controller.IsValid() ? Controller->GetPreviewScene() : nullptr;
|
||||
check(PreviewScene.IsValid());
|
||||
|
||||
TSharedRef<FEditorViewportClient> ViewportClient = MakeShared<FEditorViewportClient>(nullptr, static_cast<FPreviewScene*>(PreviewScene.Get()), SharedThis(this));
|
||||
FVector CameraLocation = FVector::ZeroVector;
|
||||
FVector CameraTarget = FVector::ZeroVector;
|
||||
ResolveDefaultCamera(PreviewScene, CameraLocation, CameraTarget);
|
||||
ViewportClient->SetViewLocation(CameraLocation);
|
||||
ViewportClient->SetViewRotation((CameraTarget - CameraLocation).Rotation());
|
||||
ViewportClient->SetLookAtLocation(CameraTarget);
|
||||
ViewportClient->SetViewModes(VMI_Lit, VMI_Lit);
|
||||
ViewportClient->SetRealtime(true);
|
||||
return ViewportClient;
|
||||
}
|
||||
|
||||
virtual TSharedPtr<SWidget> BuildViewportToolbar() override
|
||||
{
|
||||
return SNullWidget::NullWidget;
|
||||
}
|
||||
|
||||
private:
|
||||
TSharedPtr<FMorphPresetEditorController> Controller;
|
||||
};
|
||||
}
|
||||
|
||||
void SMorphPresetViewportPanel::Construct(const FArguments& InArgs)
|
||||
{
|
||||
Controller = InArgs._Controller;
|
||||
AssetEditorToolkit = InArgs._AssetEditorToolkit;
|
||||
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->OnPreviewSceneChanged().AddSP(this, &SMorphPresetViewportPanel::RebuildContent);
|
||||
Controller->OnPresetChanged().AddSP(this, &SMorphPresetViewportPanel::RebuildContent);
|
||||
}
|
||||
|
||||
ChildSlot
|
||||
[
|
||||
SAssignNew(RootBorder, SBorder)
|
||||
.Padding(8.0f)
|
||||
];
|
||||
|
||||
RebuildContent();
|
||||
RegisterActiveTimer(0.0f, FWidgetActiveTimerDelegate::CreateSP(this, &SMorphPresetViewportPanel::HandleActiveTimer));
|
||||
}
|
||||
|
||||
SMorphPresetViewportPanel::~SMorphPresetViewportPanel()
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->OnPreviewSceneChanged().RemoveAll(this);
|
||||
Controller->OnPresetChanged().RemoveAll(this);
|
||||
}
|
||||
}
|
||||
|
||||
void SMorphPresetViewportPanel::RebuildContent()
|
||||
{
|
||||
if (!RootBorder.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Controller.IsValid() || !Controller->GetPreviewScene().IsValid())
|
||||
{
|
||||
RootBorder->SetContent(
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("PreviewMeshLabel", "Preview Mesh"))
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(SObjectPropertyEntryBox)
|
||||
.AllowedClass(USkeletalMesh::StaticClass())
|
||||
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedPreviewMeshPath)
|
||||
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnPreviewMeshChanged)
|
||||
.DisplayUseSelected(true)
|
||||
.DisplayBrowse(true)
|
||||
.AllowClear(true)
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.0f)
|
||||
.VAlign(VAlign_Center)
|
||||
.HAlign(HAlign_Center)
|
||||
.Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("NoPreviewMesh", "Assign a skeletal mesh to see the Morph Target preset in the viewport."))
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
RootBorder->SetContent(
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("LipSyncMappingLabel", "Lip Sync Mapping"))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.0f)
|
||||
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
|
||||
[
|
||||
SNew(SObjectPropertyEntryBox)
|
||||
.AllowedClass(ULipSyncMappingDataAsset::StaticClass())
|
||||
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedLipSyncMappingPath)
|
||||
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnLipSyncMappingChanged)
|
||||
.DisplayUseSelected(true)
|
||||
.DisplayBrowse(true)
|
||||
.AllowClear(true)
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("LipSyncAudioLabel", "Lip Sync Audio"))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.0f)
|
||||
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||
[
|
||||
SNew(SObjectPropertyEntryBox)
|
||||
.AllowedClass(USoundBase::StaticClass())
|
||||
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedLipSyncSoundPath)
|
||||
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnLipSyncSoundChanged)
|
||||
.DisplayUseSelected(true)
|
||||
.DisplayBrowse(true)
|
||||
.AllowClear(true)
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(this, &SMorphPresetViewportPanel::GetLipSyncPlaybackText)
|
||||
.OnClicked(this, &SMorphPresetViewportPanel::OnPlayLipSyncClicked)
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("StopLipSync", "Stop"))
|
||||
.OnClicked(this, &SMorphPresetViewportPanel::OnStopLipSyncClicked)
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(12.0f, 0.0f, 6.0f, 0.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("LipSyncSmoothLabel", "Smooth"))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SSlider)
|
||||
.MinValue(0.0f)
|
||||
.MaxValue(1.0f)
|
||||
.Value(this, &SMorphPresetViewportPanel::GetLipSyncSmoothness)
|
||||
.OnValueChanged(this, &SMorphPresetViewportPanel::OnLipSyncSmoothnessChanged)
|
||||
]
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
|
||||
[
|
||||
SNew(SEditableTextBox)
|
||||
.HintText(LOCTEXT("LipSyncTranscriptHint", "Paste audio transcript here for text-assisted visemes"))
|
||||
.Text(this, &SMorphPresetViewportPanel::GetLipSyncTranscript)
|
||||
.OnTextChanged(this, &SMorphPresetViewportPanel::OnLipSyncTranscriptChanged)
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(this, &SMorphPresetViewportPanel::GetPreviewStatusText)
|
||||
.Visibility(this, &SMorphPresetViewportPanel::GetPreviewStatusVisibility)
|
||||
.ColorAndOpacity(FSlateColor(FLinearColor(1.0f, 0.56f, 0.12f)))
|
||||
.AutoWrapText(true)
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.0f)
|
||||
[
|
||||
SNew(SMorphPresetPreviewViewport)
|
||||
.Controller(Controller)
|
||||
]);
|
||||
}
|
||||
|
||||
void SMorphPresetViewportPanel::OnPreviewMeshChanged(const FAssetData& AssetData)
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->SetPreviewMesh(Cast<USkeletalMesh>(AssetData.GetAsset()));
|
||||
}
|
||||
}
|
||||
|
||||
void SMorphPresetViewportPanel::OnLipSyncSoundChanged(const FAssetData& AssetData)
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->SetLipSyncPreviewSound(Cast<USoundBase>(AssetData.GetAsset()));
|
||||
}
|
||||
}
|
||||
|
||||
void SMorphPresetViewportPanel::OnLipSyncMappingChanged(const FAssetData& AssetData)
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->SetLipSyncPreviewMapping(Cast<ULipSyncMappingDataAsset>(AssetData.GetAsset()));
|
||||
}
|
||||
}
|
||||
|
||||
FString SMorphPresetViewportPanel::GetSelectedPreviewMeshPath() const
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
if (USkeletalMesh* Mesh = Controller->GetPreviewMesh())
|
||||
{
|
||||
return Mesh->GetPathName();
|
||||
}
|
||||
}
|
||||
|
||||
return FString();
|
||||
}
|
||||
|
||||
FString SMorphPresetViewportPanel::GetSelectedLipSyncSoundPath() const
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
if (USoundBase* Sound = Controller->GetLipSyncPreviewSound())
|
||||
{
|
||||
return Sound->GetPathName();
|
||||
}
|
||||
}
|
||||
|
||||
return FString();
|
||||
}
|
||||
|
||||
FString SMorphPresetViewportPanel::GetSelectedLipSyncMappingPath() const
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
if (ULipSyncMappingDataAsset* Mapping = Controller->GetLipSyncPreviewMapping())
|
||||
{
|
||||
return Mapping->GetPathName();
|
||||
}
|
||||
}
|
||||
|
||||
return FString();
|
||||
}
|
||||
|
||||
FReply SMorphPresetViewportPanel::OnPlayLipSyncClicked()
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
if (Controller->IsLipSyncPreviewAudioPlaying())
|
||||
{
|
||||
Controller->StopLipSyncPreviewAudio();
|
||||
}
|
||||
else
|
||||
{
|
||||
Controller->PlayLipSyncPreviewAudio();
|
||||
}
|
||||
}
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SMorphPresetViewportPanel::OnStopLipSyncClicked()
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->StopLipSyncPreviewAudio();
|
||||
}
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FText SMorphPresetViewportPanel::GetLipSyncPlaybackText() const
|
||||
{
|
||||
return Controller.IsValid() && Controller->IsLipSyncPreviewAudioPlaying()
|
||||
? LOCTEXT("PauseLipSync", "Pause Lip Sync")
|
||||
: LOCTEXT("PlayLipSync", "Play Lip Sync");
|
||||
}
|
||||
|
||||
float SMorphPresetViewportPanel::GetLipSyncSmoothness() const
|
||||
{
|
||||
return Controller.IsValid() ? Controller->GetLipSyncPreviewSmoothness() : 0.55f;
|
||||
}
|
||||
|
||||
void SMorphPresetViewportPanel::OnLipSyncSmoothnessChanged(float NewValue)
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->SetLipSyncPreviewSmoothness(NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
FText SMorphPresetViewportPanel::GetLipSyncTranscript() const
|
||||
{
|
||||
return Controller.IsValid() ? Controller->GetLipSyncPreviewTranscript() : FText::GetEmpty();
|
||||
}
|
||||
|
||||
void SMorphPresetViewportPanel::OnLipSyncTranscriptChanged(const FText& NewText)
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->SetLipSyncPreviewTranscript(NewText);
|
||||
}
|
||||
}
|
||||
|
||||
FText SMorphPresetViewportPanel::GetPreviewStatusText() const
|
||||
{
|
||||
return Controller.IsValid() ? Controller->GetPreviewStatusText() : FText::GetEmpty();
|
||||
}
|
||||
|
||||
EVisibility SMorphPresetViewportPanel::GetPreviewStatusVisibility() const
|
||||
{
|
||||
return !GetPreviewStatusText().IsEmpty() ? EVisibility::Visible : EVisibility::Collapsed;
|
||||
}
|
||||
|
||||
EActiveTimerReturnType SMorphPresetViewportPanel::HandleActiveTimer(double, float InDeltaTime)
|
||||
{
|
||||
if (Controller.IsValid())
|
||||
{
|
||||
Controller->Tick(InDeltaTime);
|
||||
}
|
||||
|
||||
return EActiveTimerReturnType::Continue;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// Vendored for FunnyGirlEditor (adapted from UE Persona private SAnimEditorBase).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/GCObject.h"
|
||||
#include "IPersonaPreviewScene.h"
|
||||
#include "Widgets/SCompoundWidget.h"
|
||||
#include "PersonaDelegates.h"
|
||||
|
||||
class UAnimationAsset;
|
||||
class UAnimPreviewInstance;
|
||||
|
||||
class FUNNYGIRLEDITOR_API SAnimEditorBase : public SCompoundWidget, public FGCObject
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SAnimEditorBase)
|
||||
: _DisplayAnimTimeline(true)
|
||||
, _DisplayAnimScrubBar(false)
|
||||
, _DisplayAnimScrubBarEditing(false)
|
||||
{}
|
||||
|
||||
SLATE_EVENT(FOnObjectsSelected, OnObjectsSelected)
|
||||
SLATE_ARGUMENT(bool, DisplayAnimTimeline)
|
||||
SLATE_ARGUMENT(bool, DisplayAnimScrubBar)
|
||||
SLATE_ARGUMENT(bool, DisplayAnimScrubBarEditing)
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments& InArgs, const TSharedPtr<IPersonaPreviewScene>& InPreviewScene);
|
||||
|
||||
float GetViewMinInput() const { return ViewMinInput; }
|
||||
float GetViewMaxInput() const { return ViewMaxInput; }
|
||||
float GetMinInput() const { return 0.0f; }
|
||||
float GetMaxInput() const { return GetSequenceLength(); }
|
||||
|
||||
void SetInputViewRange(float InViewMinInput, float InViewMaxInput);
|
||||
float GetScrubValue() const;
|
||||
void OnSelectionChanged(const TArray<UObject*>& SelectedItems);
|
||||
|
||||
// FGCObject
|
||||
virtual void AddReferencedObjects(FReferenceCollector& Collector) override {}
|
||||
virtual FString GetReferencerName() const override { return TEXT("FunnyGirlVendoredSAnimEditorBase"); }
|
||||
|
||||
virtual TSharedRef<SWidget> CreateDocumentAnchor();
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> CreateAnimTimelineWidget() = 0;
|
||||
virtual TSharedRef<SWidget> ConstructAnimScrubPanel(bool bDisplayAnimScrubBarEditing);
|
||||
virtual UAnimationAsset* GetEditorObject() const = 0;
|
||||
float GetSequenceLength() const;
|
||||
|
||||
TSharedRef<IPersonaPreviewScene> GetPreviewScene() const { return PreviewScenePtr.Pin().ToSharedRef(); }
|
||||
|
||||
protected:
|
||||
TWeakPtr<IPersonaPreviewScene> PreviewScenePtr;
|
||||
TSharedPtr<SVerticalBox> EditorPanels;
|
||||
TSharedPtr<SVerticalBox> NonScrollEditorPanels;
|
||||
FOnObjectsSelected OnObjectsSelectedDelegate;
|
||||
float ViewMinInput = 0.0f;
|
||||
float ViewMaxInput = 0.0f;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Factories/Factory.h"
|
||||
#include "LipSyncMappingAssetFactory.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class FUNNYGIRLEDITOR_API ULipSyncMappingAssetFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ULipSyncMappingAssetFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual FString GetDefaultNewAssetName() const override;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AssetTypeActions_Base.h"
|
||||
|
||||
class FLipSyncMappingAssetTypeActions : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
virtual FText GetAssetDescription(const FAssetData& AssetData) const override;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user