第一次提交
This commit is contained in:
1
Plugins/GGS/Config/BaseGenericGameSystem.ini
Normal file
1
Plugins/GGS/Config/BaseGenericGameSystem.ini
Normal file
@@ -0,0 +1 @@
|
||||
[CoreRedirects]
|
||||
9
Plugins/GGS/Config/FilterPlugin.ini
Normal file
9
Plugins/GGS/Config/FilterPlugin.ini
Normal file
@@ -0,0 +1,9 @@
|
||||
[FilterPlugin]
|
||||
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
|
||||
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
|
||||
;
|
||||
; Examples:
|
||||
; /README.txt
|
||||
; /Extras/...
|
||||
; /Binaries/ThirdParty/*.dll
|
||||
/Config/*
|
||||
92
Plugins/GGS/GenericGameSystem.uplugin
Normal file
92
Plugins/GGS/GenericGameSystem.uplugin
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 7,
|
||||
"VersionName": "1.5.1",
|
||||
"FriendlyName": "GenericGameSystem",
|
||||
"Description": "Common functionalities for Interaction, context based svf/vfx play, Camera modes management, and CommonUI extensions.",
|
||||
"Category": "Gameplay",
|
||||
"CreatedBy": "YuewuDev",
|
||||
"CreatedByURL": "https://yuewu.dev/en",
|
||||
"DocsURL": "https://www.yuewu.dev/en/wiki",
|
||||
"MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/6904d4d1-c7af-4973-b10e-a88c4436dab5",
|
||||
"SupportURL": "https://discord.com/invite/xMRXAB2",
|
||||
"EngineVersion": "5.7.0",
|
||||
"CanContainContent": false,
|
||||
"Installed": true,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "GenericEffectsSystem",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Android",
|
||||
"Linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "GenericCameraSystem",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Android",
|
||||
"Linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "GenericUISystem",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Android",
|
||||
"Linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "GenericGameSystem",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Android",
|
||||
"Linux"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Plugins": [
|
||||
{
|
||||
"Name": "Niagara",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "CommonUI",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "EnhancedInput",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "ModularGameplay",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "TargetingSystem",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "SmartObjects",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "GameplayBehaviors",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "GameplayBehaviorSmartObjects",
|
||||
"Enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Plugins/GGS/Resources/Icon128.png
Normal file
BIN
Plugins/GGS/Resources/Icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,54 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class GenericCameraSystem : ModuleRules
|
||||
{
|
||||
public GenericCameraSystem(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"GameplayTags"
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "GCMS_CameraMode.h"
|
||||
|
||||
#include "GCMS_CameraSystemComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Engine/Canvas.h"
|
||||
#include "GameFramework/Character.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode)
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// FGMS_CameraModeView
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
FGCMS_CameraModeView::FGCMS_CameraModeView()
|
||||
: Location(ForceInit)
|
||||
, Rotation(ForceInit)
|
||||
, ControlRotation(ForceInit)
|
||||
, FieldOfView(80.0f)
|
||||
{
|
||||
}
|
||||
|
||||
void FGCMS_CameraModeView::Blend(const FGCMS_CameraModeView& Other, float OtherWeight)
|
||||
{
|
||||
if (OtherWeight <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (OtherWeight >= 1.0f)
|
||||
{
|
||||
*this = Other;
|
||||
return;
|
||||
}
|
||||
|
||||
Location = FMath::Lerp(Location, Other.Location, OtherWeight);
|
||||
|
||||
const FRotator DeltaRotation = (Other.Rotation - Rotation).GetNormalized();
|
||||
Rotation = Rotation + (OtherWeight * DeltaRotation);
|
||||
|
||||
const FRotator DeltaControlRotation = (Other.ControlRotation - ControlRotation).GetNormalized();
|
||||
ControlRotation = ControlRotation + (OtherWeight * DeltaControlRotation);
|
||||
|
||||
SprintArmSocketOffset = FMath::Lerp(SprintArmSocketOffset, Other.SprintArmSocketOffset, OtherWeight);
|
||||
SprintArmTargetOffset = FMath::Lerp(SprintArmTargetOffset, Other.SprintArmTargetOffset, OtherWeight);
|
||||
SprintArmLength = FMath::Lerp(SprintArmLength, Other.SprintArmLength, OtherWeight);
|
||||
|
||||
FieldOfView = FMath::Lerp(FieldOfView, Other.FieldOfView, OtherWeight);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// UGCMS_CameraMode
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
UGCMS_CameraMode::UGCMS_CameraMode()
|
||||
{
|
||||
FieldOfView = 80.0f;
|
||||
ViewPitchMin = -89.0f;
|
||||
ViewPitchMax = 89.0f;
|
||||
|
||||
BlendTime = 0.5f;
|
||||
BlendFunction = EGCMS_CameraModeBlendFunction::EaseOut;
|
||||
BlendExponent = 4.0f;
|
||||
BlendAlpha = 1.0f;
|
||||
BlendWeight = 1.0f;
|
||||
ActiveTime = 0.0f;
|
||||
MaxActiveTime = 0.0f;
|
||||
}
|
||||
|
||||
UWorld* UGCMS_CameraMode::GetWorld() const
|
||||
{
|
||||
return HasAnyFlags(RF_ClassDefaultObject) ? nullptr : GetOuter()->GetWorld();
|
||||
}
|
||||
|
||||
AActor* UGCMS_CameraMode::GetTargetActor() const
|
||||
{
|
||||
if (UGCMS_CameraSystemComponent* Component = Cast<UGCMS_CameraSystemComponent>(GetOuter()))
|
||||
{
|
||||
return Component->GetOwner();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FVector UGCMS_CameraMode::GetPivotLocation_Implementation() const
|
||||
{
|
||||
const AActor* TargetActor = GetTargetActor();
|
||||
check(TargetActor);
|
||||
|
||||
if (const APawn* TargetPawn = Cast<APawn>(TargetActor))
|
||||
{
|
||||
// Height adjustments for characters to account for crouching.
|
||||
if (const ACharacter* TargetCharacter = Cast<ACharacter>(TargetPawn))
|
||||
{
|
||||
const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject<ACharacter>();
|
||||
check(TargetCharacterCDO);
|
||||
|
||||
const UCapsuleComponent* CapsuleComp = TargetCharacter->GetCapsuleComponent();
|
||||
check(CapsuleComp);
|
||||
|
||||
const UCapsuleComponent* CapsuleCompCDO = TargetCharacterCDO->GetCapsuleComponent();
|
||||
check(CapsuleCompCDO);
|
||||
|
||||
const float DefaultHalfHeight = CapsuleCompCDO->GetUnscaledCapsuleHalfHeight();
|
||||
const float ActualHalfHeight = CapsuleComp->GetUnscaledCapsuleHalfHeight();
|
||||
const float HeightAdjustment = (DefaultHalfHeight - ActualHalfHeight) + TargetCharacterCDO->BaseEyeHeight;
|
||||
|
||||
return TargetCharacter->GetActorLocation() + (FVector::UpVector * HeightAdjustment);
|
||||
}
|
||||
|
||||
return TargetPawn->GetPawnViewLocation();
|
||||
}
|
||||
|
||||
return TargetActor->GetActorLocation();
|
||||
}
|
||||
|
||||
FRotator UGCMS_CameraMode::GetPivotRotation_Implementation() const
|
||||
{
|
||||
const AActor* TargetActor = GetTargetActor();
|
||||
check(TargetActor);
|
||||
|
||||
if (const APawn* TargetPawn = Cast<APawn>(TargetActor))
|
||||
{
|
||||
return TargetPawn->GetViewRotation();
|
||||
}
|
||||
|
||||
return TargetActor->GetActorRotation();
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode::UpdateCameraMode(float DeltaTime)
|
||||
{
|
||||
ActiveTime += DeltaTime;
|
||||
|
||||
if (MaxActiveTime > 0 && ActiveTime >= MaxActiveTime)
|
||||
{
|
||||
if (UGCMS_CameraSystemComponent* Component = Cast<UGCMS_CameraSystemComponent>(GetOuter()))
|
||||
{
|
||||
Component->PushDefaultCameraMode();
|
||||
}
|
||||
}
|
||||
|
||||
UpdateView(DeltaTime);
|
||||
UpdateBlending(DeltaTime);
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode::UpdateView(float DeltaTime)
|
||||
{
|
||||
FVector PivotLocation = GetPivotLocation();
|
||||
FRotator PivotRotation = GetPivotRotation();
|
||||
|
||||
PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax);
|
||||
|
||||
OnUpdateView(DeltaTime, PivotLocation, PivotRotation);
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode::SetBlendWeight(float Weight)
|
||||
{
|
||||
BlendWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
|
||||
|
||||
// Since we're setting the blend weight directly, we need to calculate the blend alpha to account for the blend function.
|
||||
const float InvExponent = (BlendExponent > 0.0f) ? (1.0f / BlendExponent) : 1.0f;
|
||||
|
||||
switch (BlendFunction)
|
||||
{
|
||||
case EGCMS_CameraModeBlendFunction::Linear:
|
||||
BlendAlpha = BlendWeight;
|
||||
break;
|
||||
|
||||
case EGCMS_CameraModeBlendFunction::EaseIn:
|
||||
BlendAlpha = FMath::InterpEaseIn(0.0f, 1.0f, BlendWeight, InvExponent);
|
||||
break;
|
||||
|
||||
case EGCMS_CameraModeBlendFunction::EaseOut:
|
||||
BlendAlpha = FMath::InterpEaseOut(0.0f, 1.0f, BlendWeight, InvExponent);
|
||||
break;
|
||||
|
||||
case EGCMS_CameraModeBlendFunction::EaseInOut:
|
||||
BlendAlpha = FMath::InterpEaseInOut(0.0f, 1.0f, BlendWeight, InvExponent);
|
||||
break;
|
||||
|
||||
default:
|
||||
checkf(false, TEXT("SetBlendWeight: Invalid BlendFunction [%d]\n"), (uint8)BlendFunction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UCameraComponent* UGCMS_CameraMode::GetAssociatedCamera() const
|
||||
{
|
||||
if (UGCMS_CameraSystemComponent* Component = Cast<UGCMS_CameraSystemComponent>(GetOuter()))
|
||||
{
|
||||
return Component->GetAssociatedCamera();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USpringArmComponent* UGCMS_CameraMode::GetAssociatedSprintArm() const
|
||||
{
|
||||
if (UGCMS_CameraSystemComponent* Component = Cast<UGCMS_CameraSystemComponent>(GetOuter()))
|
||||
{
|
||||
return Component->GetAssociatedSprintArm();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode::UpdateBlending(float DeltaTime)
|
||||
{
|
||||
if (BlendTime > 0.0f)
|
||||
{
|
||||
BlendAlpha += (DeltaTime / BlendTime);
|
||||
BlendAlpha = FMath::Min(BlendAlpha, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
BlendAlpha = 1.0f;
|
||||
}
|
||||
|
||||
const float Exponent = (BlendExponent > 0.0f) ? BlendExponent : 1.0f;
|
||||
|
||||
switch (BlendFunction)
|
||||
{
|
||||
case EGCMS_CameraModeBlendFunction::Linear:
|
||||
BlendWeight = BlendAlpha;
|
||||
break;
|
||||
|
||||
case EGCMS_CameraModeBlendFunction::EaseIn:
|
||||
BlendWeight = FMath::InterpEaseIn(0.0f, 1.0f, BlendAlpha, Exponent);
|
||||
break;
|
||||
|
||||
case EGCMS_CameraModeBlendFunction::EaseOut:
|
||||
BlendWeight = FMath::InterpEaseOut(0.0f, 1.0f, BlendAlpha, Exponent);
|
||||
break;
|
||||
|
||||
case EGCMS_CameraModeBlendFunction::EaseInOut:
|
||||
BlendWeight = FMath::InterpEaseInOut(0.0f, 1.0f, BlendAlpha, Exponent);
|
||||
break;
|
||||
|
||||
default:
|
||||
checkf(false, TEXT("UpdateBlending: Invalid BlendFunction [%d]\n"), (uint8)BlendFunction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode::OnUpdateView_Implementation(float DeltaTime, FVector PivotLocation, FRotator PivotRotation)
|
||||
{
|
||||
View.Location = PivotLocation;
|
||||
View.Rotation = PivotRotation;
|
||||
View.ControlRotation = View.Rotation;
|
||||
View.FieldOfView = FieldOfView;
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode::DrawDebug(UCanvas* Canvas) const
|
||||
{
|
||||
check(Canvas);
|
||||
|
||||
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
|
||||
|
||||
DisplayDebugManager.SetDrawColor(FColor::White);
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT(" GMS_CameraMode: %s (%f)"), *GetName(), BlendWeight));
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "GCMS_CameraModeStack.h"
|
||||
|
||||
#include "Engine/Canvas.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraModeStack)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// UGCMS_CameraModeStack
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
UGCMS_CameraModeStack::UGCMS_CameraModeStack()
|
||||
{
|
||||
bIsActive = true;
|
||||
}
|
||||
|
||||
void UGCMS_CameraModeStack::ActivateStack()
|
||||
{
|
||||
if (!bIsActive)
|
||||
{
|
||||
bIsActive = true;
|
||||
|
||||
// Notify camera modes that they are being activated.
|
||||
for (UGCMS_CameraMode* CameraMode : CameraModeStack)
|
||||
{
|
||||
check(CameraMode);
|
||||
CameraMode->OnActivation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraModeStack::DeactivateStack()
|
||||
{
|
||||
if (bIsActive)
|
||||
{
|
||||
bIsActive = false;
|
||||
|
||||
// Notify camera modes that they are being deactivated.
|
||||
for (UGCMS_CameraMode* CameraMode : CameraModeStack)
|
||||
{
|
||||
check(CameraMode);
|
||||
CameraMode->OnDeactivation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraModeStack::PushCameraMode(TSubclassOf<UGCMS_CameraMode> CameraModeClass)
|
||||
{
|
||||
if (!CameraModeClass)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// get camera from pool.
|
||||
UGCMS_CameraMode* NewCameraMode = GetCameraModeInstance(CameraModeClass);
|
||||
check(NewCameraMode);
|
||||
|
||||
int32 StackSize = CameraModeStack.Num();
|
||||
|
||||
if ((StackSize > 0) && (CameraModeStack[0] == NewCameraMode))
|
||||
{
|
||||
// Already top of stack.
|
||||
return;
|
||||
}
|
||||
|
||||
// See if it's already in the stack and remove it.
|
||||
// Figure out how much it was contributing to the stack.
|
||||
int32 ExistingStackIndex = INDEX_NONE;
|
||||
float ExistingStackContribution = 1.0f;
|
||||
|
||||
for (int32 StackIndex = 0; StackIndex < StackSize; ++StackIndex)
|
||||
{
|
||||
if (CameraModeStack[StackIndex] == NewCameraMode)
|
||||
{
|
||||
ExistingStackIndex = StackIndex;
|
||||
ExistingStackContribution *= NewCameraMode->GetBlendWeight();
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExistingStackContribution *= (1.0f - CameraModeStack[StackIndex]->GetBlendWeight());
|
||||
}
|
||||
}
|
||||
|
||||
// existing in stack, remove it before add.
|
||||
if (ExistingStackIndex != INDEX_NONE)
|
||||
{
|
||||
CameraModeStack.RemoveAt(ExistingStackIndex);
|
||||
StackSize--;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExistingStackContribution = 0.0f;
|
||||
}
|
||||
|
||||
// Decide what initial weight to start with.
|
||||
const bool bShouldBlend = ((NewCameraMode->GetBlendTime() > 0.0f) && (StackSize > 0));
|
||||
const float BlendWeight = (bShouldBlend ? ExistingStackContribution : 1.0f);
|
||||
|
||||
NewCameraMode->SetBlendWeight(BlendWeight);
|
||||
|
||||
// Add new entry to top of stack.
|
||||
CameraModeStack.Insert(NewCameraMode, 0);
|
||||
|
||||
// Make sure stack bottom is always weighted 100%.
|
||||
CameraModeStack.Last()->SetBlendWeight(1.0f);
|
||||
|
||||
// Let the camera mode know if it's being added to the stack.
|
||||
if (ExistingStackIndex == INDEX_NONE)
|
||||
{
|
||||
NewCameraMode->OnActivation();
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraModeStack::PopCameraMode(TSubclassOf<UGCMS_CameraMode> CameraModeClass)
|
||||
{
|
||||
}
|
||||
|
||||
bool UGCMS_CameraModeStack::EvaluateStack(float DeltaTime, FGCMS_CameraModeView& OutCameraModeView)
|
||||
{
|
||||
if (!bIsActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Update camera modes.
|
||||
UpdateStack(DeltaTime);
|
||||
|
||||
//Blend values from camera modes.
|
||||
BlendStack(OutCameraModeView);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
UGCMS_CameraMode* UGCMS_CameraModeStack::GetCameraModeInstance(TSubclassOf<UGCMS_CameraMode> CameraModeClass)
|
||||
{
|
||||
check(CameraModeClass);
|
||||
|
||||
// First see if we already created one.
|
||||
for (UGCMS_CameraMode* CameraMode : CameraModeInstances)
|
||||
{
|
||||
if ((CameraMode != nullptr) && (CameraMode->GetClass() == CameraModeClass))
|
||||
{
|
||||
return CameraMode;
|
||||
}
|
||||
}
|
||||
|
||||
// Not found, so we need to create it.
|
||||
UGCMS_CameraMode* NewCameraMode = NewObject<UGCMS_CameraMode>(GetOuter(), CameraModeClass, NAME_None, RF_NoFlags);
|
||||
check(NewCameraMode);
|
||||
|
||||
CameraModeInstances.Add(NewCameraMode);
|
||||
|
||||
return NewCameraMode;
|
||||
}
|
||||
|
||||
void UGCMS_CameraModeStack::UpdateStack(float DeltaTime)
|
||||
{
|
||||
const int32 StackSize = CameraModeStack.Num();
|
||||
if (StackSize <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int32 RemoveCount = 0;
|
||||
int32 RemoveIndex = INDEX_NONE;
|
||||
|
||||
for (int32 StackIndex = 0; StackIndex < StackSize; ++StackIndex)
|
||||
{
|
||||
UGCMS_CameraMode* CameraMode = CameraModeStack[StackIndex];
|
||||
check(CameraMode);
|
||||
|
||||
CameraMode->UpdateCameraMode(DeltaTime);
|
||||
|
||||
if (CameraMode->GetBlendWeight() >= 1.0f)
|
||||
{
|
||||
// Everything below this mode is now irrelevant and can be removed.
|
||||
RemoveIndex = (StackIndex + 1);
|
||||
RemoveCount = (StackSize - RemoveIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (RemoveCount > 0)
|
||||
{
|
||||
// Let the camera modes know they being removed from the stack.
|
||||
for (int32 StackIndex = RemoveIndex; StackIndex < StackSize; ++StackIndex)
|
||||
{
|
||||
UGCMS_CameraMode* CameraMode = CameraModeStack[StackIndex];
|
||||
check(CameraMode);
|
||||
|
||||
CameraMode->OnDeactivation();
|
||||
}
|
||||
|
||||
CameraModeStack.RemoveAt(RemoveIndex, RemoveCount);
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraModeStack::BlendStack(FGCMS_CameraModeView& OutCameraModeView) const
|
||||
{
|
||||
const int32 StackSize = CameraModeStack.Num();
|
||||
if (StackSize <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Start at the bottom and blend up the stack
|
||||
const UGCMS_CameraMode* CameraMode = CameraModeStack[StackSize - 1];
|
||||
check(CameraMode);
|
||||
|
||||
OutCameraModeView = CameraMode->GetCameraModeView();
|
||||
|
||||
for (int32 StackIndex = (StackSize - 2); StackIndex >= 0; --StackIndex)
|
||||
{
|
||||
CameraMode = CameraModeStack[StackIndex];
|
||||
check(CameraMode);
|
||||
|
||||
OutCameraModeView.Blend(CameraMode->GetCameraModeView(), CameraMode->GetBlendWeight());
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraModeStack::DrawDebug(UCanvas* Canvas) const
|
||||
{
|
||||
check(Canvas);
|
||||
|
||||
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
|
||||
|
||||
DisplayDebugManager.SetDrawColor(FColor::Green);
|
||||
DisplayDebugManager.DrawString(FString(TEXT(" --- Camera Modes (Begin) ---")));
|
||||
|
||||
for (const UGCMS_CameraMode* CameraMode : CameraModeStack)
|
||||
{
|
||||
check(CameraMode);
|
||||
CameraMode->DrawDebug(Canvas);
|
||||
}
|
||||
|
||||
DisplayDebugManager.SetDrawColor(FColor::Green);
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT(" --- Camera Modes (End) ---")));
|
||||
}
|
||||
|
||||
void UGCMS_CameraModeStack::GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const
|
||||
{
|
||||
if (CameraModeStack.Num() == 0)
|
||||
{
|
||||
OutWeightOfTopLayer = 1.0f;
|
||||
OutTagOfTopLayer = FGameplayTag();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
UGCMS_CameraMode* TopEntry = CameraModeStack.Last();
|
||||
check(TopEntry);
|
||||
OutWeightOfTopLayer = TopEntry->GetBlendWeight();
|
||||
OutTagOfTopLayer = TopEntry->GetCameraTypeTag();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// // Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
//
|
||||
// #include "GCMS_CameraMode_ThirdPerson.h"
|
||||
//
|
||||
// #include "GCMS_CameraAssistInterface.h"
|
||||
// #include "GCMS_CameraMode.h"
|
||||
// #include "Components/PrimitiveComponent.h"
|
||||
// #include "GCMS_CameraPenetrationAvoidanceFeeler.h"
|
||||
// #include "Curves/CurveVector.h"
|
||||
// #include "Engine/Canvas.h"
|
||||
// #include "GameFramework/Controller.h"
|
||||
// #include "GameFramework/Character.h"
|
||||
// #include "Math/RotationMatrix.h"
|
||||
//
|
||||
// #include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode_ThirdPerson)
|
||||
//
|
||||
//
|
||||
// UGCMS_CameraMode_ThirdPerson::UGCMS_CameraMode_ThirdPerson()
|
||||
// {
|
||||
// TargetOffsetCurve = nullptr;
|
||||
// }
|
||||
//
|
||||
// void UGCMS_CameraMode_ThirdPerson::UpdateView_Implementation(float DeltaTime)
|
||||
// {
|
||||
// UpdateForTarget(DeltaTime);
|
||||
// UpdateCrouchOffset(DeltaTime);
|
||||
//
|
||||
// FVector PivotLocation = GetPivotLocation() + CurrentCrouchOffset;
|
||||
// FRotator PivotRotation = GetPivotRotation();
|
||||
//
|
||||
// PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax);
|
||||
//
|
||||
// View.Location = PivotLocation;
|
||||
// View.Rotation = PivotRotation;
|
||||
// View.ControlRotation = View.Rotation;
|
||||
// View.FieldOfView = FieldOfView;
|
||||
//
|
||||
// // Apply third person offset using pitch.
|
||||
// if (!bUseRuntimeFloatCurves)
|
||||
// {
|
||||
// if (TargetOffsetCurve)
|
||||
// {
|
||||
// const FVector TargetOffset = TargetOffsetCurve->GetVectorValue(PivotRotation.Pitch);
|
||||
// View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset);
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// FVector TargetOffset(0.0f);
|
||||
//
|
||||
// TargetOffset.X = TargetOffsetX.GetRichCurveConst()->Eval(PivotRotation.Pitch);
|
||||
// TargetOffset.Y = TargetOffsetY.GetRichCurveConst()->Eval(PivotRotation.Pitch);
|
||||
// TargetOffset.Z = TargetOffsetZ.GetRichCurveConst()->Eval(PivotRotation.Pitch);
|
||||
//
|
||||
// View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset);
|
||||
// }
|
||||
//
|
||||
// // Adjust final desired camera location to prevent any penetration
|
||||
// UpdatePreventPenetration(DeltaTime);
|
||||
// }
|
||||
//
|
||||
// void UGCMS_CameraMode_ThirdPerson::UpdateForTarget(float DeltaTime)
|
||||
// {
|
||||
// if (const ACharacter* TargetCharacter = Cast<ACharacter>(GetTargetActor()))
|
||||
// {
|
||||
// if (TargetCharacter->bIsCrouched)
|
||||
// {
|
||||
// const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject<ACharacter>();
|
||||
// const float CrouchedHeightAdjustment = TargetCharacterCDO->CrouchedEyeHeight - TargetCharacterCDO->BaseEyeHeight;
|
||||
//
|
||||
// SetTargetCrouchOffset(FVector(0.f, 0.f, CrouchedHeightAdjustment));
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// SetTargetCrouchOffset(FVector::ZeroVector);
|
||||
// }
|
||||
//
|
||||
// void UGCMS_CameraMode_ThirdPerson::SetTargetCrouchOffset(FVector NewTargetOffset)
|
||||
// {
|
||||
// CrouchOffsetBlendPct = 0.0f;
|
||||
// InitialCrouchOffset = CurrentCrouchOffset;
|
||||
// TargetCrouchOffset = NewTargetOffset;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// void UGCMS_CameraMode_ThirdPerson::UpdateCrouchOffset(float DeltaTime)
|
||||
// {
|
||||
// if (CrouchOffsetBlendPct < 1.0f)
|
||||
// {
|
||||
// CrouchOffsetBlendPct = FMath::Min(CrouchOffsetBlendPct + DeltaTime * CrouchOffsetBlendMultiplier, 1.0f);
|
||||
// CurrentCrouchOffset = FMath::InterpEaseInOut(InitialCrouchOffset, TargetCrouchOffset, CrouchOffsetBlendPct, 1.0f);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// CurrentCrouchOffset = TargetCrouchOffset;
|
||||
// CrouchOffsetBlendPct = 1.0f;
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,290 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "GCMS_CameraMode_WithPenetrationAvoidance.h"
|
||||
#include "Engine/Canvas.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "Components/PrimitiveComponent.h"
|
||||
#include "GameFramework/Controller.h"
|
||||
#include "Engine/HitResult.h"
|
||||
#include "GCMS_CameraAssistInterface.h"
|
||||
#include "GameFramework/CameraBlockingVolume.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode_WithPenetrationAvoidance)
|
||||
|
||||
namespace GMS_CameraMode_WithPenetrationAvoidance_Statics
|
||||
{
|
||||
static const FName NAME_IgnoreCameraCollision = TEXT("IgnoreCameraCollision");
|
||||
}
|
||||
|
||||
|
||||
UGCMS_CameraMode_WithPenetrationAvoidance::UGCMS_CameraMode_WithPenetrationAvoidance()
|
||||
{
|
||||
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +00.0f, 0.0f), 1.00f, 1.00f, 14.f, 0));
|
||||
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3));
|
||||
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, -16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3));
|
||||
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5));
|
||||
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, -32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5));
|
||||
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+20.0f, +00.0f, 0.0f), 1.00f, 1.00f, 00.f, 4));
|
||||
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(-20.0f, +00.0f, 0.0f), 0.50f, 0.50f, 00.f, 4));
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode_WithPenetrationAvoidance::UpdatePreventPenetration(float DeltaTime)
|
||||
{
|
||||
if (!bPreventPenetration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AActor* TargetActor = GetTargetActor();
|
||||
|
||||
APawn* TargetPawn = Cast<APawn>(TargetActor);
|
||||
AController* TargetController = TargetPawn ? TargetPawn->GetController() : nullptr;
|
||||
IGCMS_CameraAssistInterface* TargetControllerAssist = Cast<IGCMS_CameraAssistInterface>(TargetController);
|
||||
|
||||
IGCMS_CameraAssistInterface* TargetActorAssist = Cast<IGCMS_CameraAssistInterface>(TargetActor);
|
||||
|
||||
TOptional<AActor*> OptionalPPTarget = TargetActorAssist ? TargetActorAssist->GetCameraPreventPenetrationTarget() : TOptional<AActor*>();
|
||||
AActor* PPActor = OptionalPPTarget.IsSet() ? OptionalPPTarget.GetValue() : TargetActor;
|
||||
IGCMS_CameraAssistInterface* PPActorAssist = OptionalPPTarget.IsSet() ? Cast<IGCMS_CameraAssistInterface>(PPActor) : nullptr;
|
||||
|
||||
const UPrimitiveComponent* PPActorRootComponent = Cast<UPrimitiveComponent>(PPActor->GetRootComponent());
|
||||
if (PPActorRootComponent)
|
||||
{
|
||||
// Attempt at picking SafeLocation automatically, so we reduce camera translation when aiming.
|
||||
// Our camera is our reticle, so we want to preserve our aim and keep that as steady and smooth as possible.
|
||||
// Pick closest point on capsule to our aim line.
|
||||
FVector ClosestPointOnLineToCapsuleCenter;
|
||||
FVector SafeLocation = PPActor->GetActorLocation();
|
||||
FMath::PointDistToLine(SafeLocation, View.Rotation.Vector(), View.Location, ClosestPointOnLineToCapsuleCenter);
|
||||
|
||||
// Adjust Safe distance height to be same as aim line, but within capsule.
|
||||
float const PushInDistance = PenetrationAvoidanceFeelers[0].Extent + CollisionPushOutDistance;
|
||||
float const MaxHalfHeight = PPActor->GetSimpleCollisionHalfHeight() - PushInDistance;
|
||||
SafeLocation.Z = FMath::Clamp(ClosestPointOnLineToCapsuleCenter.Z, SafeLocation.Z - MaxHalfHeight, SafeLocation.Z + MaxHalfHeight);
|
||||
|
||||
float DistanceSqr;
|
||||
PPActorRootComponent->GetSquaredDistanceToCollision(ClosestPointOnLineToCapsuleCenter, DistanceSqr, SafeLocation);
|
||||
// Push back inside capsule to avoid initial penetration when doing line checks.
|
||||
if (PenetrationAvoidanceFeelers.Num() > 0)
|
||||
{
|
||||
SafeLocation += (SafeLocation - ClosestPointOnLineToCapsuleCenter).GetSafeNormal() * PushInDistance;
|
||||
}
|
||||
|
||||
// Then aim line to desired camera position
|
||||
bool const bSingleRayPenetrationCheck = !bDoPredictiveAvoidance;
|
||||
PreventCameraPenetration(bSingleRayPenetrationCheck, DeltaTime, PPActor, SafeLocation, View.Location, AimLineToDesiredPosBlockedPct);
|
||||
|
||||
IGCMS_CameraAssistInterface* AssistArray[] = {TargetControllerAssist, TargetActorAssist, PPActorAssist};
|
||||
|
||||
if (AimLineToDesiredPosBlockedPct < ReportPenetrationPercent)
|
||||
{
|
||||
for (IGCMS_CameraAssistInterface* Assist : AssistArray)
|
||||
{
|
||||
if (Assist)
|
||||
{
|
||||
// camera is too close, tell the assists
|
||||
Assist->OnCameraPenetratingTarget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode_WithPenetrationAvoidance::PreventCameraPenetration(bool bSingleRayOnly, const float& DeltaTime, const AActor* ViewTarget, FVector const& SafeLoc, FVector& CameraLoc,
|
||||
float& DistBlockedPct)
|
||||
{
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
DebugActorsHitDuringCameraPenetration.Reset();
|
||||
#endif
|
||||
|
||||
float HardBlockedPct = DistBlockedPct;
|
||||
float SoftBlockedPct = DistBlockedPct;
|
||||
|
||||
FVector BaseRay = CameraLoc - SafeLoc;
|
||||
FRotationMatrix BaseRayMatrix(BaseRay.Rotation());
|
||||
FVector BaseRayLocalUp, BaseRayLocalFwd, BaseRayLocalRight;
|
||||
|
||||
BaseRayMatrix.GetScaledAxes(BaseRayLocalFwd, BaseRayLocalRight, BaseRayLocalUp);
|
||||
|
||||
float DistBlockedPctThisFrame = 1.f;
|
||||
|
||||
int32 const NumRaysToShoot = bSingleRayOnly ? FMath::Min(1, PenetrationAvoidanceFeelers.Num()) : PenetrationAvoidanceFeelers.Num();
|
||||
FCollisionQueryParams SphereParams(SCENE_QUERY_STAT(CameraPen), false, nullptr/*PlayerCamera*/);
|
||||
|
||||
SphereParams.AddIgnoredActor(ViewTarget);
|
||||
|
||||
//TODO ILyraCameraTarget.GetIgnoredActorsForCameraPentration();
|
||||
//if (IgnoreActorForCameraPenetration)
|
||||
//{
|
||||
// SphereParams.AddIgnoredActor(IgnoreActorForCameraPenetration);
|
||||
//}
|
||||
|
||||
FCollisionShape SphereShape = FCollisionShape::MakeSphere(0.f);
|
||||
UWorld* World = GetWorld();
|
||||
|
||||
for (int32 RayIdx = 0; RayIdx < NumRaysToShoot; ++RayIdx)
|
||||
{
|
||||
FGCMS_CameraPenetrationAvoidanceFeeler& Feeler = PenetrationAvoidanceFeelers[RayIdx];
|
||||
if (Feeler.FramesUntilNextTrace <= 0)
|
||||
{
|
||||
// calc ray target
|
||||
FVector RayTarget;
|
||||
{
|
||||
FVector RotatedRay = BaseRay.RotateAngleAxis(Feeler.AdjustmentRot.Yaw, BaseRayLocalUp);
|
||||
RotatedRay = RotatedRay.RotateAngleAxis(Feeler.AdjustmentRot.Pitch, BaseRayLocalRight);
|
||||
RayTarget = SafeLoc + RotatedRay;
|
||||
}
|
||||
|
||||
// cast for world and pawn hits separately. this is so we can safely ignore the
|
||||
// camera's target pawn
|
||||
SphereShape.Sphere.Radius = Feeler.Extent;
|
||||
ECollisionChannel TraceChannel = ECC_Camera; //(Feeler.PawnWeight > 0.f) ? ECC_Pawn : ECC_Camera;
|
||||
|
||||
// do multi-line check to make sure the hits we throw out aren't
|
||||
// masking real hits behind (these are important rays).
|
||||
|
||||
// MT-> passing camera as actor so that camerablockingvolumes know when it's the camera doing traces
|
||||
FHitResult Hit;
|
||||
const bool bHit = World->SweepSingleByChannel(Hit, SafeLoc, RayTarget, FQuat::Identity, TraceChannel, SphereShape, SphereParams);
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (World->TimeSince(LastDrawDebugTime) < 1.f)
|
||||
{
|
||||
DrawDebugSphere(World, SafeLoc, SphereShape.Sphere.Radius, 8, FColor::Red);
|
||||
DrawDebugSphere(World, bHit ? Hit.Location : RayTarget, SphereShape.Sphere.Radius, 8, FColor::Red);
|
||||
DrawDebugLine(World, SafeLoc, bHit ? Hit.Location : RayTarget, FColor::Red);
|
||||
}
|
||||
#endif // ENABLE_DRAW_DEBUG
|
||||
|
||||
Feeler.FramesUntilNextTrace = Feeler.TraceInterval;
|
||||
|
||||
const AActor* HitActor = Hit.GetActor();
|
||||
|
||||
if (bHit && HitActor)
|
||||
{
|
||||
bool bIgnoreHit = false;
|
||||
|
||||
if (HitActor->ActorHasTag(GMS_CameraMode_WithPenetrationAvoidance_Statics::NAME_IgnoreCameraCollision))
|
||||
{
|
||||
bIgnoreHit = true;
|
||||
SphereParams.AddIgnoredActor(HitActor);
|
||||
}
|
||||
|
||||
// Ignore CameraBlockingVolume hits that occur in front of the ViewTarget.
|
||||
if (!bIgnoreHit && HitActor->IsA<ACameraBlockingVolume>())
|
||||
{
|
||||
const FVector ViewTargetForwardXY = ViewTarget->GetActorForwardVector().GetSafeNormal2D();
|
||||
const FVector ViewTargetLocation = ViewTarget->GetActorLocation();
|
||||
const FVector HitOffset = Hit.Location - ViewTargetLocation;
|
||||
const FVector HitDirectionXY = HitOffset.GetSafeNormal2D();
|
||||
const float DotHitDirection = FVector::DotProduct(ViewTargetForwardXY, HitDirectionXY);
|
||||
if (DotHitDirection > 0.0f)
|
||||
{
|
||||
bIgnoreHit = true;
|
||||
// Ignore this CameraBlockingVolume on the remaining sweeps.
|
||||
SphereParams.AddIgnoredActor(HitActor);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr<const AActor>(HitActor));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (!bIgnoreHit)
|
||||
{
|
||||
float const Weight = Cast<APawn>(Hit.GetActor()) ? Feeler.PawnWeight : Feeler.WorldWeight;
|
||||
float NewBlockPct = Hit.Time;
|
||||
NewBlockPct += (1.f - NewBlockPct) * (1.f - Weight);
|
||||
|
||||
// Recompute blocked pct taking into account pushout distance.
|
||||
NewBlockPct = ((Hit.Location - SafeLoc).Size() - CollisionPushOutDistance) / (RayTarget - SafeLoc).Size();
|
||||
DistBlockedPctThisFrame = FMath::Min(NewBlockPct, DistBlockedPctThisFrame);
|
||||
|
||||
// This feeler got a hit, so do another trace next frame
|
||||
Feeler.FramesUntilNextTrace = 0;
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr<const AActor>(HitActor));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (RayIdx == 0)
|
||||
{
|
||||
// don't interpolate toward this one, snap to it
|
||||
// assumes ray 0 is the center/main ray
|
||||
HardBlockedPct = DistBlockedPctThisFrame;
|
||||
}
|
||||
else
|
||||
{
|
||||
SoftBlockedPct = DistBlockedPctThisFrame;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
--Feeler.FramesUntilNextTrace;
|
||||
}
|
||||
}
|
||||
|
||||
if (bResetInterpolation)
|
||||
{
|
||||
DistBlockedPct = DistBlockedPctThisFrame;
|
||||
}
|
||||
else if (DistBlockedPct < DistBlockedPctThisFrame)
|
||||
{
|
||||
// interpolate smoothly out
|
||||
if (PenetrationBlendOutTime > DeltaTime)
|
||||
{
|
||||
DistBlockedPct = DistBlockedPct + DeltaTime / PenetrationBlendOutTime * (DistBlockedPctThisFrame - DistBlockedPct);
|
||||
}
|
||||
else
|
||||
{
|
||||
DistBlockedPct = DistBlockedPctThisFrame;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (DistBlockedPct > HardBlockedPct)
|
||||
{
|
||||
DistBlockedPct = HardBlockedPct;
|
||||
}
|
||||
else if (DistBlockedPct > SoftBlockedPct)
|
||||
{
|
||||
// interpolate smoothly in
|
||||
if (PenetrationBlendInTime > DeltaTime)
|
||||
{
|
||||
DistBlockedPct = DistBlockedPct - DeltaTime / PenetrationBlendInTime * (DistBlockedPct - SoftBlockedPct);
|
||||
}
|
||||
else
|
||||
{
|
||||
DistBlockedPct = SoftBlockedPct;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DistBlockedPct = FMath::Clamp<float>(DistBlockedPct, 0.f, 1.f);
|
||||
if (DistBlockedPct < (1.f - ZERO_ANIMWEIGHT_THRESH))
|
||||
{
|
||||
CameraLoc = SafeLoc + (CameraLoc - SafeLoc) * DistBlockedPct;
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraMode_WithPenetrationAvoidance::DrawDebug(UCanvas* Canvas) const
|
||||
{
|
||||
Super::DrawDebug(Canvas);
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
|
||||
for (int i = 0; i < DebugActorsHitDuringCameraPenetration.Num(); i++)
|
||||
{
|
||||
DisplayDebugManager.DrawString(
|
||||
FString::Printf(TEXT("HitActorDuringPenetration[%d]: %s")
|
||||
, i
|
||||
, *DebugActorsHitDuringCameraPenetration[i]->GetName()));
|
||||
}
|
||||
|
||||
LastDrawDebugTime = GetWorld()->GetTimeSeconds();
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "GCMS_CameraPenetrationAvoidanceFeeler.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraPenetrationAvoidanceFeeler)
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "GCMS_CameraSystemComponent.h"
|
||||
#include "GameFramework/HUD.h"
|
||||
#include "DisplayDebugHelpers.h"
|
||||
#include "Engine/Canvas.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GCMS_CameraMode.h"
|
||||
#include "GCMS_CameraModeStack.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraSystemComponent)
|
||||
|
||||
|
||||
UGCMS_CameraSystemComponent::UGCMS_CameraSystemComponent(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
CameraModeStack = nullptr;
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
FieldOfViewOffset = 0.0f;
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::OnShowDebugInfo(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DisplayInfo, float& YL, float& YPos)
|
||||
{
|
||||
if (DisplayInfo.IsDisplayOn(TEXT("CAMERA")))
|
||||
{
|
||||
if (const UGCMS_CameraSystemComponent* CameraComponent = GetCameraSystemComponent(HUD->GetCurrentDebugTargetActor()))
|
||||
{
|
||||
CameraComponent->DrawDebug(Canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::OnRegister()
|
||||
{
|
||||
Super::OnRegister();
|
||||
|
||||
if (!CameraModeStack)
|
||||
{
|
||||
CameraModeStack = NewObject<UGCMS_CameraModeStack>(this);
|
||||
check(CameraModeStack);
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
if (CameraModeStack && CameraModeStack->IsStackActivate() && AssociatedCameraComponent && AssociatedSprintArmComponent)
|
||||
{
|
||||
UpdateCameraModes();
|
||||
FGCMS_CameraModeView CameraModeView;
|
||||
CameraModeStack->EvaluateStack(DeltaTime, CameraModeView);
|
||||
|
||||
// Keep player controller in sync with the latest view.
|
||||
if (APawn* TargetPawn = Cast<APawn>(GetTargetActor()))
|
||||
{
|
||||
if (APlayerController* PC = TargetPawn->GetController<APlayerController>())
|
||||
{
|
||||
PC->SetControlRotation(CameraModeView.ControlRotation);
|
||||
}
|
||||
}
|
||||
|
||||
AssociatedCameraComponent->FieldOfView = CameraModeView.FieldOfView;
|
||||
|
||||
AssociatedSprintArmComponent->TargetArmLength = CameraModeView.SprintArmLength;
|
||||
|
||||
AssociatedSprintArmComponent->SocketOffset = CameraModeView.SprintArmSocketOffset;
|
||||
AssociatedSprintArmComponent->TargetOffset = CameraModeView.SprintArmTargetOffset;
|
||||
}
|
||||
}
|
||||
|
||||
UCameraComponent* UGCMS_CameraSystemComponent::GetAssociatedCamera() const
|
||||
{
|
||||
return AssociatedCameraComponent;
|
||||
}
|
||||
|
||||
USpringArmComponent* UGCMS_CameraSystemComponent::GetAssociatedSprintArm() const
|
||||
{
|
||||
return AssociatedSprintArmComponent;
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::Activate(bool bReset)
|
||||
{
|
||||
Super::Activate(bReset);
|
||||
if (CameraModeStack)
|
||||
{
|
||||
if (IsActive())
|
||||
{
|
||||
CameraModeStack->ActivateStack();
|
||||
}
|
||||
else
|
||||
{
|
||||
CameraModeStack->DeactivateStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::Deactivate()
|
||||
{
|
||||
Super::Deactivate();
|
||||
if (CameraModeStack)
|
||||
{
|
||||
CameraModeStack->DeactivateStack();
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::UpdateCameraModes()
|
||||
{
|
||||
check(CameraModeStack);
|
||||
|
||||
if (CameraModeStack->IsStackActivate())
|
||||
{
|
||||
if (DetermineCameraModeDelegate.IsBound())
|
||||
{
|
||||
if (const TSubclassOf<UGCMS_CameraMode> CameraMode = DetermineCameraModeDelegate.Execute())
|
||||
{
|
||||
CameraModeStack->PushCameraMode(CameraMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::PushCameraMode(TSubclassOf<UGCMS_CameraMode> NewCameraMode)
|
||||
{
|
||||
if (CameraModeStack->IsStackActivate())
|
||||
{
|
||||
if (NewCameraMode)
|
||||
{
|
||||
CameraModeStack->PushCameraMode(NewCameraMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::PushDefaultCameraMode()
|
||||
{
|
||||
if (CameraModeStack->IsStackActivate())
|
||||
{
|
||||
if (DefaultCameraMode)
|
||||
{
|
||||
CameraModeStack->PushCameraMode(DefaultCameraMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::Initialize(UCameraComponent* NewCameraComponent, USpringArmComponent* NewSpringArmComponent)
|
||||
{
|
||||
if (!CameraModeStack)
|
||||
{
|
||||
CameraModeStack = NewObject<UGCMS_CameraModeStack>(this);
|
||||
check(CameraModeStack);
|
||||
}
|
||||
|
||||
AssociatedCameraComponent = NewCameraComponent;
|
||||
AssociatedSprintArmComponent = NewSpringArmComponent;
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::DrawDebug(UCanvas* Canvas) const
|
||||
{
|
||||
check(Canvas);
|
||||
|
||||
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
|
||||
|
||||
DisplayDebugManager.SetFont(GEngine->GetSmallFont());
|
||||
DisplayDebugManager.SetDrawColor(FColor::Yellow);
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT("GCMS_CameraSystemComponent: %s"), *GetNameSafe(GetTargetActor())));
|
||||
|
||||
DisplayDebugManager.SetDrawColor(FColor::White);
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT(" Location: %s"), *AssociatedCameraComponent->GetComponentLocation().ToCompactString()));
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT(" Rotation: %s"), *AssociatedCameraComponent->GetComponentRotation().ToCompactString()));
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmLength: %f"), AssociatedSprintArmComponent->TargetArmLength));
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmSocketOffset: %s"), *AssociatedSprintArmComponent->SocketOffset.ToCompactString()));
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmTargetOffset: %s"), *AssociatedSprintArmComponent->TargetOffset.ToCompactString()));
|
||||
|
||||
DisplayDebugManager.DrawString(FString::Printf(TEXT(" FOV: %f"), AssociatedCameraComponent->FieldOfView));
|
||||
|
||||
check(CameraModeStack);
|
||||
CameraModeStack->DrawDebug(Canvas);
|
||||
}
|
||||
|
||||
void UGCMS_CameraSystemComponent::GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const
|
||||
{
|
||||
check(CameraModeStack);
|
||||
CameraModeStack->GetBlendInfo(/*out*/ OutWeightOfTopLayer, /*out*/ OutTagOfTopLayer);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "GenericCameraSystem.h"
|
||||
|
||||
#include "GameFramework/HUD.h"
|
||||
#include "GCMS_CameraSystemComponent.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FGenericCameraSystemModule"
|
||||
|
||||
void FGenericCameraSystemModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
#if WITH_EDITOR
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (!IsRunningDedicatedServer())
|
||||
{
|
||||
AHUD::OnShowDebugInfo.AddStatic(&UGCMS_CameraSystemComponent::OnShowDebugInfo);
|
||||
}
|
||||
#endif // ENABLE_DRAW_DEBUG
|
||||
#endif
|
||||
}
|
||||
|
||||
void FGenericCameraSystemModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FGenericCameraSystemModule, GenericCameraSystem)
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
|
||||
#include "GCMS_CameraAssistInterface.generated.h"
|
||||
|
||||
/** */
|
||||
UINTERFACE(BlueprintType)
|
||||
class UGCMS_CameraAssistInterface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class GENERICCAMERASYSTEM_API IGCMS_CameraAssistInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Get the list of actors that we're allowing the camera to penetrate. Useful in 3rd person cameras
|
||||
* when you need the following camera to ignore things like the a collection of view targets, the pawn,
|
||||
* a vehicle..etc.
|
||||
*/
|
||||
virtual void GetIgnoredActorsForCameraPentration(TArray<const AActor*>& OutActorsAllowPenetration) const { }
|
||||
|
||||
/**
|
||||
* The target actor to prevent penetration on. Normally, this is almost always the view target, which if
|
||||
* unimplemented will remain true. However, sometimes the view target, isn't the same as the root actor
|
||||
* you need to keep in frame.
|
||||
*/
|
||||
virtual TOptional<AActor*> GetCameraPreventPenetrationTarget() const
|
||||
{
|
||||
return TOptional<AActor*>();
|
||||
}
|
||||
|
||||
/** Called if the camera penetrates the focal target. Useful if you want to hide the target actor when being overlapped. */
|
||||
virtual void OnCameraPenetratingTarget() { }
|
||||
};
|
||||
235
Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode.h
Normal file
235
Plugins/GGS/Source/GenericCameraSystem/Public/GCMS_CameraMode.h
Normal file
@@ -0,0 +1,235 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
|
||||
#include "GCMS_CameraMode.generated.h"
|
||||
|
||||
class USpringArmComponent;
|
||||
class UCameraComponent;
|
||||
class AActor;
|
||||
class UCanvas;
|
||||
|
||||
/**
|
||||
* EGCMS_CameraModeBlendFunction
|
||||
*
|
||||
* Blend function used for transitioning between camera modes.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EGCMS_CameraModeBlendFunction : uint8
|
||||
{
|
||||
// Does a simple linear interpolation.
|
||||
Linear,
|
||||
|
||||
// Immediately accelerates, but smoothly decelerates into the target. Ease amount controlled by the exponent.
|
||||
EaseIn,
|
||||
|
||||
// Smoothly accelerates, but does not decelerate into the target. Ease amount controlled by the exponent.
|
||||
EaseOut,
|
||||
|
||||
// Smoothly accelerates and decelerates. Ease amount controlled by the exponent.
|
||||
EaseInOut,
|
||||
|
||||
COUNT UMETA(Hidden)
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* GCMS_CameraModeView
|
||||
*
|
||||
* View data produced by the camera mode that is used to blend camera modes.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICCAMERASYSTEM_API FGCMS_CameraModeView
|
||||
{
|
||||
public:
|
||||
GENERATED_BODY()
|
||||
|
||||
FGCMS_CameraModeView();
|
||||
|
||||
void Blend(const FGCMS_CameraModeView& Other, float OtherWeight);
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
|
||||
FVector Location{ForceInit};
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
|
||||
FRotator Rotation{ForceInit};
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
|
||||
FVector SprintArmSocketOffset{ForceInit};
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
|
||||
FVector SprintArmTargetOffset{ForceInit};
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
|
||||
float SprintArmLength{ForceInit};
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
|
||||
FRotator ControlRotation{ForceInit};
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
|
||||
float FieldOfView{ForceInit};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* UGCMS_CameraMode
|
||||
*
|
||||
* Base class for all camera modes.
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class GENERICCAMERASYSTEM_API UGCMS_CameraMode : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UGCMS_CameraMode();
|
||||
|
||||
virtual UWorld* GetWorld() const override;
|
||||
|
||||
/**
|
||||
* @return Returns the target actor that owning camera is looking at. 返回所属相机当前跟随的目标Actor。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode")
|
||||
AActor* GetTargetActor() const;
|
||||
|
||||
const FGCMS_CameraModeView& GetCameraModeView() const { return View; }
|
||||
|
||||
// Called when this camera mode is activated on the camera mode stack.
|
||||
virtual void OnActivation()
|
||||
{
|
||||
K2_OnActivation();
|
||||
};
|
||||
|
||||
// Called when this camera mode is deactivated on the camera mode stack.
|
||||
virtual void OnDeactivation()
|
||||
{
|
||||
K2_OnDeactivation();
|
||||
};
|
||||
|
||||
void UpdateCameraMode(float DeltaTime);
|
||||
|
||||
float GetBlendTime() const { return BlendTime; }
|
||||
float GetBlendWeight() const { return BlendWeight; }
|
||||
void SetBlendWeight(float Weight);
|
||||
|
||||
FGameplayTag GetCameraTypeTag() const
|
||||
{
|
||||
return CameraTypeTag;
|
||||
}
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode")
|
||||
UCameraComponent* GetAssociatedCamera() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode")
|
||||
USpringArmComponent* GetAssociatedSprintArm() const;
|
||||
|
||||
virtual void DrawDebug(UCanvas* Canvas) const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Called when this camera mode activated.
|
||||
* 在此相机模式激活时调用。
|
||||
*/
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="CameraMode", meta=(DisplayName="OnActivation"))
|
||||
void K2_OnActivation();
|
||||
|
||||
/**
|
||||
* Called when this camera mode deactivated.
|
||||
* 在此相机模式禁用时调用。
|
||||
*/
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="CameraMode", meta=(DisplayName="OnDeactivation"))
|
||||
void K2_OnDeactivation();
|
||||
|
||||
/**
|
||||
* Return the pivot location of this camera mode.
|
||||
* @details Default will return Character's capsule location(Taking crouching state in count), or fallback to Pawn's ViewLocation or fallback to PawnLocation.
|
||||
* 返回此相机模式的轴心位置。
|
||||
* @细节,默认实现是返回考虑到Character的蹲伏状态的胶囊体中心点,或者回退到Pawn的ViewLocation,或者回退到Pawn的Location。
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category="CameraMode")
|
||||
FVector GetPivotLocation() const;
|
||||
|
||||
/**
|
||||
* Return the pivot rotation of this camera mode.
|
||||
* @details Default will return TargetActor(Pawn)'s view rotation or fallback to actor rotation.
|
||||
* 返回此相机模式的轴心旋转。
|
||||
* @细节 默认会返回TargetActor(Pawn)的ViewRotation,或者回退到Actor的旋转。
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category="CameraMode")
|
||||
FRotator GetPivotRotation() const;
|
||||
|
||||
virtual void UpdateView(float DeltaTime);
|
||||
|
||||
virtual void UpdateBlending(float DeltaTime);
|
||||
|
||||
/**
|
||||
* This is where you update View(CameraModeView)
|
||||
* @param DeltaTime
|
||||
* @param PivotLocation Location returned from GetPivotLocation
|
||||
* @param PivotRotation Rotation returned from GetPivotRotation
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category="CameraMode")
|
||||
void OnUpdateView(float DeltaTime, FVector PivotLocation, FRotator PivotRotation);
|
||||
virtual void OnUpdateView_Implementation(float DeltaTime, FVector PivotLocation, FRotator PivotRotation);
|
||||
|
||||
protected:
|
||||
// A tag that can be queried by gameplay code that cares when a kind of camera mode is active
|
||||
// without having to ask about a specific mode (e.g., when aiming downsights to get more accuracy)
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Blending")
|
||||
FGameplayTag CameraTypeTag;
|
||||
|
||||
// View output produced by the camera mode.
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="View")
|
||||
FGCMS_CameraModeView View;
|
||||
|
||||
// The horizontal field of view (in degrees).
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "5.0", UIMax = "170", ClampMin = "5.0", ClampMax = "170.0"))
|
||||
float FieldOfView;
|
||||
|
||||
// Minimum view pitch (in degrees).
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "-89.9", UIMax = "89.9", ClampMin = "-89.9", ClampMax = "89.9"))
|
||||
float ViewPitchMin;
|
||||
|
||||
// Maximum view pitch (in degrees).
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "-89.9", UIMax = "89.9", ClampMin = "-89.9", ClampMax = "89.9"))
|
||||
float ViewPitchMax;
|
||||
|
||||
/**
|
||||
* How long it takes to blend in this mode.
|
||||
* 此相机模式的混入时间。
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Blending")
|
||||
float BlendTime;
|
||||
|
||||
/**
|
||||
* Function used for blending.
|
||||
* 用于混合的方式。
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Blending")
|
||||
EGCMS_CameraModeBlendFunction BlendFunction;
|
||||
|
||||
// Exponent used by blend functions to control the shape of the curve.
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Blending")
|
||||
float BlendExponent;
|
||||
|
||||
// Linear blend alpha used to determine the blend weight.
|
||||
UPROPERTY(VisibleInstanceOnly, Category="Blending")
|
||||
float BlendAlpha;
|
||||
|
||||
// Blend weight calculated using the blend alpha and function.
|
||||
UPROPERTY(VisibleInstanceOnly, Category="Blending")
|
||||
float BlendWeight;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Duration")
|
||||
float ActiveTime;
|
||||
|
||||
/**
|
||||
* The max active time of this camera mode, When active time reach this value, this camera mode will auto popup.
|
||||
* 此相机模式的最大激活时间,当当前激活时间到达此时长,会返回到默认相机模式。
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Duraction")
|
||||
float MaxActiveTime;
|
||||
|
||||
protected:
|
||||
/** If true, skips all interpolation and puts camera in ideal location. Automatically set to false next frame. */
|
||||
UPROPERTY(transient)
|
||||
uint32 bResetInterpolation : 1;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GCMS_CameraMode.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GCMS_CameraModeStack.generated.h"
|
||||
|
||||
|
||||
/**
|
||||
* UGCMS_CameraModeStack
|
||||
*
|
||||
* Stack used for blending camera modes.
|
||||
*/
|
||||
UCLASS()
|
||||
class GENERICCAMERASYSTEM_API UGCMS_CameraModeStack : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UGCMS_CameraModeStack();
|
||||
|
||||
void ActivateStack();
|
||||
void DeactivateStack();
|
||||
|
||||
bool IsStackActivate() const { return bIsActive; }
|
||||
|
||||
void PushCameraMode(TSubclassOf<UGCMS_CameraMode> CameraModeClass);
|
||||
|
||||
void PopCameraMode(TSubclassOf<UGCMS_CameraMode> CameraModeClass);
|
||||
|
||||
bool EvaluateStack(float DeltaTime, FGCMS_CameraModeView& OutCameraModeView);
|
||||
|
||||
void DrawDebug(UCanvas* Canvas) const;
|
||||
|
||||
// Gets the tag associated with the top layer and the blend weight of it
|
||||
void GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Get or create new camera mode.
|
||||
*/
|
||||
UGCMS_CameraMode* GetCameraModeInstance(TSubclassOf<UGCMS_CameraMode> CameraModeClass);
|
||||
|
||||
void UpdateStack(float DeltaTime);
|
||||
void BlendStack(FGCMS_CameraModeView& OutCameraModeView) const;
|
||||
|
||||
protected:
|
||||
bool bIsActive;
|
||||
|
||||
/**
|
||||
* Cached camera mode instance pool.
|
||||
*/
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UGCMS_CameraMode>> CameraModeInstances;
|
||||
|
||||
/**
|
||||
* The list of active camera mode.
|
||||
*/
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UGCMS_CameraMode>> CameraModeStack;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// // Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
//
|
||||
// #pragma once
|
||||
//
|
||||
// #include "Curves/CurveFloat.h"
|
||||
// #include "DrawDebugHelpers.h"
|
||||
// #include "GCMS_CameraMode_WithPenetrationAvoidance.h"
|
||||
// #include "GCMS_CameraMode_ThirdPerson.generated.h"
|
||||
//
|
||||
// class UCurveVector;
|
||||
//
|
||||
// /**
|
||||
// * UGCMS_CameraMode_ThirdPerson
|
||||
// *
|
||||
// * A basic third person camera mode.
|
||||
// */
|
||||
// UCLASS(Abstract, Blueprintable)
|
||||
// class UGCMS_CameraMode_ThirdPerson : public UGCMS_CameraMode_WithPenetrationAvoidance
|
||||
// {
|
||||
// GENERATED_BODY()
|
||||
//
|
||||
// public:
|
||||
// UGCMS_CameraMode_ThirdPerson();
|
||||
//
|
||||
// protected:
|
||||
//
|
||||
// virtual void UpdateView_Implementation(float DeltaTime) override;
|
||||
//
|
||||
// UFUNCTION(BlueprintCallable, Category="Third Person")
|
||||
// void UpdateForTarget(float DeltaTime);
|
||||
//
|
||||
// protected:
|
||||
// // Curve that defines local-space offsets from the target using the view pitch to evaluate the curve.
|
||||
// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "!bUseRuntimeFloatCurves"))
|
||||
// TObjectPtr<const UCurveVector> TargetOffsetCurve;
|
||||
//
|
||||
// // UE-103986: Live editing of RuntimeFloatCurves during PIE does not work (unlike curve assets).
|
||||
// // Once that is resolved this will become the default and TargetOffsetCurve will be removed.
|
||||
// UPROPERTY(EditDefaultsOnly, Category = "Third Person")
|
||||
// bool bUseRuntimeFloatCurves;
|
||||
//
|
||||
// // time will be [-ViewPitchMin,ViewPitchMax]
|
||||
// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves"))
|
||||
// FRuntimeFloatCurve TargetOffsetX;
|
||||
//
|
||||
// // time will be [-ViewPitchMin,ViewPitchMax]
|
||||
// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves"))
|
||||
// FRuntimeFloatCurve TargetOffsetY;
|
||||
//
|
||||
// // time will be [-ViewPitchMin,ViewPitchMax]
|
||||
// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves"))
|
||||
// FRuntimeFloatCurve TargetOffsetZ;
|
||||
//
|
||||
// // Alters the speed that a crouch offset is blended in or out
|
||||
// UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Third Person")
|
||||
// float CrouchOffsetBlendMultiplier = 5.0f;
|
||||
//
|
||||
// protected:
|
||||
// void SetTargetCrouchOffset(FVector NewTargetOffset);
|
||||
// void UpdateCrouchOffset(float DeltaTime);
|
||||
//
|
||||
// FVector InitialCrouchOffset = FVector::ZeroVector;
|
||||
// FVector TargetCrouchOffset = FVector::ZeroVector;
|
||||
// float CrouchOffsetBlendPct = 1.0f;
|
||||
// FVector CurrentCrouchOffset = FVector::ZeroVector;
|
||||
// };
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "GCMS_CameraMode.h"
|
||||
#include "GCMS_CameraPenetrationAvoidanceFeeler.h"
|
||||
#include "GCMS_CameraMode_WithPenetrationAvoidance.generated.h"
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class GENERICCAMERASYSTEM_API UGCMS_CameraMode_WithPenetrationAvoidance : public UGCMS_CameraMode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UGCMS_CameraMode_WithPenetrationAvoidance();
|
||||
|
||||
// Penetration prevention
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance")
|
||||
float PenetrationBlendInTime = 0.1f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance")
|
||||
float PenetrationBlendOutTime = 0.15f;
|
||||
|
||||
/** If true, does collision checks to keep the camera out of the world. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance")
|
||||
bool bPreventPenetration = true;
|
||||
|
||||
/** If true, try to detect nearby walls and move the camera in anticipation. Helps prevent popping. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance")
|
||||
bool bDoPredictiveAvoidance = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PenetrationAvoidance")
|
||||
float CollisionPushOutDistance = 2.f;
|
||||
|
||||
/** When the camera's distance is pushed into this percentage of its full distance due to penetration */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PenetrationAvoidance")
|
||||
float ReportPenetrationPercent = 0.f;
|
||||
|
||||
/**
|
||||
* These are the feeler rays that are used to find where to place the camera.
|
||||
* Index: 0 : This is the normal feeler we use to prevent collisions.
|
||||
* Index: 1+ : These feelers are used if you bDoPredictiveAvoidance=true, to scan for potential impacts if the player
|
||||
* were to rotate towards that direction and primitively collide the camera so that it pulls in before
|
||||
* impacting the occluder.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, Category = "PenetrationAvoidance")
|
||||
TArray<FGCMS_CameraPenetrationAvoidanceFeeler> PenetrationAvoidanceFeelers;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
float AimLineToDesiredPosBlockedPct;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<const AActor>> DebugActorsHitDuringCameraPenetration;
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
mutable float LastDrawDebugTime = -MAX_FLT;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
UFUNCTION(BlueprintCallable, Category="CameraMode|PenetrationAvoidance")
|
||||
void UpdatePreventPenetration(float DeltaTime);
|
||||
UFUNCTION(BlueprintCallable, Category="CameraMode|PenetrationAvoidance")
|
||||
void PreventCameraPenetration(bool bSingleRayOnly, const float& DeltaTime, const AActor* ViewTarget, const FVector& SafeLoc, FVector& CameraLoc, float& DistBlockedPct);
|
||||
|
||||
virtual void DrawDebug(UCanvas* Canvas) const override;
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GCMS_CameraPenetrationAvoidanceFeeler.generated.h"
|
||||
|
||||
|
||||
/**
|
||||
* Struct defining a feeler ray used for camera penetration avoidance.
|
||||
*/
|
||||
USTRUCT()
|
||||
struct GENERICCAMERASYSTEM_API FGCMS_CameraPenetrationAvoidanceFeeler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** FRotator describing deviance from main ray */
|
||||
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
|
||||
FRotator AdjustmentRot;
|
||||
|
||||
/** how much this feeler affects the final position if it hits the world */
|
||||
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
|
||||
float WorldWeight;
|
||||
|
||||
/** how much this feeler affects the final position if it hits a APawn (setting to 0 will not attempt to collide with pawns at all) */
|
||||
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
|
||||
float PawnWeight;
|
||||
|
||||
/** extent to use for collision when tracing this feeler */
|
||||
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
|
||||
float Extent;
|
||||
|
||||
/** minimum frame interval between traces with this feeler if nothing was hit last frame */
|
||||
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
|
||||
int32 TraceInterval;
|
||||
|
||||
/** number of frames since this feeler was used */
|
||||
UPROPERTY(transient)
|
||||
int32 FramesUntilNextTrace;
|
||||
|
||||
|
||||
FGCMS_CameraPenetrationAvoidanceFeeler()
|
||||
: AdjustmentRot(ForceInit)
|
||||
, WorldWeight(0)
|
||||
, PawnWeight(0)
|
||||
, Extent(0)
|
||||
, TraceInterval(0)
|
||||
, FramesUntilNextTrace(0)
|
||||
{
|
||||
}
|
||||
|
||||
FGCMS_CameraPenetrationAvoidanceFeeler(const FRotator& InAdjustmentRot,
|
||||
const float& InWorldWeight,
|
||||
const float& InPawnWeight,
|
||||
const float& InExtent,
|
||||
const int32& InTraceInterval = 0,
|
||||
const int32& InFramesUntilNextTrace = 0)
|
||||
: AdjustmentRot(InAdjustmentRot)
|
||||
, WorldWeight(InWorldWeight)
|
||||
, PawnWeight(InPawnWeight)
|
||||
, Extent(InExtent)
|
||||
, TraceInterval(InTraceInterval)
|
||||
, FramesUntilNextTrace(InFramesUntilNextTrace)
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
|
||||
#include "GCMS_CameraSystemComponent.generated.h"
|
||||
|
||||
class UCameraComponent;
|
||||
class USpringArmComponent;
|
||||
class UCanvas;
|
||||
class AHUD;
|
||||
class UGCMS_CameraMode;
|
||||
class UGCMS_CameraModeStack;
|
||||
class UObject;
|
||||
struct FFrame;
|
||||
struct FGameplayTag;
|
||||
struct FMinimalViewInfo;
|
||||
template <class TClass>
|
||||
class TSubclassOf;
|
||||
|
||||
DECLARE_DELEGATE_RetVal(TSubclassOf<UGCMS_CameraMode>, FGMSCameraModeDelegate);
|
||||
|
||||
|
||||
/**
|
||||
* UGCMS_CameraSystemComponent
|
||||
*
|
||||
* The base camera component class used by GMS camera system.
|
||||
*/
|
||||
UCLASS(ClassGroup=GCMS, meta=(BlueprintSpawnableComponent))
|
||||
class GENERICCAMERASYSTEM_API UGCMS_CameraSystemComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UGCMS_CameraSystemComponent(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
static void OnShowDebugInfo(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DisplayInfo, float& YL, float& YPos);
|
||||
|
||||
// Returns the camera component if one exists on the specified actor.
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCMS|Camera", meta=(DefaultToSelf="Actor"))
|
||||
static UGCMS_CameraSystemComponent* GetCameraSystemComponent(const AActor* Actor) { return (Actor ? Actor->FindComponentByClass<UGCMS_CameraSystemComponent>() : nullptr); }
|
||||
|
||||
/**
|
||||
* Returns the target actor that the camera is looking at.
|
||||
*/
|
||||
virtual AActor* GetTargetActor() const { return GetOwner(); }
|
||||
|
||||
// Delegate used to query for the best camera mode.
|
||||
FGMSCameraModeDelegate DetermineCameraModeDelegate;
|
||||
|
||||
// Add an offset to the field of view. The offset is only for one frame, it gets cleared once it is applied.
|
||||
void AddFieldOfViewOffset(float FovOffset) { FieldOfViewOffset += FovOffset; }
|
||||
|
||||
// Push specified Camera Mode.
|
||||
UFUNCTION(BlueprintCallable, Category="GCMS|Camera")
|
||||
void PushCameraMode(TSubclassOf<UGCMS_CameraMode> NewCameraMode);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="GCMS|Camera")
|
||||
void PushDefaultCameraMode();
|
||||
|
||||
/**
|
||||
* Initialize it with camera component and sprint arm component.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GCMS|Camera")
|
||||
void Initialize(UCameraComponent* NewCameraComponent, USpringArmComponent* NewSpringArmComponent);
|
||||
|
||||
virtual void DrawDebug(UCanvas* Canvas) const;
|
||||
|
||||
/**
|
||||
* Gets the camera mode tag associated with the top layer and the blend weight of it
|
||||
* 返回顶层相机模式的tag和当前权重。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera")
|
||||
void GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera")
|
||||
UCameraComponent* GetAssociatedCamera() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera")
|
||||
USpringArmComponent* GetAssociatedSprintArm() const;
|
||||
|
||||
virtual void Activate(bool bReset) override;
|
||||
virtual void Deactivate() override;
|
||||
|
||||
protected:
|
||||
virtual void OnRegister() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
virtual void UpdateCameraModes();
|
||||
|
||||
protected:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UCameraComponent> AssociatedCameraComponent;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<USpringArmComponent> AssociatedSprintArmComponent;
|
||||
|
||||
// Stack used to blend the camera modes.
|
||||
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="GCMS|Camera", meta=(ShowInnerProperties))
|
||||
TObjectPtr<UGCMS_CameraModeStack> CameraModeStack;
|
||||
|
||||
// Default camera mode will used.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCMS|Camera")
|
||||
TSubclassOf<UGCMS_CameraMode> DefaultCameraMode;
|
||||
|
||||
// Offset applied to the field of view. The offset is only for one frame, it gets cleared once it is applied.
|
||||
float FieldOfViewOffset;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FGenericCameraSystemModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class GenericEffectsSystem : ModuleRules
|
||||
{
|
||||
public GenericEffectsSystem(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"PhysicsCore",
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"GameplayTags",
|
||||
"Niagara",
|
||||
"DeveloperSettings"
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Feedback/GES_AnimNotify_ContextEffects.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NiagaraFunctionLibrary.h"
|
||||
#include "NiagaraSystem.h"
|
||||
#include "Feedback/GES_ContextEffectsInterface.h"
|
||||
#include "Feedback/GES_ContextEffectsLibrary.h"
|
||||
#include "Feedback/GES_ContextEffectsPreviewSetting.h"
|
||||
#include "Feedback/GES_ContextEffectsSubsystem.h"
|
||||
#include "Kismet/KismetMathLibrary.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_AnimNotify_ContextEffects)
|
||||
|
||||
|
||||
bool UGES_ContextEffectsSpawnParametersProvider::ProvideParameters_Implementation(USkeletalMeshComponent* InMeshComp, const UGES_AnimNotify_ContextEffects* InNotifyNotify,
|
||||
UAnimSequenceBase* InAnimation, FVector& OutSpawnLocation,
|
||||
FRotator& OutSpawnRotation) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UGES_AnimNotify_ContextEffects::UGES_AnimNotify_ContextEffects(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
|
||||
{
|
||||
#if WITH_EDITORONLY_DATA
|
||||
NotifyColor = FColor::Blue;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UGES_AnimNotify_ContextEffects::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UGES_AnimNotify_ContextEffects::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
}
|
||||
#endif
|
||||
|
||||
FString UGES_AnimNotify_ContextEffects::GetNotifyName_Implementation() const
|
||||
{
|
||||
// If the Effect Tag is valid, pass the string name to the notify name
|
||||
if (Effect.IsValid())
|
||||
{
|
||||
return Effect.ToString();
|
||||
}
|
||||
|
||||
return Super::GetNotifyName_Implementation();
|
||||
}
|
||||
|
||||
void UGES_AnimNotify_ContextEffects::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
|
||||
const FAnimNotifyEventReference& EventReference)
|
||||
{
|
||||
Super::Notify(MeshComp, Animation, EventReference);
|
||||
|
||||
if (!MeshComp)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FVector SpawnLocation = FVector::ZeroVector;
|
||||
FRotator SpawnRotation = FRotator::ZeroRotator;
|
||||
|
||||
bool bValidProvider = !bAttached && IsValid(SpawnParametersProvider) && SpawnParametersProvider->ProvideParameters(MeshComp, this, Animation, SpawnLocation, SpawnRotation);
|
||||
|
||||
if (!bValidProvider && MeshComp->GetOwner())
|
||||
{
|
||||
SpawnRotation = MeshComp->GetOwner()->GetActorTransform().TransformRotation(RotationOffset.Quaternion()).Rotator();
|
||||
SpawnLocation = MeshComp->GetOwner()->GetActorTransform().TransformPosition(LocationOffset);
|
||||
}
|
||||
// else
|
||||
// {
|
||||
// SpawnRotation = UKismetMathLibrary::ComposeRotators(SpawnRotation, RotationOffset);
|
||||
// SpawnLocation = SpawnLocation + UKismetMathLibrary::Quat_RotateVector(SpawnRotation.Quaternion(), LocationOffset);
|
||||
// }
|
||||
|
||||
// Make sure both MeshComp and Owning Actor is valid
|
||||
if (AActor* OwningActor = MeshComp->GetOwner())
|
||||
{
|
||||
// Prepare Trace Data
|
||||
bool bHitSuccess = false;
|
||||
FHitResult HitResult;
|
||||
FCollisionQueryParams QueryParams;
|
||||
|
||||
if (TraceProperties.bIgnoreActor)
|
||||
{
|
||||
QueryParams.AddIgnoredActor(OwningActor);
|
||||
}
|
||||
|
||||
QueryParams.bReturnPhysicalMaterial = true;
|
||||
|
||||
if (bPerformTrace)
|
||||
{
|
||||
// If trace is needed, set up Start Location to Attached
|
||||
FVector TraceStart = bAttached ? MeshComp->GetSocketLocation(SocketName) + LocationOffset : SpawnLocation;
|
||||
|
||||
// Make sure World is valid
|
||||
if (UWorld* World = OwningActor->GetWorld())
|
||||
{
|
||||
// Call Line Trace, Pass in relevant properties
|
||||
bHitSuccess = World->LineTraceSingleByChannel(HitResult, TraceStart, (TraceStart + TraceProperties.EndTraceLocationOffset),
|
||||
TraceProperties.TraceChannel, QueryParams, FCollisionResponseParams::DefaultResponseParam);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare Contexts in advance
|
||||
FGameplayTagContainer SourceContext;
|
||||
|
||||
FGameplayTagContainer TargetContext;
|
||||
|
||||
// Set up Array of Objects that implement the Context Effects Interface
|
||||
TArray<UObject*> Implementers;
|
||||
|
||||
// Determine if the Owning Actor is one of the Objects that implements the Context Effects Interface
|
||||
if (OwningActor->Implements<UGES_ContextEffectsInterface>())
|
||||
{
|
||||
// If so, add it to the Array
|
||||
Implementers.Add(OwningActor);
|
||||
}
|
||||
|
||||
// Cycle through Owning Actor's Components and determine if any of them is a Component implementing the Context Effect Interface
|
||||
for (const auto Component : OwningActor->GetComponents())
|
||||
{
|
||||
if (Component)
|
||||
{
|
||||
// If the Component implements the Context Effects Interface, add it to the list
|
||||
if (Component->Implements<UGES_ContextEffectsInterface>())
|
||||
{
|
||||
Implementers.Add(Component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FGES_SpawnContextEffectsInput Input;
|
||||
Input.EffectName = Effect;
|
||||
Input.bAttached = bAttached;
|
||||
Input.Bone = SocketName;
|
||||
Input.ComponentToAttach = MeshComp;
|
||||
Input.Location = bHitSuccess?HitResult.Location:SpawnLocation;
|
||||
Input.Rotation = SpawnRotation;
|
||||
Input.LocationOffset = LocationOffset;
|
||||
Input.RotationOffset = RotationOffset;
|
||||
Input.AnimationSequence = Animation;
|
||||
Input.bHitSuccess = bHitSuccess;
|
||||
Input.HitResult = HitResult;
|
||||
Input.SourceContext = SourceContext;
|
||||
Input.TargetContext = TargetContext;
|
||||
Input.VFXScale = VFXProperties.Scale;
|
||||
Input.AudioVolume = AudioProperties.VolumeMultiplier;
|
||||
Input.AudioPitch = AudioProperties.PitchMultiplier;
|
||||
|
||||
// Cycle through all objects implementing the Context Effect Interface
|
||||
for (UObject* Implementer : Implementers)
|
||||
{
|
||||
// If the object is still valid, Execute the AnimMotionEffect Event on it, passing in relevant data
|
||||
if (Implementer)
|
||||
{
|
||||
IGES_ContextEffectsInterface::Execute_PlayContextEffectsWithInput(Implementer, Input);
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
PerformEditorPreview(OwningActor, SourceContext, TargetContext, MeshComp);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UGES_AnimNotify_ContextEffects::ValidateAssociatedAssets()
|
||||
{
|
||||
Super::ValidateAssociatedAssets();
|
||||
}
|
||||
|
||||
void UGES_AnimNotify_ContextEffects::SetParameters(FGameplayTag EffectIn, FVector LocationOffsetIn, FRotator RotationOffsetIn,
|
||||
FGES_ContextEffectAnimNotifyVFXSettings VFXPropertiesIn, FGES_ContextEffectAnimNotifyAudioSettings AudioPropertiesIn,
|
||||
bool bAttachedIn, FName SocketNameIn, bool bPerformTraceIn, FGES_ContextEffectAnimNotifyTraceSettings TracePropertiesIn)
|
||||
{
|
||||
Effect = EffectIn;
|
||||
LocationOffset = LocationOffsetIn;
|
||||
RotationOffset = RotationOffsetIn;
|
||||
VFXProperties.Scale = VFXPropertiesIn.Scale;
|
||||
AudioProperties.PitchMultiplier = AudioPropertiesIn.PitchMultiplier;
|
||||
AudioProperties.VolumeMultiplier = AudioPropertiesIn.VolumeMultiplier;
|
||||
bAttached = bAttachedIn;
|
||||
SocketName = SocketNameIn;
|
||||
bPerformTrace = bPerformTraceIn;
|
||||
TraceProperties.EndTraceLocationOffset = TracePropertiesIn.EndTraceLocationOffset;
|
||||
TraceProperties.TraceChannel = TracePropertiesIn.TraceChannel;
|
||||
TraceProperties.bIgnoreActor = TracePropertiesIn.bIgnoreActor;
|
||||
}
|
||||
|
||||
void UGES_AnimNotify_ContextEffects::PerformEditorPreview(AActor* OwningActor, FGameplayTagContainer& SourceContext, FGameplayTagContainer& TargetContext, USkeletalMeshComponent* MeshComp)
|
||||
{
|
||||
if (!bAttached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const UGES_ContextEffectsSettings* ContextEffectsSettings = GetDefault<UGES_ContextEffectsSettings>();
|
||||
if (ContextEffectsSettings == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This is for Anim Editor previewing, it is a deconstruction of the calls made by the Interface and the Subsystem
|
||||
if (!ContextEffectsSettings->bPreviewInEditor || ContextEffectsSettings->PreviewSetting.IsNull())
|
||||
return;
|
||||
|
||||
UWorld* World = OwningActor->GetWorld();
|
||||
|
||||
// Get the world, make sure it's an Editor Preview World
|
||||
if (!World || World->WorldType != EWorldType::EditorPreview)
|
||||
return;
|
||||
|
||||
// const auto& PreviewProperties = ContextEffectsSettings->PreviewProperties;
|
||||
const UGES_ContextEffectsPreviewSetting* PreviewSetting = ContextEffectsSettings->PreviewSetting.LoadSynchronous();
|
||||
|
||||
if (PreviewSetting == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Add Preview contexts if necessary
|
||||
SourceContext.AppendTags(PreviewSetting->PreviewSourceContext);
|
||||
TargetContext.AppendTags(PreviewSetting->PreviewTargetContext);
|
||||
|
||||
// Convert given Surface Type to Context and Add it to the Contexts for this Preview
|
||||
if (PreviewSetting->bPreviewPhysicalSurfaceAsContext)
|
||||
{
|
||||
TEnumAsByte<EPhysicalSurface> PhysicalSurfaceType = PreviewSetting->PreviewPhysicalSurface;
|
||||
|
||||
if (const FGameplayTag* SurfaceContextPtr = ContextEffectsSettings->SurfaceTypeToContextMap.Find(PhysicalSurfaceType))
|
||||
{
|
||||
FGameplayTag SurfaceContext = *SurfaceContextPtr;
|
||||
|
||||
SourceContext.AddTag(SurfaceContext);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < PreviewSetting->PreviewContextEffectsLibraries.Num(); ++i)
|
||||
{
|
||||
// Libraries are soft referenced, so you will want to try to load them now
|
||||
if (UObject* EffectsLibrariesObj = PreviewSetting->PreviewContextEffectsLibraries[i].TryLoad())
|
||||
{
|
||||
// Check if it is in fact a UGES_ContextEffectLibrary type
|
||||
if (UGES_ContextEffectsLibrary* EffectLibrary = Cast<UGES_ContextEffectsLibrary>(EffectsLibrariesObj))
|
||||
{
|
||||
// Prepare Sounds and Niagara System Arrays
|
||||
TArray<USoundBase*> TotalSounds;
|
||||
TArray<UNiagaraSystem*> TotalNiagaraSystems;
|
||||
TArray<UParticleSystem*> TotalParticleSystems;
|
||||
|
||||
|
||||
// Attempt to load the Effect Library content (will cache in Transient data on the Effect Library Asset)
|
||||
EffectLibrary->LoadEffects();
|
||||
|
||||
// If the Effect Library is valid and marked as Loaded, Get Effects from it
|
||||
if (EffectLibrary && EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Loaded)
|
||||
{
|
||||
// Prepare local arrays
|
||||
TArray<USoundBase*> Sounds;
|
||||
TArray<UNiagaraSystem*> NiagaraSystems;
|
||||
TArray<UParticleSystem*> ParticleSystems;
|
||||
|
||||
|
||||
// Get the Effects
|
||||
EffectLibrary->GetEffects(Effect, SourceContext, TargetContext, Sounds, NiagaraSystems, ParticleSystems);
|
||||
|
||||
// Append to the accumulating arrays
|
||||
TotalSounds.Append(Sounds);
|
||||
TotalNiagaraSystems.Append(NiagaraSystems);
|
||||
TotalParticleSystems.Append(ParticleSystems);
|
||||
}
|
||||
|
||||
// Cycle through Sounds and call Spawn Sound Attached, passing in relevant data
|
||||
for (USoundBase* Sound : TotalSounds)
|
||||
{
|
||||
UGameplayStatics::SpawnSoundAttached(Sound, MeshComp, SocketName, LocationOffset, RotationOffset, EAttachLocation::KeepRelativeOffset,
|
||||
false, AudioProperties.VolumeMultiplier, AudioProperties.PitchMultiplier, 0.0f, nullptr, nullptr, true);
|
||||
}
|
||||
|
||||
// Cycle through Niagara Systems and call Spawn System Attached, passing in relevant data
|
||||
for (UNiagaraSystem* NiagaraSystem : TotalNiagaraSystems)
|
||||
{
|
||||
UNiagaraFunctionLibrary::SpawnSystemAttached(NiagaraSystem, MeshComp, SocketName, LocationOffset,
|
||||
RotationOffset, VFXProperties.Scale, EAttachLocation::KeepRelativeOffset, true, ENCPoolMethod::None, true, true);
|
||||
}
|
||||
|
||||
// Cycle through Particle Systems and call Spawn System Attached, passing in relevant data
|
||||
for (UParticleSystem* ParticleSystem : TotalParticleSystems)
|
||||
{
|
||||
UGameplayStatics::SpawnEmitterAttached(ParticleSystem, MeshComp, SocketName, LocationOffset,
|
||||
RotationOffset, VFXProperties.Scale, EAttachLocation::KeepRelativeOffset, true, EPSCPoolMethod::None, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Feedback/GES_ContextEffectComponent.h"
|
||||
|
||||
#include "GameplayTagAssetInterface.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Feedback/GES_ContextEffectsSubsystem.h"
|
||||
#include "PhysicalMaterials/PhysicalMaterial.h"
|
||||
#include "GES_LogChannels.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectComponent)
|
||||
|
||||
class UAnimSequenceBase;
|
||||
class USceneComponent;
|
||||
|
||||
|
||||
// Sets default values for this component's properties
|
||||
UGES_ContextEffectComponent::UGES_ContextEffectComponent()
|
||||
{
|
||||
// Disable component tick, enable Auto Activate
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
bAutoActivate = true;
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
// Called when the game starts
|
||||
void UGES_ContextEffectComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// ...
|
||||
CurrentContexts.AppendTags(DefaultEffectContexts);
|
||||
CurrentContextEffectsLibraries = DefaultContextEffectsLibraries;
|
||||
|
||||
// On Begin Play, Load and Add Context Effects pairings
|
||||
if (const UWorld* World = GetWorld())
|
||||
{
|
||||
if (UGES_ContextEffectsSubsystem* ContextEffectsSubsystem = World->GetSubsystem<UGES_ContextEffectsSubsystem>())
|
||||
{
|
||||
ContextEffectsSubsystem->LoadAndAddContextEffectsLibraries(GetOwner(), CurrentContextEffectsLibraries);
|
||||
}
|
||||
}
|
||||
if (bAutoSetupTagsProvider)
|
||||
{
|
||||
SetupTagsProvider();
|
||||
}
|
||||
}
|
||||
|
||||
void UGES_ContextEffectComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
// On End PLay, remove unnecessary context effects pairings
|
||||
if (const UWorld* World = GetWorld())
|
||||
{
|
||||
if (UGES_ContextEffectsSubsystem* ContextEffectsSubsystem = World->GetSubsystem<UGES_ContextEffectsSubsystem>())
|
||||
{
|
||||
ContextEffectsSubsystem->UnloadAndRemoveContextEffectsLibraries(GetOwner());
|
||||
}
|
||||
}
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void UGES_ContextEffectComponent::SetupTagsProvider()
|
||||
{
|
||||
if (GetOwner()->GetClass()->ImplementsInterface(UGameplayTagAssetInterface::StaticClass()))
|
||||
{
|
||||
SetGameplayTagsProvider(GetOwner());
|
||||
}
|
||||
else
|
||||
{
|
||||
TArray<UActorComponent*> Components = GetOwner()->GetComponentsByInterface(UGameplayTagAssetInterface::StaticClass());
|
||||
if (Components.IsValidIndex(0))
|
||||
{
|
||||
SetGameplayTagsProvider(Components[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void UGES_ContextEffectComponent::PlayContextEffectsWithInput_Implementation(FGES_SpawnContextEffectsInput Input)
|
||||
{
|
||||
AggregateContexts(Input);
|
||||
InjectPhysicalSurfaceToContexts(Input.HitResult, Input.SourceContext);
|
||||
|
||||
// Prep Components
|
||||
TArray<UAudioComponent*> AudioComponentsToAdd;
|
||||
TArray<UNiagaraComponent*> NiagaraComponentsToAdd;
|
||||
TArray<UParticleSystemComponent*> ParticleComponentsToAdd;
|
||||
|
||||
|
||||
// Cycle through Active Audio Components and cache
|
||||
for (UAudioComponent* ActiveAudioComponent : ActiveAudioComponents)
|
||||
{
|
||||
if (ActiveAudioComponent)
|
||||
{
|
||||
AudioComponentsToAdd.Add(ActiveAudioComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through Active Niagara Components and cache
|
||||
for (UNiagaraComponent* ActiveNiagaraComponent : ActiveNiagaraComponents)
|
||||
{
|
||||
if (ActiveNiagaraComponent)
|
||||
{
|
||||
NiagaraComponentsToAdd.Add(ActiveNiagaraComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through Active Particle Components and cache
|
||||
for (UParticleSystemComponent* ActiveParticleComponent : ActiveParticleComponents)
|
||||
{
|
||||
if (ActiveParticleComponent)
|
||||
{
|
||||
ParticleComponentsToAdd.Add(ActiveParticleComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// Get World
|
||||
if (const UWorld* World = GetWorld())
|
||||
{
|
||||
// Get Subsystem
|
||||
if (UGES_ContextEffectsSubsystem* ContextEffectsSubsystem = World->GetSubsystem<UGES_ContextEffectsSubsystem>())
|
||||
{
|
||||
// Set up components
|
||||
FGES_SpawnContextEffectsOutput Output;
|
||||
|
||||
// Spawn effects
|
||||
ContextEffectsSubsystem->SpawnContextEffectsExt(GetOwner(), Input, Output);
|
||||
|
||||
// Append resultant effects
|
||||
AudioComponentsToAdd.Append(Output.AudioComponents);
|
||||
NiagaraComponentsToAdd.Append(Output.NiagaraComponents);
|
||||
ParticleComponentsToAdd.Append(Output.ParticlesComponents);
|
||||
}
|
||||
}
|
||||
|
||||
// Append Active Audio Components
|
||||
ActiveAudioComponents.Empty();
|
||||
ActiveAudioComponents.Append(AudioComponentsToAdd);
|
||||
|
||||
// Append Active
|
||||
ActiveNiagaraComponents.Empty();
|
||||
ActiveNiagaraComponents.Append(NiagaraComponentsToAdd);
|
||||
|
||||
ActiveParticleComponents.Empty();
|
||||
ActiveParticleComponents.Append(ParticleComponentsToAdd);
|
||||
}
|
||||
|
||||
void UGES_ContextEffectComponent::AggregateContexts(FGES_SpawnContextEffectsInput& Input) const
|
||||
{
|
||||
if (Input.SourceContextType == EGES_EffectsContextType::Merge)
|
||||
{
|
||||
FGameplayTagContainer TotalContexts;
|
||||
// Aggregate contexts
|
||||
TotalContexts.AppendTags(Input.SourceContext);
|
||||
TotalContexts.AppendTags(CurrentContexts);
|
||||
if (IGameplayTagAssetInterface* TagAssetInterface = Cast<IGameplayTagAssetInterface>(GameplayTagsProvider))
|
||||
{
|
||||
FGameplayTagContainer RetTags;
|
||||
TagAssetInterface->GetOwnedGameplayTags(RetTags);
|
||||
TotalContexts.AppendTags(RetTags);
|
||||
}
|
||||
Input.SourceContext = TotalContexts;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void UGES_ContextEffectComponent::InjectPhysicalSurfaceToContexts(const FHitResult& InHitResult, FGameplayTagContainer& Contexts)
|
||||
{
|
||||
// Check if converting Physical Surface Type to Context
|
||||
if (!bConvertPhysicalSurfaceToContext)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Phys Mat Type Pointer
|
||||
TWeakObjectPtr<UPhysicalMaterial> PhysicalSurfaceTypePtr = InHitResult.PhysMaterial;
|
||||
|
||||
// Check if pointer is okay
|
||||
if (PhysicalSurfaceTypePtr.IsValid())
|
||||
{
|
||||
// Get the Surface Type Pointer
|
||||
TEnumAsByte<EPhysicalSurface> PhysicalSurfaceType = PhysicalSurfaceTypePtr->SurfaceType;
|
||||
|
||||
// If Settings are valid
|
||||
if (const UGES_ContextEffectsSettings* ContextEffectsSettings = GetDefault<UGES_ContextEffectsSettings>())
|
||||
{
|
||||
if (ContextEffectsSettings->SurfaceTypeToContextMap.IsEmpty())
|
||||
{
|
||||
GES_CLOG(Warning, "No surface type to context map, Please check ContextEffectsSetting in ProjectSettings!");
|
||||
if (FallbackPhysicalSurface.IsValid())
|
||||
{
|
||||
Contexts.AddTag(FallbackPhysicalSurface);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert Surface Type to known
|
||||
if (const FGameplayTag* SurfaceContextPtr = ContextEffectsSettings->SurfaceTypeToContextMap.Find(PhysicalSurfaceType))
|
||||
{
|
||||
FGameplayTag SurfaceContext = *SurfaceContextPtr;
|
||||
|
||||
Contexts.AddTag(SurfaceContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
GES_CLOG(Warning, "No surface type(%d) to context map found, Please check ContextEffectsSetting in ProjectSettings!", PhysicalSurfaceType.GetValue());
|
||||
if (FallbackPhysicalSurface.IsValid())
|
||||
{
|
||||
Contexts.AddTag(FallbackPhysicalSurface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FallbackPhysicalSurface.IsValid())
|
||||
{
|
||||
Contexts.AddTag(FallbackPhysicalSurface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGES_ContextEffectComponent::SetGameplayTagsProvider(UObject* Provider)
|
||||
{
|
||||
if (!IsValid(Provider))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (IGameplayTagAssetInterface* TagAssetInterface = Cast<IGameplayTagAssetInterface>(Provider))
|
||||
{
|
||||
GameplayTagsProvider = Provider;
|
||||
}
|
||||
else
|
||||
{
|
||||
GES_CLOG(Warning, "Passed in GameplayTagsProvider(%s) Doesn't implement GameplayTagAssetInterface, it can't provide gameplay tags.", *GetNameSafe(Provider->GetClass()));
|
||||
}
|
||||
}
|
||||
|
||||
void UGES_ContextEffectComponent::UpdateEffectContexts(FGameplayTagContainer NewEffectContexts)
|
||||
{
|
||||
// Reset and update
|
||||
CurrentContexts.Reset(NewEffectContexts.Num());
|
||||
CurrentContexts.AppendTags(NewEffectContexts);
|
||||
}
|
||||
|
||||
void UGES_ContextEffectComponent::UpdateLibraries(
|
||||
TSet<TSoftObjectPtr<UGES_ContextEffectsLibrary>> NewContextEffectsLibraries)
|
||||
{
|
||||
// Clear out existing Effects
|
||||
CurrentContextEffectsLibraries = NewContextEffectsLibraries;
|
||||
|
||||
// Get World
|
||||
if (const UWorld* World = GetWorld())
|
||||
{
|
||||
// Get Subsystem
|
||||
if (UGES_ContextEffectsSubsystem* ContextEffectsSubsystem = World->GetSubsystem<UGES_ContextEffectsSubsystem>())
|
||||
{
|
||||
// Load and Add Libraries to Subsystem
|
||||
ContextEffectsSubsystem->LoadAndAddContextEffectsLibraries(GetOwner(), CurrentContextEffectsLibraries);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Feedback/GES_ContextEffectsEnumLibrary.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsEnumLibrary)
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Feedback/GES_ContextEffectsLibrary.h"
|
||||
#include "NiagaraSystem.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
#include "UObject/ObjectSaveContext.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsLibrary)
|
||||
|
||||
|
||||
void UGES_ContextEffectsLibrary::GetEffects(const FGameplayTag Effect, const FGameplayTagContainer& SourceContext, const FGameplayTagContainer& TargetContext,
|
||||
TArray<USoundBase*>& Sounds, TArray<UNiagaraSystem*>& NiagaraSystems, TArray<UParticleSystem*>& ParticleSystems)
|
||||
{
|
||||
// Make sure Effect is valid and Library is loaded
|
||||
if (Effect.IsValid() && SourceContext.IsValid() && EffectsLoadState == EGES_ContextEffectsLibraryLoadState::Loaded)
|
||||
{
|
||||
// Loop through Context Effects
|
||||
for (const auto& ActiveContextEffect : ActiveContextEffects)
|
||||
{
|
||||
bool bMatchesEffectTag = Effect.MatchesTagExact(ActiveContextEffect->EffectTag);
|
||||
bool bMatchesSourceContext = ActiveContextEffect->SourceTagQuery.Matches(SourceContext) && (ActiveContextEffect->SourceTagQuery.IsEmpty() == SourceContext.IsEmpty());
|
||||
|
||||
// Target context is optional
|
||||
bool bMatchesTargetContext = ActiveContextEffect->TargetTagQuery.IsEmpty() || ActiveContextEffect->TargetTagQuery.Matches(TargetContext);
|
||||
|
||||
// Make sure the Effect is an exact Tag Match and ensure the Context has all tags in the Effect (and neither or both are empty)
|
||||
if (bMatchesEffectTag && bMatchesSourceContext && bMatchesTargetContext)
|
||||
{
|
||||
// Get all Matching Sounds and Niagara Systems
|
||||
Sounds.Append(ActiveContextEffect->Sounds);
|
||||
NiagaraSystems.Append(ActiveContextEffect->NiagaraSystems);
|
||||
ParticleSystems.Append(ActiveContextEffect->ParticleSystems);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGES_ContextEffectsLibrary::LoadEffects()
|
||||
{
|
||||
// Load Effects into Library if not currently loading
|
||||
if (EffectsLoadState != EGES_ContextEffectsLibraryLoadState::Loading)
|
||||
{
|
||||
// Set load state to loading
|
||||
EffectsLoadState = EGES_ContextEffectsLibraryLoadState::Loading;
|
||||
|
||||
// Clear out any old Active Effects
|
||||
ActiveContextEffects.Empty();
|
||||
|
||||
// Call internal loading function
|
||||
LoadEffectsInternal();
|
||||
}
|
||||
}
|
||||
|
||||
EGES_ContextEffectsLibraryLoadState UGES_ContextEffectsLibrary::GetContextEffectsLibraryLoadState()
|
||||
{
|
||||
// Return current Load State
|
||||
return EffectsLoadState;
|
||||
}
|
||||
|
||||
void UGES_ContextEffectsLibrary::LoadEffectsInternal()
|
||||
{
|
||||
// TODO Add Async Loading for Libraries
|
||||
|
||||
// Copy data for async load
|
||||
TArray<FGES_ContextEffects> LocalContextEffects = ContextEffects;
|
||||
|
||||
// Prepare Active Context Effects Array
|
||||
TArray<UGES_ActiveContextEffects*> ActiveContextEffectsArray;
|
||||
|
||||
// Loop through Context Effects
|
||||
for (const FGES_ContextEffects& ContextEffect : LocalContextEffects)
|
||||
{
|
||||
// Make sure Tags are Valid
|
||||
if (ContextEffect.EffectTag.IsValid() && !ContextEffect.SourceTagQuery.IsEmpty())
|
||||
{
|
||||
// Create new Active Context Effect
|
||||
UGES_ActiveContextEffects* NewActiveContextEffects = NewObject<UGES_ActiveContextEffects>(this);
|
||||
|
||||
// Pass relevant tag data
|
||||
NewActiveContextEffects->EffectTag = ContextEffect.EffectTag;
|
||||
NewActiveContextEffects->SourceTagQuery = ContextEffect.SourceTagQuery;
|
||||
NewActiveContextEffects->TargetTagQuery = ContextEffect.TargetTagQuery;
|
||||
|
||||
|
||||
// Try to load and add Effects to New Active Context Effects
|
||||
for (const FSoftObjectPath& Effect : ContextEffect.Effects)
|
||||
{
|
||||
if (UObject* Object = Effect.TryLoad())
|
||||
{
|
||||
if (Object->IsA(USoundBase::StaticClass()))
|
||||
{
|
||||
if (USoundBase* SoundBase = Cast<USoundBase>(Object))
|
||||
{
|
||||
NewActiveContextEffects->Sounds.Add(SoundBase);
|
||||
}
|
||||
}
|
||||
else if (Object->IsA(UNiagaraSystem::StaticClass()))
|
||||
{
|
||||
if (UNiagaraSystem* NiagaraSystem = Cast<UNiagaraSystem>(Object))
|
||||
{
|
||||
NewActiveContextEffects->NiagaraSystems.Add(NiagaraSystem);
|
||||
}
|
||||
}
|
||||
else if (Object->IsA(UParticleSystem::StaticClass()))
|
||||
{
|
||||
if (UParticleSystem* ParticleSystem = Cast<UParticleSystem>(Object))
|
||||
{
|
||||
NewActiveContextEffects->ParticleSystems.Add(ParticleSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add New Active Context to the Active Context Effects Array
|
||||
ActiveContextEffectsArray.Add(NewActiveContextEffects);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Call Load Complete after Async Load
|
||||
// Mark loading complete
|
||||
this->OnContextEffectLibraryLoadingComplete(ActiveContextEffectsArray);
|
||||
}
|
||||
|
||||
void UGES_ContextEffectsLibrary::OnContextEffectLibraryLoadingComplete(
|
||||
TArray<UGES_ActiveContextEffects*> InActiveContextEffects)
|
||||
{
|
||||
// Flag data as loaded
|
||||
EffectsLoadState = EGES_ContextEffectsLibraryLoadState::Loaded;
|
||||
|
||||
// Append incoming Context Effects Array to current list of Active Context Effects
|
||||
ActiveContextEffects.Append(InActiveContextEffects);
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UGES_ContextEffectsLibrary::PreSave(FObjectPreSaveContext SaveContext)
|
||||
{
|
||||
for (FGES_ContextEffects& ContextEffect : ContextEffects)
|
||||
{
|
||||
if (!ContextEffect.Context.IsEmpty())
|
||||
{
|
||||
FGameplayTagQueryExpression Expression;
|
||||
Expression.AllTagsMatch().AddTags(ContextEffect.Context);
|
||||
ContextEffect.SourceTagQuery.Build(Expression, FString::Format(TEXT("Has all tags({0})"), {ContextEffect.Context.ToStringSimple()}));
|
||||
ContextEffect.Context = FGameplayTagContainer();
|
||||
}
|
||||
ContextEffect.EditorFriendlyName = ContextEffect.EffectTag.IsValid()
|
||||
? FString::Format(TEXT("Effect({0}) Source({1}) Target({2})"),
|
||||
{
|
||||
ContextEffect.EffectTag.GetTagName().ToString(), ContextEffect.SourceTagQuery.GetDescription(),
|
||||
ContextEffect.TargetTagQuery.GetDescription()
|
||||
})
|
||||
: TEXT("Invalid Effect");
|
||||
}
|
||||
Super::PreSave(SaveContext);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Feedback/GES_ContextEffectsPreviewSetting.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsPreviewSetting)
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Feedback/GES_ContextEffectsStructLibrary.h"
|
||||
#include "Animation/AnimSequenceBase.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsStructLibrary)
|
||||
@@ -0,0 +1,289 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Feedback/GES_ContextEffectsSubsystem.h"
|
||||
|
||||
#include "Feedback/GES_ContextEffectsLibrary.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NiagaraFunctionLibrary.h"
|
||||
#include "NiagaraSystem.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GES_ContextEffectsSubsystem)
|
||||
|
||||
class AActor;
|
||||
class UAudioComponent;
|
||||
class UNiagaraSystem;
|
||||
class USceneComponent;
|
||||
class USoundBase;
|
||||
|
||||
void UGES_ContextEffectsSubsystem::SpawnContextEffects(UObject* WorldContextObject, TSoftObjectPtr<UGES_ContextEffectsLibrary> EffectsLibrary, FGES_SpawnContextEffectsInput Input,
|
||||
FGES_SpawnContextEffectsOutput& Output)
|
||||
{
|
||||
if (WorldContextObject == nullptr || WorldContextObject->GetWorld() == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EffectsLibrary.IsNull())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare Arrays for Sounds and Niagara Systems
|
||||
TArray<USoundBase*> TotalSounds;
|
||||
TArray<UNiagaraSystem*> TotalNiagaraSystems;
|
||||
TArray<UParticleSystem*> TotalParticleSystems;
|
||||
|
||||
// Cycle through Effect Libraries
|
||||
if (UGES_ContextEffectsLibrary* EffectLibrary = EffectsLibrary.LoadSynchronous())
|
||||
{
|
||||
if (EffectLibrary && EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Unloaded)
|
||||
{
|
||||
// Sync load effects
|
||||
EffectLibrary->LoadEffects();
|
||||
}
|
||||
|
||||
// Check if the Effect Library is valid and data Loaded
|
||||
if (EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Loaded)
|
||||
{
|
||||
// Set up local list of Sounds and Niagara Systems
|
||||
TArray<USoundBase*> Sounds;
|
||||
TArray<UNiagaraSystem*> NiagaraSystems;
|
||||
TArray<UParticleSystem*> ParticleSystems;
|
||||
|
||||
// Get Sounds and Niagara Systems
|
||||
EffectLibrary->GetEffects(Input.EffectName, Input.SourceContext, Input.TargetContext, Sounds, NiagaraSystems, ParticleSystems);
|
||||
|
||||
// Append to accumulating array
|
||||
TotalSounds.Append(Sounds);
|
||||
TotalNiagaraSystems.Append(NiagaraSystems);
|
||||
TotalParticleSystems.Append(ParticleSystems);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through found Sounds
|
||||
for (USoundBase* Sound : TotalSounds)
|
||||
{
|
||||
if (Input.bAttached)
|
||||
{
|
||||
// Spawn Sounds Attached, add Audio Component to List of ACs
|
||||
UAudioComponent* AudioComponent = UGameplayStatics::SpawnSoundAttached(Sound, Input.ComponentToAttach, Input.Bone, Input.LocationOffset, Input.RotationOffset,
|
||||
EAttachLocation::KeepRelativeOffset,
|
||||
false, Input.AudioVolume, Input.AudioPitch, 0.0f, nullptr, nullptr, true);
|
||||
Output.AudioComponents.Add(AudioComponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
UAudioComponent* AudioComponent = UGameplayStatics::SpawnSoundAtLocation(WorldContextObject, Sound, Input.Location, Input.Rotation, Input.AudioVolume, Input.AudioPitch, 0.0f, nullptr,
|
||||
nullptr, true);
|
||||
Output.AudioComponents.Add(AudioComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through found Niagara Systems
|
||||
for (UNiagaraSystem* NiagaraSystem : TotalNiagaraSystems)
|
||||
{
|
||||
if (Input.bAttached)
|
||||
{
|
||||
// Spawn Niagara Systems Attached, add Niagara Component to List of NCs
|
||||
UNiagaraComponent* NiagaraComponent = UNiagaraFunctionLibrary::SpawnSystemAttached(NiagaraSystem, Input.ComponentToAttach, Input.Bone, Input.LocationOffset,
|
||||
Input.RotationOffset, Input.VFXScale, EAttachLocation::KeepRelativeOffset, true,
|
||||
ENCPoolMethod::None,
|
||||
true,
|
||||
true);
|
||||
Output.NiagaraComponents.Add(NiagaraComponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
UNiagaraComponent* NiagaraComponent = UNiagaraFunctionLibrary::SpawnSystemAtLocation(WorldContextObject, NiagaraSystem, Input.Location,
|
||||
Input.Rotation, Input.VFXScale, true, true,
|
||||
ENCPoolMethod::None, true);
|
||||
|
||||
Output.NiagaraComponents.Add(NiagaraComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through found Particle Systems
|
||||
for (UParticleSystem* ParticleSystem : TotalParticleSystems)
|
||||
{
|
||||
if (Input.bAttached)
|
||||
{
|
||||
// Spawn Particle Systems Attached, add Niagara Component to List of NCs
|
||||
UParticleSystemComponent* ParticleComponent = UGameplayStatics::SpawnEmitterAttached(ParticleSystem, Input.ComponentToAttach, Input.Bone, Input.LocationOffset,
|
||||
Input.RotationOffset, Input.VFXScale, EAttachLocation::KeepRelativeOffset, true,
|
||||
EPSCPoolMethod::None,
|
||||
true);
|
||||
Output.ParticlesComponents.Add(ParticleComponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
UParticleSystemComponent* ParticleComponent = UGameplayStatics::SpawnEmitterAtLocation(WorldContextObject, ParticleSystem, Input.Location, Input.Rotation, Input.VFXScale, true,
|
||||
EPSCPoolMethod::None, true);
|
||||
Output.ParticlesComponents.Add(ParticleComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGES_ContextEffectsSubsystem::SpawnContextEffectsExt(const AActor* SpawningActor, const FGES_SpawnContextEffectsInput& Input, FGES_SpawnContextEffectsOutput& Output)
|
||||
{
|
||||
// First determine if this Actor has a matching Set of Libraries
|
||||
if (TObjectPtr<UGES_ContextEffectsSet>* EffectsLibrariesSetPtr = ActiveActorEffectsMap.Find(SpawningActor))
|
||||
{
|
||||
// Validate the pointers from the Map Find
|
||||
if (UGES_ContextEffectsSet* EffectsLibraries = *EffectsLibrariesSetPtr)
|
||||
{
|
||||
// Prepare Arrays for Sounds and Niagara Systems
|
||||
TArray<USoundBase*> TotalSounds;
|
||||
TArray<UNiagaraSystem*> TotalNiagaraSystems;
|
||||
TArray<UParticleSystem*> TotalParticleSystems;
|
||||
|
||||
// Cycle through Effect Libraries
|
||||
for (UGES_ContextEffectsLibrary* EffectLibrary : EffectsLibraries->ContextEffectsLibraries)
|
||||
{
|
||||
// Check if the Effect Library is valid and data Loaded
|
||||
if (EffectLibrary && EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Loaded)
|
||||
{
|
||||
// Set up local list of Sounds and Niagara Systems
|
||||
TArray<USoundBase*> Sounds;
|
||||
TArray<UNiagaraSystem*> NiagaraSystems;
|
||||
TArray<UParticleSystem*> ParticleSystems;
|
||||
|
||||
// Get Sounds and Niagara Systems
|
||||
EffectLibrary->GetEffects(Input.EffectName, Input.SourceContext, Input.TargetContext, Sounds, NiagaraSystems, ParticleSystems);
|
||||
|
||||
// Append to accumulating array
|
||||
TotalSounds.Append(Sounds);
|
||||
TotalNiagaraSystems.Append(NiagaraSystems);
|
||||
TotalParticleSystems.Append(ParticleSystems);
|
||||
}
|
||||
else if (EffectLibrary && EffectLibrary->GetContextEffectsLibraryLoadState() == EGES_ContextEffectsLibraryLoadState::Unloaded)
|
||||
{
|
||||
// Else load effects
|
||||
EffectLibrary->LoadEffects();
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through found Sounds
|
||||
for (USoundBase* Sound : TotalSounds)
|
||||
{
|
||||
if (Input.bAttached)
|
||||
{
|
||||
// Spawn Sounds Attached, add Audio Component to List of ACs
|
||||
UAudioComponent* AudioComponent = UGameplayStatics::SpawnSoundAttached(Sound, Input.ComponentToAttach, Input.Bone, Input.LocationOffset, Input.RotationOffset,
|
||||
EAttachLocation::KeepRelativeOffset,
|
||||
false, Input.AudioVolume, Input.AudioPitch, 0.0f, nullptr, nullptr, true);
|
||||
Output.AudioComponents.Add(AudioComponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
UAudioComponent* AudioComponent = UGameplayStatics::SpawnSoundAtLocation(SpawningActor, Sound, Input.Location, Input.Rotation, Input.AudioVolume, Input.AudioPitch, 0.0f, nullptr,
|
||||
nullptr, true);
|
||||
Output.AudioComponents.Add(AudioComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through found Niagara Systems
|
||||
for (UNiagaraSystem* NiagaraSystem : TotalNiagaraSystems)
|
||||
{
|
||||
if (Input.bAttached)
|
||||
{
|
||||
// Spawn Niagara Systems Attached, add Niagara Component to List of NCs
|
||||
UNiagaraComponent* NiagaraComponent = UNiagaraFunctionLibrary::SpawnSystemAttached(NiagaraSystem, Input.ComponentToAttach, Input.Bone, Input.LocationOffset,
|
||||
Input.RotationOffset, Input.VFXScale, EAttachLocation::KeepRelativeOffset, true,
|
||||
ENCPoolMethod::None,
|
||||
true,
|
||||
true);
|
||||
Output.NiagaraComponents.Add(NiagaraComponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
UNiagaraComponent* NiagaraComponent = UNiagaraFunctionLibrary::SpawnSystemAtLocation(SpawningActor, NiagaraSystem, Input.Location,
|
||||
Input.Rotation, Input.VFXScale, true, true,
|
||||
ENCPoolMethod::None, true);
|
||||
|
||||
Output.NiagaraComponents.Add(NiagaraComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through found Particle Systems
|
||||
for (UParticleSystem* ParticleSystem : TotalParticleSystems)
|
||||
{
|
||||
if (Input.bAttached)
|
||||
{
|
||||
// Spawn Particle Systems Attached, add Niagara Component to List of NCs
|
||||
UParticleSystemComponent* ParticleComponent = UGameplayStatics::SpawnEmitterAttached(ParticleSystem, Input.ComponentToAttach, Input.Bone, Input.LocationOffset,
|
||||
Input.RotationOffset, Input.VFXScale, EAttachLocation::KeepRelativeOffset, true,
|
||||
EPSCPoolMethod::None,
|
||||
true);
|
||||
Output.ParticlesComponents.Add(ParticleComponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
UParticleSystemComponent* ParticleComponent = UGameplayStatics::SpawnEmitterAtLocation(SpawningActor, ParticleSystem, Input.Location, Input.Rotation, Input.VFXScale, true,
|
||||
EPSCPoolMethod::None, true);
|
||||
Output.ParticlesComponents.Add(ParticleComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UGES_ContextEffectsSubsystem::GetContextFromSurfaceType(
|
||||
TEnumAsByte<EPhysicalSurface> PhysicalSurface, FGameplayTag& Context)
|
||||
{
|
||||
// Get Project Settings
|
||||
if (const UGES_ContextEffectsSettings* ProjectSettings = GetDefault<UGES_ContextEffectsSettings>())
|
||||
{
|
||||
// Find which Gameplay Tag the Surface Type is mapped to
|
||||
if (const FGameplayTag* GameplayTagPtr = ProjectSettings->SurfaceTypeToContextMap.Find(PhysicalSurface))
|
||||
{
|
||||
Context = *GameplayTagPtr;
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if Context is Valid
|
||||
return Context.IsValid();
|
||||
}
|
||||
|
||||
void UGES_ContextEffectsSubsystem::LoadAndAddContextEffectsLibraries(AActor* OwningActor,
|
||||
TSet<TSoftObjectPtr<UGES_ContextEffectsLibrary>> ContextEffectsLibraries)
|
||||
{
|
||||
// Early out if Owning Actor is invalid or if the associated Libraries is 0 (or less)
|
||||
if (OwningActor == nullptr || ContextEffectsLibraries.Num() <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new Context Effect Set
|
||||
UGES_ContextEffectsSet* EffectsLibrariesSet = NewObject<UGES_ContextEffectsSet>(this);
|
||||
|
||||
// Cycle through Libraries getting Soft Obj Refs
|
||||
for (const TSoftObjectPtr<UGES_ContextEffectsLibrary>& ContextEffectSoftObj : ContextEffectsLibraries)
|
||||
{
|
||||
// Load Library Assets from Soft Obj refs
|
||||
// TODO Support Async Loading of Asset Data
|
||||
if (UGES_ContextEffectsLibrary* EffectsLibrary = ContextEffectSoftObj.LoadSynchronous())
|
||||
{
|
||||
// Call load on valid Libraries
|
||||
EffectsLibrary->LoadEffects();
|
||||
|
||||
// Add new library to Set
|
||||
EffectsLibrariesSet->ContextEffectsLibraries.Add(EffectsLibrary);
|
||||
}
|
||||
}
|
||||
|
||||
// Update Active Actor Effects Map
|
||||
ActiveActorEffectsMap.Emplace(OwningActor, EffectsLibrariesSet);
|
||||
}
|
||||
|
||||
void UGES_ContextEffectsSubsystem::UnloadAndRemoveContextEffectsLibraries(AActor* OwningActor)
|
||||
{
|
||||
// Early out if Owning Actor is invalid
|
||||
if (OwningActor == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove ref from Active Actor/Effects Set Map
|
||||
ActiveActorEffectsMap.Remove(OwningActor);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "GES_LogChannels.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogGES)
|
||||
|
||||
|
||||
FString GetGESLogContextString(const UObject* ContextObject)
|
||||
{
|
||||
ENetRole Role = ROLE_None;
|
||||
FString RoleName = TEXT("None");
|
||||
FString Name = "None";
|
||||
|
||||
if (const AActor* Actor = Cast<AActor>(ContextObject))
|
||||
{
|
||||
Role = Actor->GetLocalRole();
|
||||
Name = Actor->GetName();
|
||||
}
|
||||
else if (const UActorComponent* Component = Cast<UActorComponent>(ContextObject))
|
||||
{
|
||||
if (AActor* ActorOwner = Cast<AActor>(Component->GetOuter()))
|
||||
{
|
||||
Role = ActorOwner->GetLocalRole();
|
||||
Name = ActorOwner->GetName();
|
||||
}
|
||||
else
|
||||
{
|
||||
const AActor* Owner = Component->GetOwner();
|
||||
Role = IsValid(Owner) ? Owner->GetLocalRole() : ROLE_None;
|
||||
Name = IsValid(Owner) ? Owner->GetName() : TEXT("None");
|
||||
}
|
||||
}
|
||||
else if (IsValid(ContextObject))
|
||||
{
|
||||
Name = ContextObject->GetName();
|
||||
}
|
||||
|
||||
if (Role != ROLE_None)
|
||||
{
|
||||
RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client");
|
||||
}
|
||||
return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "GES_Tags.h"
|
||||
|
||||
namespace GMS_MovementModeTags
|
||||
{
|
||||
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Root, FName{TEXTVIEW("GES")},"Generic Effects System")
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "GenericEffectsSystem.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FGenericEffectsSystemModule"
|
||||
|
||||
void FGenericEffectsSystemModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
|
||||
}
|
||||
|
||||
void FGenericEffectsSystemModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FGenericEffectsSystemModule, GenericEffectsSystem)
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Animation/AnimNotifies/AnimNotify.h"
|
||||
#include "Chaos/ChaosEngineInterface.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "GES_ContextEffectsStructLibrary.h"
|
||||
#include "Engine/EngineTypes.h"
|
||||
#include "GES_AnimNotify_ContextEffects.generated.h"
|
||||
|
||||
class UGES_AnimNotify_ContextEffects;
|
||||
|
||||
/**
|
||||
* Base class for customizing spawn behavior of context effects.
|
||||
* 自定义情景效果生成行为的基类。
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable, EditInlineNew, DefaultToInstanced, CollapseCategories, Const)
|
||||
class UGES_ContextEffectsSpawnParametersProvider : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Provides spawn parameters for context effects.
|
||||
* 为情景效果提供生成参数。
|
||||
* @param InMeshComp The skeletal mesh component. 骨骼网格组件。
|
||||
* @param InNotifyNotify The context effects notify. 情景效果通知。
|
||||
* @param InAnimation The animation sequence. 动画序列。
|
||||
* @param OutSpawnLocation The spawn location (output). 生成位置(输出)。
|
||||
* @param OutSpawnRotation The spawn rotation (output). 生成旋转(输出)。
|
||||
* @return True if parameters were provided, false otherwise. 如果提供了参数则返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category="GES|AnimNotify")
|
||||
bool ProvideParameters(USkeletalMeshComponent* InMeshComp, const UGES_AnimNotify_ContextEffects* InNotifyNotify, UAnimSequenceBase* InAnimation,
|
||||
FVector& OutSpawnLocation, FRotator& OutSpawnRotation) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Animation notify for playing context effects.
|
||||
* 用于播放情景效果的动画通知。
|
||||
*/
|
||||
UCLASS(const, hidecategories = Object, CollapseCategories, Config = Game, meta = (DisplayName = "Play Context Effects"))
|
||||
class GENERICEFFECTSSYSTEM_API UGES_AnimNotify_ContextEffects : public UAnimNotify
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor for the context effects animation notify.
|
||||
* 情景效果动画通知构造函数。
|
||||
*/
|
||||
UGES_AnimNotify_ContextEffects(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
|
||||
|
||||
/**
|
||||
* Called after the object is loaded.
|
||||
* 对象加载后调用。
|
||||
*/
|
||||
virtual void PostLoad() override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Handles property changes in the editor.
|
||||
* 处理编辑器中的属性更改。
|
||||
* @param PropertyChangedEvent The property change event. 属性更改事件。
|
||||
*/
|
||||
virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Retrieves the display name for the notify.
|
||||
* 获取通知的显示名称。
|
||||
* @return The notify name. 通知名称。
|
||||
*/
|
||||
virtual FString GetNotifyName_Implementation() const override;
|
||||
|
||||
/**
|
||||
* Called when the notify is triggered during animation.
|
||||
* 动画期间通知触发时调用。
|
||||
* @param MeshComp The skeletal mesh component. 骨骼网格组件。
|
||||
* @param Animation The animation sequence. 动画序列。
|
||||
* @param EventReference The animation notify event reference. 动画通知事件引用。
|
||||
*/
|
||||
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Validates associated assets in the editor.
|
||||
* 在编辑器中验证相关资产。
|
||||
*/
|
||||
virtual void ValidateAssociatedAssets() override;
|
||||
#endif
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Sets parameters for the context effects notify in the editor.
|
||||
* 在编辑器中设置情景效果通知的参数。
|
||||
* @param EffectIn The effect tag. 效果标签。
|
||||
* @param LocationOffsetIn The location offset. 位置偏移。
|
||||
* @param RotationOffsetIn The rotation offset. 旋转偏移。
|
||||
* @param VFXPropertiesIn The VFX settings. VFX设置。
|
||||
* @param AudioPropertiesIn The audio settings. 音频设置。
|
||||
* @param bAttachedIn Whether to attach to mesh. 是否附加到网格。
|
||||
* @param SocketNameIn The socket name for attachment. 附加的插槽名称。
|
||||
* @param bPerformTraceIn Whether to perform a trace. 是否执行追踪。
|
||||
* @param TracePropertiesIn The trace settings. 追踪设置。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|AnimNotify")
|
||||
void SetParameters(FGameplayTag EffectIn, FVector LocationOffsetIn, FRotator RotationOffsetIn,
|
||||
FGES_ContextEffectAnimNotifyVFXSettings VFXPropertiesIn, FGES_ContextEffectAnimNotifyAudioSettings AudioPropertiesIn,
|
||||
bool bAttachedIn, FName SocketNameIn, bool bPerformTraceIn, FGES_ContextEffectAnimNotifyTraceSettings TracePropertiesIn);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* The effect to play.
|
||||
* 要播放的效果。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (DisplayName = "Effect", ExposeOnSpawn = true))
|
||||
FGameplayTag Effect;
|
||||
|
||||
/**
|
||||
* Location offset for effect spawning (socket if attached, mesh if not).
|
||||
* 效果生成的位置偏移(附加时为插槽,否则为网格)。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true))
|
||||
FVector LocationOffset = FVector::ZeroVector;
|
||||
|
||||
/**
|
||||
* Rotation offset for effect spawning.
|
||||
* 效果生成的旋转偏移。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true))
|
||||
FRotator RotationOffset = FRotator::ZeroRotator;
|
||||
|
||||
/**
|
||||
* Visual effects settings for the notify.
|
||||
* 通知的视觉效果设置。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true))
|
||||
FGES_ContextEffectAnimNotifyVFXSettings VFXProperties;
|
||||
|
||||
/**
|
||||
* Audio settings for the notify.
|
||||
* 通知的音频设置。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true))
|
||||
FGES_ContextEffectAnimNotifyAudioSettings AudioProperties;
|
||||
|
||||
/**
|
||||
* Whether to attach the effect to the mesh component.
|
||||
* 是否将效果附加到网格组件。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AttachmentProperties", meta = (DisplayName="Attach To Mesh", ExposeOnSpawn = true))
|
||||
uint32 bAttached : 1;
|
||||
|
||||
/**
|
||||
* Optional provider for custom spawn location/rotation if not attached.
|
||||
* 如果未附加,可选的自定义生成位置/旋转提供者。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category="AttachmentProperties", meta=(EditCondition="!bAttached"))
|
||||
TObjectPtr<UGES_ContextEffectsSpawnParametersProvider> SpawnParametersProvider;
|
||||
|
||||
/**
|
||||
* Socket name to attach the effect to.
|
||||
* 附加效果的插槽名称。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AttachmentProperties", meta=(EditCondition="bAttached"))
|
||||
FName SocketName{NAME_None};
|
||||
|
||||
/**
|
||||
* Whether to perform a trace for surface type conversion.
|
||||
* 是否执行追踪以进行表面类型转换。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true))
|
||||
uint32 bPerformTrace : 1;
|
||||
|
||||
/**
|
||||
* Trace settings for the notify.
|
||||
* 通知的追踪设置。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AnimNotify", meta = (ExposeOnSpawn = true, EditCondition = "bPerformTrace"))
|
||||
FGES_ContextEffectAnimNotifyTraceSettings TraceProperties;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/**
|
||||
* Performs a preview of the context effects in the editor.
|
||||
* 在编辑器中执行情景效果预览。
|
||||
* @param InOwningActor The owning actor. 拥有演员。
|
||||
* @param InSourceContext The source context tags. 源情景标签。
|
||||
* @param InTargetContext The target context tags. 目标情景标签。
|
||||
* @param InMeshComp The skeletal mesh component. 骨骼网格组件。
|
||||
*/
|
||||
void PerformEditorPreview(AActor* InOwningActor, FGameplayTagContainer& InSourceContext, FGameplayTagContainer& InTargetContext, USkeletalMeshComponent* InMeshComp);
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GES_ContextEffectsInterface.h"
|
||||
#include "GES_ContextEffectComponent.generated.h"
|
||||
|
||||
namespace EEndPlayReason
|
||||
{
|
||||
enum Type : int;
|
||||
}
|
||||
|
||||
class UAnimSequenceBase;
|
||||
class UAudioComponent;
|
||||
class UGES_ContextEffectsLibrary;
|
||||
class UNiagaraComponent;
|
||||
class UObject;
|
||||
class USceneComponent;
|
||||
struct FFrame;
|
||||
struct FHitResult;
|
||||
|
||||
/**
|
||||
* Component that implements context effects interface for handling effects playback.
|
||||
* 实现情景效果接口的组件,用于处理效果播放。
|
||||
*/
|
||||
UCLASS(ClassGroup = (GES), hidecategories = (Variable, Tags, ComponentTick, ComponentReplication, Activation, Cooking, AssetUserData, Collision), CollapseCategories,
|
||||
meta = (BlueprintSpawnableComponent))
|
||||
class GENERICEFFECTSSYSTEM_API UGES_ContextEffectComponent : public UActorComponent, public IGES_ContextEffectsInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor for the context effect component.
|
||||
* 情景效果组件构造函数。
|
||||
*/
|
||||
UGES_ContextEffectComponent();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Called when the game starts.
|
||||
* 游戏开始时调用。
|
||||
*/
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/**
|
||||
* Called when the game ends.
|
||||
* 游戏结束时调用。
|
||||
* @param EndPlayReason The reason for ending play. 结束播放的原因。
|
||||
*/
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
/**
|
||||
* Sets up the gameplay tags provider.
|
||||
* 设置游戏标签提供者。
|
||||
*/
|
||||
virtual void SetupTagsProvider();
|
||||
|
||||
public:
|
||||
/**
|
||||
* Plays context effects based on input parameters.
|
||||
* 根据输入参数播放情景效果。
|
||||
* @param Input The context effects input data. 情景效果输入数据。
|
||||
*/
|
||||
virtual void PlayContextEffectsWithInput_Implementation(FGES_SpawnContextEffectsInput Input) override;
|
||||
|
||||
/**
|
||||
* Aggregates context tags for effect playback.
|
||||
* 为效果播放聚合情景标签。
|
||||
* @param Input The context effects input data. 情景效果输入数据。
|
||||
*/
|
||||
void AggregateContexts(FGES_SpawnContextEffectsInput& Input) const;
|
||||
|
||||
/**
|
||||
* Injects physical surface information into context tags.
|
||||
* 将物理表面信息注入情景标签。
|
||||
* @param InHitResult The hit result. 命中结果。
|
||||
* @param Contexts The context tags (output). 情景标签(输出)。
|
||||
*/
|
||||
void InjectPhysicalSurfaceToContexts(const FHitResult& InHitResult, FGameplayTagContainer& Contexts);
|
||||
|
||||
/**
|
||||
* Sets the gameplay tags provider.
|
||||
* 设置游戏标签提供者。
|
||||
* @param Provider The object providing gameplay tags. 提供游戏标签的对象。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffect")
|
||||
void SetGameplayTagsProvider(UObject* Provider);
|
||||
|
||||
/**
|
||||
* Automatically converts physical surface to context tags.
|
||||
* 自动将物理表面转换为情景标签。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings")
|
||||
bool bConvertPhysicalSurfaceToContext = true;
|
||||
|
||||
/**
|
||||
* Fallback surface type when no mapping or valid physical material exists.
|
||||
* 当无映射或有效物理材料时使用的备用表面类型。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Settings", meta=(Categories="GES.SurfaceType"))
|
||||
FGameplayTag FallbackPhysicalSurface;
|
||||
|
||||
/**
|
||||
* Default context tags for effect playback.
|
||||
* 用于效果播放的默认情景标签。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category="Settings")
|
||||
FGameplayTagContainer DefaultEffectContexts;
|
||||
|
||||
/**
|
||||
* Default context effects libraries for this actor.
|
||||
* 此演员的默认情景效果库。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category="Settings")
|
||||
TSet<TSoftObjectPtr<UGES_ContextEffectsLibrary>> DefaultContextEffectsLibraries;
|
||||
|
||||
/**
|
||||
* Updates the current effect contexts, overriding default contexts.
|
||||
* 更新当前效果情景,覆盖默认情景。
|
||||
* @param NewEffectContexts The new context tags. 新情景标签。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffect")
|
||||
void UpdateEffectContexts(FGameplayTagContainer NewEffectContexts);
|
||||
|
||||
/**
|
||||
* Updates the context effects libraries.
|
||||
* 更新情景效果库。
|
||||
* @param NewContextEffectsLibraries The new effects libraries. 新效果库。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffect")
|
||||
void UpdateLibraries(TSet<TSoftObjectPtr<UGES_ContextEffectsLibrary>> NewContextEffectsLibraries);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Current context tags for effect playback.
|
||||
* 用于效果播放的当前情景标签。
|
||||
*/
|
||||
UPROPERTY(VisibleInstanceOnly, Transient, Category="State")
|
||||
FGameplayTagContainer CurrentContexts;
|
||||
|
||||
/**
|
||||
* Automatically sets up the tags provider if enabled.
|
||||
* 如果启用,自动设置标签提供者。
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, Category="Settings")
|
||||
bool bAutoSetupTagsProvider{true};
|
||||
|
||||
/**
|
||||
* Optional object implementing GameplayTagAssetInterface for tag provision.
|
||||
* 实现GameplayTagAssetInterface的可选对象,用于提供标签。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, Category="State")
|
||||
TObjectPtr<UObject> GameplayTagsProvider{nullptr};
|
||||
|
||||
/**
|
||||
* Current context effects libraries in use.
|
||||
* 当前使用的情景效果库。
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TSet<TSoftObjectPtr<UGES_ContextEffectsLibrary>> CurrentContextEffectsLibraries;
|
||||
|
||||
/**
|
||||
* Active audio components for effect playback.
|
||||
* 用于效果播放的活跃音频组件。
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UAudioComponent>> ActiveAudioComponents;
|
||||
|
||||
/**
|
||||
* Active Niagara components for effect playback.
|
||||
* 用于效果播放的活跃Niagara组件。
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UNiagaraComponent>> ActiveNiagaraComponents;
|
||||
|
||||
/**
|
||||
* Active particle system components for effect playback.
|
||||
* 用于效果播放的活跃粒子系统组件。
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UParticleSystemComponent>> ActiveParticleComponents;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GES_ContextEffectsEnumLibrary.generated.h"
|
||||
|
||||
/**
|
||||
* Enum defining the load state of a context effects library.
|
||||
* 定义情景效果库加载状态的枚举。
|
||||
*/
|
||||
UENUM()
|
||||
enum class EGES_ContextEffectsLibraryLoadState : uint8
|
||||
{
|
||||
/**
|
||||
* Library is not loaded.
|
||||
* 库未加载。
|
||||
*/
|
||||
Unloaded = 0,
|
||||
|
||||
/**
|
||||
* Library is currently loading.
|
||||
* 库正在加载。
|
||||
*/
|
||||
Loading = 1,
|
||||
|
||||
/**
|
||||
* Library is fully loaded.
|
||||
* 库已完全加载。
|
||||
*/
|
||||
Loaded = 2
|
||||
};
|
||||
|
||||
/**
|
||||
* Enum defining how source context tags are applied.
|
||||
* 定义如何应用源情景标签的枚举。
|
||||
*/
|
||||
UENUM()
|
||||
enum class EGES_EffectsContextType : uint8
|
||||
{
|
||||
/**
|
||||
* Merge source context with existing contexts.
|
||||
* 将源情景与现有情景合并。
|
||||
*/
|
||||
Merge,
|
||||
|
||||
/**
|
||||
* Override existing contexts with source context.
|
||||
* 使用源情景覆盖现有情景。
|
||||
*/
|
||||
Override
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/HitResult.h"
|
||||
#include "GES_ContextEffectsStructLibrary.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "GES_ContextEffectsInterface.generated.h"
|
||||
|
||||
class UAnimSequenceBase;
|
||||
class UObject;
|
||||
class USceneComponent;
|
||||
struct FFrame;
|
||||
|
||||
/**
|
||||
* Interface for objects that can respond to context effects.
|
||||
* 可响应情景效果的对象接口。
|
||||
*/
|
||||
UINTERFACE(Blueprintable)
|
||||
class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsInterface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Implementation class for context effects interface.
|
||||
* 情景效果接口的实现类。
|
||||
*/
|
||||
class GENERICEFFECTSSYSTEM_API IGES_ContextEffectsInterface : public IInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Plays context effects based on input parameters.
|
||||
* 根据输入参数播放情景效果。
|
||||
* @param Input The context effects input data. 情景效果输入数据。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GES|ContextEffect")
|
||||
void PlayContextEffectsWithInput(FGES_SpawnContextEffectsInput Input);
|
||||
virtual void PlayContextEffectsWithInput_Implementation(FGES_SpawnContextEffectsInput Input) = 0;
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "GES_ContextEffectsEnumLibrary.h"
|
||||
#include "UObject/SoftObjectPath.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "UObject/WeakObjectPtr.h"
|
||||
#include "GES_ContextEffectsStructLibrary.h"
|
||||
#include "GES_ContextEffectsLibrary.generated.h"
|
||||
|
||||
class UNiagaraSystem;
|
||||
class USoundBase;
|
||||
struct FFrame;
|
||||
|
||||
/**
|
||||
* Instance of context effects for runtime use.
|
||||
* 用于运行时的情景效果实例。
|
||||
*/
|
||||
UCLASS(DisplayName="GES Context Effects Instance")
|
||||
class GENERICEFFECTSSYSTEM_API UGES_ActiveContextEffects : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Tag identifying the effect.
|
||||
* 标识效果的标签。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, Category="GES")
|
||||
FGameplayTag EffectTag;
|
||||
|
||||
/**
|
||||
* Query for source tags.
|
||||
* 源标签查询。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, Category="GES")
|
||||
FGameplayTagQuery SourceTagQuery;
|
||||
|
||||
/**
|
||||
* Query for target tags.
|
||||
* 目标标签查询。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, Category="GES")
|
||||
FGameplayTagQuery TargetTagQuery;
|
||||
|
||||
/**
|
||||
* Array of sound assets for the effect.
|
||||
* 效果的音效资产数组。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, Category="GES")
|
||||
TArray<TObjectPtr<USoundBase>> Sounds;
|
||||
|
||||
/**
|
||||
* Array of Niagara systems for the effect.
|
||||
* 效果的Niagara系统数组。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, Category="GES")
|
||||
TArray<TObjectPtr<UNiagaraSystem>> NiagaraSystems;
|
||||
|
||||
/**
|
||||
* Array of particle systems for the effect.
|
||||
* 效果的粒子系统数组。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, Category="GES")
|
||||
TArray<TObjectPtr<UParticleSystem>> ParticleSystems;
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_DELEGATE_OneParam(FGES_ContextEffectLibraryLoadingComplete, TArray<UGES_ActiveContextEffects *>, ActiveContextEffects);
|
||||
|
||||
/**
|
||||
* Data asset containing context effects definitions.
|
||||
* 包含情景效果定义的数据资产。
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsLibrary : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Array of context effects definitions.
|
||||
* 情景效果定义数组。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GES", meta = (TitleProperty="EditorFriendlyName"))
|
||||
TArray<FGES_ContextEffects> ContextEffects;
|
||||
|
||||
/**
|
||||
* Retrieves effects for a given tag and context.
|
||||
* 获取指定标签和情景的效果。
|
||||
* @param Effect The effect tag. 效果标签。
|
||||
* @param SourceContext The source context tags. 源情景标签。
|
||||
* @param TargetContext The target context tags. 目标情景标签。
|
||||
* @param Sounds The sound assets (output). 音效资产(输出)。
|
||||
* @param NiagaraSystems The Niagara systems (output). Niagara系统(输出)。
|
||||
* @param ParticleSystems The particle systems (output). 粒子系统(输出)。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffect")
|
||||
void GetEffects(const FGameplayTag Effect, const FGameplayTagContainer& SourceContext, const FGameplayTagContainer& TargetContext, TArray<USoundBase*>& Sounds,
|
||||
TArray<UNiagaraSystem*>& NiagaraSystems, TArray<UParticleSystem*>& ParticleSystems);
|
||||
|
||||
/**
|
||||
* Loads the effects in the library.
|
||||
* 加载库中的效果。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffect")
|
||||
void LoadEffects();
|
||||
|
||||
/**
|
||||
* Retrieves the load state of the effects library.
|
||||
* 获取效果库的加载状态。
|
||||
* @return The load state. 加载状态。
|
||||
*/
|
||||
EGES_ContextEffectsLibraryLoadState GetContextEffectsLibraryLoadState();
|
||||
|
||||
private:
|
||||
/**
|
||||
* Internal method for loading effects.
|
||||
* 加载效果的内部方法。
|
||||
*/
|
||||
void LoadEffectsInternal();
|
||||
|
||||
/**
|
||||
* Called when effect library loading is complete.
|
||||
* 效果库加载完成时调用。
|
||||
* @param InActiveContextEffects The loaded context effects. 已加载的情景效果。
|
||||
*/
|
||||
void OnContextEffectLibraryLoadingComplete(TArray<UGES_ActiveContextEffects*> InActiveContextEffects);
|
||||
|
||||
/**
|
||||
* Array of active context effects.
|
||||
* 活跃情景效果数组。
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UGES_ActiveContextEffects>> ActiveContextEffects;
|
||||
|
||||
/**
|
||||
* Current load state of the effects library.
|
||||
* 效果库的当前加载状态。
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
EGES_ContextEffectsLibraryLoadState EffectsLoadState = EGES_ContextEffectsLibraryLoadState::Unloaded;
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Pre-save processing for editor.
|
||||
* 编辑器预保存处理。
|
||||
*/
|
||||
virtual void PreSave(FObjectPreSaveContext SaveContext) override;
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "Chaos/ChaosEngineInterface.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GES_ContextEffectsPreviewSetting.generated.h"
|
||||
|
||||
/**
|
||||
* Data asset for context effects preview settings.
|
||||
* 情景效果预览设置的数据资产。
|
||||
*/
|
||||
UCLASS()
|
||||
class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsPreviewSetting : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Whether to use physical surface as context for preview.
|
||||
* 是否使用物理表面作为预览情景。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview)
|
||||
bool bPreviewPhysicalSurfaceAsContext = true;
|
||||
|
||||
/**
|
||||
* Physical surface type for preview.
|
||||
* 预览的物理表面类型。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview, meta = (EditCondition = "bPreviewPhysicalSurfaceAsContext"))
|
||||
TEnumAsByte<EPhysicalSurface> PreviewPhysicalSurface = EPhysicalSurface::SurfaceType_Default;
|
||||
|
||||
/**
|
||||
* Context effects libraries for preview.
|
||||
* 预览的情景效果库。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview, meta = (AllowedClasses = "/Script/GenericEffectsSystem.GES_ContextEffectsLibrary"))
|
||||
TArray<FSoftObjectPath> PreviewContextEffectsLibraries;
|
||||
|
||||
/**
|
||||
* Source context tags for preview.
|
||||
* 预览的源情景标签。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview)
|
||||
FGameplayTagContainer PreviewSourceContext;
|
||||
|
||||
/**
|
||||
* Target context tags for preview.
|
||||
* 预览的目标情景标签。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Preview)
|
||||
FGameplayTagContainer PreviewTargetContext;
|
||||
};
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "Engine/EngineTypes.h"
|
||||
#include "Engine/HitResult.h"
|
||||
#include "GES_ContextEffectsEnumLibrary.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GES_ContextEffectsStructLibrary.generated.h"
|
||||
|
||||
class UNiagaraComponent;
|
||||
class UAudioComponent;
|
||||
class UAnimSequenceBase;
|
||||
class UParticleSystemComponent;
|
||||
|
||||
/**
|
||||
* Definition of a context effect.
|
||||
* 情景效果的定义。
|
||||
*/
|
||||
USTRUCT(BlueprintType, DisplayName="GES Context Effects Definition")
|
||||
struct GENERICEFFECTSSYSTEM_API FGES_ContextEffects
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Tag identifying the effect.
|
||||
* 标识效果的标签。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GES")
|
||||
FGameplayTag EffectTag;
|
||||
|
||||
/**
|
||||
* Query for source tags.
|
||||
* 源标签查询。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GES")
|
||||
FGameplayTagQuery SourceTagQuery;
|
||||
|
||||
/**
|
||||
* Query for target tags.
|
||||
* 目标标签查询。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GES")
|
||||
FGameplayTagQuery TargetTagQuery;
|
||||
|
||||
/**
|
||||
* Array of effect assets (sounds, Niagara systems, particle systems).
|
||||
* 效果资产数组(音效、Niagara系统、粒子系统)。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GES", meta = (AllowedClasses = "/Script/Engine.SoundBase, /Script/Niagara.NiagaraSystem, /Script/Engine.ParticleSystem"))
|
||||
TArray<FSoftObjectPath> Effects;
|
||||
|
||||
/**
|
||||
* Deprecated context tags.
|
||||
* 已废弃的情景标签。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, Category="GES", meta=(DisplayName="Context(Deprecated)"))
|
||||
FGameplayTagContainer Context;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/**
|
||||
* Editor-friendly name for the effect.
|
||||
* 效果的编辑器友好名称。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category="GES", meta=(EditCondition=false, EditConditionHides))
|
||||
FString EditorFriendlyName;
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* Visual effects settings for animation notify.
|
||||
* 动画通知的视觉效果设置。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICEFFECTSSYSTEM_API FGES_ContextEffectAnimNotifyVFXSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Scale for spawning visual effects.
|
||||
* 生成视觉效果的缩放。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=FX)
|
||||
FVector Scale = FVector(1.0f, 1.0f, 1.0f);
|
||||
};
|
||||
|
||||
/**
|
||||
* Audio settings for animation notify.
|
||||
* 动画通知的音频设置。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICEFFECTSSYSTEM_API FGES_ContextEffectAnimNotifyAudioSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Volume multiplier for audio effects.
|
||||
* 音频效果的音量倍增器。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Sound)
|
||||
float VolumeMultiplier = 1.0f;
|
||||
|
||||
/**
|
||||
* Pitch multiplier for audio effects.
|
||||
* 音频效果的音高倍增器。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Sound)
|
||||
float PitchMultiplier = 1.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Trace settings for animation notify.
|
||||
* 动画通知的追踪设置。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICEFFECTSSYSTEM_API FGES_ContextEffectAnimNotifyTraceSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Trace channel for surface detection.
|
||||
* 用于表面检测的追踪通道。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Trace)
|
||||
TEnumAsByte<ECollisionChannel> TraceChannel = ECollisionChannel::ECC_Visibility;
|
||||
|
||||
/**
|
||||
* Vector offset for the end of the trace.
|
||||
* 追踪结束位置的向量偏移。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Trace)
|
||||
FVector EndTraceLocationOffset = FVector::ZeroVector;
|
||||
|
||||
/**
|
||||
* Whether to ignore the actor during tracing.
|
||||
* 追踪时是否忽略演员。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Trace)
|
||||
bool bIgnoreActor = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Input structure for spawning context effects.
|
||||
* 生成情景效果的输入结构。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICEFFECTSSYSTEM_API FGES_SpawnContextEffectsInput
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Name of the effect to spawn.
|
||||
* 要生成的效果名称。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES")
|
||||
FGameplayTag EffectName;
|
||||
|
||||
/**
|
||||
* Whether the effect is attached to a component.
|
||||
* 效果是否附加到组件。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES")
|
||||
bool bAttached{true};
|
||||
|
||||
/**
|
||||
* Location for spawning if not attached.
|
||||
* 如果未附加,生成位置。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES", meta=(EditCondition="!bAttached", EditConditionHides))
|
||||
FVector Location{FVector::ZeroVector};
|
||||
|
||||
/**
|
||||
* Rotation for spawning if not attached.
|
||||
* 如果未附加,生成旋转。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES", meta=(EditCondition="!bAttached", EditConditionHides))
|
||||
FRotator Rotation{ForceInit};
|
||||
|
||||
/**
|
||||
* Determines how source context is applied (merge or override).
|
||||
* 确定源情景的应用方式(合并或覆盖)。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Context")
|
||||
EGES_EffectsContextType SourceContextType{EGES_EffectsContextType::Merge};
|
||||
|
||||
/**
|
||||
* Optional source context tags.
|
||||
* 可选的源情景标签。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Context")
|
||||
FGameplayTagContainer SourceContext;
|
||||
|
||||
/**
|
||||
* Optional target context tags.
|
||||
* 可选的目标情景标签。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Context")
|
||||
FGameplayTagContainer TargetContext;
|
||||
|
||||
/**
|
||||
* Bone name for attachment if attached.
|
||||
* 如果附加,附加的骨骼名称。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Attachment", meta=(EditCondition="bAttached", EditConditionHides))
|
||||
FName Bone{NAME_None};
|
||||
|
||||
/**
|
||||
* Component to attach the effect to.
|
||||
* 附加效果的组件。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Attachment", meta=(EditCondition="bAttached", EditConditionHides))
|
||||
TObjectPtr<USceneComponent> ComponentToAttach{nullptr};
|
||||
|
||||
/**
|
||||
* Location offset for attached effects.
|
||||
* 附加效果的位置偏移。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Attachment", meta=(EditCondition="bAttached", EditConditionHides))
|
||||
FVector LocationOffset{FVector::ZeroVector};
|
||||
|
||||
/**
|
||||
* Rotation offset for attached effects.
|
||||
* 附加效果的旋转偏移。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Attachment", meta=(EditCondition="bAttached", EditConditionHides))
|
||||
FRotator RotationOffset{ForceInit};
|
||||
|
||||
/**
|
||||
* Optional animation sequence triggering the effect.
|
||||
* 触发效果的可选动画序列。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES")
|
||||
TObjectPtr<const UAnimSequenceBase> AnimationSequence{nullptr};
|
||||
|
||||
/**
|
||||
* Scale for visual effects.
|
||||
* 视觉效果的缩放。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Vfx")
|
||||
FVector VFXScale = FVector(1);
|
||||
|
||||
/**
|
||||
* Volume multiplier for audio effects.
|
||||
* 音频效果的音量倍增器。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Sfx")
|
||||
float AudioVolume = 1;
|
||||
|
||||
/**
|
||||
* Pitch multiplier for audio effects.
|
||||
* 音频效果的音高倍增器。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Sfx")
|
||||
float AudioPitch = 1;
|
||||
|
||||
/**
|
||||
* Whether the effect was triggered by a successful hit.
|
||||
* 效果是否由成功命中触发。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Hit")
|
||||
bool bHitSuccess{false};
|
||||
|
||||
/**
|
||||
* Optional hit result for the effect.
|
||||
* 效果的可选命中结果。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES|Hit")
|
||||
FHitResult HitResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* Output structure for spawned context effects.
|
||||
* 生成情景效果的输出结构。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICEFFECTSSYSTEM_API FGES_SpawnContextEffectsOutput
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Array of spawned audio components.
|
||||
* 生成的音频组件数组。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES")
|
||||
TArray<TObjectPtr<UAudioComponent>> AudioComponents;
|
||||
|
||||
/**
|
||||
* Array of spawned Niagara components.
|
||||
* 生成的Niagara组件数组。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES")
|
||||
TArray<TObjectPtr<UNiagaraComponent>> NiagaraComponents;
|
||||
|
||||
/**
|
||||
* Array of spawned particle system components.
|
||||
* 生成的粒子系统组件数组。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GES")
|
||||
TArray<TObjectPtr<UParticleSystemComponent>> ParticlesComponents;
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "GES_ContextEffectsStructLibrary.h"
|
||||
#include "Subsystems/WorldSubsystem.h"
|
||||
#include "GES_ContextEffectsSubsystem.generated.h"
|
||||
|
||||
class UGES_ContextEffectsPreviewSetting;
|
||||
enum EPhysicalSurface : int;
|
||||
|
||||
class AActor;
|
||||
class UAudioComponent;
|
||||
class UGES_ContextEffectsLibrary;
|
||||
class UNiagaraComponent;
|
||||
class USceneComponent;
|
||||
struct FFrame;
|
||||
struct FGameplayTag;
|
||||
struct FGameplayTagContainer;
|
||||
|
||||
/**
|
||||
* Developer settings for context effects system.
|
||||
* 情景效果系统的开发者设置。
|
||||
*/
|
||||
UCLASS(config = Game, defaultconfig)
|
||||
class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Mapping of physical surface types to context tags.
|
||||
* 物理表面类型到情景标签的映射。
|
||||
*/
|
||||
UPROPERTY(config, EditAnywhere, Category="GES")
|
||||
TMap<TEnumAsByte<EPhysicalSurface>, FGameplayTag> SurfaceTypeToContextMap;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/**
|
||||
* Enables preview in the editor.
|
||||
* 在编辑器中启用预览。
|
||||
*/
|
||||
UPROPERTY(Config, EditAnywhere, Category="PreviewProperties")
|
||||
uint32 bPreviewInEditor : 1;
|
||||
|
||||
/**
|
||||
* Preview settings for context effects in the editor.
|
||||
* 编辑器中情景效果的预览设置。
|
||||
*/
|
||||
UPROPERTY(config, EditAnywhere, Category="PreviewProperties", meta = (EditCondition = "bPreviewInEditor"))
|
||||
TSoftObjectPtr<UGES_ContextEffectsPreviewSetting> PreviewSetting;
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* Set of context effects libraries for an actor.
|
||||
* 演员的情景效果库集合。
|
||||
*/
|
||||
UCLASS()
|
||||
class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsSet : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Set of context effects libraries.
|
||||
* 情景效果库集合。
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TSet<TObjectPtr<UGES_ContextEffectsLibrary>> ContextEffectsLibraries;
|
||||
};
|
||||
|
||||
/**
|
||||
* World subsystem for managing context effects.
|
||||
* 管理情景效果的世界子系统。
|
||||
*/
|
||||
UCLASS()
|
||||
class GENERICEFFECTSSYSTEM_API UGES_ContextEffectsSubsystem : public UWorldSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Spawns context effects using a single effects library.
|
||||
* 使用单个效果库生成情景效果。
|
||||
* @param WorldContextObject The world context object. 世界上下文对象。
|
||||
* @param EffectsLibrary The effects library to use. 要使用的效果库。
|
||||
* @param Input The context effects input data. 情景效果输入数据。
|
||||
* @param Output The context effects output data (output). 情景效果输出数据(输出)。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffects", meta=(WorldContext = "WorldContextObject"))
|
||||
void SpawnContextEffects(UObject* WorldContextObject, TSoftObjectPtr<UGES_ContextEffectsLibrary> EffectsLibrary, FGES_SpawnContextEffectsInput Input, FGES_SpawnContextEffectsOutput& Output);
|
||||
|
||||
/**
|
||||
* Spawns context effects for an actor with extended input.
|
||||
* 为演员生成情景效果,使用扩展输入。
|
||||
* @param SpawningActor The actor spawning the effects. 生成效果的演员。
|
||||
* @param Input The context effects input data. 情景效果输入数据。
|
||||
* @param Output The context effects output data (output). 情景效果输出数据(输出)。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffects")
|
||||
void SpawnContextEffectsExt(const AActor* SpawningActor, const FGES_SpawnContextEffectsInput& Input, FGES_SpawnContextEffectsOutput& Output);
|
||||
|
||||
/**
|
||||
* Retrieves the context tag for a given physical surface.
|
||||
* 获取指定物理表面的情景标签。
|
||||
* @param PhysicalSurface The physical surface type. 物理表面类型。
|
||||
* @param Context The context tag (output). 情景标签(输出)。
|
||||
* @return True if a context tag was found, false otherwise. 如果找到情景标签则返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffects")
|
||||
bool GetContextFromSurfaceType(TEnumAsByte<EPhysicalSurface> PhysicalSurface, FGameplayTag& Context);
|
||||
|
||||
/**
|
||||
* Loads and adds context effects libraries for an actor.
|
||||
* 为演员加载并添加情景效果库。
|
||||
* @param OwningActor The actor owning the libraries. 拥有库的演员。
|
||||
* @param ContextEffectsLibraries The libraries to load. 要加载的库。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffects")
|
||||
void LoadAndAddContextEffectsLibraries(AActor* OwningActor, TSet<TSoftObjectPtr<UGES_ContextEffectsLibrary>> ContextEffectsLibraries);
|
||||
|
||||
/**
|
||||
* Unloads and removes context effects libraries for an actor.
|
||||
* 为演员卸载并移除情景效果库。
|
||||
* @param OwningActor The actor owning the libraries. 拥有库的演员。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GES|ContextEffects")
|
||||
void UnloadAndRemoveContextEffectsLibraries(AActor* OwningActor);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Map of actors to their active context effects sets.
|
||||
* 演员到其活跃情景效果集合的映射。
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TMap<TObjectPtr<AActor>, TObjectPtr<UGES_ContextEffectsSet>> ActiveActorEffectsMap;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Logging/LogMacros.h"
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
|
||||
/**
|
||||
* Gets the context string for logging purposes.
|
||||
* 获取用于日志记录的上下文字符串。
|
||||
* @param ContextObject The object providing the context (optional). 提供上下文的对象(可选)。
|
||||
* @return The context string. 上下文字符串。
|
||||
*/
|
||||
FString GetGESLogContextString(const UObject* ContextObject = nullptr);
|
||||
|
||||
/**
|
||||
* Log category for generic effects system messages.
|
||||
* 通用效果系统消息的日志类别。
|
||||
*/
|
||||
GENERICEFFECTSSYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGES, Log, All);
|
||||
|
||||
/**
|
||||
* Macro for logging effects system messages.
|
||||
* 用于记录效果系统消息的宏。
|
||||
* @details Logs messages with function name and formatted message. 记录包含函数名和格式化消息的日志。
|
||||
*/
|
||||
#define GES_LOG(Verbosity, Format, ...) \
|
||||
{ \
|
||||
UE_LOG(LogGES, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \
|
||||
}
|
||||
|
||||
/**
|
||||
* Macro for context-based logging for effects system.
|
||||
* 用于效果系统的基于上下文的日志记录宏。
|
||||
* @details Logs messages with function name, context, and formatted message. 记录包含函数名、上下文和格式化消息的日志。
|
||||
*/
|
||||
#define GES_CLOG(Verbosity, Format, ...) \
|
||||
{ \
|
||||
UE_LOG(LogGES, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGESLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \
|
||||
}
|
||||
|
||||
/**
|
||||
* Macro for context-based logging with an explicit owner.
|
||||
* 使用显式拥有者进行基于上下文的日志记录宏。
|
||||
* @details Logs messages with function name, owner context, and formatted message. 记录包含函数名、拥有者上下文和格式化消息的日志。
|
||||
*/
|
||||
#define GES_OWNED_CLOG(LogOwner, Verbosity, Format, ...) \
|
||||
{ \
|
||||
UE_LOG(LogGES, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGESLogContextString(LogOwner), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \
|
||||
}
|
||||
10
Plugins/GGS/Source/GenericEffectsSystem/Public/GES_Tags.h
Normal file
10
Plugins/GGS/Source/GenericEffectsSystem/Public/GES_Tags.h
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "NativeGameplayTags.h"
|
||||
|
||||
namespace GES_Tags
|
||||
{
|
||||
GENERICEFFECTSSYSTEM_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Root)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FGenericEffectsSystemModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class GenericGameSystem : ModuleRules
|
||||
{
|
||||
public GenericGameSystem(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new[]
|
||||
{
|
||||
"Core", "SmartObjectsModule"
|
||||
}
|
||||
);
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"ModularGameplay",
|
||||
"NetCore",
|
||||
"PhysicsCore",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"GameplayTags",
|
||||
"UMG",
|
||||
"TargetingSystem",
|
||||
"GameplayTasks",
|
||||
"GameplayAbilities",
|
||||
"GameplayBehaviorsModule",
|
||||
"SmartObjectsModule",
|
||||
"GameplayBehaviorSmartObjectsModule"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "GGS_LogChannels.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameplayBehavior.h"
|
||||
#include "GameplayTask.h"
|
||||
#include "Abilities/GameplayAbility.h"
|
||||
#include "GameplayTask.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogGGS)
|
||||
|
||||
FString GetGGSLogContextString(const UObject* ContextObject)
|
||||
{
|
||||
ENetRole Role = ROLE_None;
|
||||
FString RoleName = TEXT("None");
|
||||
FString Name = "None";
|
||||
|
||||
if (const AActor* Actor = Cast<AActor>(ContextObject))
|
||||
{
|
||||
Role = Actor->GetLocalRole();
|
||||
Name = Actor->GetName();
|
||||
}
|
||||
else if (const UActorComponent* Component = Cast<UActorComponent>(ContextObject))
|
||||
{
|
||||
Role = Component->GetOwnerRole();
|
||||
Name = Component->GetOwner()->GetName();
|
||||
}
|
||||
else if (const UGameplayBehavior* Behavior = Cast<UGameplayBehavior>(ContextObject))
|
||||
{
|
||||
Role = Behavior->GetAvatar()->GetLocalRole();
|
||||
Name = Behavior->GetAvatar()->GetName();
|
||||
}
|
||||
else if (const UGameplayTask* Task = Cast<UGameplayTask>(ContextObject))
|
||||
{
|
||||
Role = Task->GetAvatarActor()->GetLocalRole();
|
||||
Name = Task->GetAvatarActor()->GetName();
|
||||
}
|
||||
else if (const UGameplayAbility* Ability = Cast<UGameplayAbility>(ContextObject))
|
||||
{
|
||||
Role = Ability->GetAvatarActorFromActorInfo()->GetLocalRole();
|
||||
Name = Ability->GetAvatarActorFromActorInfo()->GetName();
|
||||
}
|
||||
else if (IsValid(ContextObject))
|
||||
{
|
||||
Name = ContextObject->GetName();
|
||||
}
|
||||
|
||||
if (Role != ROLE_None)
|
||||
{
|
||||
RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client");
|
||||
}
|
||||
return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
#include "GenericGameSystem.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FGenericGameSystemModule"
|
||||
|
||||
void FGenericGameSystemModule::StartupModule()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void FGenericGameSystemModule::ShutdownModule()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FGenericGameSystemModule, GenericGameSystem)
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/Abilities/GGS_GameplayAbility_Interaction.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GGS_LogChannels.h"
|
||||
#include "SmartObjectBlueprintFunctionLibrary.h"
|
||||
#include "Interaction/GGS_InteractionDefinition.h"
|
||||
#include "Interaction/GGS_InteractionSystemComponent.h"
|
||||
#include "Misc/DataValidation.h"
|
||||
|
||||
UGGS_GameplayAbility_Interaction::UGGS_GameplayAbility_Interaction()
|
||||
{
|
||||
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
|
||||
ReplicationPolicy = EGameplayAbilityReplicationPolicy::ReplicateYes;
|
||||
}
|
||||
|
||||
void UGGS_GameplayAbility_Interaction::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
|
||||
const FGameplayEventData* TriggerEventData)
|
||||
{
|
||||
InteractionSystem = UGGS_InteractionSystemComponent::GetInteractionSystemComponent(ActorInfo->AvatarActor.Get());
|
||||
if (InteractionSystem == nullptr)
|
||||
{
|
||||
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
|
||||
return;
|
||||
}
|
||||
InteractionSystem->OnInteractableActorChangedEvent.AddDynamic(this, &ThisClass::OnInteractActorChanged);
|
||||
|
||||
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
|
||||
}
|
||||
|
||||
void UGGS_GameplayAbility_Interaction::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
|
||||
bool bReplicateEndAbility, bool bWasCancelled)
|
||||
{
|
||||
if (UGGS_InteractionSystemComponent* UserComponent = UGGS_InteractionSystemComponent::GetInteractionSystemComponent(ActorInfo->AvatarActor.Get()))
|
||||
{
|
||||
UserComponent->OnInteractableActorChangedEvent.RemoveDynamic(this, &ThisClass::OnInteractActorChanged);
|
||||
}
|
||||
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
|
||||
}
|
||||
|
||||
bool UGGS_GameplayAbility_Interaction::TryClaimInteraction(int32 Index, FSmartObjectClaimHandle& ClaimedHandle)
|
||||
{
|
||||
USmartObjectSubsystem* Subsystem = USmartObjectSubsystem::GetCurrent(GetWorld());
|
||||
|
||||
check(Subsystem!=nullptr)
|
||||
const TArray<FGGS_InteractionOption>& InteractionInstances = InteractionSystem->GetInteractionOptions();
|
||||
if (!InteractionInstances.IsValidIndex(Index))
|
||||
{
|
||||
GGS_CLOG(Error, "Interaction at index(%d) not exist!!", Index)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (InteractionInstances[Index].Definition == nullptr)
|
||||
{
|
||||
GGS_CLOG(Error, "Interaction at index(%d) has invalid definition!", Index)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (InteractionInstances[Index].SlotState != ESmartObjectSlotState::Free)
|
||||
{
|
||||
GGS_CLOG(Error, "Interaction(%s) was Claimed/Occupied!", *InteractionInstances[Index].Definition->Text.ToString())
|
||||
return false;
|
||||
}
|
||||
|
||||
const FGGS_InteractionOption& CurrentOption = InteractionInstances[Index];
|
||||
|
||||
FSmartObjectClaimHandle NewlyClaimedHandle = USmartObjectBlueprintFunctionLibrary::MarkSmartObjectSlotAsClaimed(GetWorld(), CurrentOption.RequestResult.SlotHandle, GetAvatarActorFromActorInfo());
|
||||
|
||||
// A valid claimed handle can point to an object that is no longer part of the simulation
|
||||
if (!Subsystem->IsClaimedSmartObjectValid(NewlyClaimedHandle))
|
||||
{
|
||||
GGS_CLOG(Error, "Interaction(%s) refers to an object that is no longer available.!", *InteractionInstances[Index].Definition->Text.ToString())
|
||||
return false;
|
||||
}
|
||||
|
||||
ClaimedHandle = NewlyClaimedHandle;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void UGGS_GameplayAbility_Interaction::OnInteractActorChanged_Implementation(AActor* OldActor, AActor* NewActor)
|
||||
{
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
EDataValidationResult UGGS_GameplayAbility_Interaction::IsDataValid(class FDataValidationContext& Context) const
|
||||
{
|
||||
if (ReplicationPolicy != EGameplayAbilityReplicationPolicy::ReplicateYes)
|
||||
{
|
||||
Context.AddError(FText::FromString(TEXT("Core Interaction ability must be Replicated to allow client->server communications via RPC.")));
|
||||
return EDataValidationResult::Invalid;
|
||||
}
|
||||
if (NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::LocalOnly || NetExecutionPolicy == EGameplayAbilityNetExecutionPolicy::ServerOnly)
|
||||
{
|
||||
Context.AddError(FText::FromString(TEXT("Core Interaction ability must not be Local/Server only.")));
|
||||
return EDataValidationResult::Invalid;
|
||||
}
|
||||
if (!AbilityTriggers.IsEmpty())
|
||||
{
|
||||
Context.AddError(FText::FromString(TEXT("Core Interaction ability doesn't allow event triggering!")));
|
||||
return EDataValidationResult::Invalid;
|
||||
}
|
||||
if (InstancingPolicy != EGameplayAbilityInstancingPolicy::InstancedPerActor)
|
||||
{
|
||||
Context.AddError(FText::FromString(TEXT("Core Interaction ability's instancing policy must be InstancedPerActor")));
|
||||
return EDataValidationResult::Invalid;
|
||||
}
|
||||
return Super::IsDataValid(Context);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.h"
|
||||
#include "Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.h"
|
||||
|
||||
UGGS_GameplayBehaviorConfig_InteractionWithAbility::UGGS_GameplayBehaviorConfig_InteractionWithAbility()
|
||||
{
|
||||
BehaviorClass = UGGS_GameplayBehavior_InteractionWithAbility::StaticClass();
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
EDataValidationResult UGGS_GameplayBehaviorConfig_InteractionWithAbility::IsDataValid(class FDataValidationContext& Context) const
|
||||
{
|
||||
if (BehaviorClass == nullptr || !BehaviorClass->GetClass()->IsChildOf(UGGS_GameplayBehavior_InteractionWithAbility::StaticClass()))
|
||||
{
|
||||
return EDataValidationResult::Invalid;
|
||||
}
|
||||
return Super::IsDataValid(Context);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/Behaviors/GGS_GameplayBehavior_InteractionWithAbility.h"
|
||||
|
||||
#include "AbilitySystemComponent.h"
|
||||
#include "AbilitySystemGlobals.h"
|
||||
#include "GGS_LogChannels.h"
|
||||
#include "Abilities/GameplayAbility.h"
|
||||
#include "Interaction/Behaviors/GGS_GameplayBehaviorConfig_InteractionWithAbility.h"
|
||||
#include "Interaction/GGS_InteractionSystemComponent.h"
|
||||
|
||||
bool UGGS_GameplayBehavior_InteractionWithAbility::Trigger(AActor& InAvatar, const UGameplayBehaviorConfig* Config, AActor* SmartObjectOwner)
|
||||
{
|
||||
bTransientIsTriggering = true;
|
||||
bTransientIsActive = false;
|
||||
TransientAvatar = &InAvatar;
|
||||
TransientSmartObjectOwner = SmartObjectOwner;
|
||||
|
||||
UGGS_InteractionSystemComponent* InteractionSystem = InAvatar.FindComponentByClass<UGGS_InteractionSystemComponent>();
|
||||
|
||||
if (!InteractionSystem)
|
||||
{
|
||||
GGS_CLOG(Error, "Missing InteractionSystem Component!")
|
||||
return false;
|
||||
}
|
||||
|
||||
UAbilitySystemComponent* Asc = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(&InAvatar);
|
||||
if (!Asc)
|
||||
{
|
||||
GGS_CLOG(Error, "Missing Ability System Component!")
|
||||
return false;
|
||||
}
|
||||
|
||||
TSubclassOf<UGameplayAbility> AbilityClass{nullptr};
|
||||
int32 AbilityLevel = 0;
|
||||
if (!CheckValidAbilitySetting(Config, AbilityClass, AbilityLevel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FGameplayAbilitySpec* Handle = Asc->FindAbilitySpecFromClass(AbilityClass))
|
||||
{
|
||||
GGS_CLOG(Error, "Try granting repeated interaction ability of class:%s, which is not supported!", *AbilityClass->GetName())
|
||||
return false;
|
||||
}
|
||||
|
||||
GrantedAbilityClass = AbilityClass;
|
||||
|
||||
AbilityEndedDelegateHandle = Asc->OnAbilityEnded.AddUObject(this, &ThisClass::OnAbilityEndedCallback);
|
||||
|
||||
//Ability trigger by event when activation polciy=ServerInitied won't work.
|
||||
AbilitySpecHandle = Asc->K2_GiveAbilityAndActivateOnce(AbilityClass, AbilityLevel);
|
||||
|
||||
if (!AbilitySpecHandle.IsValid())
|
||||
{
|
||||
GGS_CLOG(Error, "Can't active ability of class:%s! Check ability settings!", *AbilityClass->GetName())
|
||||
return false;
|
||||
}
|
||||
|
||||
// Special case: behavior already interrupted
|
||||
if (bBehaviorWasInterrupted && AbilitySpecHandle.IsValid() && !bAbilityEnded)
|
||||
{
|
||||
Asc->ClearAbility(AbilitySpecHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AbilitySpecHandle.IsValid() && bAbilityEnded)
|
||||
{
|
||||
GGS_CLOG(Verbose, "Instantly executed interaction ability:%s,handle%s", *AbilityClass->GetName(), *AbilitySpecHandle.ToString())
|
||||
EndBehavior(InAvatar, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
GGS_CLOG(Verbose, "Granted and activate interaction ability:%s,handle%s", *AbilityClass->GetName(), *AbilitySpecHandle.ToString())
|
||||
|
||||
//TODO what happens when avatar or target get destoryied?
|
||||
// SOOwner销毁的时候, 需要Abort当前行为, 目的是清除赋予的Ability
|
||||
// SmartObjectOwner->OnDestroyed.AddDynamic(this, &ThisClass::OnSmartObjectOwnerDestroyed);
|
||||
GGS_CLOG(Verbose, "Interaction begins with ability:%s", *AbilityClass->GetName())
|
||||
|
||||
bTransientIsTriggering = false;
|
||||
bTransientIsActive = true;
|
||||
return bTransientIsActive;
|
||||
}
|
||||
|
||||
void UGGS_GameplayBehavior_InteractionWithAbility::EndBehavior(AActor& Avatar, const bool bInterrupted)
|
||||
{
|
||||
GGS_CLOG(Verbose, "Interaction behavior ended %s", bInterrupted?TEXT("due to interruption!"):TEXT("normally!"))
|
||||
|
||||
// clear ability stuff.
|
||||
if (UAbilitySystemComponent* Asc = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(&Avatar))
|
||||
{
|
||||
if (AbilityEndedDelegateHandle.IsValid())
|
||||
{
|
||||
Asc->OnAbilityEnded.Remove(AbilityEndedDelegateHandle);
|
||||
AbilityEndedDelegateHandle.Reset();
|
||||
}
|
||||
|
||||
// Special case: behavior interrupting active ability, so cancel ability.
|
||||
if (bInterrupted && bTransientIsActive && !bAbilityEnded && AbilitySpecHandle.IsValid())
|
||||
{
|
||||
if (const FGameplayAbilitySpec* Spec = Asc->FindAbilitySpecFromHandle(AbilitySpecHandle))
|
||||
{
|
||||
GGS_CLOG(Verbose, "Cancel ability(%s) because behavior was interrupted.", *Spec->Ability.GetClass()->GetName())
|
||||
Asc->CancelAbilityHandle(AbilitySpecHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (bInterrupted && !bTransientIsActive && AbilitySpecHandle.IsValid())
|
||||
{
|
||||
Asc->ClearAbility(AbilitySpecHandle);
|
||||
}
|
||||
}
|
||||
|
||||
Super::EndBehavior(Avatar, bInterrupted);
|
||||
|
||||
bBehaviorWasInterrupted = bInterrupted;
|
||||
}
|
||||
|
||||
bool UGGS_GameplayBehavior_InteractionWithAbility::CheckValidAbilitySetting(const UGameplayBehaviorConfig* Config, TSubclassOf<UGameplayAbility>& OutAbilityClass, int32& OutAbilityLevel)
|
||||
{
|
||||
// Ability class validation.
|
||||
const UGGS_GameplayBehaviorConfig_InteractionWithAbility* InteractionConfig = Cast<const UGGS_GameplayBehaviorConfig_InteractionWithAbility>(Config);
|
||||
if (!InteractionConfig)
|
||||
{
|
||||
GGS_CLOG(Error, "Invalid GameplayBehaviorConfig! expect Config be type of %s.", *UGGS_GameplayBehaviorConfig_InteractionWithAbility::StaticClass()->GetName())
|
||||
return false;
|
||||
}
|
||||
|
||||
const TSubclassOf<UGameplayAbility> AbilityClass = InteractionConfig->AbilityToGrant.LoadSynchronous();
|
||||
if (!AbilityClass)
|
||||
{
|
||||
GGS_CLOG(Error, "Invalid AbilityToGrant Class!")
|
||||
return false;
|
||||
}
|
||||
OutAbilityClass = AbilityClass;
|
||||
OutAbilityLevel = InteractionConfig->AbilityLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
void UGGS_GameplayBehavior_InteractionWithAbility::OnAbilityEndedCallback(const FAbilityEndedData& EndedData)
|
||||
{
|
||||
if (bAbilityEnded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// check for ability granted by this behavior.
|
||||
if (EndedData.AbilitySpecHandle == AbilitySpecHandle || EndedData.AbilityThatEnded->GetClass() == GrantedAbilityClass)
|
||||
{
|
||||
bAbilityEnded = true;
|
||||
bAbilityWasCancelled = EndedData.bWasCancelled;
|
||||
|
||||
// Special case: behavior already active and abilities ended, ending behavior normally.
|
||||
if (!bTransientIsTriggering && bTransientIsActive)
|
||||
{
|
||||
GGS_CLOG(Verbose, "Interaction ability(%s) %s.", *EndedData.AbilityThatEnded.GetClass()->GetName(),
|
||||
EndedData.bWasCancelled?TEXT("was canceled"):TEXT("ended normally"))
|
||||
EndBehavior(*GetAvatar(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/GGS_InteractableInterface.h"
|
||||
|
||||
|
||||
// Add default functionality here for any IGGS_InteractableInterface functions that are not pure virtual.
|
||||
|
||||
FText IGGS_InteractableInterface::GetInteractionDisplayNameText_Implementation() const
|
||||
{
|
||||
if (UObject* Object = _getUObject())
|
||||
{
|
||||
return FText::FromString(GetNameSafe(Object));
|
||||
}
|
||||
return FText::GetEmpty();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/GGS_InteractionDefinition.h"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/GGS_InteractionStructLibrary.h"
|
||||
#include "Interaction/GGS_InteractionDefinition.h"
|
||||
|
||||
FString FGGS_InteractionOption::ToString() const
|
||||
{
|
||||
return FString::Format(TEXT("{0} {1} {2}"), {
|
||||
Definition ? Definition->Text.ToString() : TEXT("Null Definition"), SlotState == ESmartObjectSlotState::Free ? TEXT("Valid") : TEXT("Invalid"), SlotIndex
|
||||
});
|
||||
}
|
||||
|
||||
bool operator==(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS)
|
||||
{
|
||||
return Lhs.Definition == RHS.Definition
|
||||
&& Lhs.RequestResult == RHS.RequestResult
|
||||
&& Lhs.SlotIndex == RHS.SlotIndex
|
||||
&& Lhs.SlotState == RHS.SlotState;
|
||||
}
|
||||
|
||||
bool operator!=(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS)
|
||||
{
|
||||
return !(Lhs == RHS);
|
||||
}
|
||||
|
||||
bool operator<(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS)
|
||||
{
|
||||
return Lhs.SlotIndex < RHS.SlotIndex;
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/GGS_InteractionSystemComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameplayBehaviorSmartObjectBehaviorDefinition.h"
|
||||
#include "GGS_LogChannels.h"
|
||||
#include "SmartObjectComponent.h"
|
||||
#include "SmartObjectSubsystem.h"
|
||||
#include "Interaction/GGS_InteractableInterface.h"
|
||||
#include "Interaction/GGS_SmartObjectFunctionLibrary.h"
|
||||
#include "Interaction/GGS_InteractionStructLibrary.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "Net/Core/PushModel/PushModel.h"
|
||||
|
||||
// Sets default values for this component's properties
|
||||
UGGS_InteractionSystemComponent::UGGS_InteractionSystemComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
SetIsReplicatedByDefault(true);
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
FDoRepLifetimeParams Params;
|
||||
Params.bIsPushBased = true;
|
||||
Params.Condition = COND_OwnerOnly;
|
||||
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, InteractableActor, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, NumsOfInteractableActors, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, InteractingOption, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, InteractionOptions, Params);
|
||||
}
|
||||
|
||||
UGGS_InteractionSystemComponent* UGGS_InteractionSystemComponent::GetInteractionSystemComponent(const AActor* Actor)
|
||||
{
|
||||
return IsValid(Actor) ? Actor->FindComponentByClass<UGGS_InteractionSystemComponent>() : nullptr;
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::CycleInteractableActors_Implementation(bool bNext)
|
||||
{
|
||||
if (bInteracting || InteractableActors.Num() <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int32 Index = InteractableActor != nullptr ? InteractableActors.IndexOfByKey(InteractableActor) : 0;
|
||||
if (!InteractableActors.IsValidIndex(Index)) //一个都没
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (bNext)
|
||||
{
|
||||
Index = FMath::Clamp(Index + 1, 0, InteractableActors.Num());
|
||||
}
|
||||
else
|
||||
{
|
||||
Index = FMath::Clamp(Index - 1, 0, InteractableActors.Num());
|
||||
}
|
||||
if (InteractableActors.IsValidIndex(Index) && InteractableActors[Index] != nullptr && InteractableActors[Index] !=
|
||||
InteractableActor)
|
||||
{
|
||||
SetInteractableActor(InteractableActors[Index]);
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::SearchInteractableActors()
|
||||
{
|
||||
OnSearchInteractableActorsEvent.Broadcast();
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::SetInteractableActors(TArray<AActor*> NewActors)
|
||||
{
|
||||
if (!GetOwner()->HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InteractableActors = NewActors;
|
||||
SetInteractableActorsNum(InteractableActors.Num());
|
||||
OnInteractableActorsChanged();
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::SetInteractableActorsNum(int32 NewNum)
|
||||
{
|
||||
if (NewNum != NumsOfInteractableActors)
|
||||
{
|
||||
const int32 PrevNum = NumsOfInteractableActors;
|
||||
NumsOfInteractableActors = NewNum;
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, NumsOfInteractableActors, this)
|
||||
OnInteractableActorsNumChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::SetInteractableActor(AActor* InActor)
|
||||
{
|
||||
if (InActor != InteractableActor)
|
||||
{
|
||||
AActor* OldActor = InteractableActor;
|
||||
InteractableActor = InActor;
|
||||
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractableActor, this)
|
||||
OnInteractableActorChanged(OldActor);
|
||||
}
|
||||
}
|
||||
|
||||
FSmartObjectRequestFilter UGGS_InteractionSystemComponent::GetSmartObjectRequestFilter_Implementation()
|
||||
{
|
||||
return DefaultRequestFilter;
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::StartInteraction(int32 NewIndex)
|
||||
{
|
||||
if (bInteracting)
|
||||
{
|
||||
GGS_CLOG(Warning, "Can't start interaction(%d) while already interacting(%d)", NewIndex, InteractingOption)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!InteractionOptions.IsValidIndex(NewIndex))
|
||||
{
|
||||
GGS_CLOG(Warning, "Try start invalid interaction(%d)", NewIndex)
|
||||
return;
|
||||
}
|
||||
|
||||
int32 Prev = InteractingOption;
|
||||
InteractingOption = NewIndex;
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractingOption, this);
|
||||
OnInteractingOptionChanged(Prev);
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::EndInteraction()
|
||||
{
|
||||
if (!bInteracting)
|
||||
{
|
||||
//GGS_CLOG(Warning, TEXT("no need to end interaction when there's no any active interaction."))
|
||||
return;
|
||||
}
|
||||
|
||||
int32 Prev = InteractingOption;
|
||||
InteractingOption = INDEX_NONE;
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractingOption, this);
|
||||
OnInteractingOptionChanged(Prev);
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::InstantInteraction(int32 NewIndex)
|
||||
{
|
||||
if (bInteracting)
|
||||
{
|
||||
GGS_CLOG(Warning, "Can't trigger instant interaction(%d) while already interacting(%d)", NewIndex, InteractingOption)
|
||||
return;
|
||||
}
|
||||
if (!InteractionOptions.IsValidIndex(NewIndex))
|
||||
{
|
||||
GGS_CLOG(Warning, "Try trigger invalid interaction(%d)", NewIndex)
|
||||
return;
|
||||
}
|
||||
|
||||
int32 Prev = InteractingOption;
|
||||
InteractingOption = NewIndex;
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractingOption, this);
|
||||
OnInteractingOptionChanged(Prev);
|
||||
|
||||
int32 Prev2 = InteractingOption;
|
||||
InteractingOption = INDEX_NONE;
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractingOption, this);
|
||||
OnInteractingOptionChanged(Prev2);
|
||||
}
|
||||
|
||||
bool UGGS_InteractionSystemComponent::IsInteracting() const
|
||||
{
|
||||
return bInteracting;
|
||||
}
|
||||
|
||||
int32 UGGS_InteractionSystemComponent::GetInteractingOption() const
|
||||
{
|
||||
return InteractingOption;
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::OnInteractableActorChanged(AActor* OldActor)
|
||||
{
|
||||
if (GetOwner()->GetLocalRole() >= ROLE_Authority)
|
||||
{
|
||||
RefreshOptionsForActor();
|
||||
}
|
||||
|
||||
if (IsValid(OldActor) && OldActor->GetClass()->ImplementsInterface(UGGS_InteractableInterface::StaticClass()))
|
||||
{
|
||||
IGGS_InteractableInterface::Execute_OnInteractionDeselected(OldActor, GetOwner());
|
||||
}
|
||||
|
||||
if (IsValid(InteractableActor) && InteractableActor->GetClass()->ImplementsInterface(UGGS_InteractableInterface::StaticClass()))
|
||||
{
|
||||
IGGS_InteractableInterface::Execute_OnInteractionSelected(InteractableActor, GetOwner());
|
||||
}
|
||||
|
||||
OnInteractableActorChangedEvent.Broadcast(OldActor, InteractableActor);
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::OnInteractableActorsNumChanged()
|
||||
{
|
||||
OnInteractableActorNumChangedEvent.Broadcast(NumsOfInteractableActors);
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::OnInteractableActorsChanged_Implementation()
|
||||
{
|
||||
if (!bInteracting)
|
||||
{
|
||||
// update potential actor.
|
||||
if (!IsValid(InteractableActor) || !InteractableActors.Contains(InteractableActor))
|
||||
{
|
||||
if (InteractableActors.IsValidIndex(0) && IsValid(InteractableActors[0]))
|
||||
{
|
||||
SetInteractableActor(InteractableActors[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetInteractableActor(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
if (bNewActorHasPriority)
|
||||
{
|
||||
if (IsValid(InteractableActor) && InteractableActors.IsValidIndex(0) && InteractableActors[0] != InteractableActor)
|
||||
{
|
||||
SetInteractableActor(InteractableActors[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::OnSmartObjectEventCallback(const FSmartObjectEventData& EventData)
|
||||
{
|
||||
check(InteractableActor != nullptr);
|
||||
|
||||
for (int32 i = 0; i < InteractionOptions.Num(); i++)
|
||||
{
|
||||
const FGGS_InteractionOption& Option = InteractionOptions[i];
|
||||
if (EventData.SmartObjectHandle == Option.RequestResult.SmartObjectHandle && EventData.SlotHandle == Option.RequestResult.SlotHandle)
|
||||
{
|
||||
if (EventData.Reason == ESmartObjectChangeReason::OnOccupied || EventData.Reason == ESmartObjectChangeReason::OnReleased || EventData.Reason == ESmartObjectChangeReason::OnClaimed)
|
||||
{
|
||||
RefreshOptionsForActor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::OnInteractionOptionsChanged()
|
||||
{
|
||||
for (FGGS_InteractionOption& InteractionOption : InteractionOptions)
|
||||
{
|
||||
GGS_CLOG(Verbose, "Available Options:%s", *InteractionOption.ToString())
|
||||
}
|
||||
OnInteractionOptionsChangedEvent.Broadcast();
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::OnInteractingOptionChanged_Implementation(int32 PrevOptionIndex)
|
||||
{
|
||||
bool bPrevInteracting = bInteracting;
|
||||
bInteracting = InteractingOption != INDEX_NONE;
|
||||
|
||||
if (IsValid(InteractableActor) && InteractableActor->GetClass()->ImplementsInterface(UGGS_InteractableInterface::StaticClass()))
|
||||
{
|
||||
if (!bPrevInteracting && bInteracting)
|
||||
{
|
||||
IGGS_InteractableInterface::Execute_OnInteractionStarted(InteractableActor, GetOwner(), InteractingOption);
|
||||
}
|
||||
if (bPrevInteracting && !bInteracting)
|
||||
{
|
||||
IGGS_InteractableInterface::Execute_OnInteractionEnded(InteractableActor, GetOwner(), PrevOptionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
OnInteractingStateChangedEvent.Broadcast(bInteracting);
|
||||
}
|
||||
|
||||
void UGGS_InteractionSystemComponent::RefreshOptionsForActor()
|
||||
{
|
||||
USmartObjectSubsystem* Subsystem = USmartObjectSubsystem::GetCurrent(GetWorld());
|
||||
|
||||
if (!Subsystem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// getting new options for current interact actor.
|
||||
TArray<FGGS_InteractionOption> NewOptions;
|
||||
{
|
||||
TArray<FSmartObjectRequestResult> Results;
|
||||
if (IsValid(InteractableActor) && UGGS_SmartObjectFunctionLibrary::FindSmartObjectsWithInteractionEntranceInActor(GetSmartObjectRequestFilter(), InteractableActor, Results, GetOwner()))
|
||||
{
|
||||
for (int32 i = 0; i < Results.Num(); i++)
|
||||
{
|
||||
FGGS_InteractionOption Option;
|
||||
UGGS_InteractionDefinition* FoundDefinition;
|
||||
if (UGGS_SmartObjectFunctionLibrary::FindInteractionDefinitionFromSmartObjectSlot(this, Results[i].SlotHandle, FoundDefinition))
|
||||
{
|
||||
Option.Definition = FoundDefinition;
|
||||
Option.SlotState = Subsystem->GetSlotState(Results[i].SlotHandle);
|
||||
Option.RequestResult = Results[i];
|
||||
Option.SlotIndex = i;
|
||||
Option.BehaviorDefinition = Subsystem->GetBehaviorDefinitionByRequestResult(Results[i], USmartObjectBehaviorDefinition::StaticClass());
|
||||
NewOptions.Add(Option);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check any options changed.
|
||||
bool bOptionsChanged = false;
|
||||
{
|
||||
if (NewOptions.Num() == InteractionOptions.Num())
|
||||
{
|
||||
NewOptions.Sort();
|
||||
|
||||
for (int OptionIndex = 0; OptionIndex < NewOptions.Num(); OptionIndex++)
|
||||
{
|
||||
const FGGS_InteractionOption& NewOption = NewOptions[OptionIndex];
|
||||
const FGGS_InteractionOption& CurrentOption = InteractionOptions[OptionIndex];
|
||||
|
||||
if (NewOption != CurrentOption)
|
||||
{
|
||||
bOptionsChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bOptionsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (bOptionsChanged)
|
||||
{
|
||||
// unregister event callbacks for existing options.
|
||||
for (int32 i = 0; i < InteractionOptions.Num(); i++)
|
||||
{
|
||||
auto& Handle = InteractionOptions[i].RequestResult.SlotHandle;
|
||||
if (SlotCallbacks.Contains(Handle))
|
||||
{
|
||||
if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Handle))
|
||||
{
|
||||
OnEventDelegate->Remove(SlotCallbacks[Handle]);
|
||||
SlotCallbacks.Remove(Handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (FGGS_InteractionOption& Option : InteractionOptions)
|
||||
{
|
||||
if (SlotCallbacks.Contains(Option.RequestResult.SlotHandle))
|
||||
{
|
||||
if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Option.RequestResult.SlotHandle))
|
||||
{
|
||||
OnEventDelegate->Remove(SlotCallbacks[Option.RequestResult.SlotHandle]);
|
||||
SlotCallbacks.Remove(Option.RequestResult.SlotHandle);
|
||||
}
|
||||
}
|
||||
// if (Option.DelegateHandle.IsValid())
|
||||
// {
|
||||
// if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Option.RequestResult.SlotHandle))
|
||||
// {
|
||||
// OnEventDelegate->Remove(Option.DelegateHandle);
|
||||
// Option.DelegateHandle.Reset();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
InteractionOptions = NewOptions;
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, InteractionOptions, this)
|
||||
|
||||
GGS_CLOG(Verbose, "Interaction options changed, nums of options:%d", InteractionOptions.Num())
|
||||
|
||||
// register slot event callbacks.
|
||||
// for (int32 i = 0; i < InteractionOptions.Num(); i++)
|
||||
// {
|
||||
// FGGS_InteractionOption& Option = InteractionOptions[i];
|
||||
// if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Option.RequestResult.SlotHandle))
|
||||
// {
|
||||
// Option.DelegateHandle = OnEventDelegate->AddUObject(this, &ThisClass::OnSmartObjectEventCallback);
|
||||
// }
|
||||
// }
|
||||
|
||||
for (int32 i = 0; i < InteractionOptions.Num(); i++)
|
||||
{
|
||||
auto& Handle = InteractionOptions[i].RequestResult.SlotHandle;
|
||||
if (FOnSmartObjectEvent* OnEventDelegate = Subsystem->GetSlotEventDelegate(Handle))
|
||||
{
|
||||
FDelegateHandle DelegateHandle = OnEventDelegate->AddUObject(this, &ThisClass::OnSmartObjectEventCallback);
|
||||
SlotCallbacks.Emplace(Handle, DelegateHandle);
|
||||
}
|
||||
}
|
||||
|
||||
OnInteractionOptionsChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/GGS_SmartObjectFunctionLibrary.h"
|
||||
#include "GameplayBehaviorSmartObjectBehaviorDefinition.h"
|
||||
#include "GameplayBehaviorConfig.h"
|
||||
#include "SmartObjectBlueprintFunctionLibrary.h"
|
||||
#include "SmartObjectDefinition.h"
|
||||
#include "Engine/World.h"
|
||||
#include "SmartObjectSubsystem.h"
|
||||
#include "Interaction/GGS_InteractionDefinition.h"
|
||||
|
||||
UGameplayBehaviorConfig* UGGS_SmartObjectFunctionLibrary::GetGameplayBehaviorConfig(const USmartObjectBehaviorDefinition* BehaviorDefinition)
|
||||
{
|
||||
if (const UGameplayBehaviorSmartObjectBehaviorDefinition* Definition = Cast<UGameplayBehaviorSmartObjectBehaviorDefinition>(BehaviorDefinition))
|
||||
{
|
||||
return Definition->GameplayBehaviorConfig;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UGGS_SmartObjectFunctionLibrary::FindGameplayBehaviorConfig(const USmartObjectBehaviorDefinition* BehaviorDefinition, TSubclassOf<UGameplayBehaviorConfig> DesiredClass,
|
||||
UGameplayBehaviorConfig*& OutConfig)
|
||||
{
|
||||
if (UClass* RealClass = DesiredClass)
|
||||
{
|
||||
if (UGameplayBehaviorConfig* Config = GetGameplayBehaviorConfig(BehaviorDefinition))
|
||||
{
|
||||
if (Config->GetClass()->IsChildOf(RealClass))
|
||||
{
|
||||
OutConfig = Config;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UGGS_SmartObjectFunctionLibrary::FindSmartObjectsWithInteractionEntranceInActor(const FSmartObjectRequestFilter& Filter, AActor* SearchActor, TArray<FSmartObjectRequestResult>& OutResults,
|
||||
const AActor* UserActor)
|
||||
{
|
||||
if (!IsValid(SearchActor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TArray<FSmartObjectRequestResult> Results;
|
||||
USmartObjectBlueprintFunctionLibrary::FindSmartObjectsInActor(Filter, SearchActor, Results, UserActor);
|
||||
if (Results.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter results which has definiton entry.
|
||||
for (int32 i = 0; i < Results.Num(); i++)
|
||||
{
|
||||
UGGS_InteractionDefinition* FoundDefinition;
|
||||
if (FindInteractionDefinitionFromSmartObjectSlot(SearchActor, Results[i].SlotHandle, FoundDefinition))
|
||||
{
|
||||
OutResults.Add(Results[i]);
|
||||
}
|
||||
}
|
||||
return !OutResults.IsEmpty();
|
||||
}
|
||||
|
||||
bool UGGS_SmartObjectFunctionLibrary::FindInteractionDefinitionFromSmartObjectSlot(UObject* WorldContext, FSmartObjectSlotHandle SmartObjectSlotHandle, UGGS_InteractionDefinition*& OutDefinition)
|
||||
{
|
||||
if (WorldContext && WorldContext->GetWorld() && SmartObjectSlotHandle.IsValid())
|
||||
{
|
||||
if (USmartObjectSubsystem* Subsystem = WorldContext->GetWorld()->GetSubsystem<USmartObjectSubsystem>())
|
||||
{
|
||||
Subsystem->ReadSlotData(SmartObjectSlotHandle, [ &OutDefinition](FConstSmartObjectSlotView SlotView)
|
||||
{
|
||||
if (const FGGS_SmartObjectInteractionEntranceData* Entry = SlotView.GetDefinitionDataPtr<FGGS_SmartObjectInteractionEntranceData>())
|
||||
{
|
||||
if (!Entry->DefinitionDA.IsNull())
|
||||
{
|
||||
OutDefinition = Entry->DefinitionDA.LoadSynchronous();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return OutDefinition != nullptr;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/Targeting/GGS_TargetingFilterTask_InteractionSmartObjects.h"
|
||||
#include "Interaction/GGS_InteractionSystemComponent.h"
|
||||
#include "Interaction/GGS_SmartObjectFunctionLibrary.h"
|
||||
|
||||
bool UGGS_TargetingFilterTask_InteractionSmartObjects::ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const
|
||||
{
|
||||
if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle))
|
||||
{
|
||||
if (AActor* Actor = TargetData.HitResult.GetActor())
|
||||
{
|
||||
if (UGGS_InteractionSystemComponent* InteractionSys = UGGS_InteractionSystemComponent::GetInteractionSystemComponent(SourceContext->SourceActor))
|
||||
{
|
||||
TArray<FSmartObjectRequestResult> Results;
|
||||
|
||||
return !UGGS_SmartObjectFunctionLibrary::FindSmartObjectsWithInteractionEntranceInActor(InteractionSys->GetSmartObjectRequestFilter(), Actor, Results, InteractionSys->GetOwner());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Interaction/Tasks/GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "GameplayBehavior.h"
|
||||
#include "GameplayBehaviorConfig.h"
|
||||
#include "GameplayBehaviorSmartObjectBehaviorDefinition.h"
|
||||
#include "GameplayBehaviorSubsystem.h"
|
||||
#include "GGS_LogChannels.h"
|
||||
#include "SmartObjectComponent.h"
|
||||
#include "SmartObjectSubsystem.h"
|
||||
|
||||
UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
|
||||
{
|
||||
bBehaviorFinished = false;
|
||||
}
|
||||
|
||||
UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior* UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::UseSmartObjectWithGameplayBehavior(UGameplayAbility* OwningAbility,
|
||||
FSmartObjectClaimHandle ClaimHandle, ESmartObjectClaimPriority ClaimPriority)
|
||||
{
|
||||
if (OwningAbility == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior* MyTask = NewAbilityTask<UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior>(OwningAbility);
|
||||
if (MyTask == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
MyTask->SetClaimHandle(ClaimHandle);
|
||||
return MyTask;
|
||||
}
|
||||
|
||||
void UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::Activate()
|
||||
{
|
||||
Super::Activate();
|
||||
bool bSuccess = false;
|
||||
ON_SCOPE_EXIT
|
||||
{
|
||||
if (!bSuccess)
|
||||
{
|
||||
EndTask();
|
||||
}
|
||||
};
|
||||
|
||||
if (!ensureMsgf(ClaimedHandle.IsValid(), TEXT("SmartObject handle must be valid at this point.")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
APawn* Pawn = Cast<APawn>(GetAvatarActor());
|
||||
if (Pawn == nullptr)
|
||||
{
|
||||
GGS_CLOG(Error, "Pawn required to use GameplayBehavior with claim handle: %s.", *LexToString(ClaimedHandle));
|
||||
return;
|
||||
}
|
||||
USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(Pawn->GetWorld());
|
||||
if (!ensureMsgf(SmartObjectSubsystem != nullptr, TEXT("SmartObjectSubsystem must be accessible at this point.")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A valid claimed handle can point to an object that is no longer part of the simulation
|
||||
if (!SmartObjectSubsystem->IsClaimedSmartObjectValid(ClaimedHandle))
|
||||
{
|
||||
GGS_CLOG(Log, "Claim handle: %s refers to an object that is no longer available.", *LexToString(ClaimedHandle));
|
||||
return;
|
||||
}
|
||||
|
||||
// Register a callback to be notified if the claimed slot became unavailable
|
||||
SmartObjectSubsystem->RegisterSlotInvalidationCallback(ClaimedHandle, FOnSlotInvalidated::CreateUObject(this, &UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnSlotInvalidated));
|
||||
|
||||
bSuccess = StartInteraction();
|
||||
}
|
||||
|
||||
bool UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::StartInteraction()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(World);
|
||||
if (!ensure(SmartObjectSubsystem))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UGameplayBehaviorSmartObjectBehaviorDefinition* SmartObjectGameplayBehaviorDefinition = SmartObjectSubsystem->MarkSlotAsOccupied<
|
||||
UGameplayBehaviorSmartObjectBehaviorDefinition>(ClaimedHandle);
|
||||
const UGameplayBehaviorConfig* GameplayBehaviorConfig = SmartObjectGameplayBehaviorDefinition != nullptr ? SmartObjectGameplayBehaviorDefinition->GameplayBehaviorConfig : nullptr;
|
||||
GameplayBehavior = GameplayBehaviorConfig != nullptr ? GameplayBehaviorConfig->GetBehavior(*World) : nullptr;
|
||||
if (GameplayBehavior == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const USmartObjectComponent* SmartObjectComponent = SmartObjectSubsystem->GetSmartObjectComponent(ClaimedHandle);
|
||||
AActor& InteractorActor = *GetAvatarActor();
|
||||
AActor* InteracteeActor = SmartObjectComponent ? SmartObjectComponent->GetOwner() : nullptr;
|
||||
const bool bBehaviorActive = UGameplayBehaviorSubsystem::TriggerBehavior(*GameplayBehavior, InteractorActor, GameplayBehaviorConfig, InteracteeActor);
|
||||
// Behavior can be successfully triggered AND ended synchronously. We are only interested to register callback when still running
|
||||
if (bBehaviorActive)
|
||||
{
|
||||
OnBehaviorFinishedNotifyHandle = GameplayBehavior->GetOnBehaviorFinishedDelegate().AddUObject(this, &UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnSmartObjectBehaviorFinished);
|
||||
}
|
||||
|
||||
return bBehaviorActive;
|
||||
}
|
||||
|
||||
void UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnSmartObjectBehaviorFinished(UGameplayBehavior& Behavior, AActor& Avatar, const bool bInterrupted)
|
||||
{
|
||||
// Adding an ensure in case the assumptions change in the future.
|
||||
ensure(GetAvatarActor() != nullptr);
|
||||
|
||||
// make sure we handle the right pawn - we can get this notify for a different
|
||||
// Avatar if the behavior sending it out is not instanced (CDO is being used to perform actions)
|
||||
if (GetAvatarActor() == &Avatar)
|
||||
{
|
||||
Behavior.GetOnBehaviorFinishedDelegate().Remove(OnBehaviorFinishedNotifyHandle);
|
||||
bBehaviorFinished = true;
|
||||
EndTask();
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnDestroy(bool bInOwnerFinished)
|
||||
{
|
||||
if (ClaimedHandle.IsValid())
|
||||
{
|
||||
USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(GetWorld());
|
||||
check(SmartObjectSubsystem);
|
||||
SmartObjectSubsystem->MarkSlotAsFree(ClaimedHandle);
|
||||
SmartObjectSubsystem->UnregisterSlotInvalidationCallback(ClaimedHandle);
|
||||
ClaimedHandle.Invalidate();
|
||||
}
|
||||
|
||||
if (TaskState != EGameplayTaskState::Finished)
|
||||
{
|
||||
if (GameplayBehavior != nullptr && bBehaviorFinished)
|
||||
{
|
||||
OnSucceeded.Broadcast();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnFailed.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
Super::OnDestroy(bInOwnerFinished);
|
||||
}
|
||||
|
||||
void UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior::OnSlotInvalidated(const FSmartObjectClaimHandle& ClaimHandle, const ESmartObjectSlotState State)
|
||||
{
|
||||
if (!bBehaviorFinished && GameplayBehavior != nullptr)
|
||||
{
|
||||
check(GetAvatarActor());
|
||||
GameplayBehavior->GetOnBehaviorFinishedDelegate().Remove(OnBehaviorFinishedNotifyHandle);
|
||||
GameplayBehavior->AbortBehavior(*GetAvatarActor());
|
||||
}
|
||||
EndTask();
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Ragdoll/GGS_RagdollComponent.h"
|
||||
#include "GGS_LogChannels.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Engine/SkinnedAsset.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "Net/Core/PushModel/PushModel.h"
|
||||
|
||||
|
||||
// Sets default values for this component's properties
|
||||
UGGS_RagdollComponent::UGGS_RagdollComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
|
||||
{
|
||||
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
|
||||
// off to improve performance if you don't need them.
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
SetIsReplicatedByDefault(true);
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
const FGGS_RagdollState& UGGS_RagdollComponent::GetRagdollState() const
|
||||
{
|
||||
return RagdollState;
|
||||
}
|
||||
|
||||
bool UGGS_RagdollComponent::IsRagdollAllowedToStart() const
|
||||
{
|
||||
if (!IsValid(MeshComponent))
|
||||
{
|
||||
GGS_CLOG(Warning, "Missing skeletal mesh component for the Ragdoll to work.")
|
||||
return false;
|
||||
}
|
||||
if (bRagdolling)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FBodyInstance* PelvisBodyInstance = MeshComponent->GetBodyInstance(PelvisBoneName);
|
||||
FBodyInstance* SpineBodyInstance = MeshComponent->GetBodyInstance(SpineBoneName);
|
||||
if (PelvisBodyInstance == nullptr || SpineBodyInstance == nullptr)
|
||||
{
|
||||
GGS_CLOG(Warning, "A physics asset with the %s and %s bones are required for the Ragdoll to work.(Also ensure mesh component has collision enabled)", *PelvisBoneName.ToString(),
|
||||
*SpineBoneName.ToString())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::SetMeshComponent_Implementation(USkeletalMeshComponent* InMeshComponent)
|
||||
{
|
||||
MeshComponent = InMeshComponent;
|
||||
}
|
||||
|
||||
bool UGGS_RagdollComponent::IsRagdolling_Implementation() const
|
||||
{
|
||||
return bRagdolling;
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::StartRagdoll()
|
||||
{
|
||||
if (GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy || !IsRagdollAllowedToStart())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (GetOwner()->GetLocalRole() >= ROLE_Authority)
|
||||
{
|
||||
MulticastStartRagdoll();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ACharacter* Character = Cast<ACharacter>(GetOwner()))
|
||||
{
|
||||
Character->GetCharacterMovement()->FlushServerMoves();
|
||||
}
|
||||
ServerStartRagdoll();
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::ServerStartRagdoll_Implementation()
|
||||
{
|
||||
if (IsRagdollAllowedToStart())
|
||||
{
|
||||
MulticastStartRagdoll();
|
||||
GetOwner()->ForceNetUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::MulticastStartRagdoll_Implementation()
|
||||
{
|
||||
LocalStartRagdoll();
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::LocalStartRagdoll()
|
||||
{
|
||||
if (!IsRagdollAllowedToStart())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MeshComponent->bUpdateJointsFromAnimation = true; // Required for the flail animation to work properly.
|
||||
|
||||
if (!MeshComponent->IsRunningParallelEvaluation() && !MeshComponent->GetBoneSpaceTransforms().IsEmpty())
|
||||
{
|
||||
MeshComponent->UpdateRBJointMotors();
|
||||
}
|
||||
|
||||
// Stop any active montages.
|
||||
|
||||
static constexpr auto BlendOutDuration{0.2f};
|
||||
|
||||
MeshComponent->GetAnimInstance()->Montage_Stop(BlendOutDuration);
|
||||
|
||||
if (IsValid(CharacterOwner))
|
||||
{
|
||||
// Disable movement corrections and reset network smoothing.
|
||||
CharacterOwner->GetCharacterMovement()->NetworkSmoothingMode = ENetworkSmoothingMode::Disabled;
|
||||
CharacterOwner->GetCharacterMovement()->bIgnoreClientMovementErrorChecksAndCorrection = true;
|
||||
}
|
||||
|
||||
// Detach the mesh so that character transformation changes will not affect it in any way.
|
||||
|
||||
MeshComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
|
||||
|
||||
// Disable capsule collision and enable mesh physics simulation.
|
||||
|
||||
UPrimitiveComponent* RootPrimitive = Cast<UPrimitiveComponent>(GetOwner()->GetRootComponent());
|
||||
{
|
||||
RootPrimitive->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
}
|
||||
|
||||
MeshComponent->SetCollisionObjectType(ECC_PhysicsBody);
|
||||
MeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
MeshComponent->SetSimulatePhysics(true);
|
||||
|
||||
// This is required for the ragdoll to behave properly when any body instance is set to simulated in a physics asset.
|
||||
// TODO Check the need for this in future engine versions.
|
||||
MeshComponent->ResetAllBodiesSimulatePhysics();
|
||||
|
||||
const auto* PelvisBody{MeshComponent->GetBodyInstance(PelvisBoneName)};
|
||||
FVector PelvisLocation;
|
||||
|
||||
FPhysicsCommand::ExecuteRead(PelvisBody->ActorHandle, [this, &PelvisLocation](const FPhysicsActorHandle& ActorHandle)
|
||||
{
|
||||
PelvisLocation = FPhysicsInterface::GetTransform_AssumesLocked(ActorHandle, true).GetLocation();
|
||||
RagdollState.Velocity = FPhysicsInterface::GetLinearVelocity_AssumesLocked(ActorHandle);
|
||||
});
|
||||
|
||||
RagdollState.PullForce = 0.0f;
|
||||
|
||||
if (bLimitInitialRagdollSpeed)
|
||||
{
|
||||
// Limit the ragdoll's speed for a few frames, because for some unclear reason,
|
||||
// it can get a much higher initial speed than the character's last speed.
|
||||
|
||||
// TODO Find a better solution or wait for a fix in future engine versions.
|
||||
|
||||
static constexpr auto MinSpeedLimit{200.0f};
|
||||
|
||||
RagdollState.SpeedLimitFrameTimeRemaining = 8;
|
||||
RagdollState.SpeedLimit = FMath::Max(MinSpeedLimit, UE_REAL_TO_FLOAT(GetOwner()->GetVelocity().Size()));
|
||||
|
||||
ConstraintRagdollSpeed();
|
||||
}
|
||||
|
||||
if (PawnOwner->GetLocalRole() >= ROLE_Authority)
|
||||
{
|
||||
SetRagdollTargetLocation(FVector::ZeroVector);
|
||||
}
|
||||
|
||||
if (PawnOwner->IsLocallyControlled() || (PawnOwner->GetLocalRole() >= ROLE_Authority && !IsValid(PawnOwner->GetController())))
|
||||
{
|
||||
SetRagdollTargetLocation(PelvisLocation);
|
||||
}
|
||||
|
||||
// Clear the character movement mode and set the locomotion action to Ragdoll.
|
||||
|
||||
if (IsValid(CharacterOwner))
|
||||
{
|
||||
CharacterOwner->GetCharacterMovement()->SetMovementMode(MOVE_None);
|
||||
}
|
||||
bRagdolling = true;
|
||||
OnRagdollStarted();
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::OnRagdollStarted_Implementation()
|
||||
{
|
||||
OnRagdollStartedEvent.Broadcast();
|
||||
}
|
||||
|
||||
bool UGGS_RagdollComponent::IsRagdollAllowedToStop() const
|
||||
{
|
||||
return bRagdolling;
|
||||
}
|
||||
|
||||
bool UGGS_RagdollComponent::StopRagdoll()
|
||||
{
|
||||
if (GetOwner()->GetLocalRole() <= ROLE_SimulatedProxy || !IsRagdollAllowedToStop())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetOwner()->GetLocalRole() >= ROLE_Authority)
|
||||
{
|
||||
MulticastStopRagdoll();
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerStopRagdoll();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::ServerStopRagdoll_Implementation()
|
||||
{
|
||||
if (IsRagdollAllowedToStop())
|
||||
{
|
||||
MulticastStopRagdoll();
|
||||
GetOwner()->ForceNetUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::MulticastStopRagdoll_Implementation()
|
||||
{
|
||||
LocalStopRagdoll();
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::LocalStopRagdoll()
|
||||
{
|
||||
if (!IsRagdollAllowedToStop())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MeshComponent->SnapshotPose(RagdollState.FinalRagdollPose);
|
||||
|
||||
const auto PelvisTransform{MeshComponent->GetSocketTransform(PelvisBoneName)};
|
||||
const auto PelvisRotation{PelvisTransform.Rotator()};
|
||||
|
||||
// Disable mesh physics simulation and enable capsule collision.
|
||||
|
||||
MeshComponent->bUpdateJointsFromAnimation = false;
|
||||
|
||||
MeshComponent->SetSimulatePhysics(false);
|
||||
MeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
MeshComponent->SetCollisionObjectType(ECC_Pawn);
|
||||
|
||||
UPrimitiveComponent* RootPrimitive = Cast<UPrimitiveComponent>(GetOwner()->GetRootComponent());
|
||||
{
|
||||
RootPrimitive->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
}
|
||||
|
||||
|
||||
bool bGrounded;
|
||||
const auto NewActorLocation{RagdollTraceGround(bGrounded)};
|
||||
|
||||
// Determine whether the ragdoll is facing upward or downward and set the actor rotation accordingly.
|
||||
|
||||
const auto bRagdollFacingUpward{FMath::UnwindDegrees(PelvisRotation.Roll) <= 0.0f};
|
||||
|
||||
auto NewActorRotation{GetOwner()->GetActorRotation()};
|
||||
NewActorRotation.Yaw = bRagdollFacingUpward ? PelvisRotation.Yaw - 180.0f : PelvisRotation.Yaw;
|
||||
|
||||
GetOwner()->SetActorLocationAndRotation(NewActorLocation, NewActorRotation, false, nullptr, ETeleportType::TeleportPhysics);
|
||||
|
||||
// Attach the mesh back and restore its default relative location.
|
||||
|
||||
const auto& ActorTransform{GetOwner()->GetActorTransform()};
|
||||
|
||||
FVector BaseTranslationOffset{FVector::Zero()};
|
||||
FQuat BaseRotationOffset;
|
||||
if (IsValid(CharacterOwner))
|
||||
{
|
||||
BaseTranslationOffset = CharacterOwner->GetBaseTranslationOffset();
|
||||
BaseRotationOffset = CharacterOwner->GetBaseRotationOffset();
|
||||
}
|
||||
MeshComponent->SetWorldLocationAndRotationNoPhysics(ActorTransform.TransformPositionNoScale(BaseTranslationOffset),
|
||||
ActorTransform.TransformRotation(BaseRotationOffset).Rotator());
|
||||
|
||||
MeshComponent->AttachToComponent(RootPrimitive, FAttachmentTransformRules::KeepWorldTransform);
|
||||
|
||||
if (MeshComponent->ShouldUseUpdateRateOptimizations())
|
||||
{
|
||||
// Disable URO for one frame to force the animation blueprint to update and get rid of the incorrect mesh pose.
|
||||
|
||||
MeshComponent->bEnableUpdateRateOptimizations = false;
|
||||
|
||||
GetWorldTimerManager().SetTimerForNextTick(FTimerDelegate::CreateWeakLambda(this, [this]
|
||||
{
|
||||
MeshComponent->bEnableUpdateRateOptimizations = true;
|
||||
}));
|
||||
}
|
||||
|
||||
// Restore the pelvis transform to the state it was in before we changed
|
||||
// the character and mesh transforms to keep its world transform unchanged.
|
||||
|
||||
const auto& ReferenceSkeleton{MeshComponent->GetSkinnedAsset()->GetRefSkeleton()};
|
||||
|
||||
const auto PelvisBoneIndex{ReferenceSkeleton.FindBoneIndex(PelvisBoneName)};
|
||||
if (PelvisBoneIndex >= 0)
|
||||
{
|
||||
// We expect the pelvis bone to be the root bone or attached to it, so we can safely use the mesh transform here.
|
||||
RagdollState.FinalRagdollPose.LocalTransforms[PelvisBoneIndex] = PelvisTransform.GetRelativeTransform(MeshComponent->GetComponentTransform());
|
||||
}
|
||||
|
||||
bRagdolling = false;
|
||||
|
||||
if (IsValid(CharacterOwner))
|
||||
{
|
||||
if (bGrounded)
|
||||
{
|
||||
CharacterOwner->GetCharacterMovement()->SetMovementMode(CharacterOwner->GetCharacterMovement()->DefaultLandMovementMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
CharacterOwner->GetCharacterMovement()->SetMovementMode(MOVE_Falling);
|
||||
CharacterOwner->GetCharacterMovement()->Velocity = RagdollState.Velocity;
|
||||
}
|
||||
}
|
||||
|
||||
OnRagdollEnded(bGrounded);
|
||||
|
||||
if (bGrounded && bPlayGetupMontageAfterRagdollEndedOnGround)
|
||||
{
|
||||
if (UAnimMontage* SelectedMontage = SelectGetUpMontage(bRagdollFacingUpward))
|
||||
{
|
||||
MeshComponent->GetAnimInstance()->Montage_Play(SelectedMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::OnRagdollEnded_Implementation(bool bGrounded)
|
||||
{
|
||||
OnRagdollEndedEvent.Broadcast(bGrounded);
|
||||
// If the ragdoll is on the ground, set the movement mode to walking and play a get up montage. If not, set
|
||||
// the movement mode to falling and update the character movement velocity to match the last ragdoll velocity.
|
||||
|
||||
// AlsCharacterMovement->SetMovementModeLocked(false);
|
||||
//
|
||||
|
||||
//
|
||||
// SetLocomotionAction(FGameplayTag::EmptyTag);
|
||||
//
|
||||
// if (bGrounded && MeshComponent->GetAnimInstance()->Montage_Play(SelectGetUpMontage(bRagdollFacingUpward)) > 0.0f)
|
||||
// {
|
||||
// AlsCharacterMovement->SetInputBlocked(true);
|
||||
//
|
||||
// SetLocomotionAction(AlsLocomotionActionTags::GettingUp);
|
||||
// }
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::SetRagdollTargetLocation(const FVector& NewTargetLocation)
|
||||
{
|
||||
if (RagdollTargetLocation != NewTargetLocation)
|
||||
{
|
||||
RagdollTargetLocation = NewTargetLocation;
|
||||
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, RagdollTargetLocation, this)
|
||||
|
||||
if (GetOwner()->GetLocalRole() == ROLE_AutonomousProxy)
|
||||
{
|
||||
ServerSetRagdollTargetLocation(RagdollTargetLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::ServerSetRagdollTargetLocation_Implementation(const FVector_NetQuantize& NewTargetLocation)
|
||||
{
|
||||
SetRagdollTargetLocation(NewTargetLocation);
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::RefreshRagdoll(float DeltaTime)
|
||||
{
|
||||
if (!bRagdolling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we are dealing with physics here, we should not use functions such as USkinnedMeshComponent::GetSocketTransform() as
|
||||
// they may return an incorrect result in situations like when the animation blueprint is not ticking or when URO is enabled.
|
||||
|
||||
const auto* PelvisBody{MeshComponent->GetBodyInstance(PelvisBoneName)};
|
||||
FVector PelvisLocation;
|
||||
|
||||
FPhysicsCommand::ExecuteRead(PelvisBody->ActorHandle, [this, &PelvisLocation](const FPhysicsActorHandle& ActorHandle)
|
||||
{
|
||||
PelvisLocation = FPhysicsInterface::GetTransform_AssumesLocked(ActorHandle, true).GetLocation();
|
||||
RagdollState.Velocity = FPhysicsInterface::GetLinearVelocity_AssumesLocked(ActorHandle);
|
||||
});
|
||||
|
||||
const auto bLocallyControlled{PawnOwner->IsLocallyControlled() || (PawnOwner->GetLocalRole() >= ROLE_Authority && !IsValid(PawnOwner->GetController()))};
|
||||
|
||||
if (bLocallyControlled)
|
||||
{
|
||||
SetRagdollTargetLocation(PelvisLocation);
|
||||
}
|
||||
|
||||
// Prevent the capsule from going through the ground when the ragdoll is lying on the ground.
|
||||
|
||||
// While we could get rid of the line trace here and just use RagdollTargetLocation
|
||||
// as the character's location, we don't do that because the camera depends on the
|
||||
// capsule's bottom location, so its removal will cause the camera to behave erratically.
|
||||
|
||||
bool bGrounded;
|
||||
PawnOwner->SetActorLocation(RagdollTraceGround(bGrounded), false, nullptr, ETeleportType::TeleportPhysics);
|
||||
|
||||
// Zero target location means that it hasn't been replicated yet, so we can't apply the logic below.
|
||||
|
||||
if (!bLocallyControlled && !RagdollTargetLocation.IsZero())
|
||||
{
|
||||
// Apply ragdoll location corrections.
|
||||
|
||||
static constexpr auto PullForce{750.0f};
|
||||
static constexpr auto InterpolationHalfLife{1.2f};
|
||||
|
||||
RagdollState.PullForce = FMath::Lerp(RagdollState.PullForce, PullForce, DamperExactAlpha(DeltaTime, InterpolationHalfLife));
|
||||
|
||||
const auto HorizontalSpeedSquared{RagdollState.Velocity.SizeSquared2D()};
|
||||
|
||||
const auto PullForceBoneName{
|
||||
HorizontalSpeedSquared > FMath::Square(300.0f) ? SpineBoneName : PelvisBoneName
|
||||
};
|
||||
|
||||
auto* PullForceBody{MeshComponent->GetBodyInstance(PullForceBoneName)};
|
||||
|
||||
FPhysicsCommand::ExecuteWrite(PullForceBody->ActorHandle, [this](const FPhysicsActorHandle& ActorHandle)
|
||||
{
|
||||
if (!FPhysicsInterface::IsRigidBody(ActorHandle))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto PullForceVector{
|
||||
RagdollTargetLocation - FPhysicsInterface::GetTransform_AssumesLocked(ActorHandle, true).GetLocation()
|
||||
};
|
||||
|
||||
static constexpr auto MinPullForceDistance{5.0f};
|
||||
static constexpr auto MaxPullForceDistance{50.0f};
|
||||
|
||||
if (PullForceVector.SizeSquared() > FMath::Square(MinPullForceDistance))
|
||||
{
|
||||
FPhysicsInterface::AddForce_AssumesLocked(
|
||||
ActorHandle, PullForceVector.GetClampedToMaxSize(MaxPullForceDistance) * RagdollState.PullForce, true, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Use the speed to scale ragdoll joint strength for physical animation.
|
||||
|
||||
static constexpr auto ReferenceSpeed{1000.0f};
|
||||
static constexpr auto Stiffness{25000.0f};
|
||||
|
||||
const auto SpeedAmount{Clamp01(UE_REAL_TO_FLOAT(RagdollState.Velocity.Size() / ReferenceSpeed))};
|
||||
|
||||
MeshComponent->SetAllMotorsAngularDriveParams(SpeedAmount * Stiffness, 0.0f, 0.0f);
|
||||
|
||||
// Limit the speed of ragdoll bodies.
|
||||
|
||||
if (RagdollState.SpeedLimitFrameTimeRemaining > 0)
|
||||
{
|
||||
RagdollState.SpeedLimitFrameTimeRemaining -= 1;
|
||||
|
||||
ConstraintRagdollSpeed();
|
||||
}
|
||||
}
|
||||
|
||||
FVector UGGS_RagdollComponent::RagdollTraceGround(bool& bGrounded) const
|
||||
{
|
||||
auto RagdollLocation{!RagdollTargetLocation.IsZero() ? FVector{RagdollTargetLocation} : GetOwner()->GetActorLocation()};
|
||||
|
||||
ACharacter* Character = Cast<ACharacter>(GetOwner());
|
||||
if (!IsValid(Character))
|
||||
return RagdollLocation;
|
||||
|
||||
// We use a sphere sweep instead of a simple line trace to keep capsule
|
||||
// movement consistent between Ragdoll and regular character movement.
|
||||
|
||||
const auto CapsuleRadius{Character->GetCapsuleComponent()->GetScaledCapsuleRadius()};
|
||||
const auto CapsuleHalfHeight{Character->GetCapsuleComponent()->GetScaledCapsuleHalfHeight()};
|
||||
|
||||
const FVector TraceStart{RagdollLocation.X, RagdollLocation.Y, RagdollLocation.Z + 2.0f * CapsuleRadius};
|
||||
const FVector TraceEnd{RagdollLocation.X, RagdollLocation.Y, RagdollLocation.Z - CapsuleHalfHeight + CapsuleRadius};
|
||||
|
||||
const auto CollisionChannel{Character->GetCharacterMovement()->UpdatedComponent->GetCollisionObjectType()};
|
||||
|
||||
FCollisionQueryParams QueryParameters{TEXT("RagdollTraceGround"), false, GetOwner()};
|
||||
FCollisionResponseParams CollisionResponses;
|
||||
Character->GetCharacterMovement()->InitCollisionParams(QueryParameters, CollisionResponses);
|
||||
|
||||
FHitResult Hit;
|
||||
bGrounded = GetWorld()->SweepSingleByChannel(Hit, TraceStart, TraceEnd, FQuat::Identity,
|
||||
CollisionChannel, FCollisionShape::MakeSphere(CapsuleRadius),
|
||||
QueryParameters, CollisionResponses);
|
||||
|
||||
// #if ENABLE_DRAW_DEBUG
|
||||
// UAlsDebugUtility::DrawSweepSingleSphere(GetWorld(), TraceStart, TraceEnd, CapsuleRadius,
|
||||
// bGrounded, Hit, {0.0f, 0.25f, 1.0f},
|
||||
// {0.0f, 0.75f, 1.0f}, 0.0f);
|
||||
// #endif
|
||||
|
||||
return {
|
||||
RagdollLocation.X, RagdollLocation.Y,
|
||||
bGrounded
|
||||
? Hit.Location.Z + CapsuleHalfHeight - CapsuleRadius + UCharacterMovementComponent::MIN_FLOOR_DIST
|
||||
: RagdollLocation.Z
|
||||
};
|
||||
}
|
||||
|
||||
void UGGS_RagdollComponent::ConstraintRagdollSpeed() const
|
||||
{
|
||||
MeshComponent->ForEachBodyBelow(NAME_None, true, false, [this](FBodyInstance* Body)
|
||||
{
|
||||
FPhysicsCommand::ExecuteWrite(Body->ActorHandle, [this](const FPhysicsActorHandle& ActorHandle)
|
||||
{
|
||||
if (!FPhysicsInterface::IsRigidBody(ActorHandle))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto Velocity{FPhysicsInterface::GetLinearVelocity_AssumesLocked(ActorHandle)};
|
||||
if (Velocity.SizeSquared() <= FMath::Square(RagdollState.SpeedLimit))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Velocity.Normalize();
|
||||
Velocity *= RagdollState.SpeedLimit;
|
||||
|
||||
FPhysicsInterface::SetLinearVelocity_AssumesLocked(ActorHandle, Velocity);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
UAnimMontage* UGGS_RagdollComponent::SelectGetUpMontage_Implementation(bool bRagdollFacingUpward)
|
||||
{
|
||||
if (GetUpBackMontage.IsNull() || GetUpFrontMontage.IsNull())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return bRagdollFacingUpward ? GetUpBackMontage.LoadSynchronous() : GetUpFrontMontage.LoadSynchronous();
|
||||
}
|
||||
|
||||
|
||||
// Called when the game starts
|
||||
void UGGS_RagdollComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
PawnOwner = GetPawnChecked<APawn>();
|
||||
CharacterOwner = GetPawn<ACharacter>();
|
||||
|
||||
if (CharacterOwner)
|
||||
{
|
||||
MeshComponent = CharacterOwner->GetMesh();
|
||||
}
|
||||
|
||||
if (!MeshComponent)
|
||||
{
|
||||
MeshComponent = GetOwner()->FindComponentByClass<USkeletalMeshComponent>();
|
||||
}
|
||||
if (!MeshComponent)
|
||||
{
|
||||
GGS_CLOG(Warning, "Require skeletal mesh component for the Ragdoll to work.")
|
||||
}
|
||||
}
|
||||
|
||||
float UGGS_RagdollComponent::DamperExactAlpha(float DeltaTime, float HalfLife)
|
||||
{
|
||||
return 1.0f - FMath::InvExpApprox(0.6931471805599453f / (HalfLife + UE_SMALL_NUMBER) * DeltaTime);
|
||||
}
|
||||
|
||||
float UGGS_RagdollComponent::Clamp01(float Value)
|
||||
{
|
||||
return Value > 0.0f
|
||||
? Value < 1.0f
|
||||
? Value
|
||||
: 1.0f
|
||||
: 0.0f;
|
||||
}
|
||||
|
||||
|
||||
void UGGS_RagdollComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
FDoRepLifetimeParams Parameters;
|
||||
Parameters.bIsPushBased = true;
|
||||
|
||||
Parameters.Condition = COND_SkipOwner;
|
||||
|
||||
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, RagdollTargetLocation, Parameters)
|
||||
}
|
||||
|
||||
// Called every frame
|
||||
void UGGS_RagdollComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
RefreshRagdoll(DeltaTime);
|
||||
// ...
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Ragdoll/GGS_RagdollStructLibrary.h"
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "Utilities/GGS_SocketRelationshipMapping.h"
|
||||
#include "Engine/StreamableRenderAsset.h"
|
||||
#include "Engine/SkeletalMesh.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Animation/Skeleton.h"
|
||||
#include "UObject/ObjectSaveContext.h"
|
||||
|
||||
bool UGGS_SocketRelationshipMapping::FindSocketAdjustment(const USkeletalMeshComponent* InParentMeshComponent, const UStreamableRenderAsset* InMeshAsset, FName InSocketName,
|
||||
FGGS_SocketAdjustment& OutAdjustment) const
|
||||
{
|
||||
if (InParentMeshComponent == nullptr || InMeshAsset == nullptr || InSocketName.IsNone())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
USkeleton* Skeleton = InParentMeshComponent->GetSkeletalMeshAsset()->GetSkeleton();
|
||||
if (!Skeleton)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FString SkeletonName = Skeleton->GetName();
|
||||
|
||||
for (const FGGS_SocketRelationship& Relationship : Relationships)
|
||||
{
|
||||
UStreamableRenderAsset* Key{nullptr};
|
||||
if (!Relationship.MeshAsset.IsNull())
|
||||
{
|
||||
Key = Relationship.MeshAsset.LoadSynchronous();
|
||||
}
|
||||
if (!Key || Key->GetName() != InMeshAsset->GetName())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int32 i = Relationship.Adjustments.Num() - 1; i >= 0; i--)
|
||||
{
|
||||
const FGGS_SocketAdjustment& Adjustment = Relationship.Adjustments[i];
|
||||
bool bMatchSkeleton = Adjustment.ForSkeletons.IsEmpty() ? true : Adjustment.ForSkeletons.Contains(SkeletonName);
|
||||
if (bMatchSkeleton && Adjustment.SocketName == InSocketName)
|
||||
{
|
||||
OutAdjustment = Adjustment;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UGGS_SocketRelationshipMapping::FindSocketAdjustmentInMappings(TArray<TSoftObjectPtr<UGGS_SocketRelationshipMapping>> InMappings, const USkeletalMeshComponent* InParentMeshComponent,
|
||||
const UStreamableRenderAsset* InMeshAsset, FName InSocketName,
|
||||
FGGS_SocketAdjustment& OutAdjustment)
|
||||
{
|
||||
for (TSoftObjectPtr<UGGS_SocketRelationshipMapping> Mapping : InMappings)
|
||||
{
|
||||
if (Mapping.IsNull())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (const UGGS_SocketRelationshipMapping* LoadedMapping = Mapping.LoadSynchronous())
|
||||
{
|
||||
if (LoadedMapping->FindSocketAdjustment(InParentMeshComponent, InMeshAsset, InSocketName, OutAdjustment))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
void UGGS_SocketRelationshipMapping::PreSave(FObjectPreSaveContext SaveContext)
|
||||
{
|
||||
for (FGGS_SocketRelationship& Relationship : Relationships)
|
||||
{
|
||||
if (Relationship.MeshAsset.IsNull())
|
||||
{
|
||||
Relationship.EditorFriendlyName = TEXT("Invalid!");
|
||||
}
|
||||
else
|
||||
{
|
||||
UStreamableRenderAsset* MeshAsset = Relationship.MeshAsset.LoadSynchronous();
|
||||
Relationship.EditorFriendlyName = MeshAsset->GetName();
|
||||
for (FGGS_SocketAdjustment& Adjustment : Relationship.Adjustments)
|
||||
{
|
||||
if (Adjustment.SocketName == NAME_None)
|
||||
{
|
||||
Adjustment.EditorFriendlyName = "Empty adjustments!";
|
||||
}
|
||||
if (Adjustment.ForSkeletons.IsEmpty())
|
||||
{
|
||||
Adjustment.EditorFriendlyName = Adjustment.SocketName.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
FString SkeletonNames;
|
||||
for (const FString& ForSkeleton : Adjustment.ForSkeletons)
|
||||
{
|
||||
SkeletonNames = SkeletonNames.Append(ForSkeleton);
|
||||
}
|
||||
Adjustment.EditorFriendlyName = FString::Format(TEXT("{0} on {1}"), {Adjustment.SocketName.ToString(), SkeletonNames});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Super::PreSave(SaveContext);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
|
||||
/**
|
||||
* Gets the context string for logging purposes.
|
||||
* 获取用于日志记录的上下文字符串。
|
||||
* @param ContextObject The object providing the context (optional). 提供上下文的对象(可选)。
|
||||
* @return The context string. 上下文字符串。
|
||||
*/
|
||||
FString GetGGSLogContextString(const UObject* ContextObject = nullptr);
|
||||
|
||||
/**
|
||||
* Log category for generic game system messages.
|
||||
* 通用游戏系统消息的日志类别。
|
||||
*/
|
||||
GENERICGAMESYSTEM_API DECLARE_LOG_CATEGORY_EXTERN(LogGGS, Log, All);
|
||||
|
||||
/**
|
||||
* Macro for logging generic game system messages.
|
||||
* 用于记录通用游戏系统消息的宏。
|
||||
* @details Logs messages with function name and formatted message. 记录包含函数名和格式化消息的日志。
|
||||
*/
|
||||
#define GGS_LOG(Verbosity, Format, ...) \
|
||||
{ \
|
||||
UE_LOG(LogGGS, Verbosity, TEXT("%S: %s"),__FUNCTION__, *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \
|
||||
}
|
||||
|
||||
/**
|
||||
* Macro for context-based logging for generic game system.
|
||||
* 用于通用游戏系统的基于上下文的日志记录宏。
|
||||
* @details Logs messages with function name, context, and formatted message. 记录包含函数名、上下文和格式化消息的日志。
|
||||
*/
|
||||
#define GGS_CLOG(Verbosity, Format, ...) \
|
||||
{ \
|
||||
UE_LOG(LogGGS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGGSLogContextString(this), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \
|
||||
}
|
||||
|
||||
/**
|
||||
* Macro for context-based logging with an explicit owner.
|
||||
* 使用显式拥有者进行基于上下文的日志记录宏。
|
||||
* @details Logs messages with function name, owner context, and formatted message. 记录包含函数名、拥有者上下文和格式化消息的日志。
|
||||
*/
|
||||
#define GGS_OWNED_CLOG(LogOwner, Verbosity, Format, ...) \
|
||||
{ \
|
||||
UE_LOG(LogGGS, Verbosity, TEXT("%S: ctx(%s) %s"),__FUNCTION__, *GetGGSLogContextString(LogOwner), *FString::Printf(TEXT(Format), ##__VA_ARGS__)) \
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FGenericGameSystemModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SmartObjectRuntime.h"
|
||||
#include "Abilities/GameplayAbility.h"
|
||||
#include "GGS_GameplayAbility_Interaction.generated.h"
|
||||
|
||||
class UGGS_InteractionSystemComponent;
|
||||
class USmartObjectComponent;
|
||||
|
||||
/**
|
||||
* Core gameplay ability for handling interactions.
|
||||
* 处理交互的核心游戏技能。
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class GENERICGAMESYSTEM_API UGGS_GameplayAbility_Interaction : public UGameplayAbility
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor for the interaction gameplay ability.
|
||||
* 交互游戏技能构造函数。
|
||||
*/
|
||||
UGGS_GameplayAbility_Interaction();
|
||||
|
||||
/**
|
||||
* Activates the interaction ability.
|
||||
* 激活交互技能。
|
||||
* @param Handle The ability specification handle. 技能规格句柄。
|
||||
* @param ActorInfo Information about the actor using the ability. 使用技能的Actor信息。
|
||||
* @param ActivationInfo Information about the ability activation. 技能激活信息。
|
||||
* @param TriggerEventData Optional event data triggering the ability. 触发技能的可选事件数据。
|
||||
*/
|
||||
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
|
||||
const FGameplayEventData* TriggerEventData) override;
|
||||
|
||||
/**
|
||||
* Ends the interaction ability.
|
||||
* 结束交互技能。
|
||||
* @param Handle The ability specification handle. 技能规格句柄。
|
||||
* @param ActorInfo Information about the actor using the ability. 使用技能的Actor信息。
|
||||
* @param ActivationInfo Information about the ability activation. 技能激活信息。
|
||||
* @param bReplicateEndAbility Whether to replicate the end ability call. 是否同步结束技能调用。
|
||||
* @param bWasCancelled Whether the ability was cancelled. 技能是否被取消。
|
||||
*/
|
||||
virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility,
|
||||
bool bWasCancelled) override;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Attempts to claim an interaction with a smart object.
|
||||
* 尝试认领与智能对象的交互。
|
||||
* @param Index The interaction option index. 交互选项索引。
|
||||
* @param ClaimedHandle The claimed smart object handle (output). 认领的智能对象句柄(输出)。
|
||||
* @return True if the interaction was claimed successfully, false otherwise. 如果交互成功认领返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GCS|Interaction", meta=(ExpandBoolAsExecs=ReturnValue))
|
||||
bool TryClaimInteraction(int32 Index, FSmartObjectClaimHandle& ClaimedHandle);
|
||||
|
||||
/**
|
||||
* Called when the interactable actor changes.
|
||||
* 可交互演员变更时调用。
|
||||
* @param OldActor The previous interactable actor. 之前的可交互演员。
|
||||
* @param NewActor The new interactable actor. 新的可交互演员。
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category="GCS|Interaction")
|
||||
void OnInteractActorChanged(AActor* OldActor, AActor* NewActor);
|
||||
|
||||
/**
|
||||
* Reference to the interaction system component.
|
||||
* 交互系统组件的引用。
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, Category="GCS|Interaction")
|
||||
TObjectPtr<UGGS_InteractionSystemComponent> InteractionSystem{nullptr};
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/**
|
||||
* Validates data in the editor.
|
||||
* 在编辑器中验证数据。
|
||||
* @param Context The data validation context. 数据验证上下文。
|
||||
* @return The validation result. 验证结果。
|
||||
*/
|
||||
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayBehaviorConfig.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GGS_GameplayBehaviorConfig_InteractionWithAbility.generated.h"
|
||||
|
||||
class UGameplayAbility;
|
||||
class UUserWidget;
|
||||
|
||||
/**
|
||||
* Configuration for ability-based interaction behavior.
|
||||
* 基于技能的交互行为配置。
|
||||
*/
|
||||
UCLASS(DisplayName="Gameplay Behavior Config Interaction (GGS)")
|
||||
class GENERICGAMESYSTEM_API UGGS_GameplayBehaviorConfig_InteractionWithAbility : public UGameplayBehaviorConfig
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor for the interaction behavior config.
|
||||
* 交互行为配置构造函数。
|
||||
*/
|
||||
UGGS_GameplayBehaviorConfig_InteractionWithAbility();
|
||||
|
||||
/**
|
||||
* The ability to grant and activate when interaction begins.
|
||||
* 交互开始时赋予并激活的技能。
|
||||
* @note Must be instanced and not LocalOnly. Does not support event-triggered abilities.
|
||||
* @注意 必须是实例化的技能,不能是LocalOnly。不支持事件触发的技能。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction")
|
||||
TSoftClassPtr<UGameplayAbility> AbilityToGrant;
|
||||
|
||||
/**
|
||||
* The level of the ability, used for visual distinctions.
|
||||
* 技能等级,用于视觉区分。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
int32 AbilityLevel{0};
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/**
|
||||
* Validates data in the editor.
|
||||
* 在编辑器中验证数据。
|
||||
* @param Context The data validation context. 数据验证上下文。
|
||||
* @return The validation result. 验证结果。
|
||||
*/
|
||||
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayAbilitySpecHandle.h"
|
||||
#include "GameplayBehavior.h"
|
||||
#include "Abilities/GameplayAbilityTypes.h"
|
||||
#include "GGS_GameplayBehavior_InteractionWithAbility.generated.h"
|
||||
|
||||
/**
|
||||
* Gameplay behavior for ability-based interactions.
|
||||
* 基于技能的交互游戏行为。
|
||||
*/
|
||||
UCLASS(DisplayName="GameplayBehavior_Interaction (GGS)", NotBlueprintable)
|
||||
class GENERICGAMESYSTEM_API UGGS_GameplayBehavior_InteractionWithAbility : public UGameplayBehavior
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Triggers the interaction behavior.
|
||||
* 触发交互行为。
|
||||
* @param InAvatar The avatar actor. 化身演员。
|
||||
* @param Config The behavior config. 行为配置。
|
||||
* @param SmartObjectOwner The smart object owner. 智能对象拥有者。
|
||||
* @return True if the behavior was triggered successfully, false otherwise. 如果行为成功触发返回true,否则返回false。
|
||||
*/
|
||||
virtual bool Trigger(AActor& InAvatar, const UGameplayBehaviorConfig* Config, AActor* SmartObjectOwner) override;
|
||||
|
||||
/**
|
||||
* Ends the behavior.
|
||||
* 结束行为。
|
||||
* @param Avatar The avatar actor. 化身演员。
|
||||
* @param bInterrupted Whether the behavior was interrupted. 行为是否被中断。
|
||||
*/
|
||||
virtual void EndBehavior(AActor& Avatar, const bool bInterrupted) override;
|
||||
|
||||
/**
|
||||
* Checks the validity of the ability settings.
|
||||
* 检查技能设置的有效性。
|
||||
* @param Config The behavior config. 行为配置。
|
||||
* @param OutAbilityClass The ability class (output). 技能类(输出)。
|
||||
* @param OutAbilityLevel The ability level (output). 技能等级(输出)。
|
||||
* @return True if the settings are valid, false otherwise. 如果设置有效返回true,否则返回false。
|
||||
*/
|
||||
bool CheckValidAbilitySetting(const UGameplayBehaviorConfig* Config, TSubclassOf<UGameplayAbility>& OutAbilityClass, int32& OutAbilityLevel);
|
||||
|
||||
/**
|
||||
* The ability class granted for the interaction.
|
||||
* 为交互授予的技能类。
|
||||
*/
|
||||
UPROPERTY()
|
||||
TSubclassOf<UGameplayAbility> GrantedAbilityClass{nullptr};
|
||||
|
||||
/**
|
||||
* Handle for the granted ability spec.
|
||||
* 授予技能规格的句柄。
|
||||
*/
|
||||
FGameplayAbilitySpecHandle AbilitySpecHandle;
|
||||
|
||||
/**
|
||||
* Indicates if the behavior was interrupted.
|
||||
* 表示行为是否被中断。
|
||||
*/
|
||||
bool bBehaviorWasInterrupted = false;
|
||||
|
||||
/**
|
||||
* Indicates if the ability has ended.
|
||||
* 表示技能是否已结束。
|
||||
*/
|
||||
bool bAbilityEnded = false;
|
||||
|
||||
/**
|
||||
* Indicates if the ability was cancelled.
|
||||
* 表示技能是否被取消。
|
||||
*/
|
||||
bool bAbilityWasCancelled = false;
|
||||
|
||||
/**
|
||||
* Delegate handle for ability end notification.
|
||||
* 技能结束通知的委托句柄。
|
||||
*/
|
||||
FDelegateHandle AbilityEndedDelegateHandle;
|
||||
|
||||
/**
|
||||
* Called when the ability ends.
|
||||
* 技能结束时调用。
|
||||
* @param EndedData The ability end data. 技能结束数据。
|
||||
*/
|
||||
virtual void OnAbilityEndedCallback(const FAbilityEndedData& EndedData);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "GGS_InteractableInterface.generated.h"
|
||||
|
||||
/**
|
||||
* Interface for actors to handle interaction events.
|
||||
* 处理交互事件的演员接口。
|
||||
*/
|
||||
UINTERFACE()
|
||||
class GENERICGAMESYSTEM_API UGGS_InteractableInterface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Implementation class for interactable actors.
|
||||
* 可交互演员的实现类。
|
||||
*/
|
||||
class GENERICGAMESYSTEM_API IGGS_InteractableInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Retrieves the display name for the interactable actor.
|
||||
* 获取可交互演员的显示名称。
|
||||
* @return The display name. 显示名称。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction")
|
||||
FText GetInteractionDisplayName() const;
|
||||
virtual FText GetInteractionDisplayNameText_Implementation() const;
|
||||
|
||||
/**
|
||||
* Called when the actor is selected by the interaction system.
|
||||
* 演员被交互系统选中时调用。
|
||||
* @param Instigator The instigating actor, usually the player. 发起者,通常是玩家。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction")
|
||||
void OnInteractionSelected(AActor* Instigator);
|
||||
|
||||
/**
|
||||
* Called when the actor is deselected by the interaction system.
|
||||
* 演员被交互系统取消选中时调用。
|
||||
* @param Instigator The instigating actor. 发起者。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction")
|
||||
void OnInteractionDeselected(AActor* Instigator);
|
||||
|
||||
/**
|
||||
* Called when interaction with the actor starts.
|
||||
* 与演员交互开始时调用。
|
||||
* @param Instigator The instigating actor. 发起者。
|
||||
* @param Index The interaction option index. 交互选项索引。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction")
|
||||
void OnInteractionStarted(AActor* Instigator, int32 Index);
|
||||
|
||||
/**
|
||||
* Called when interaction with the actor ends.
|
||||
* 与演员交互结束时调用。
|
||||
* @param Instigator The instigating actor. 发起者。
|
||||
* @param Index The interaction option index. 交互选项索引。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction")
|
||||
void OnInteractionEnded(AActor* Instigator, int32 Index);
|
||||
|
||||
/**
|
||||
* Called when an interaction option is selected.
|
||||
* 交互选项被选中时调用。
|
||||
* @param Instigator The instigating actor. 发起者。
|
||||
* @param OptionIndex The selected option index. 选中的选项索引。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="GGS|Interaction")
|
||||
void OnInteractionOptionSelected(AActor* Instigator, int32 OptionIndex);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GGS_InteractionDefinition.generated.h"
|
||||
|
||||
/**
|
||||
* Base class for interaction settings, used in smart object interaction entrances.
|
||||
* 交互设置基类,用于智能对象交互入口。
|
||||
*/
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class GENERICGAMESYSTEM_API UGGS_InteractionDefinition : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Display text for the interaction.
|
||||
* 交互的显示文本。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction")
|
||||
FText Text;
|
||||
|
||||
/**
|
||||
* Sub-text for the interaction.
|
||||
* 交互的子文本。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction")
|
||||
FText SubText;
|
||||
|
||||
/**
|
||||
* Input action that triggers the interaction.
|
||||
* 触发交互的输入动作。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction", meta = (RowType = "/Script/CommonUI.CommonInputActionDataBase"))
|
||||
FDataTableRowHandle TriggeringInputAction;
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "SmartObjectRuntime.h"
|
||||
#include "SmartObjectSubsystem.h"
|
||||
#include "SmartObjectTypes.h"
|
||||
#if ENGINE_MINOR_VERSION >= 6
|
||||
#include "SmartObjectRequestTypes.h"
|
||||
#endif
|
||||
#include "Engine/DataTable.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GGS_InteractionStructLibrary.generated.h"
|
||||
|
||||
class UGGS_InteractionSystemComponent;
|
||||
class UAbilitySystemComponent;
|
||||
class UGGS_InteractionDefinition;
|
||||
|
||||
/**
|
||||
* Structure wrapping an interaction definition for smart object interaction.
|
||||
* 封装智能对象交互的交互定义结构。
|
||||
*/
|
||||
USTRUCT(DisplayName="Interaction Entrance")
|
||||
struct GENERICGAMESYSTEM_API FGGS_SmartObjectInteractionEntranceData : public FSmartObjectDefinitionData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Interaction definition containing static data for player interaction.
|
||||
* 包含玩家交互静态数据的交互定义。
|
||||
* @note Replicated across the network.
|
||||
* @注意 通过网络同步。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category="Interaction", meta=(DisplayName="Definition"))
|
||||
TSoftObjectPtr<UGGS_InteractionDefinition> DefinitionDA{nullptr};
|
||||
};
|
||||
|
||||
/**
|
||||
* Structure representing an interaction option.
|
||||
* 表示交互选项的结构。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICGAMESYSTEM_API FGGS_InteractionOption
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Interaction definition associated with this option.
|
||||
* 与此选项关联的交互定义。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction")
|
||||
TObjectPtr<UGGS_InteractionDefinition> Definition{nullptr};
|
||||
|
||||
/**
|
||||
* Smart object request result for this option. Not replicated.
|
||||
* 此选项的智能对象请求结果。未网络同步。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, NotReplicated, Category="Interaction")
|
||||
FSmartObjectRequestResult RequestResult;
|
||||
|
||||
/**
|
||||
* Smart object behavior definition for this option.
|
||||
* 此选项的智能对象行为定义。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, NotReplicated, Category="Interaction")
|
||||
TObjectPtr<const USmartObjectBehaviorDefinition> BehaviorDefinition;
|
||||
|
||||
/**
|
||||
* Index of the associated smart object slot, used for UI sorting.
|
||||
* 关联智能对象槽的索引,用于UI排序。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction")
|
||||
int32 SlotIndex{-1};
|
||||
|
||||
/**
|
||||
* State of the associated smart object slot, used for UI input rules.
|
||||
* 关联智能对象槽的状态,用于UI输入规则。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction")
|
||||
ESmartObjectSlotState SlotState{ESmartObjectSlotState::Free};
|
||||
|
||||
/**
|
||||
* Equality operator for comparing interaction options.
|
||||
* 交互选项的相等比较运算符。
|
||||
*/
|
||||
friend bool operator==(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS);
|
||||
|
||||
/**
|
||||
* Inequality operator for comparing interaction options.
|
||||
* 交互选项的不等比较运算符。
|
||||
*/
|
||||
friend bool operator!=(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS);
|
||||
|
||||
/**
|
||||
* Less-than operator for sorting interaction options by slot index.
|
||||
* 按槽索引排序交互选项的比较运算符。
|
||||
*/
|
||||
friend bool operator<(const FGGS_InteractionOption& Lhs, const FGGS_InteractionOption& RHS);
|
||||
|
||||
/**
|
||||
* Converts the interaction option to a string representation.
|
||||
* 将交互选项转换为字符串表示。
|
||||
* @return String representation of the option. 选项的字符串表示。
|
||||
*/
|
||||
FString ToString() const;
|
||||
};
|
||||
@@ -0,0 +1,332 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GGS_InteractionStructLibrary.h"
|
||||
#include "SmartObjectSubsystem.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GGS_InteractionSystemComponent.generated.h"
|
||||
|
||||
class UCommonUserWidget;
|
||||
class UGameplayBehavior;
|
||||
|
||||
/**
|
||||
* Delegate for interaction events.
|
||||
* 交互事件的委托。
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FInteractionEventSignature);
|
||||
|
||||
/**
|
||||
* Delegate for changes in the interactable actor.
|
||||
* 可交互演员变更的委托。
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FInteractableActorChangedSignature, AActor*, OldActor, AActor*, NewActor);
|
||||
|
||||
/**
|
||||
* Delegate for changes in the interacting state.
|
||||
* 交互状态变更的委托。
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FInteractingStateChangedSignature, bool, bInteracting);
|
||||
|
||||
/**
|
||||
* Delegate for changes in the number of interactable actors.
|
||||
* 可交互演员数量变更的委托。
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FInteractableActorNumChangedSignature, int32, ActorsNum);
|
||||
|
||||
/**
|
||||
* Component for managing interactions with smart objects.
|
||||
* 管理与智能对象交互的组件。
|
||||
*/
|
||||
UCLASS(Blueprintable, BlueprintType, ClassGroup=(GGS), meta=(BlueprintSpawnableComponent))
|
||||
class GENERICGAMESYSTEM_API UGGS_InteractionSystemComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor for the interaction system component.
|
||||
* 交互系统组件构造函数。
|
||||
*/
|
||||
UGGS_InteractionSystemComponent();
|
||||
|
||||
/**
|
||||
* Retrieves lifetime replicated properties.
|
||||
* 获取生命周期内同步的属性。
|
||||
* @param OutLifetimeProps The replicated properties. 同步的属性。
|
||||
*/
|
||||
virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
/**
|
||||
* Retrieves the interaction system component from an actor.
|
||||
* 从演员获取交互系统组件。
|
||||
* @param Actor The actor to query. 要查询的演员。
|
||||
* @return The interaction system component. 交互系统组件。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGS|InteractionSystem", meta=(DefaultToSelf="Actor"))
|
||||
static UGGS_InteractionSystemComponent* GetInteractionSystemComponent(const AActor* Actor);
|
||||
|
||||
/**
|
||||
* Cycles through interactable actors.
|
||||
* 循环切换可交互演员。
|
||||
* @param bNext Whether to cycle to the next actor. 是否切换到下一个演员。
|
||||
*/
|
||||
UFUNCTION(Server, Reliable, BlueprintCallable, Category="GGS|InteractionSystem")
|
||||
void CycleInteractableActors(bool bNext);
|
||||
|
||||
/**
|
||||
* Triggers a search for potential interactable actors.
|
||||
* 触发潜在可交互演员的搜索。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem")
|
||||
void SearchInteractableActors();
|
||||
|
||||
/**
|
||||
* Sets a new array of interactable actors.
|
||||
* 设置新的可交互演员数组。
|
||||
* @param NewActors The new interactable actors. 新的可交互演员。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem")
|
||||
void SetInteractableActors(TArray<AActor*> NewActors);
|
||||
|
||||
/**
|
||||
* Sets the number of interactable actors.
|
||||
* 设置可交互演员的数量。
|
||||
* @param NewNum The new number of interactable actors. 可交互演员的新数量。
|
||||
*/
|
||||
void SetInteractableActorsNum(int32 NewNum);
|
||||
|
||||
/**
|
||||
* Retrieves the array of interactable actors.
|
||||
* 获取可交互演员数组。
|
||||
* @return The interactable actors. 可交互演员。
|
||||
*/
|
||||
TArray<AActor*> GetInteractableActors() const { return InteractableActors; }
|
||||
|
||||
/**
|
||||
* Retrieves the number of interactable actors.
|
||||
* 获取可交互演员的数量。
|
||||
* @return The number of interactable actors. 可交互演员数量。
|
||||
*/
|
||||
int32 GetNumOfInteractableActors() const { return NumsOfInteractableActors; }
|
||||
|
||||
/**
|
||||
* Sets the current interactable actor.
|
||||
* 设置当前可交互演员。
|
||||
* @param InActor The actor to set. 要设置的演员。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem")
|
||||
void SetInteractableActor(AActor* InActor);
|
||||
|
||||
/**
|
||||
* Retrieves the current interactable actor.
|
||||
* 获取当前可交互演员。
|
||||
* @return The interactable actor. 可交互演员。
|
||||
*/
|
||||
AActor* GetInteractableActor() const { return InteractableActor; }
|
||||
|
||||
/**
|
||||
* Delegate for when the interactable actor changes.
|
||||
* 可交互演员变更时的委托。
|
||||
*/
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FInteractableActorChangedSignature OnInteractableActorChangedEvent;
|
||||
|
||||
/**
|
||||
* Delegate for when the number of interactable actors changes.
|
||||
* 可交互演员数量变更时的委托。
|
||||
*/
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FInteractableActorNumChangedSignature OnInteractableActorNumChangedEvent;
|
||||
|
||||
/**
|
||||
* Delegate for when the interacting state changes.
|
||||
* 交互状态变更时的委托。
|
||||
*/
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FInteractingStateChangedSignature OnInteractingStateChangedEvent;
|
||||
|
||||
/**
|
||||
* Delegate for when the interaction options change.
|
||||
* 交互选项变更时的委托。
|
||||
*/
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FInteractionEventSignature OnInteractionOptionsChangedEvent;
|
||||
|
||||
/**
|
||||
* Delegate for when a search for interactable actors is triggered.
|
||||
* 触发可交互演员搜索时的委托。
|
||||
*/
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FInteractionEventSignature OnSearchInteractableActorsEvent;
|
||||
|
||||
/**
|
||||
* Retrieves the smart object request filter.
|
||||
* 获取智能对象请求过滤器。
|
||||
* @return The smart object request filter. 智能对象请求过滤器。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category="GGS|InteractionSystem")
|
||||
FSmartObjectRequestFilter GetSmartObjectRequestFilter();
|
||||
virtual FSmartObjectRequestFilter GetSmartObjectRequestFilter_Implementation();
|
||||
|
||||
/**
|
||||
* Starts an interaction with the specified option index.
|
||||
* 开始与指定选项索引的交互。
|
||||
* @param NewIndex The interaction option index. 交互选项索引。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem")
|
||||
virtual void StartInteraction(int32 NewIndex = 0);
|
||||
|
||||
/**
|
||||
* Ends the current interaction.
|
||||
* 结束当前交互。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem")
|
||||
virtual void EndInteraction();
|
||||
|
||||
/**
|
||||
* Performs an instant interaction with the specified option index.
|
||||
* 执行与指定选项索引的即时交互。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GGS|InteractionSystem")
|
||||
void InstantInteraction(int32 NewIndex = 0);
|
||||
|
||||
/**
|
||||
* Checks if an interaction is in progress.
|
||||
* 检查是否正在进行交互。
|
||||
* @return True if interacting, false otherwise. 如果正在交互返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGS|InteractionSystem")
|
||||
bool IsInteracting() const;
|
||||
|
||||
/**
|
||||
* Retrieves the current interacting option index.
|
||||
* 获取当前交互选项索引。
|
||||
* @return The interacting option index. 交互选项索引。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGS|InteractionSystem")
|
||||
int32 GetInteractingOption() const;
|
||||
|
||||
/**
|
||||
* Retrieves the current interaction options.
|
||||
* 获取当前交互选项。
|
||||
* @return The interaction options. 交互选项。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GGS|InteractionSystem")
|
||||
const TArray<FGGS_InteractionOption>& GetInteractionOptions() const { return InteractionOptions; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Called when the interactable actor changes.
|
||||
* 可交互演员变更时调用。
|
||||
* @param OldActor The previous interactable actor. 之前的可交互演员。
|
||||
*/
|
||||
UFUNCTION()
|
||||
virtual void OnInteractableActorChanged(AActor* OldActor);
|
||||
|
||||
/**
|
||||
* Called when the number of interactable actors changes.
|
||||
* 可交互演员数量变更时调用。
|
||||
*/
|
||||
UFUNCTION()
|
||||
virtual void OnInteractableActorsNumChanged();
|
||||
|
||||
/**
|
||||
* Called when the potential interactable actors changes.
|
||||
* 可交互演员变更时调用。
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category="GGS|InteractionSystem")
|
||||
void OnInteractableActorsChanged();
|
||||
|
||||
/**
|
||||
* Called when a smart object event occurs.
|
||||
* 智能对象事件发生时调用。
|
||||
* @param EventData The smart object event data. 智能对象事件数据。
|
||||
*/
|
||||
UFUNCTION()
|
||||
virtual void OnSmartObjectEventCallback(const FSmartObjectEventData& EventData);
|
||||
|
||||
/**
|
||||
* Called when interaction options change.
|
||||
* 交互选项变更时调用。
|
||||
*/
|
||||
UFUNCTION()
|
||||
virtual void OnInteractionOptionsChanged();
|
||||
|
||||
/**
|
||||
* Called when the interacting option index changes.
|
||||
* 交互选项索引变更时调用。
|
||||
* @param PrevOptionIndex The previous option index. 之前的选项索引。
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category="GGS|InteractionSystem")
|
||||
void OnInteractingOptionChanged(int32 PrevOptionIndex);
|
||||
|
||||
/**
|
||||
* Refreshes interaction options based on smart object request results.
|
||||
* 根据智能对象请求结果刷新交互选项。
|
||||
*/
|
||||
virtual void RefreshOptionsForActor();
|
||||
|
||||
/**
|
||||
* Array of potential interactable actors. Not replicated.
|
||||
* 潜在可交互演员数组。未网络同步。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem")
|
||||
TArray<TObjectPtr<AActor>> InteractableActors;
|
||||
|
||||
/**
|
||||
* Number of potential interactable actors, replicated to owning client.
|
||||
* 潜在可交互演员数量,同步到拥有客户端。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing=OnInteractableActorsNumChanged, Category="GGS|InteractionSystem")
|
||||
int32 NumsOfInteractableActors{0};
|
||||
|
||||
/**
|
||||
* Current selected interactable actor, replicated for owner only.
|
||||
* 当前选中的可交互演员,仅针对拥有者同步。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GGS|InteractionSystem", ReplicatedUsing=OnInteractableActorChanged)
|
||||
TObjectPtr<AActor> InteractableActor;
|
||||
|
||||
/**
|
||||
* Default filter for searching interactable smart objects.
|
||||
* 搜索可交互智能对象的默认过滤器。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem")
|
||||
FSmartObjectRequestFilter DefaultRequestFilter;
|
||||
|
||||
/**
|
||||
* If checked, whenever potential interactable actors changes, the first actor in the list will be selected as currency interactable actor.
|
||||
* 如果勾选,始终使用潜在交互演员中的第一个作为当前选择。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, ReplicatedUsing=OnInteractableActorsNumChanged, Category="GGS|InteractionSystem")
|
||||
bool bNewActorHasPriority{false};
|
||||
|
||||
/**
|
||||
* Current available interaction options, replicated for owner only.
|
||||
* 当前可用的交互选项,仅针对拥有者同步。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem", ReplicatedUsing=OnInteractionOptionsChanged)
|
||||
TArray<FGGS_InteractionOption> InteractionOptions;
|
||||
|
||||
/**
|
||||
* Indicates if an interaction is in progress.
|
||||
* 表示是否正在进行交互。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem")
|
||||
bool bInteracting{false};
|
||||
|
||||
/**
|
||||
* Current interacting option index (-1 if no interaction).
|
||||
* 当前交互选项索引(无交互时为-1)。
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GGS|InteractionSystem", ReplicatedUsing=OnInteractingOptionChanged)
|
||||
int32 InteractingOption{INDEX_NONE};
|
||||
|
||||
/**
|
||||
* Map of smart object slot handles to delegate handles.
|
||||
* 智能对象槽句柄到委托句柄的映射。
|
||||
*/
|
||||
TMap<FSmartObjectSlotHandle, FDelegateHandle> SlotCallbacks;
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GGS_InteractionStructLibrary.h"
|
||||
#include "SmartObjectSubsystem.h"
|
||||
#include "SmartObjectTypes.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "GGS_SmartObjectFunctionLibrary.generated.h"
|
||||
|
||||
class UGameplayInteractionSmartObjectBehaviorDefinition;
|
||||
class UGameplayBehaviorSmartObjectBehaviorDefinition;
|
||||
class UGameplayBehaviorConfig;
|
||||
class USmartObjectBehaviorDefinition;
|
||||
|
||||
/**
|
||||
* Blueprint function library for smart object interactions.
|
||||
* 智能对象交互的蓝图函数库。
|
||||
*/
|
||||
UCLASS()
|
||||
class GENERICGAMESYSTEM_API UGGS_SmartObjectFunctionLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Retrieves the gameplay behavior config from a smart object behavior definition.
|
||||
* 从智能对象行为定义获取游戏行为配置。
|
||||
* @param BehaviorDefinition The smart object behavior definition. 智能对象行为定义。
|
||||
* @return The gameplay behavior config. 游戏行为配置。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GGS|SmartObject")
|
||||
static UGameplayBehaviorConfig* GetGameplayBehaviorConfig(const USmartObjectBehaviorDefinition* BehaviorDefinition);
|
||||
|
||||
/**
|
||||
* Finds a specific gameplay behavior config by class.
|
||||
* 按类查找特定游戏行为配置。
|
||||
* @param BehaviorDefinition The smart object behavior definition. 智能对象行为定义。
|
||||
* @param DesiredClass The desired config class. 期望的配置类。
|
||||
* @param OutConfig The found config (output). 找到的配置(输出)。
|
||||
* @return True if the config was found, false otherwise. 如果找到配置返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "GGS|SmartObject", meta=(DeterminesOutputType="DesiredClass", DynamicOutputParam="OutConfig", ExpandBoolAsExecs="ReturnValue"))
|
||||
static bool FindGameplayBehaviorConfig(const USmartObjectBehaviorDefinition* BehaviorDefinition, TSubclassOf<UGameplayBehaviorConfig> DesiredClass, UGameplayBehaviorConfig*& OutConfig);
|
||||
|
||||
/**
|
||||
* Searches for smart object slots with interaction entrances on an actor.
|
||||
* 在演员上搜索带有交互入口的智能对象槽。
|
||||
* @param Filter The search filter. 搜索过滤器。
|
||||
* @param SearchActor The actor to search. 要搜索的演员。
|
||||
* @param OutResults The found smart object slot candidates (output). 找到的智能对象槽候选(输出)。
|
||||
* @param UserActor Optional actor for additional data in condition evaluation. 用于条件评估的可选演员。
|
||||
* @return True if at least one candidate was found, false otherwise. 如果找到至少一个候选返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = "GGS|SmartObject", Meta = (ReturnDisplayName = "bSuccess"))
|
||||
static bool FindSmartObjectsWithInteractionEntranceInActor(const FSmartObjectRequestFilter& Filter, AActor* SearchActor, TArray<FSmartObjectRequestResult>& OutResults,
|
||||
const AActor* UserActor = nullptr);
|
||||
|
||||
/**
|
||||
* Finds the interaction definition for a smart object slot.
|
||||
* 查找智能对象槽的交互定义。
|
||||
* @param WorldContext The world context object. 世界上下文对象。
|
||||
* @param SmartObjectSlotHandle The smart object slot handle. 智能对象槽句柄。
|
||||
* @param OutDefinition The interaction definition (output). 交互定义(输出)。
|
||||
* @return True if the definition was found, false otherwise. 如果找到定义返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GGS|SmartObject", meta=(WorldContext="WorldContext", ExpandBoolAsExecs="ReturnValue"))
|
||||
static bool FindInteractionDefinitionFromSmartObjectSlot(UObject* WorldContext, FSmartObjectSlotHandle SmartObjectSlotHandle, UGGS_InteractionDefinition*& OutDefinition);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Tasks/TargetingFilterTask_BasicFilterTemplate.h"
|
||||
#include "GGS_TargetingFilterTask_InteractionSmartObjects.generated.h"
|
||||
|
||||
/**
|
||||
* Filter task for selecting interactable smart objects.
|
||||
* 选择可交互智能对象的过滤任务。
|
||||
*/
|
||||
UCLASS(meta=(DisplayName="(GGS)FilterTask:InteractionSmartObject"))
|
||||
class GENERICGAMESYSTEM_API UGGS_TargetingFilterTask_InteractionSmartObjects : public UTargetingFilterTask_BasicFilterTemplate
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Determines if a target should be filtered based on interaction criteria.
|
||||
* 根据交互标准确定是否过滤目标。
|
||||
* @param TargetingHandle The targeting request handle. 目标请求句柄。
|
||||
* @param TargetData The target data. 目标数据。
|
||||
* @return True if the target should be filtered, false otherwise. 如果目标应被过滤返回true,否则返回false。
|
||||
*/
|
||||
virtual bool ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const override;
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Abilities/Tasks/AbilityTask.h"
|
||||
#include "SmartObjectRuntime.h"
|
||||
#include "SmartObjectTypes.h"
|
||||
#include "GGS_AbilityTask_UseSmartObjectWithGameplayBehavior.generated.h"
|
||||
|
||||
class UGameplayBehavior;
|
||||
|
||||
/**
|
||||
* Ability task for using a smart object with gameplay behavior.
|
||||
* 使用智能对象和游戏行为的技能任务。
|
||||
*/
|
||||
UCLASS()
|
||||
class GENERICGAMESYSTEM_API UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior : public UAbilityTask
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor for the ability task.
|
||||
* 技能任务构造函数。
|
||||
*/
|
||||
UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
|
||||
|
||||
/**
|
||||
* Creates an ability task to use a smart object with gameplay behavior.
|
||||
* 创建使用智能对象和游戏行为的技能任务。
|
||||
* @param OwningAbility The owning gameplay ability. 拥有的游戏技能。
|
||||
* @param ClaimHandle The smart object claim handle. 智能对象认领句柄。
|
||||
* @param ClaimPriority The claim priority. 认领优先级。
|
||||
* @return The created ability task. 创建的技能任务。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "GGS|Interaction", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
|
||||
static UGGS_AbilityTask_UseSmartObjectWithGameplayBehavior* UseSmartObjectWithGameplayBehavior(UGameplayAbility* OwningAbility, FSmartObjectClaimHandle ClaimHandle,
|
||||
ESmartObjectClaimPriority ClaimPriority = ESmartObjectClaimPriority::Normal);
|
||||
|
||||
/**
|
||||
* Sets the smart object claim handle.
|
||||
* 设置智能对象认领句柄。
|
||||
* @param Handle The claim handle. 认领句柄。
|
||||
*/
|
||||
void SetClaimHandle(const FSmartObjectClaimHandle& Handle) { ClaimedHandle = Handle; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Activates the ability task.
|
||||
* 激活技能任务。
|
||||
*/
|
||||
virtual void Activate() override;
|
||||
|
||||
/**
|
||||
* Called when the task is destroyed.
|
||||
* 任务销毁时调用。
|
||||
* @param bInOwnerFinished Whether the owner finished the task. 拥有者是否完成任务。
|
||||
*/
|
||||
virtual void OnDestroy(bool bInOwnerFinished) override;
|
||||
|
||||
/**
|
||||
* Starts the interaction with the smart object.
|
||||
* 开始与智能对象的交互。
|
||||
* @return True if the interaction started successfully, false otherwise. 如果交互成功开始返回true,否则返回false。
|
||||
*/
|
||||
bool StartInteraction();
|
||||
|
||||
/**
|
||||
* Called when the smart object behavior finishes.
|
||||
* 智能对象行为完成时调用。
|
||||
* @param Behavior The gameplay behavior. 游戏行为。
|
||||
* @param Avatar The avatar actor. 化身演员。
|
||||
* @param bInterrupted Whether the behavior was interrupted. 行为是否被中断。
|
||||
*/
|
||||
void OnSmartObjectBehaviorFinished(UGameplayBehavior& Behavior, AActor& Avatar, const bool bInterrupted);
|
||||
|
||||
/**
|
||||
* Called when the smart object slot is invalidated.
|
||||
* 智能对象槽失效时调用。
|
||||
* @param ClaimHandle The claim handle. 认领句柄。
|
||||
* @param State The slot state. 槽状态。
|
||||
*/
|
||||
void OnSlotInvalidated(const FSmartObjectClaimHandle& ClaimHandle, const ESmartObjectSlotState State);
|
||||
|
||||
/**
|
||||
* Delegate for when the interaction succeeds.
|
||||
* 交互成功时的委托。
|
||||
*/
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FGenericGameplayTaskDelegate OnSucceeded;
|
||||
|
||||
/**
|
||||
* Delegate for when the interaction fails.
|
||||
* 交互失败时的委托。
|
||||
*/
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FGenericGameplayTaskDelegate OnFailed;
|
||||
|
||||
/**
|
||||
* The gameplay behavior for the interaction.
|
||||
* 交互的游戏行为。
|
||||
*/
|
||||
UPROPERTY()
|
||||
TObjectPtr<UGameplayBehavior> GameplayBehavior;
|
||||
|
||||
/**
|
||||
* The claimed smart object handle.
|
||||
* 认领的智能对象句柄。
|
||||
*/
|
||||
FSmartObjectClaimHandle ClaimedHandle;
|
||||
|
||||
/**
|
||||
* Delegate handle for behavior finished notification.
|
||||
* 行为完成通知的委托句柄。
|
||||
*/
|
||||
FDelegateHandle OnBehaviorFinishedNotifyHandle;
|
||||
|
||||
/**
|
||||
* Indicates if the behavior has finished.
|
||||
* 表示行为是否已完成。
|
||||
*/
|
||||
bool bBehaviorFinished;
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GGS_RagdollStructLibrary.h"
|
||||
#include "Components/PawnComponent.h"
|
||||
|
||||
#include "GGS_RagdollComponent.generated.h"
|
||||
|
||||
class USkeletalMeshComponent;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGGS_RagdollStartedSignature);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGGS_RagdollEndedSignature, bool, bGrounded);
|
||||
|
||||
UCLASS(ClassGroup=(GGS), Blueprintable, meta=(BlueprintSpawnableComponent))
|
||||
class GENERICGAMESYSTEM_API UGGS_RagdollComponent : public UPawnComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Sets default values for this component's properties
|
||||
UGGS_RagdollComponent(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
|
||||
const FGGS_RagdollState& GetRagdollState() const;
|
||||
|
||||
bool IsRagdollAllowedToStart() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GGS|Ragdoll")
|
||||
void SetMeshComponent(USkeletalMeshComponent* InMeshComponent);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "GGS|Ragdoll")
|
||||
bool IsRagdolling() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "GGS|Ragdoll")
|
||||
void StartRagdoll();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "GGS|Ragdoll")
|
||||
virtual void LocalStartRagdoll();
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Event")
|
||||
FGGS_RagdollStartedSignature OnRagdollStartedEvent;
|
||||
UPROPERTY(BlueprintAssignable, Category="Event")
|
||||
FGGS_RagdollEndedSignature OnRagdollEndedEvent;
|
||||
|
||||
private:
|
||||
UFUNCTION(Server, Reliable)
|
||||
void ServerStartRagdoll();
|
||||
|
||||
UFUNCTION(NetMulticast, Reliable)
|
||||
void MulticastStartRagdoll();
|
||||
|
||||
protected:
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "GGS|Ragdoll")
|
||||
void OnRagdollStarted();
|
||||
|
||||
public:
|
||||
bool IsRagdollAllowedToStop() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "GGS|Ragdoll", Meta = (ReturnDisplayName = "Success"))
|
||||
bool StopRagdoll();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "GGS|Ragdoll")
|
||||
virtual void LocalStopRagdoll();
|
||||
|
||||
private:
|
||||
UFUNCTION(Server, Reliable)
|
||||
void ServerStopRagdoll();
|
||||
|
||||
UFUNCTION(NetMulticast, Reliable)
|
||||
void MulticastStopRagdoll();
|
||||
|
||||
protected:
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "GGS|Ragdoll")
|
||||
UAnimMontage* SelectGetUpMontage(bool bRagdollFacingUpward);
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "GGS|Ragdoll")
|
||||
void OnRagdollEnded(bool bGrounded);
|
||||
virtual void OnRagdollEnded_Implementation(bool bGrounded);
|
||||
|
||||
private:
|
||||
void SetRagdollTargetLocation(const FVector& NewTargetLocation);
|
||||
|
||||
UFUNCTION(Server, Unreliable)
|
||||
void ServerSetRagdollTargetLocation(const FVector_NetQuantize& NewTargetLocation);
|
||||
|
||||
void RefreshRagdoll(float DeltaTime);
|
||||
|
||||
FVector RagdollTraceGround(bool& bGrounded) const;
|
||||
|
||||
void ConstraintRagdollSpeed() const;
|
||||
|
||||
protected:
|
||||
// Called when the game starts
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
float DamperExactAlpha(float DeltaTime, float HalfLife);
|
||||
|
||||
float Clamp01(float Value);
|
||||
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Ragdoll Settings")
|
||||
FName PelvisBoneName{TEXT("pelvis")};
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Ragdoll Settings")
|
||||
FName SpineBoneName{TEXT("spine_03")};
|
||||
|
||||
// If checked, the ragdoll's speed will be limited by the character's last speed for a few frames
|
||||
// after activation. This hack is used to prevent the ragdoll from getting a very high initial speed
|
||||
// at unstable FPS, which can be reproduced by jumping and activating the ragdoll at the same time.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ragdoll Settings")
|
||||
uint8 bLimitInitialRagdollSpeed : 1 {true};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ragdoll Settings")
|
||||
bool bPlayGetupMontageAfterRagdollEndedOnGround{false};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ragdoll Settings", meta=(EditCondition="bPlayGetupMontageAfterRagdollEndedOnGround"))
|
||||
TSoftObjectPtr<UAnimMontage> GetUpFrontMontage;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ragdoll Settings", meta=(EditCondition="bPlayGetupMontageAfterRagdollEndedOnGround"))
|
||||
TSoftObjectPtr<UAnimMontage> GetUpBackMontage;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Ragdoll State", Transient)
|
||||
bool bRagdolling{false};
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Ragdoll State", Transient, Replicated)
|
||||
FVector_NetQuantize RagdollTargetLocation{ForceInit};
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Ragdoll State", Transient)
|
||||
FGGS_RagdollState RagdollState;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Ragdoll State", Transient)
|
||||
TObjectPtr<USkeletalMeshComponent> MeshComponent{nullptr};
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Ragdoll State", Transient)
|
||||
TObjectPtr<APawn> PawnOwner{nullptr};
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Ragdoll State", Transient)
|
||||
TObjectPtr<ACharacter> CharacterOwner{nullptr};
|
||||
|
||||
public:
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
// Called every frame
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "Animation/PoseSnapshot.h"
|
||||
#include "GGS_RagdollStructLibrary.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICGAMESYSTEM_API FGGS_RagdollState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS")
|
||||
FVector Velocity{ForceInit};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS", Meta = (ForceUnits = "N"))
|
||||
float PullForce{0.0f};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS", Meta = (ClampMin = 0))
|
||||
int32 SpeedLimitFrameTimeRemaining{0};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS", Meta = (ClampMin = 0, ForceUnits = "cm/s"))
|
||||
float SpeedLimit{0.0f};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GGS")
|
||||
FPoseSnapshot FinalRagdollPose;
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GGS_SocketRelationshipMapping.generated.h"
|
||||
|
||||
/**
|
||||
* Structure for socket adjustments.
|
||||
* 插槽调整结构。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICGAMESYSTEM_API FGGS_SocketAdjustment
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Array of skeleton names for the adjustment.
|
||||
* 调整适用的骨骼名称数组。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category="GGS")
|
||||
TArray<FString> ForSkeletons;
|
||||
|
||||
/**
|
||||
* Name of the socket.
|
||||
* 插槽名称。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS")
|
||||
FName SocketName{NAME_None};
|
||||
|
||||
/**
|
||||
* Relative transform for the socket.
|
||||
* 插槽的相对变换。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS")
|
||||
FTransform RelativeTransform;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/**
|
||||
* Editor-friendly name for the adjustment.
|
||||
* 调整的编辑器友好名称。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category="GGS", meta=(EditCondition=false, EditConditionHides))
|
||||
FString EditorFriendlyName;
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* Structure for socket relationships.
|
||||
* 插槽关系结构。
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct GENERICGAMESYSTEM_API FGGS_SocketRelationship
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Mesh asset associated with the relationship.
|
||||
* 与关系关联的网格资产。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS")
|
||||
TSoftObjectPtr<UStreamableRenderAsset> MeshAsset;
|
||||
|
||||
/**
|
||||
* Array of socket adjustments for the mesh.
|
||||
* 网格的插槽调整数组。
|
||||
* @note Will look from bottom to top; 从下往上查询。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS", meta=(TitleProperty="EditorFriendlyName"))
|
||||
TArray<FGGS_SocketAdjustment> Adjustments;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/**
|
||||
* Editor-friendly name for the relationship.
|
||||
* 关系的编辑器友好名称。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category="GGS", meta=(EditCondition=false, EditConditionHides))
|
||||
FString EditorFriendlyName;
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* Data asset for defining socket relationships for mesh attachments.
|
||||
* 定义网格附件插槽关系的数据资产。
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class GENERICGAMESYSTEM_API UGGS_SocketRelationshipMapping : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Finds a socket adjustment for a given mesh and socket.
|
||||
* 查找给定网格和插槽的插槽调整。
|
||||
* @param InParentMeshComponent The parent mesh component. 父网格组件。
|
||||
* @param InMeshAsset The mesh asset. 网格资产。
|
||||
* @param InSocketName The socket name. 插槽名称。
|
||||
* @param OutAdjustment The found socket adjustment (output). 找到的插槽调整(输出)。
|
||||
* @return True if an adjustment was found, false otherwise. 如果找到调整返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure=False, Category="GGS|Utilities")
|
||||
bool FindSocketAdjustment(const USkeletalMeshComponent* InParentMeshComponent, const UStreamableRenderAsset* InMeshAsset, FName InSocketName,
|
||||
FGGS_SocketAdjustment& OutAdjustment) const;
|
||||
|
||||
/**
|
||||
* Finds a socket adjustment across multiple mappings.
|
||||
* 在多个映射中查找插槽调整。
|
||||
* @param InMappings The socket relationship mappings. 插槽关系映射。
|
||||
* @param InParentMeshComponent The parent mesh component. 父网格组件。
|
||||
* @param InMeshAsset The mesh asset. 网格资产。
|
||||
* @param InSocketName The socket name. 插槽名称。
|
||||
* @param OutAdjustment The found socket adjustment (output). 找到的插槽调整(输出)。
|
||||
* @return True if an adjustment was found, false otherwise. 如果找到调整返回true,否则返回false。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="GGS|Utilities")
|
||||
static bool FindSocketAdjustmentInMappings(TArray<TSoftObjectPtr<UGGS_SocketRelationshipMapping>> InMappings, const USkeletalMeshComponent* InParentMeshComponent,
|
||||
const UStreamableRenderAsset* InMeshAsset, FName InSocketName,
|
||||
FGGS_SocketAdjustment& OutAdjustment);
|
||||
|
||||
/**
|
||||
* Array of socket relationships.
|
||||
* 插槽关系数组。
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GGS", meta=(TitleProperty="EditorFriendlyName"))
|
||||
TArray<FGGS_SocketRelationship> Relationships;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/**
|
||||
* Pre-save processing for editor.
|
||||
* 编辑器预保存处理。
|
||||
*/
|
||||
virtual void PreSave(FObjectPreSaveContext SaveContext) override;
|
||||
#endif
|
||||
};
|
||||
39
Plugins/GGS/Source/GenericUISystem/GenericUISystem.Build.cs
Normal file
39
Plugins/GGS/Source/GenericUISystem/GenericUISystem.Build.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class GenericUISystem : ModuleRules
|
||||
{
|
||||
public GenericUISystem(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"CommonUI"
|
||||
}
|
||||
);
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"ApplicationCore",
|
||||
"EnhancedInput",
|
||||
"PropertyPath",
|
||||
"GameplayTags",
|
||||
"UMG",
|
||||
"InputCore",
|
||||
"CommonInput",
|
||||
"DeveloperSettings",
|
||||
"ModularGameplay"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "GUIS_GenericUISystemSettings.h"
|
||||
|
||||
const UGUIS_GenericUISystemSettings* UGUIS_GenericUISystemSettings::Get()
|
||||
{
|
||||
return GetDefault<UGUIS_GenericUISystemSettings>();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "GUIS_LogChannels.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogGUIS)
|
||||
DEFINE_LOG_CATEGORY(LogGUIS_Extension);
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "GenericUISystem.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FGenericUISystemModule"
|
||||
|
||||
void FGenericUISystemModule::StartupModule()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void FGenericUISystemModule::ShutdownModule()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FGenericUISystemModule, GenericUISystem)
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "UI/Actions/GUIS_AsyncAction_CreateWidget.h"
|
||||
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Blueprint/WidgetBlueprintLibrary.h"
|
||||
#include "Engine/AssetManager.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/StreamableManager.h"
|
||||
#include "UI/GUIS_GameUIFunctionLibrary.h"
|
||||
#include "UObject/Stack.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_AsyncAction_CreateWidget)
|
||||
|
||||
class UUserWidget;
|
||||
|
||||
static const FName InputFilterReason_Template = FName(TEXT("CreatingWidgetAsync"));
|
||||
|
||||
UGUIS_AsyncAction_CreateWidget::UGUIS_AsyncAction_CreateWidget(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
, bSuspendInputUntilComplete(true)
|
||||
{
|
||||
}
|
||||
|
||||
UGUIS_AsyncAction_CreateWidget* UGUIS_AsyncAction_CreateWidget::CreateWidgetAsync(UObject* InWorldContextObject, TSoftClassPtr<UUserWidget> InUserWidgetSoftClass, APlayerController* InOwningPlayer,
|
||||
bool bSuspendInputUntilComplete)
|
||||
{
|
||||
if (InUserWidgetSoftClass.IsNull())
|
||||
{
|
||||
FFrame::KismetExecutionMessage(TEXT("CreateWidgetAsync was passed a null UserWidgetSoftClass"), ELogVerbosity::Error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UWorld* World = GEngine->GetWorldFromContextObject(InWorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
|
||||
|
||||
UGUIS_AsyncAction_CreateWidget* Action = NewObject<UGUIS_AsyncAction_CreateWidget>();
|
||||
Action->UserWidgetSoftClass = InUserWidgetSoftClass;
|
||||
Action->OwningPlayer = InOwningPlayer;
|
||||
Action->World = World;
|
||||
Action->GameInstance = World->GetGameInstance();
|
||||
Action->bSuspendInputUntilComplete = bSuspendInputUntilComplete;
|
||||
Action->RegisterWithGameInstance(World);
|
||||
|
||||
return Action;
|
||||
}
|
||||
|
||||
void UGUIS_AsyncAction_CreateWidget::Activate()
|
||||
{
|
||||
SuspendInputToken = bSuspendInputUntilComplete ? UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(OwningPlayer.Get(), InputFilterReason_Template) : NAME_None;
|
||||
|
||||
TWeakObjectPtr<UGUIS_AsyncAction_CreateWidget> LocalWeakThis(this);
|
||||
StreamingHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(
|
||||
UserWidgetSoftClass.ToSoftObjectPath(),
|
||||
FStreamableDelegate::CreateUObject(this, &ThisClass::OnWidgetLoaded),
|
||||
FStreamableManager::AsyncLoadHighPriority
|
||||
);
|
||||
|
||||
// Setup a cancel delegate so that we can resume input if this handler is canceled.
|
||||
StreamingHandle->BindCancelDelegate(FStreamableDelegate::CreateWeakLambda(this,
|
||||
[this]()
|
||||
{
|
||||
UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(OwningPlayer.Get(), SuspendInputToken);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
void UGUIS_AsyncAction_CreateWidget::Cancel()
|
||||
{
|
||||
Super::Cancel();
|
||||
|
||||
if (StreamingHandle.IsValid())
|
||||
{
|
||||
StreamingHandle->CancelHandle();
|
||||
StreamingHandle.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_AsyncAction_CreateWidget::OnWidgetLoaded()
|
||||
{
|
||||
if (bSuspendInputUntilComplete)
|
||||
{
|
||||
UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(OwningPlayer.Get(), SuspendInputToken);
|
||||
}
|
||||
|
||||
// If the load as successful, create it, otherwise don't complete this.
|
||||
TSubclassOf<UUserWidget> UserWidgetClass = UserWidgetSoftClass.Get();
|
||||
if (UserWidgetClass)
|
||||
{
|
||||
UUserWidget* UserWidget = UWidgetBlueprintLibrary::Create(World.Get(), UserWidgetClass, OwningPlayer.Get());
|
||||
OnComplete.Broadcast(UserWidget);
|
||||
}
|
||||
|
||||
StreamingHandle.Reset();
|
||||
|
||||
SetReadyToDestroy();
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "UI/Actions/GUIS_AsyncAction_PushContentToUILayer.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "UI/GUIS_GameUILayout.h"
|
||||
#include "UObject/Stack.h"
|
||||
#include "Widgets/CommonActivatableWidgetContainer.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_AsyncAction_PushContentToUILayer)
|
||||
|
||||
UGUIS_AsyncAction_PushContentToUILayer::UGUIS_AsyncAction_PushContentToUILayer(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
UGUIS_AsyncAction_PushContentToUILayer* UGUIS_AsyncAction_PushContentToUILayer::PushContentToUILayer(UGUIS_GameUILayout* UILayout,
|
||||
TSoftClassPtr<UCommonActivatableWidget> InWidgetClass, FGameplayTag InLayerName,
|
||||
bool bSuspendInputUntilComplete)
|
||||
{
|
||||
if (!IsValid(UILayout))
|
||||
{
|
||||
FFrame::KismetExecutionMessage(TEXT("PushContentToUILayer was passed a invalid Layout"), ELogVerbosity::Error);
|
||||
return nullptr;
|
||||
}
|
||||
if (InWidgetClass.IsNull())
|
||||
{
|
||||
FFrame::KismetExecutionMessage(TEXT("PushContentToUILayer was passed a null WidgetClass"), ELogVerbosity::Error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (UWorld* World = GEngine->GetWorldFromContextObject(UILayout->GetWorld(), EGetWorldErrorMode::LogAndReturnNull))
|
||||
{
|
||||
UGUIS_AsyncAction_PushContentToUILayer* Action = NewObject<UGUIS_AsyncAction_PushContentToUILayer>();
|
||||
Action->WidgetClass = InWidgetClass;
|
||||
Action->RootLayout = UILayout;
|
||||
Action->OwningPlayerPtr = UILayout->GetOwningPlayer();
|
||||
Action->LayerName = InLayerName;
|
||||
Action->bSuspendInputUntilComplete = bSuspendInputUntilComplete;
|
||||
Action->RegisterWithGameInstance(World);
|
||||
|
||||
return Action;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UGUIS_AsyncAction_PushContentToUILayer* UGUIS_AsyncAction_PushContentToUILayer::PushContentToUILayerForPlayer(APlayerController* PlayerController,
|
||||
TSoftClassPtr<UCommonActivatableWidget> InWidgetClass,
|
||||
FGameplayTag InLayerName, bool bSuspendInputUntilComplete)
|
||||
{
|
||||
if (!IsValid(PlayerController))
|
||||
{
|
||||
FFrame::KismetExecutionMessage(TEXT("PushContentToUILayerForPlayer was passed a invalid PlayerController"), ELogVerbosity::Error);
|
||||
return nullptr;
|
||||
}
|
||||
if (InWidgetClass.IsNull())
|
||||
{
|
||||
FFrame::KismetExecutionMessage(TEXT("PushContentToUILayer was passed a null WidgetClass"), ELogVerbosity::Error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UGUIS_GameUILayout* UILayout = UGUIS_GameUIFunctionLibrary::GetGameUILayoutForPlayer(PlayerController);
|
||||
|
||||
if (UILayout == nullptr)
|
||||
{
|
||||
FFrame::KismetExecutionMessage(TEXT("PushContentToUILayerForPlayer failed to find UILayout for player."), ELogVerbosity::Error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (UWorld* World = GEngine->GetWorldFromContextObject(UILayout->GetWorld(), EGetWorldErrorMode::LogAndReturnNull))
|
||||
{
|
||||
UGUIS_AsyncAction_PushContentToUILayer* Action = NewObject<UGUIS_AsyncAction_PushContentToUILayer>();
|
||||
Action->WidgetClass = InWidgetClass;
|
||||
Action->RootLayout = UILayout;
|
||||
Action->OwningPlayerPtr = PlayerController;
|
||||
Action->LayerName = InLayerName;
|
||||
Action->bSuspendInputUntilComplete = bSuspendInputUntilComplete;
|
||||
Action->RegisterWithGameInstance(World);
|
||||
|
||||
return Action;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
void UGUIS_AsyncAction_PushContentToUILayer::Cancel()
|
||||
{
|
||||
Super::Cancel();
|
||||
|
||||
if (StreamingHandle.IsValid())
|
||||
{
|
||||
StreamingHandle->CancelHandle();
|
||||
StreamingHandle.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_AsyncAction_PushContentToUILayer::Activate()
|
||||
{
|
||||
// if (UGUIS_GameUILayout* RootLayout = UGUIS_GameUILayout::GetPrimaryGameLayout(OwningPlayerPtr.Get()))
|
||||
if (RootLayout.IsValid())
|
||||
{
|
||||
TWeakObjectPtr<UGUIS_AsyncAction_PushContentToUILayer> WeakThis = this;
|
||||
StreamingHandle = RootLayout->PushWidgetToLayerStackAsync<UCommonActivatableWidget>(LayerName, bSuspendInputUntilComplete, WidgetClass,
|
||||
[this, WeakThis](EGUIS_AsyncWidgetLayerState State, UCommonActivatableWidget* Widget)
|
||||
{
|
||||
if (WeakThis.IsValid())
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case EGUIS_AsyncWidgetLayerState::Initialize:
|
||||
BeforePush.Broadcast(Widget);
|
||||
break;
|
||||
case EGUIS_AsyncWidgetLayerState::AfterPush:
|
||||
AfterPush.Broadcast(Widget);
|
||||
SetReadyToDestroy();
|
||||
break;
|
||||
case EGUIS_AsyncWidgetLayerState::Canceled:
|
||||
SetReadyToDestroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
SetReadyToDestroy();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SetReadyToDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "UI/Actions/GUIS_AsyncAction_ShowModel.h"
|
||||
#include "GUIS_GenericUISystemSettings.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "UI/GUIS_GameUIFunctionLibrary.h"
|
||||
#include "UI/GUIS_GameUILayout.h"
|
||||
#include "UI/GUIS_GameplayTags.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_AsyncAction_ShowModel)
|
||||
|
||||
UGUIS_AsyncAction_ShowModel::UGUIS_AsyncAction_ShowModel(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
// UGUIS_AsyncAction_ShowModel* UGUIS_AsyncAction_ShowModel::ShowModal(UObject* InWorldContextObject, FGameplayTag ModalTag, UGUIS_ModalDefinition* ModalDefinition)
|
||||
// {
|
||||
// const UGUIS_GameUIData* UIData = UGUIS_GenericUISystemSettings::GetGameUIData();
|
||||
// if (!UIData)
|
||||
// return nullptr;
|
||||
//
|
||||
// const TSoftClassPtr<UGUIS_GameModalWidget> SoftModalWidgetClass = UIData->FindWidgetClassForModal(ModalTag);
|
||||
// if (SoftModalWidgetClass.IsNull())
|
||||
// return nullptr;
|
||||
// const TSubclassOf<UGUIS_GameModalWidget> ModalWidgetClass = SoftModalWidgetClass.LoadSynchronous();
|
||||
// if (ModalWidgetClass == nullptr)
|
||||
// return nullptr;
|
||||
//
|
||||
// UGUIS_AsyncAction_ShowModel* Action = NewObject<UGUIS_AsyncAction_ShowModel>();
|
||||
// Action->ModalWidgetClass = ModalWidgetClass;
|
||||
// Action->WorldContextObject = InWorldContextObject;
|
||||
// Action->ModalDefinition = ModalDefinition;
|
||||
// Action->RegisterWithGameInstance(InWorldContextObject);
|
||||
//
|
||||
// return Action;
|
||||
// }
|
||||
|
||||
UGUIS_AsyncAction_ShowModel* UGUIS_AsyncAction_ShowModel::ShowModal(UObject* InWorldContextObject, TSoftClassPtr<UGUIS_ModalDefinition> ModalDefinition)
|
||||
{
|
||||
if (ModalDefinition.IsNull())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ModalDefinition.LoadSynchronous();
|
||||
|
||||
const UGUIS_ModalDefinition* Modal = ModalDefinition->GetDefaultObject<UGUIS_ModalDefinition>();
|
||||
if (Modal == nullptr)
|
||||
return nullptr;
|
||||
|
||||
if (Modal->ModalWidget.IsNull())
|
||||
return nullptr;
|
||||
|
||||
const TSubclassOf<UGUIS_GameModalWidget> ModalWidgetClass = Modal->ModalWidget.LoadSynchronous();
|
||||
if (ModalWidgetClass == nullptr)
|
||||
return nullptr;
|
||||
|
||||
UGUIS_AsyncAction_ShowModel* Action = NewObject<UGUIS_AsyncAction_ShowModel>();
|
||||
Action->ModalWidgetClass = ModalWidgetClass;
|
||||
Action->WorldContextObject = InWorldContextObject;
|
||||
Action->ModalDefinition = Modal;
|
||||
Action->RegisterWithGameInstance(InWorldContextObject);
|
||||
|
||||
return Action;
|
||||
}
|
||||
|
||||
void UGUIS_AsyncAction_ShowModel::Activate()
|
||||
{
|
||||
if (WorldContextObject && !TargetPlayerController)
|
||||
{
|
||||
if (UUserWidget* UserWidget = Cast<UUserWidget>(WorldContextObject))
|
||||
{
|
||||
TargetPlayerController = UserWidget->GetOwningPlayer<APlayerController>();
|
||||
}
|
||||
else if (APlayerController* PC = Cast<APlayerController>(WorldContextObject))
|
||||
{
|
||||
TargetPlayerController = PC;
|
||||
}
|
||||
else if (UWorld* World = WorldContextObject->GetWorld())
|
||||
{
|
||||
if (UGameInstance* GameInstance = World->GetGameInstance<UGameInstance>())
|
||||
{
|
||||
TargetPlayerController = GameInstance->GetPrimaryPlayerController(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TargetPlayerController)
|
||||
{
|
||||
if (UGUIS_GameUILayout* Layout = UGUIS_GameUIFunctionLibrary::GetGameUILayoutForPlayer(TargetPlayerController))
|
||||
{
|
||||
FGUIS_ModalActionResultSignature ResultCallback = FGUIS_ModalActionResultSignature::CreateUObject(this, &UGUIS_AsyncAction_ShowModel::HandleModalAction);
|
||||
const UGUIS_ModalDefinition* TempDescriptor = ModalDefinition;
|
||||
Layout->PushWidgetToLayerStack<UGUIS_GameModalWidget>(GUIS_GameUILayerTags::Modal, ModalWidgetClass, [TempDescriptor, ResultCallback](UGUIS_GameModalWidget& ModalInstance)
|
||||
{
|
||||
ModalInstance.SetupModal(TempDescriptor, ResultCallback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't make the confirmation, just handle an unknown result and broadcast nothing
|
||||
HandleModalAction(GUIS_GameModalActionTags::Unknown);
|
||||
}
|
||||
|
||||
|
||||
void UGUIS_AsyncAction_ShowModel::HandleModalAction(FGameplayTag ModalActionTag)
|
||||
{
|
||||
OnModalAction.Broadcast(ModalActionTag);
|
||||
SetReadyToDestroy();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Actions/GUIS_UIAction.h"
|
||||
|
||||
|
||||
UGUIS_UIAction::UGUIS_UIAction(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
bool UGUIS_UIAction::IsCompatible(const UObject* Data) const
|
||||
{
|
||||
return IsCompatibleInternal(Data);
|
||||
}
|
||||
|
||||
|
||||
bool UGUIS_UIAction::CanInvokeInternal_Implementation(const UObject* Data, APlayerController* PlayerController) const
|
||||
{
|
||||
// 与其他验证不同, 这个默认不通过, Override里修改
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UGUIS_UIAction::CanInvoke(const UObject* Data, APlayerController* PlayerController) const
|
||||
{
|
||||
return CanInvokeInternal(Data, PlayerController);
|
||||
}
|
||||
|
||||
void UGUIS_UIAction::InvokeAction(const UObject* Data, APlayerController* PlayerController) const
|
||||
{
|
||||
// if (CanInvoke(Data, User))
|
||||
{
|
||||
InvokeActionInternal(Data, PlayerController);
|
||||
}
|
||||
}
|
||||
|
||||
FText UGUIS_UIAction::GetActionName() const
|
||||
{
|
||||
return DisplayName;
|
||||
}
|
||||
|
||||
FName UGUIS_UIAction::GetActionID() const
|
||||
{
|
||||
return ActionID;
|
||||
}
|
||||
|
||||
bool UGUIS_UIAction::IsCompatibleInternal_Implementation(const UObject* Data) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void UGUIS_UIAction::InvokeActionInternal_Implementation(const UObject* Data, APlayerController* PlayerController) const
|
||||
{
|
||||
}
|
||||
|
||||
UWorld* UGUIS_UIAction::GetWorld() const
|
||||
{
|
||||
if (UObject* Outer = GetOuter())
|
||||
{
|
||||
return Outer->GetWorld();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Actions/GUIS_UIActionFactory.h"
|
||||
#include "Misc/DataValidation.h"
|
||||
#include "UI/Actions/GUIS_UIAction.h"
|
||||
|
||||
TArray<UGUIS_UIAction*> UGUIS_UIActionFactory::FindAvailableUIActionsForData(const UObject* Data) const
|
||||
{
|
||||
TArray<UGUIS_UIAction*> Ret;
|
||||
for (UGUIS_UIAction* Action : PotentialActions)
|
||||
{
|
||||
if (Action != nullptr && Action->IsCompatible(Data))
|
||||
{
|
||||
Ret.Add(Action);
|
||||
}
|
||||
}
|
||||
return Ret;
|
||||
}
|
||||
|
||||
|
||||
#if WITH_EDITOR
|
||||
EDataValidationResult UGUIS_UIActionFactory::IsDataValid(FDataValidationContext& Context) const
|
||||
{
|
||||
FText ValidationMessage;
|
||||
for (int32 i = 0; i < PotentialActions.Num(); i++)
|
||||
{
|
||||
if (PotentialActions[0] == nullptr)
|
||||
{
|
||||
Context.AddError(FText::FromString(FString::Format(TEXT("Invalid action on index:{0}"), {i})));
|
||||
return EDataValidationResult::Invalid;
|
||||
}
|
||||
}
|
||||
return Super::IsDataValid(Context);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Actions/GUIS_UIActionWidget.h"
|
||||
|
||||
#include "GUIS_LogChannels.h"
|
||||
#include "Input/CommonUIInputTypes.h"
|
||||
#include "UI/GUIS_GameplayTags.h"
|
||||
#include "UI/Actions/GUIS_AsyncAction_ShowModel.h"
|
||||
#include "UI/Actions/GUIS_UIActionFactory.h"
|
||||
|
||||
void UGUIS_UIActionWidget::SetAssociatedData(UObject* Data)
|
||||
{
|
||||
if (Data == nullptr)
|
||||
{
|
||||
UnregisterActions();
|
||||
}
|
||||
AssociatedData = Data;
|
||||
}
|
||||
|
||||
void UGUIS_UIActionWidget::RegisterActions()
|
||||
{
|
||||
if (!AssociatedData.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsValid(ActionFactory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<UGUIS_UIAction*> Actions = ActionFactory->FindAvailableUIActionsForData(AssociatedData.Get());
|
||||
|
||||
for (const UGUIS_UIAction* Action : Actions)
|
||||
{
|
||||
if (Action->CanInvoke(AssociatedData.Get(), GetOwningPlayer()))
|
||||
{
|
||||
FBindUIActionArgs BindArgs(Action->GetInputActionData(), Action->GetShouldDisplayInActionBar(),
|
||||
FSimpleDelegate::CreateLambda([this,Action]()
|
||||
{
|
||||
HandleUIAction(Action);
|
||||
}));
|
||||
|
||||
ActionBindings.Add(RegisterUIActionBinding(BindArgs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_UIActionWidget::RegisterActionsWithFactory(TSoftObjectPtr<UGUIS_UIActionFactory> InActionFactory)
|
||||
{
|
||||
if (InActionFactory.IsNull())
|
||||
{
|
||||
UE_LOG(LogGUIS, Warning, TEXT("Passed invalid action factory!"))
|
||||
return;
|
||||
}
|
||||
|
||||
UGUIS_UIActionFactory* Factory = InActionFactory.LoadSynchronous();
|
||||
|
||||
if (Factory == nullptr)
|
||||
{
|
||||
UE_LOG(LogGUIS, Warning, TEXT("Failed to load action factory!"))
|
||||
return;
|
||||
}
|
||||
|
||||
ActionFactory = Factory;
|
||||
|
||||
RegisterActions();
|
||||
}
|
||||
|
||||
void UGUIS_UIActionWidget::UnregisterActions()
|
||||
{
|
||||
for (FUIActionBindingHandle& ActionBinding : ActionBindings)
|
||||
{
|
||||
ActionBinding.Unregister();
|
||||
}
|
||||
|
||||
ActionBindings.Empty();
|
||||
CancelAction();
|
||||
}
|
||||
|
||||
void UGUIS_UIActionWidget::CancelAction()
|
||||
{
|
||||
if (ModalTask)
|
||||
{
|
||||
ModalTask->OnModalAction.RemoveDynamic(this, &ThisClass::HandleModalAction);
|
||||
ModalTask->Cancel();
|
||||
ModalTask = nullptr;
|
||||
}
|
||||
CurrentAction = nullptr;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
const FText UGUIS_UIActionWidget::GetPaletteCategory()
|
||||
{
|
||||
return FText::FromString(TEXT("Generic UI"));
|
||||
}
|
||||
#endif
|
||||
|
||||
void UGUIS_UIActionWidget::HandleUIAction(const UGUIS_UIAction* Action)
|
||||
{
|
||||
if (ModalTask && ModalTask->IsActive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (AssociatedData.IsValid())
|
||||
{
|
||||
if (Action->GetRequiresConfirmation() && !Action->GetConfirmationModalClass().IsNull())
|
||||
{
|
||||
ModalTask = UGUIS_AsyncAction_ShowModel::ShowModal(GetWorld(), Action->GetConfirmationModalClass());
|
||||
CurrentAction = Action;
|
||||
ModalTask->OnModalAction.AddDynamic(this, &ThisClass::HandleModalAction);
|
||||
ModalTask->Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Action->CanInvoke(AssociatedData.Get(), GetOwningPlayer()))
|
||||
{
|
||||
Action->InvokeAction(AssociatedData.Get(), GetOwningPlayer());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_UIActionWidget::HandleModalAction(FGameplayTag ActionTag)
|
||||
{
|
||||
if (ActionTag == GUIS_GameModalActionTags::Yes || ActionTag == GUIS_GameModalActionTags::Ok)
|
||||
{
|
||||
if (CurrentAction && CurrentAction->CanInvoke(AssociatedData.Get(), GetOwningPlayer()))
|
||||
{
|
||||
CurrentAction->InvokeAction(AssociatedData.Get(), GetOwningPlayer());
|
||||
}
|
||||
CancelAction();
|
||||
}
|
||||
if (ActionTag == GUIS_GameModalActionTags::No)
|
||||
{
|
||||
CancelAction();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Common/GUIS_DetailSectionsBuilder.h"
|
||||
|
||||
TArray<TSoftClassPtr<UGUIS_ListEntryDetailSection>> UGUIS_DetailSectionsBuilder::GatherDetailSections_Implementation(const UObject* Data)
|
||||
{
|
||||
TArray<TSoftClassPtr<UGUIS_ListEntryDetailSection>> Sections;
|
||||
return Sections;
|
||||
}
|
||||
|
||||
|
||||
TArray<TSoftClassPtr<UGUIS_ListEntryDetailSection>> UGUIS_DetailSectionBuilder_Class::GatherDetailSections_Implementation(const UObject* Data)
|
||||
{
|
||||
TArray<TSoftClassPtr<UGUIS_ListEntryDetailSection>> Sections;
|
||||
|
||||
// Find extensions for it using the super chain of the setting so that we get any
|
||||
// class based extensions for this setting.
|
||||
for (UClass* Class = Data->GetClass(); Class; Class = Class->GetSuperClass())
|
||||
{
|
||||
FGUIS_EntryDetailsClassSections* ExtensionForClass = SectionsForClasses.Find(Class);
|
||||
if (ExtensionForClass)
|
||||
{
|
||||
Sections.Append(ExtensionForClass->Sections);
|
||||
}
|
||||
}
|
||||
|
||||
return Sections;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Common/GUIS_ListEntry.h"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Common/GUIS_ListEntryDetailSection.h"
|
||||
|
||||
|
||||
void UGUIS_ListEntryDetailSection::SetListItemObject(UObject* ListItemObject)
|
||||
{
|
||||
NativeOnListItemObjectSet(ListItemObject);
|
||||
}
|
||||
|
||||
void UGUIS_ListEntryDetailSection::NativeOnListItemObjectSet(UObject* ListItemObject)
|
||||
{
|
||||
OnListItemObjectSet(ListItemObject);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Common/GUIS_ListEntryDetailView.h"
|
||||
#include "Components/VerticalBox.h"
|
||||
#include "Components/VerticalBoxSlot.h"
|
||||
#include "Engine/AssetManager.h"
|
||||
#include "Engine/StreamableManager.h"
|
||||
#include "UI/Common/GUIS_ListEntryDetailSection.h"
|
||||
#include "UI/Common/GUIS_DetailSectionsBuilder.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_ListEntryDetailView)
|
||||
|
||||
#define LOCTEXT_NAMESPACE "EntryDetailsView"
|
||||
|
||||
UGUIS_ListEntryDetailView::UGUIS_ListEntryDetailView(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
, ExtensionWidgetPool(*this)
|
||||
{
|
||||
}
|
||||
|
||||
void UGUIS_ListEntryDetailView::ReleaseSlateResources(bool bReleaseChildren)
|
||||
{
|
||||
Super::ReleaseSlateResources(bReleaseChildren);
|
||||
|
||||
ExtensionWidgetPool.ReleaseAllSlateResources();
|
||||
}
|
||||
|
||||
void UGUIS_ListEntryDetailView::NativeOnInitialized()
|
||||
{
|
||||
Super::NativeOnInitialized();
|
||||
|
||||
if (!IsDesignTime())
|
||||
{
|
||||
SetListItemObject(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_ListEntryDetailView::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
}
|
||||
|
||||
void UGUIS_ListEntryDetailView::SetListItemObject(UObject* InListItemObject)
|
||||
{
|
||||
// Ignore requests to show the same setting multiple times in a row.
|
||||
if (InListItemObject && InListItemObject == CurrentListItemObject)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentListItemObject = InListItemObject;
|
||||
|
||||
if (Box_DetailSections)
|
||||
{
|
||||
// First release the widgets back into the pool.
|
||||
for (UWidget* ChildExtension : Box_DetailSections->GetAllChildren())
|
||||
{
|
||||
ExtensionWidgetPool.Release(Cast<UUserWidget>(ChildExtension));
|
||||
}
|
||||
|
||||
// Remove the widgets from their container.
|
||||
Box_DetailSections->ClearChildren();
|
||||
|
||||
if (InListItemObject)
|
||||
{
|
||||
TArray<TSoftClassPtr<UGUIS_ListEntryDetailSection>> SectionClasses;
|
||||
if (SectionsBuilder)
|
||||
{
|
||||
SectionClasses = SectionsBuilder->GatherDetailSections(InListItemObject);
|
||||
}
|
||||
|
||||
if (StreamingHandle.IsValid())
|
||||
{
|
||||
StreamingHandle->CancelHandle();
|
||||
}
|
||||
|
||||
bool bEverythingAlreadyLoaded = true;
|
||||
|
||||
TArray<FSoftObjectPath> SectionPaths;
|
||||
SectionPaths.Reserve(SectionClasses.Num());
|
||||
for (TSoftClassPtr<UGUIS_ListEntryDetailSection> SoftClassPtr : SectionClasses)
|
||||
{
|
||||
bEverythingAlreadyLoaded &= SoftClassPtr.IsValid();
|
||||
SectionPaths.Add(SoftClassPtr.ToSoftObjectPath());
|
||||
}
|
||||
|
||||
if (bEverythingAlreadyLoaded)
|
||||
{
|
||||
for (TSoftClassPtr<UGUIS_ListEntryDetailSection> SoftClassPtr : SectionClasses)
|
||||
{
|
||||
CreateDetailsExtension(InListItemObject, SoftClassPtr.Get());
|
||||
}
|
||||
|
||||
ExtensionWidgetPool.ReleaseInactiveSlateResources();
|
||||
}
|
||||
else
|
||||
{
|
||||
TWeakObjectPtr<UObject> SettingPtr = InListItemObject;
|
||||
|
||||
StreamingHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
|
||||
MoveTemp(SectionPaths),
|
||||
FStreamableDelegate::CreateWeakLambda(this, [this, SettingPtr, SectionClasses]
|
||||
{
|
||||
for (TSoftClassPtr<UGUIS_ListEntryDetailSection> SoftClassPtr : SectionClasses)
|
||||
{
|
||||
CreateDetailsExtension(SettingPtr.Get(), SoftClassPtr.Get());
|
||||
}
|
||||
|
||||
ExtensionWidgetPool.ReleaseInactiveSlateResources();
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_ListEntryDetailView::SetSectionsBuilder(UGUIS_DetailSectionsBuilder* NewBuilder)
|
||||
{
|
||||
SectionsBuilder = NewBuilder;
|
||||
}
|
||||
|
||||
void UGUIS_ListEntryDetailView::CreateDetailsExtension(UObject* InData, TSubclassOf<UGUIS_ListEntryDetailSection> SectionClass)
|
||||
{
|
||||
if (InData && SectionClass)
|
||||
{
|
||||
if (UGUIS_ListEntryDetailSection* Section = ExtensionWidgetPool.GetOrCreateInstance(SectionClass))
|
||||
{
|
||||
Section->SetListItemObject(InData);
|
||||
UVerticalBoxSlot* ExtensionSlot = Box_DetailSections->AddChildToVerticalBox(Section);
|
||||
ExtensionSlot->SetHorizontalAlignment(HAlign_Fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Common/GUIS_ListView.h"
|
||||
|
||||
#include "UI/Common/GUIS_WidgetFactory.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Editor/WidgetCompilerLog.h"
|
||||
#endif
|
||||
|
||||
|
||||
UGUIS_ListView::UGUIS_ListView(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UGUIS_ListView::ValidateCompiledDefaults(IWidgetCompilerLog& InCompileLog) const
|
||||
{
|
||||
Super::ValidateCompiledDefaults(InCompileLog);
|
||||
// if (EntryWidgetFactories.Num() == 0)
|
||||
// {
|
||||
// InCompileLog.Error(FText::Format(FText::FromString("{0} has no Entry widget Factories defined, can't create widgets without them."), FText::FromString(GetName())));
|
||||
// }
|
||||
}
|
||||
#endif
|
||||
|
||||
void UGUIS_ListView::SetEntryWidgetFactories(TArray<UGUIS_WidgetFactory*> NewFactories)
|
||||
{
|
||||
EntryWidgetFactories = NewFactories;
|
||||
}
|
||||
|
||||
UUserWidget& UGUIS_ListView::OnGenerateEntryWidgetInternal(UObject* Item, TSubclassOf<UUserWidget> DesiredEntryClass, const TSharedRef<STableViewBase>& OwnerTable)
|
||||
{
|
||||
TSubclassOf<UUserWidget> WidgetClass = DesiredEntryClass;
|
||||
|
||||
for (const UGUIS_WidgetFactory* Factory : EntryWidgetFactories)
|
||||
{
|
||||
if (Factory)
|
||||
{
|
||||
if (const TSubclassOf<UUserWidget> EntryClass = Factory->FindWidgetClassForData(Item))
|
||||
{
|
||||
WidgetClass = EntryClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Super::OnGenerateEntryWidgetInternal(Item, WidgetClass, OwnerTable);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Common/GUIS_TileView.h"
|
||||
|
||||
#include "UI/Common/GUIS_WidgetFactory.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Editor/WidgetCompilerLog.h"
|
||||
#endif
|
||||
|
||||
|
||||
UGUIS_TileView::UGUIS_TileView(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UGUIS_TileView::ValidateCompiledDefaults(IWidgetCompilerLog& InCompileLog) const
|
||||
{
|
||||
Super::ValidateCompiledDefaults(InCompileLog);
|
||||
// if (EntryWidgetFactories.Num() == 0)
|
||||
// {
|
||||
// InCompileLog.Error(FText::Format(FText::FromString("{0} has no Entry widget Factories defined, can't create widgets without them."), FText::FromString(GetName())));
|
||||
// }
|
||||
}
|
||||
#endif
|
||||
|
||||
void UGUIS_TileView::SetEntryWidgetFactories(TArray<UGUIS_WidgetFactory*> NewFactories)
|
||||
{
|
||||
EntryWidgetFactories = NewFactories;
|
||||
}
|
||||
|
||||
UUserWidget& UGUIS_TileView::OnGenerateEntryWidgetInternal(UObject* Item, TSubclassOf<UUserWidget> DesiredEntryClass, const TSharedRef<STableViewBase>& OwnerTable)
|
||||
{
|
||||
TSubclassOf<UUserWidget> WidgetClass = DesiredEntryClass;
|
||||
|
||||
for (const UGUIS_WidgetFactory* Factory : EntryWidgetFactories)
|
||||
{
|
||||
if (Factory)
|
||||
{
|
||||
if (const TSubclassOf<UUserWidget> EntryClass = Factory->FindWidgetClassForData(Item))
|
||||
{
|
||||
WidgetClass = EntryClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Super::OnGenerateEntryWidgetInternal(Item, WidgetClass, OwnerTable);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Common/GUIS_UserWidgetInterface.h"
|
||||
|
||||
|
||||
// Add default functionality here for any IGUIS_UserWidgetInterface functions that are not pure virtual.
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Common/GUIS_WidgetFactory.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Misc/DataValidation.h"
|
||||
|
||||
|
||||
TSubclassOf<UUserWidget> UGUIS_WidgetFactory::FindWidgetClassForData_Implementation(const UObject* Data) const
|
||||
{
|
||||
return TSubclassOf<UUserWidget>();
|
||||
}
|
||||
|
||||
UGUIS_WidgetFactory::UGUIS_WidgetFactory()
|
||||
{
|
||||
}
|
||||
|
||||
bool UGUIS_WidgetFactory::OnDataValidation_Implementation(FText& ValidationMessage) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
EDataValidationResult UGUIS_WidgetFactory::IsDataValid(FDataValidationContext& Context) const
|
||||
{
|
||||
FText ValidationMessage;
|
||||
if (!OnDataValidation(ValidationMessage))
|
||||
{
|
||||
Context.AddError(ValidationMessage);
|
||||
return EDataValidationResult::Invalid;
|
||||
}
|
||||
return Super::IsDataValid(Context);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Foundation/GUIS_ButtonBase.h"
|
||||
|
||||
#include "CommonActionWidget.h"
|
||||
|
||||
|
||||
void UGUIS_ButtonBase::NativePreConstruct()
|
||||
{
|
||||
Super::NativePreConstruct();
|
||||
|
||||
OnUpdateButtonStyle();
|
||||
RefreshButtonText();
|
||||
}
|
||||
|
||||
void UGUIS_ButtonBase::UpdateInputActionWidget()
|
||||
{
|
||||
Super::UpdateInputActionWidget();
|
||||
|
||||
OnUpdateButtonStyle();
|
||||
RefreshButtonText();
|
||||
}
|
||||
|
||||
void UGUIS_ButtonBase::SetButtonText(const FText& InText)
|
||||
{
|
||||
bOverride_ButtonText = !InText.IsEmpty();
|
||||
ButtonText = InText;
|
||||
RefreshButtonText();
|
||||
}
|
||||
|
||||
void UGUIS_ButtonBase::RefreshButtonText()
|
||||
{
|
||||
if (!bOverride_ButtonText || ButtonText.IsEmpty())
|
||||
{
|
||||
if (InputActionWidget)
|
||||
{
|
||||
const FText ActionDisplayText = InputActionWidget->GetDisplayText();
|
||||
if (!ActionDisplayText.IsEmpty())
|
||||
{
|
||||
OnUpdateButtonText(ActionDisplayText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnUpdateButtonText(ButtonText);
|
||||
}
|
||||
|
||||
|
||||
void UGUIS_ButtonBase::OnInputMethodChanged(ECommonInputType CurrentInputType)
|
||||
{
|
||||
Super::OnInputMethodChanged(CurrentInputType);
|
||||
|
||||
OnUpdateButtonStyle();
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
const FText UGUIS_ButtonBase::GetPaletteCategory()
|
||||
{
|
||||
return PaletteCategory;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Foundation/GUIS_TabButtonBase.h"
|
||||
|
||||
#include "CommonLazyImage.h"
|
||||
|
||||
|
||||
// void UGUIS_TabButtonBase::SetIconFromLazyObject(TSoftObjectPtr<UObject> LazyObject)
|
||||
// {
|
||||
// if (LazyImage_Icon)
|
||||
// {
|
||||
// LazyImage_Icon->SetBrushFromLazyDisplayAsset(LazyObject);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void UGUIS_TabButtonBase::SetIconBrush(const FSlateBrush& Brush)
|
||||
// {
|
||||
// if (LazyImage_Icon)
|
||||
// {
|
||||
// LazyImage_Icon->SetBrush(Brush);
|
||||
// LazyImage_Icon->SetVisibility(ESlateVisibility::Visible);
|
||||
// }
|
||||
// }
|
||||
|
||||
// void UGUIS_TabButtonBase::SetTabLabelInfo_Implementation(const FGUIS_TabDescriptor& TabLabelInfo)
|
||||
// {
|
||||
// SetButtonText(TabLabelInfo.TabText);
|
||||
// SetIconBrush(TabLabelInfo.IconBrush);
|
||||
// }
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/Foundation/GUIS_TabDefinition.h"
|
||||
@@ -0,0 +1,267 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "UI/Foundation/GUIS_TabListWidgetBase.h"
|
||||
|
||||
#include "CommonAnimatedSwitcher.h"
|
||||
#include "CommonButtonBase.h"
|
||||
#include "CommonActivatableWidget.h"
|
||||
#include "GUIS_LogChannels.h"
|
||||
#include "Editor/WidgetCompilerLog.h"
|
||||
#include "UI/Foundation/GUIS_TabDefinition.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_TabListWidgetBase)
|
||||
|
||||
void UGUIS_TabListWidgetBase::NativeOnInitialized()
|
||||
{
|
||||
Super::NativeOnInitialized();
|
||||
}
|
||||
|
||||
void UGUIS_TabListWidgetBase::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
SetupTabs();
|
||||
}
|
||||
|
||||
void UGUIS_TabListWidgetBase::NativeDestruct()
|
||||
{
|
||||
for (FGUIS_TabDescriptor& TabInfo : PreregisteredTabInfoArray)
|
||||
{
|
||||
if (TabInfo.CreatedTabContentWidget)
|
||||
{
|
||||
TabInfo.CreatedTabContentWidget->RemoveFromParent();
|
||||
TabInfo.CreatedTabContentWidget = nullptr;
|
||||
}
|
||||
}
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
|
||||
FGUIS_TabDescriptor::FGUIS_TabDescriptor()
|
||||
{
|
||||
bHidden = false;
|
||||
}
|
||||
|
||||
UGUIS_TabListWidgetBase::UGUIS_TabListWidgetBase()
|
||||
{
|
||||
bAutoListenForInput = false;
|
||||
bDeferRebuildingTabList = true;
|
||||
}
|
||||
|
||||
bool UGUIS_TabListWidgetBase::GetPreregisteredTabInfo(const FName TabNameId, FGUIS_TabDescriptor& OutTabInfo)
|
||||
{
|
||||
const FGUIS_TabDescriptor* const FoundTabInfo = PreregisteredTabInfoArray.FindByPredicate([&](const FGUIS_TabDescriptor& TabInfo) -> bool
|
||||
{
|
||||
return TabInfo.TabId == TabNameId;
|
||||
});
|
||||
|
||||
if (!FoundTabInfo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutTabInfo = *FoundTabInfo;
|
||||
return true;
|
||||
}
|
||||
|
||||
int32 UGUIS_TabListWidgetBase::GetPreregisteredTabIndex(FName TabNameId) const
|
||||
{
|
||||
for (int32 i = 0; i < PreregisteredTabInfoArray.Num(); ++i)
|
||||
{
|
||||
if (PreregisteredTabInfoArray[i].TabId == TabNameId)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
bool UGUIS_TabListWidgetBase::FindPreregisteredTabInfo(const FName TabNameId, FGUIS_TabDescriptor& OutTabInfo)
|
||||
{
|
||||
return GetPreregisteredTabInfo(TabNameId, OutTabInfo);
|
||||
}
|
||||
|
||||
void UGUIS_TabListWidgetBase::SetTabHiddenState(FName TabNameId, bool bHidden)
|
||||
{
|
||||
for (FGUIS_TabDescriptor& TabInfo : PreregisteredTabInfoArray)
|
||||
{
|
||||
if (TabInfo.TabId == TabNameId)
|
||||
{
|
||||
TabInfo.bHidden = bHidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UGUIS_TabListWidgetBase::RegisterDynamicTab(const FGUIS_TabDescriptor& TabDescriptor)
|
||||
{
|
||||
// If it's hidden we just ignore it.
|
||||
if (TabDescriptor.bHidden)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
PendingTabLabelInfoMap.Add(TabDescriptor.TabId, TabDescriptor);
|
||||
|
||||
return RegisterTab(TabDescriptor.TabId, TabDescriptor.TabButtonType.LoadSynchronous(), TabDescriptor.CreatedTabContentWidget);
|
||||
}
|
||||
|
||||
void UGUIS_TabListWidgetBase::HandlePreLinkedSwitcherChanged()
|
||||
{
|
||||
for (const FGUIS_TabDescriptor& TabInfo : PreregisteredTabInfoArray)
|
||||
{
|
||||
// Remove tab content widget from linked switcher, as it is being disassociated.
|
||||
if (TabInfo.CreatedTabContentWidget)
|
||||
{
|
||||
TabInfo.CreatedTabContentWidget->RemoveFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
Super::HandlePreLinkedSwitcherChanged();
|
||||
}
|
||||
|
||||
void UGUIS_TabListWidgetBase::HandlePostLinkedSwitcherChanged()
|
||||
{
|
||||
if (!IsDesignTime() && GetCachedWidget().IsValid())
|
||||
{
|
||||
// Don't bother making tabs if we're in the designer or haven't been constructed yet
|
||||
SetupTabs();
|
||||
}
|
||||
|
||||
Super::HandlePostLinkedSwitcherChanged();
|
||||
}
|
||||
|
||||
void UGUIS_TabListWidgetBase::HandleTabCreation_Implementation(FName TabId, UCommonButtonBase* TabButton)
|
||||
{
|
||||
FGUIS_TabDescriptor* TabInfoPtr = nullptr;
|
||||
|
||||
FGUIS_TabDescriptor TabInfo;
|
||||
if (GetPreregisteredTabInfo(TabId, TabInfo))
|
||||
{
|
||||
TabInfoPtr = &TabInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
TabInfoPtr = PendingTabLabelInfoMap.Find(TabId);
|
||||
}
|
||||
|
||||
if (TabButton->GetClass()->ImplementsInterface(UGUIS_TabButtonInterface::StaticClass()))
|
||||
{
|
||||
if (ensureMsgf(TabInfoPtr, TEXT("A tab button was created with id %s but no label info was specified. RegisterDynamicTab should be used over RegisterTab to provide label info."),
|
||||
*TabId.ToString()))
|
||||
{
|
||||
IGUIS_TabButtonInterface::Execute_SetTabLabelInfo(TabButton, *TabInfoPtr);
|
||||
}
|
||||
}
|
||||
|
||||
PendingTabLabelInfoMap.Remove(TabId);
|
||||
}
|
||||
|
||||
bool UGUIS_TabListWidgetBase::IsFirstTabActive() const
|
||||
{
|
||||
if (PreregisteredTabInfoArray.Num() > 0)
|
||||
{
|
||||
return GetActiveTab() == PreregisteredTabInfoArray[0].TabId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UGUIS_TabListWidgetBase::IsLastTabActive() const
|
||||
{
|
||||
if (PreregisteredTabInfoArray.Num() > 0)
|
||||
{
|
||||
return GetActiveTab() == PreregisteredTabInfoArray.Last().TabId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UGUIS_TabListWidgetBase::IsTabVisible(FName TabId)
|
||||
{
|
||||
if (const UCommonButtonBase* Button = GetTabButtonBaseByID(TabId))
|
||||
{
|
||||
const ESlateVisibility TabVisibility = Button->GetVisibility();
|
||||
return (TabVisibility == ESlateVisibility::Visible
|
||||
|| TabVisibility == ESlateVisibility::HitTestInvisible
|
||||
|| TabVisibility == ESlateVisibility::SelfHitTestInvisible);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 UGUIS_TabListWidgetBase::GetVisibleTabCount()
|
||||
{
|
||||
int32 Result = 0;
|
||||
const int32 TabCount = GetTabCount();
|
||||
for (int32 Index = 0; Index < TabCount; Index++)
|
||||
{
|
||||
if (IsTabVisible(GetTabIdAtIndex(Index)))
|
||||
{
|
||||
Result++;
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
void UGUIS_TabListWidgetBase::SetupTabs()
|
||||
{
|
||||
for (FGUIS_TabDescriptor& TabInfo : PreregisteredTabInfoArray)
|
||||
{
|
||||
if (TabInfo.bHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the tab content hasn't been created already, create it.
|
||||
if (!TabInfo.CreatedTabContentWidget && !TabInfo.TabContentType.IsNull())
|
||||
{
|
||||
TabInfo.CreatedTabContentWidget = CreateWidget<UCommonUserWidget>(GetOwningPlayer(), TabInfo.TabContentType.LoadSynchronous());
|
||||
OnTabContentCreatedNative.Broadcast(TabInfo.TabId, Cast<UCommonUserWidget>(TabInfo.CreatedTabContentWidget));
|
||||
OnTabContentCreated.Broadcast(TabInfo.TabId, Cast<UCommonUserWidget>(TabInfo.CreatedTabContentWidget));
|
||||
}
|
||||
|
||||
if (UCommonAnimatedSwitcher* CurrentLinkedSwitcher = GetLinkedSwitcher())
|
||||
{
|
||||
// Add the tab content to the newly linked switcher.
|
||||
if (!CurrentLinkedSwitcher->HasChild(TabInfo.CreatedTabContentWidget))
|
||||
{
|
||||
CurrentLinkedSwitcher->AddChild(TabInfo.CreatedTabContentWidget);
|
||||
}
|
||||
}
|
||||
|
||||
// If the tab is not already registered, register it.
|
||||
if (GetTabButtonBaseByID(TabInfo.TabId) == nullptr)
|
||||
{
|
||||
RegisterTab(TabInfo.TabId, TabInfo.TabButtonType.LoadSynchronous(), TabInfo.CreatedTabContentWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UGUIS_TabListWidgetBase::PostLoad()
|
||||
{
|
||||
if (!TabDefinitions_DEPRECATED.IsEmpty())
|
||||
{
|
||||
for (TObjectPtr<UDEPRECATED_GUIS_TabDefinition> Def : TabDefinitions_DEPRECATED)
|
||||
{
|
||||
if (Def)
|
||||
{
|
||||
FGUIS_TabDescriptor Tab;
|
||||
Tab.TabId = Def->TabId;
|
||||
Tab.IconBrush = Def->IconBrush;
|
||||
Tab.TabButtonType = Def->TabButtonType;
|
||||
Tab.TabText = Def->TabText;
|
||||
PreregisteredTabInfoArray.Add(Tab);
|
||||
}
|
||||
}
|
||||
TabDefinitions_DEPRECATED.Empty();
|
||||
}
|
||||
|
||||
Super::PostLoad();
|
||||
}
|
||||
|
||||
void UGUIS_TabListWidgetBase::ValidateCompiledDefaults(class IWidgetCompilerLog& CompileLog) const
|
||||
{
|
||||
Super::ValidateCompiledDefaults(CompileLog);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "UI/GUIS_ActivatableWidget.h"
|
||||
|
||||
#include "Editor/WidgetCompilerLog.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_ActivatableWidget)
|
||||
|
||||
#define LOCTEXT_NAMESPACE "GGF"
|
||||
|
||||
UGUIS_ActivatableWidget::UGUIS_ActivatableWidget(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
void UGUIS_ActivatableWidget::SetIsBackHandler(bool bNewState)
|
||||
{
|
||||
}
|
||||
|
||||
TOptional<FUIInputConfig> UGUIS_ActivatableWidget::GetDesiredInputConfig() const
|
||||
{
|
||||
switch (InputConfig)
|
||||
{
|
||||
case EGUIS_ActivatableWidgetInputMode::GameAndMenu:
|
||||
return FUIInputConfig(ECommonInputMode::All, GameMouseCaptureMode);
|
||||
case EGUIS_ActivatableWidgetInputMode::Game:
|
||||
return FUIInputConfig(ECommonInputMode::Game, GameMouseCaptureMode);
|
||||
case EGUIS_ActivatableWidgetInputMode::Menu:
|
||||
return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture);
|
||||
case EGUIS_ActivatableWidgetInputMode::Default:
|
||||
default:
|
||||
return TOptional<FUIInputConfig>();
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
void UGUIS_ActivatableWidget::ValidateCompiledWidgetTree(const UWidgetTree& BlueprintWidgetTree, class IWidgetCompilerLog& CompileLog) const
|
||||
{
|
||||
Super::ValidateCompiledWidgetTree(BlueprintWidgetTree, CompileLog);
|
||||
|
||||
if (!GetClass()->IsFunctionImplementedInScript(GET_FUNCTION_NAME_CHECKED(UGUIS_ActivatableWidget, BP_GetDesiredFocusTarget)))
|
||||
{
|
||||
if (GetParentNativeClass(GetClass()) == StaticClass())
|
||||
{
|
||||
CompileLog.Warning(LOCTEXT("ValidateGetDesiredFocusTarget_Warning", "GetDesiredFocusTarget wasn't implemented, you're going to have trouble using gamepads on this screen."));
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO - Note for now, because we can't guarantee it isn't implemented in a native subclass of this one.
|
||||
CompileLog.Note(LOCTEXT("ValidateGetDesiredFocusTarget_Note",
|
||||
"GetDesiredFocusTarget wasn't implemented, you're going to have trouble using gamepads on this screen. If it was implemented in the native base class you can ignore this message."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
|
||||
#include "UI/GUIS_GameUIContext.h"
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "UI/GUIS_GameUIFunctionLibrary.h"
|
||||
#include "CommonInputSubsystem.h"
|
||||
#include "CommonInputTypeEnum.h"
|
||||
#include "GUIS_LogChannels.h"
|
||||
#include "Components/ListView.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "UI/GUIS_GameUILayout.h"
|
||||
#include "UI/GUIS_GameUIPolicy.h"
|
||||
#include "UI/GUIS_GameUISubsystem.h"
|
||||
#include "Widgets/CommonActivatableWidgetContainer.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUIFunctionLibrary)
|
||||
|
||||
int32 UGUIS_GameUIFunctionLibrary::InputSuspensions = 0;
|
||||
|
||||
ECommonInputType UGUIS_GameUIFunctionLibrary::GetOwningPlayerInputType(const UUserWidget* WidgetContextObject)
|
||||
{
|
||||
if (WidgetContextObject)
|
||||
{
|
||||
if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer()))
|
||||
{
|
||||
return InputSubsystem->GetCurrentInputType();
|
||||
}
|
||||
}
|
||||
|
||||
return ECommonInputType::Count;
|
||||
}
|
||||
|
||||
bool UGUIS_GameUIFunctionLibrary::IsOwningPlayerUsingTouch(const UUserWidget* WidgetContextObject)
|
||||
{
|
||||
if (WidgetContextObject)
|
||||
{
|
||||
if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer()))
|
||||
{
|
||||
return InputSubsystem->GetCurrentInputType() == ECommonInputType::Touch;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UGUIS_GameUIFunctionLibrary::IsOwningPlayerUsingGamepad(const UUserWidget* WidgetContextObject)
|
||||
{
|
||||
if (WidgetContextObject)
|
||||
{
|
||||
if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer()))
|
||||
{
|
||||
return InputSubsystem->GetCurrentInputType() == ECommonInputType::Gamepad;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UCommonActivatableWidget* UGUIS_GameUIFunctionLibrary::PushContentToUILayer_ForPlayer(const APlayerController* PlayerController, FGameplayTag LayerName,
|
||||
TSubclassOf<UCommonActivatableWidget> WidgetClass)
|
||||
{
|
||||
if (!ensure(PlayerController) || !ensure(WidgetClass != nullptr))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UGUIS_GameUILayout* UILayout = GetGameUILayoutForPlayer(PlayerController);
|
||||
|
||||
if (UILayout == nullptr)
|
||||
{
|
||||
FFrame::KismetExecutionMessage(TEXT("PushContentToUILayer_ForPlayer failed to find UILayout for player."), ELogVerbosity::Error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return UILayout->PushWidgetToLayerStack(LayerName, WidgetClass);
|
||||
}
|
||||
|
||||
void UGUIS_GameUIFunctionLibrary::PopContentFromUILayer_ForPlayer(const APlayerController* PlayerController, FGameplayTag LayerName, int32 RemainNum)
|
||||
{
|
||||
if (!ensure(PlayerController))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UGUIS_GameUILayout* UILayout = GetGameUILayoutForPlayer(PlayerController);
|
||||
|
||||
if (UILayout == nullptr)
|
||||
{
|
||||
FFrame::KismetExecutionMessage(TEXT("PopContentFromUILayer_ForPlayer failed to find UILayout for player."), ELogVerbosity::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (UCommonActivatableWidgetContainerBase* Layer = UILayout->GetLayerWidget(LayerName))
|
||||
{
|
||||
const TArray<UCommonActivatableWidget*>& List = Layer->GetWidgetList();
|
||||
|
||||
int32 MinIdx = RemainNum >= 1 ? RemainNum - 1 : 0;
|
||||
|
||||
for (int32 i = List.Num() - 1; i >= MinIdx; i--)
|
||||
{
|
||||
Layer->RemoveWidget(*List[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// void UGUIS_GameUIFunctionLibrary::PushStreamedContentToLayer_ForPlayer(const ULocalPlayer* LocalPlayer, FGameplayTag LayerName, TSoftClassPtr<UCommonActivatableWidget> WidgetClass)
|
||||
// {
|
||||
// if (!ensure(LocalPlayer) || !ensure(!WidgetClass.IsNull()))
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (UGameUIManagerSubsystem* UIManager = LocalPlayer->GetGameInstance()->GetSubsystem<UGameUIManagerSubsystem>())
|
||||
// {
|
||||
// if (UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy())
|
||||
// {
|
||||
// if (UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CastChecked<UCommonLocalPlayer>(LocalPlayer)))
|
||||
// {
|
||||
// const bool bSuspendInputUntilComplete = true;
|
||||
// RootLayout->PushWidgetToLayerStackAsync(LayerName, bSuspendInputUntilComplete, WidgetClass);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
void UGUIS_GameUIFunctionLibrary::PopContentFromUILayer(UCommonActivatableWidget* ActivatableWidget)
|
||||
{
|
||||
if (!ActivatableWidget)
|
||||
{
|
||||
// Ignore request to pop an already deleted widget
|
||||
return;
|
||||
}
|
||||
|
||||
if (const APlayerController* PlayerController = ActivatableWidget->GetOwningPlayer())
|
||||
{
|
||||
if (UGUIS_GameUILayout* UILayout = GetGameUILayoutForPlayer(PlayerController))
|
||||
{
|
||||
UE_LOG(LogGUIS, Verbose, TEXT("Popped content:%s from ui layer."), *GetNameSafe(ActivatableWidget))
|
||||
UILayout->FindAndRemoveWidgetFromLayer(ActivatableWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIFunctionLibrary::PopContentsFromUILayer(TArray<UCommonActivatableWidget*> ActivatableWidgets, bool bReverse)
|
||||
{
|
||||
if (bReverse)
|
||||
{
|
||||
for (int32 i = ActivatableWidgets.Num() - 1; i >= 0; i--)
|
||||
{
|
||||
PopContentFromUILayer(ActivatableWidgets[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int32 i = 0; i < ActivatableWidgets.Num(); i++)
|
||||
{
|
||||
PopContentFromUILayer(ActivatableWidgets[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ULocalPlayer* UGUIS_GameUIFunctionLibrary::GetLocalPlayerFromController(APlayerController* PlayerController)
|
||||
{
|
||||
if (PlayerController)
|
||||
{
|
||||
return Cast<ULocalPlayer>(PlayerController->Player);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UGUIS_GameUILayout* UGUIS_GameUIFunctionLibrary::GetGameUILayoutForPlayer(const APlayerController* PlayerController)
|
||||
{
|
||||
if (!IsValid(PlayerController))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(PlayerController->GetLocalPlayer()))
|
||||
{
|
||||
const ULocalPlayer* CommonLocalPlayer = CastChecked<ULocalPlayer>(LocalPlayer);
|
||||
if (const UGameInstance* GameInstance = CommonLocalPlayer->GetGameInstance())
|
||||
{
|
||||
if (UGUIS_GameUISubsystem* UIManager = GameInstance->GetSubsystem<UGUIS_GameUISubsystem>())
|
||||
{
|
||||
if (const UGUIS_GameUIPolicy* Policy = UIManager->GetCurrentUIPolicy())
|
||||
{
|
||||
if (UGUIS_GameUILayout* RootLayout = Policy->GetRootLayout(CommonLocalPlayer))
|
||||
{
|
||||
return RootLayout;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FName UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(APlayerController* PlayerController, FName SuspendReason)
|
||||
{
|
||||
return SuspendInputForPlayer(PlayerController ? PlayerController->GetLocalPlayer() : nullptr, SuspendReason);
|
||||
}
|
||||
|
||||
FName UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendReason)
|
||||
{
|
||||
if (UCommonInputSubsystem* CommonInputSubsystem = UCommonInputSubsystem::Get(LocalPlayer))
|
||||
{
|
||||
InputSuspensions++;
|
||||
FName SuspendToken = SuspendReason;
|
||||
SuspendToken.SetNumber(InputSuspensions);
|
||||
|
||||
CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::MouseAndKeyboard, SuspendToken, true);
|
||||
CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Gamepad, SuspendToken, true);
|
||||
CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Touch, SuspendToken, true);
|
||||
|
||||
return SuspendToken;
|
||||
}
|
||||
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
void UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(APlayerController* PlayerController, FName SuspendToken)
|
||||
{
|
||||
ResumeInputForPlayer(PlayerController ? PlayerController->GetLocalPlayer() : nullptr, SuspendToken);
|
||||
}
|
||||
|
||||
void UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendToken)
|
||||
{
|
||||
if (SuspendToken == NAME_None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (UCommonInputSubsystem* CommonInputSubsystem = UCommonInputSubsystem::Get(LocalPlayer))
|
||||
{
|
||||
CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::MouseAndKeyboard, SuspendToken, false);
|
||||
CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Gamepad, SuspendToken, false);
|
||||
CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Touch, SuspendToken, false);
|
||||
}
|
||||
}
|
||||
|
||||
UObject* UGUIS_GameUIFunctionLibrary::GetTypedListItem(TScriptInterface<IUserObjectListEntry> UserObjectListEntry, TSubclassOf<UObject> DesiredClass)
|
||||
{
|
||||
UUserWidget* EntryWidget = Cast<UUserWidget>(UserObjectListEntry.GetObject());
|
||||
if (!IsValid(EntryWidget))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
UListView* OwningListView = Cast<UListView>(UUserListEntryLibrary::GetOwningListView(EntryWidget));
|
||||
if (!IsValid(OwningListView))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UObject* ListItem = *OwningListView->ItemFromEntryWidget(*EntryWidget);
|
||||
|
||||
if (ListItem->GetClass()->IsChildOf(DesiredClass))
|
||||
{
|
||||
return ListItem;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UGUIS_GameUIFunctionLibrary::GetTypedListItemSafely(TScriptInterface<IUserObjectListEntry> UserObjectListEntry, TSubclassOf<UObject> DesiredClass, UObject*& OutItem)
|
||||
{
|
||||
OutItem = GetTypedListItem(UserObjectListEntry, DesiredClass);
|
||||
return OutItem != nullptr;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "UI/GUIS_GameUILayout.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GUIS_LogChannels.h"
|
||||
#include "Widgets/CommonActivatableWidgetContainer.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUILayout)
|
||||
|
||||
class UObject;
|
||||
|
||||
UGUIS_GameUILayout::UGUIS_GameUILayout(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
void UGUIS_GameUILayout::SetIsDormant(bool InDormant)
|
||||
{
|
||||
if (bIsDormant != InDormant)
|
||||
{
|
||||
const ULocalPlayer* LP = GetOwningLocalPlayer();
|
||||
const int32 PlayerId = LP ? LP->GetControllerId() : -1;
|
||||
const TCHAR* OldDormancyStr = bIsDormant ? TEXT("Dormant") : TEXT("Not-Dormant");
|
||||
const TCHAR* NewDormancyStr = InDormant ? TEXT("Dormant") : TEXT("Not-Dormant");
|
||||
const TCHAR* PrimaryPlayerStr = LP && LP->IsPrimaryPlayer() ? TEXT("[Primary]") : TEXT("[Non-Primary]");
|
||||
UE_LOG(LogGUIS, Display, TEXT("%s PrimaryGameLayout Dormancy changed for [%d] from [%s] to [%s]"), PrimaryPlayerStr, PlayerId, OldDormancyStr, NewDormancyStr);
|
||||
|
||||
bIsDormant = InDormant;
|
||||
OnIsDormantChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUILayout::OnIsDormantChanged()
|
||||
{
|
||||
//@TODO NDarnell Determine what to do with dormancy, in the past we treated dormancy as a way to shutoff rendering
|
||||
//and the view for the other local players when we force multiple players to use the player view of a single player.
|
||||
|
||||
//if (ULocalPlayer* LocalPlayer = GetOwningLocalPlayer<ULocalPlayer>())
|
||||
//{
|
||||
// // When the root layout is dormant, we don't want to render anything from the owner's view either
|
||||
// LocalPlayer->SetIsPlayerViewEnabled(!bIsDormant);
|
||||
//}
|
||||
|
||||
//SetVisibility(bIsDormant ? ESlateVisibility::Collapsed : ESlateVisibility::SelfHitTestInvisible);
|
||||
|
||||
//OnLayoutDormancyChanged().Broadcast(bIsDormant);
|
||||
}
|
||||
|
||||
void UGUIS_GameUILayout::RegisterLayer(FGameplayTag LayerTag, UCommonActivatableWidgetContainerBase* LayerWidget)
|
||||
{
|
||||
if (!IsDesignTime())
|
||||
{
|
||||
LayerWidget->OnTransitioningChanged.AddUObject(this, &UGUIS_GameUILayout::OnWidgetStackTransitioning);
|
||||
// TODO: Consider allowing a transition duration, we currently set it to 0, because if it's not 0, the
|
||||
// transition effect will cause focus to not transition properly to the new widgets when using
|
||||
// gamepad always.
|
||||
LayerWidget->SetTransitionDuration(0.0);
|
||||
|
||||
Layers.Add(LayerTag, LayerWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUILayout::OnWidgetStackTransitioning(UCommonActivatableWidgetContainerBase* Widget, bool bIsTransitioning)
|
||||
{
|
||||
if (bIsTransitioning)
|
||||
{
|
||||
const FName SuspendToken = UGUIS_GameUIFunctionLibrary::SuspendInputForPlayer(GetOwningLocalPlayer(), TEXT("GlobalStackTransion"));
|
||||
SuspendInputTokens.Add(SuspendToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ensure(SuspendInputTokens.Num() > 0))
|
||||
{
|
||||
const FName SuspendToken = SuspendInputTokens.Pop();
|
||||
UGUIS_GameUIFunctionLibrary::ResumeInputForPlayer(GetOwningLocalPlayer(), SuspendToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUILayout::FindAndRemoveWidgetFromLayer(UCommonActivatableWidget* ActivatableWidget)
|
||||
{
|
||||
// We're not sure what layer the widget is on so go searching.
|
||||
for (const auto& LayerKVP : Layers)
|
||||
{
|
||||
LayerKVP.Value->RemoveWidget(*ActivatableWidget);
|
||||
}
|
||||
}
|
||||
|
||||
UCommonActivatableWidgetContainerBase* UGUIS_GameUILayout::GetLayerWidget(FGameplayTag LayerName)
|
||||
{
|
||||
return Layers.FindRef(LayerName);
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
||||
|
||||
#include "UI/GUIS_GameUIPolicy.h"
|
||||
#include "UI/GUIS_GameUISubsystem.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "GUIS_LogChannels.h"
|
||||
#include "Input/CommonUIInputTypes.h"
|
||||
#include "UI/GUIS_GameUIContext.h"
|
||||
#include "UI/GUIS_GameUILayout.h"
|
||||
|
||||
#include UE_INLINE_GENERATED_CPP_BY_NAME(GUIS_GameUIPolicy)
|
||||
|
||||
// Static
|
||||
UGUIS_GameUIPolicy* UGUIS_GameUIPolicy::GetGameUIPolicy(const UObject* WorldContextObject)
|
||||
{
|
||||
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
|
||||
{
|
||||
if (UGameInstance* GameInstance = World->GetGameInstance())
|
||||
{
|
||||
if (UGUIS_GameUISubsystem* UIManager = UGameInstance::GetSubsystem<UGUIS_GameUISubsystem>(GameInstance))
|
||||
{
|
||||
return UIManager->GetCurrentUIPolicy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UGUIS_GameUISubsystem* UGUIS_GameUIPolicy::GetOwningSubsystem() const
|
||||
{
|
||||
return Cast<UGUIS_GameUISubsystem>(GetOuter());
|
||||
}
|
||||
|
||||
UWorld* UGUIS_GameUIPolicy::GetWorld() const
|
||||
{
|
||||
if (UGUIS_GameUISubsystem* Subsystem = GetOwningSubsystem())
|
||||
{
|
||||
return Subsystem->GetGameInstance()->GetWorld();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UGUIS_GameUILayout* UGUIS_GameUIPolicy::GetRootLayout(const ULocalPlayer* LocalPlayer) const
|
||||
{
|
||||
const FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer);
|
||||
return LayoutInfo ? LayoutInfo->RootLayout : nullptr;
|
||||
}
|
||||
|
||||
UGUIS_GameUIContext* UGUIS_GameUIPolicy::GetContext(const ULocalPlayer* LocalPlayer, TSubclassOf<UGUIS_GameUIContext> ContextClass)
|
||||
{
|
||||
if (const FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
|
||||
{
|
||||
for (int32 i = 0; i < LayoutInfo->Contexts.Num(); i++)
|
||||
{
|
||||
if (LayoutInfo->Contexts[i] && LayoutInfo->Contexts[i]->GetClass() == ContextClass)
|
||||
{
|
||||
return LayoutInfo->Contexts[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UGUIS_GameUIPolicy::AddContext(const ULocalPlayer* LocalPlayer, UGUIS_GameUIContext* NewContext)
|
||||
{
|
||||
if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
|
||||
{
|
||||
if (const UObject* ExistingContext = GetContext(LocalPlayer, NewContext->GetClass()))
|
||||
{
|
||||
UE_LOG(LogGUIS, Warning, TEXT("[%s] is trying to add repeat context of type(%s) for %s, which is not allowed!"), *GetName(), *NewContext->GetClass()->GetName(), *GetNameSafe(LocalPlayer));
|
||||
return false;
|
||||
}
|
||||
LayoutInfo->Contexts.Add(NewContext);
|
||||
UE_LOG(LogGUIS, Verbose, TEXT("[%s] registered context of type(%s) for %s."), *GetName(), *NewContext->GetClass()->GetName(), *GetNameSafe(LocalPlayer));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UGUIS_GameUIContext* UGUIS_GameUIPolicy::FindContext(const ULocalPlayer* LocalPlayer, TSubclassOf<UGUIS_GameUIContext> ContextClass)
|
||||
{
|
||||
if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
|
||||
{
|
||||
for (int32 i = 0; i < LayoutInfo->Contexts.Num(); i++)
|
||||
{
|
||||
if (LayoutInfo->Contexts[i] && LayoutInfo->Contexts[i]->GetClass() == ContextClass)
|
||||
{
|
||||
return LayoutInfo->Contexts[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::RemoveContext(const ULocalPlayer* LocalPlayer, TSubclassOf<UGUIS_GameUIContext> ContextClass)
|
||||
{
|
||||
if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
|
||||
{
|
||||
int32 FoundContext = INDEX_NONE;
|
||||
for (int32 i = 0; i < LayoutInfo->Contexts.Num(); i++)
|
||||
{
|
||||
if (LayoutInfo->Contexts[i] && LayoutInfo->Contexts[i]->GetClass() == ContextClass)
|
||||
{
|
||||
FoundContext = i;
|
||||
UE_LOG(LogGUIS, Verbose, TEXT("[%s] unregistered context of type(%s) for %s."), *GetName(), *LayoutInfo->Contexts[i]->GetClass()->GetName(), *GetNameSafe(LocalPlayer));
|
||||
break;
|
||||
}
|
||||
}
|
||||
LayoutInfo->Contexts.RemoveAt(FoundContext);
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::AddUIAction(const ULocalPlayer* LocalPlayer, UCommonUserWidget* Target, const FDataTableRowHandle& InputAction, bool bShouldDisplayInActionBar,
|
||||
const FGUIS_UIActionExecutedDelegate& Callback, FGUIS_UIActionBindingHandle& BindingHandle)
|
||||
{
|
||||
if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
|
||||
{
|
||||
if (IsValid(Target))
|
||||
{
|
||||
FBindUIActionArgs BindArgs(InputAction, bShouldDisplayInActionBar, FSimpleDelegate::CreateLambda([InputAction, Callback]()
|
||||
{
|
||||
Callback.ExecuteIfBound(InputAction.RowName);
|
||||
}));
|
||||
BindingHandle.Handle = Target->RegisterUIActionBinding(BindArgs);
|
||||
LayoutInfo->BindingHandles.Add(BindingHandle.Handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::RemoveUIAction(const ULocalPlayer* LocalPlayer, FGUIS_UIActionBindingHandle& BindingHandle)
|
||||
{
|
||||
if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
|
||||
{
|
||||
if (BindingHandle.Handle.IsValid())
|
||||
{
|
||||
UE_LOG(LogGUIS, Display, TEXT("Unregister binding for %s"), *BindingHandle.Handle.GetDisplayName().ToString())
|
||||
|
||||
BindingHandle.Handle.Unregister();
|
||||
LayoutInfo->BindingHandles.Remove(BindingHandle.Handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::NotifyPlayerAdded(ULocalPlayer* LocalPlayer)
|
||||
{
|
||||
NotifyPlayerRemoved(LocalPlayer);
|
||||
|
||||
if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
|
||||
{
|
||||
AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout);
|
||||
LayoutInfo->bAddedToViewport = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CreateLayoutWidget(LocalPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::NotifyPlayerRemoved(ULocalPlayer* LocalPlayer)
|
||||
{
|
||||
if (FGUIS_RootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
|
||||
{
|
||||
RemoveLayoutFromViewport(LocalPlayer, LayoutInfo->RootLayout);
|
||||
LayoutInfo->bAddedToViewport = false;
|
||||
|
||||
LayoutInfo->Contexts.Empty();
|
||||
|
||||
if (LocalMultiplayerInteractionMode == EGUIS_LocalMultiplayerInteractionMode::SingleToggle && !LocalPlayer->IsPrimaryPlayer())
|
||||
{
|
||||
UGUIS_GameUILayout* RootLayout = LayoutInfo->RootLayout;
|
||||
if (RootLayout && !RootLayout->IsDormant())
|
||||
{
|
||||
// We're removing a secondary player's root while it's in control - transfer control back to the primary player's root
|
||||
RootLayout->SetIsDormant(true);
|
||||
for (const FGUIS_RootViewportLayoutInfo& RootLayoutInfo : RootViewportLayouts)
|
||||
{
|
||||
if (RootLayoutInfo.LocalPlayer->IsPrimaryPlayer())
|
||||
{
|
||||
if (UGUIS_GameUILayout* PrimaryRootLayout = RootLayoutInfo.RootLayout)
|
||||
{
|
||||
PrimaryRootLayout->SetIsDormant(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::NotifyPlayerDestroyed(ULocalPlayer* LocalPlayer)
|
||||
{
|
||||
NotifyPlayerRemoved(LocalPlayer);
|
||||
const int32 LayoutInfoIdx = RootViewportLayouts.IndexOfByKey(LocalPlayer);
|
||||
if (LayoutInfoIdx != INDEX_NONE)
|
||||
{
|
||||
UGUIS_GameUILayout* Layout = RootViewportLayouts[LayoutInfoIdx].RootLayout;
|
||||
RootViewportLayouts.RemoveAt(LayoutInfoIdx);
|
||||
|
||||
RemoveLayoutFromViewport(LocalPlayer, Layout);
|
||||
|
||||
OnRootLayoutReleased(LocalPlayer, Layout);
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::AddLayoutToViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout)
|
||||
{
|
||||
UE_LOG(LogGUIS, Log, TEXT("[%s] is adding player [%s]'s root layout [%s] to the viewport"), *GetName(), *GetNameSafe(LocalPlayer), *GetNameSafe(Layout));
|
||||
|
||||
Layout->SetPlayerContext(FLocalPlayerContext(LocalPlayer));
|
||||
Layout->AddToPlayerScreen(1000);
|
||||
|
||||
OnRootLayoutAddedToViewport(LocalPlayer, Layout);
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::RemoveLayoutFromViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout)
|
||||
{
|
||||
TWeakPtr<SWidget> LayoutSlateWidget = Layout->GetCachedWidget();
|
||||
if (LayoutSlateWidget.IsValid())
|
||||
{
|
||||
UE_LOG(LogGUIS, Log, TEXT("[%s] is removing player [%s]'s root layout [%s] from the viewport"), *GetName(), *GetNameSafe(LocalPlayer), *GetNameSafe(Layout));
|
||||
|
||||
Layout->RemoveFromParent();
|
||||
if (LayoutSlateWidget.IsValid())
|
||||
{
|
||||
UE_LOG(LogGUIS, Log, TEXT("Player [%s]'s root layout [%s] has been removed from the viewport, but other references to its underlying Slate widget still exist. Noting in case we leak it."),
|
||||
*GetNameSafe(LocalPlayer), *GetNameSafe(Layout));
|
||||
}
|
||||
|
||||
OnRootLayoutRemovedFromViewport(LocalPlayer, Layout);
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::OnRootLayoutAddedToViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
if (GIsEditor && LocalPlayer->IsPrimaryPlayer())
|
||||
{
|
||||
// So our controller will work in PIE without needing to click in the viewport
|
||||
FSlateApplication::Get().SetUserFocusToGameViewport(0);
|
||||
}
|
||||
#endif
|
||||
BP_OnRootLayoutAddedToViewport(LocalPlayer, Layout);
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::OnRootLayoutRemovedFromViewport(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout)
|
||||
{
|
||||
BP_OnRootLayoutRemovedFromViewport(LocalPlayer, Layout);
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::OnRootLayoutReleased(ULocalPlayer* LocalPlayer, UGUIS_GameUILayout* Layout)
|
||||
{
|
||||
BP_OnRootLayoutReleased(LocalPlayer, Layout);
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::RequestPrimaryControl(UGUIS_GameUILayout* Layout)
|
||||
{
|
||||
if (LocalMultiplayerInteractionMode == EGUIS_LocalMultiplayerInteractionMode::SingleToggle && Layout->IsDormant())
|
||||
{
|
||||
for (const FGUIS_RootViewportLayoutInfo& LayoutInfo : RootViewportLayouts)
|
||||
{
|
||||
UGUIS_GameUILayout* RootLayout = LayoutInfo.RootLayout;
|
||||
if (RootLayout && !RootLayout->IsDormant())
|
||||
{
|
||||
RootLayout->SetIsDormant(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Layout->SetIsDormant(false);
|
||||
}
|
||||
}
|
||||
|
||||
void UGUIS_GameUIPolicy::CreateLayoutWidget(ULocalPlayer* LocalPlayer)
|
||||
{
|
||||
if (APlayerController* PlayerController = LocalPlayer->GetPlayerController(GetWorld()))
|
||||
{
|
||||
TSubclassOf<UGUIS_GameUILayout> LayoutWidgetClass = GetLayoutWidgetClass(LocalPlayer);
|
||||
if (ensure(LayoutWidgetClass && !LayoutWidgetClass->HasAnyClassFlags(CLASS_Abstract)))
|
||||
{
|
||||
UGUIS_GameUILayout* NewLayoutObject = CreateWidget<UGUIS_GameUILayout>(PlayerController, LayoutWidgetClass);
|
||||
RootViewportLayouts.Emplace(LocalPlayer, NewLayoutObject, true);
|
||||
|
||||
AddLayoutToViewport(LocalPlayer, NewLayoutObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TSubclassOf<UGUIS_GameUILayout> UGUIS_GameUIPolicy::GetLayoutWidgetClass(ULocalPlayer* LocalPlayer)
|
||||
{
|
||||
return LayoutClass.LoadSynchronous();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user