第一次提交

This commit is contained in:
不明不惑
2026-03-03 01:23:02 +08:00
commit 3e434877e8
1053 changed files with 102411 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,5 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Interaction/GGS_InteractionDefinition.h"

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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);
// ...
}

View File

@@ -0,0 +1,4 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Ragdoll/GGS_RagdollStructLibrary.h"

View File

@@ -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