diff --git a/Config/DefaultPHYAnimation.ini b/Config/DefaultPHYAnimation.ini new file mode 100644 index 0000000..033da39 --- /dev/null +++ b/Config/DefaultPHYAnimation.ini @@ -0,0 +1,9 @@ +[/Script/PHY.PHYAnimationSettings] +DefaultSourceLocomotionMesh=None +DefaultSourceAnimClass=None +DefaultDisplayAnimClass=None +DefaultIKRetargeter= +bHideSourceMeshInGame=False +bPreferLeaderPoseForSameSkeleton=True +bAllowRuntimeVisualMeshSwitch=True +bApplyPlayerClassMeshToAI=False diff --git a/Source/PHY/Private/Animation/PHYAnimationSettings.cpp b/Source/PHY/Private/Animation/PHYAnimationSettings.cpp new file mode 100644 index 0000000..edaced9 --- /dev/null +++ b/Source/PHY/Private/Animation/PHYAnimationSettings.cpp @@ -0,0 +1,7 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Animation/PHYAnimationSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAnimationSettings) + +// 动画配置仅承载默认值,实际应用由 Mesh Bridge 在角色初始化时读取。 diff --git a/Source/PHY/Private/Animation/PHYCharacterMeshBridgeComponent.cpp b/Source/PHY/Private/Animation/PHYCharacterMeshBridgeComponent.cpp new file mode 100644 index 0000000..ae6dcb1 --- /dev/null +++ b/Source/PHY/Private/Animation/PHYCharacterMeshBridgeComponent.cpp @@ -0,0 +1,182 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Animation/PHYCharacterMeshBridgeComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYCharacterMeshBridgeComponent) + +#include "Animation/AnimInstance.h" +#include "Animation/PHYAnimationSettings.h" +#include "Class/PHYClassComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/SkeletalMesh.h" +#include "GameFramework/Character.h" + +UPHYCharacterMeshBridgeComponent::UPHYCharacterMeshBridgeComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + PrimaryComponentTick.bCanEverTick = false; +} + +void UPHYCharacterMeshBridgeComponent::InitializeMeshBridge(ACharacter* Character, USkeletalMeshComponent* InDisplayMeshComponent) +{ + SourceMeshComponent = Character ? Character->GetMesh() : nullptr; + DisplayMeshComponent = InDisplayMeshComponent; + + if (!SourceMeshComponent || !DisplayMeshComponent) + { + return; + } + + DisplayMeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); + DisplayMeshComponent->SetGenerateOverlapEvents(false); + DisplayMeshComponent->bReceivesDecals = SourceMeshComponent->bReceivesDecals; + DisplayMeshComponent->VisibilityBasedAnimTickOption = SourceMeshComponent->VisibilityBasedAnimTickOption; + + ApplySourceDefaults(); + + const UPHYAnimationSettings* Settings = GetDefault(); + CurrentRetargeter = Settings ? Settings->DefaultIKRetargeter : FSoftObjectPath(); + + if (Settings) + { + SourceMeshComponent->SetHiddenInGame(Settings->bHideSourceMeshInGame); + } + + RefreshFollowPoseMode(); +} + +bool UPHYCharacterMeshBridgeComponent::ApplyDisplayMesh(USkeletalMesh* NewDisplayMesh, TSubclassOf NewDisplayAnimClass, FSoftObjectPath NewRetargeter) +{ + if (!DisplayMeshComponent || !NewDisplayMesh) + { + return false; + } + + const UPHYAnimationSettings* Settings = GetDefault(); + if (DisplayMeshComponent->GetSkeletalMeshAsset() && Settings && !Settings->bAllowRuntimeVisualMeshSwitch) + { + return false; + } + + DisplayMeshComponent->SetSkeletalMesh(NewDisplayMesh); + + if (NewDisplayAnimClass) + { + DisplayMeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint); + DisplayMeshComponent->SetAnimInstanceClass(NewDisplayAnimClass); + } + else if (Settings && !Settings->DefaultDisplayAnimClass.IsNull()) + { + if (UClass* DefaultDisplayAnimClass = Settings->DefaultDisplayAnimClass.LoadSynchronous()) + { + DisplayMeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint); + DisplayMeshComponent->SetAnimInstanceClass(DefaultDisplayAnimClass); + } + } + + CurrentRetargeter = NewRetargeter.IsValid() ? NewRetargeter : (Settings ? Settings->DefaultIKRetargeter : FSoftObjectPath()); + RefreshFollowPoseMode(); + return true; +} + +bool UPHYCharacterMeshBridgeComponent::ApplyPlayerClassVisualFromClassComponent(const UPHYClassComponent* ClassComponent) +{ + if (!ClassComponent) + { + return false; + } + + if (!ClassComponent->IsPlayerClassMeshAllowed()) + { + return false; + } + + USkeletalMesh* PlayerMesh = ClassComponent->GetConfiguredPlayerMesh(); + return PlayerMesh ? ApplyDisplayMesh(PlayerMesh) : false; +} + +void UPHYCharacterMeshBridgeComponent::RefreshFollowPoseMode() +{ + if (!SourceMeshComponent || !DisplayMeshComponent) + { + return; + } + + if (CanUseLeaderPose()) + { + DisplayMeshComponent->SetLeaderPoseComponent(SourceMeshComponent); + DisplayMeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint); + return; + } + + DisplayMeshComponent->SetLeaderPoseComponent(nullptr); + + const UPHYAnimationSettings* Settings = GetDefault(); + if (Settings && !Settings->DefaultDisplayAnimClass.IsNull() && !DisplayMeshComponent->GetAnimClass()) + { + if (UClass* DefaultDisplayAnimClass = Settings->DefaultDisplayAnimClass.LoadSynchronous()) + { + DisplayMeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint); + DisplayMeshComponent->SetAnimInstanceClass(DefaultDisplayAnimClass); + } + } +} + +USkeletalMeshComponent* UPHYCharacterMeshBridgeComponent::GetVisualMeshComponentForSocket(const FName SocketName) const +{ + if (!SocketName.IsNone() && DisplayMeshComponent && DisplayMeshComponent->DoesSocketExist(SocketName)) + { + return DisplayMeshComponent; + } + + if (!SocketName.IsNone() && SourceMeshComponent && SourceMeshComponent->DoesSocketExist(SocketName)) + { + return SourceMeshComponent; + } + + return DisplayMeshComponent ? DisplayMeshComponent : SourceMeshComponent; +} + +void UPHYCharacterMeshBridgeComponent::ApplySourceDefaults() +{ + if (!SourceMeshComponent) + { + return; + } + + const UPHYAnimationSettings* Settings = GetDefault(); + if (!Settings) + { + return; + } + + if (!SourceMeshComponent->GetSkeletalMeshAsset() && !Settings->DefaultSourceLocomotionMesh.IsNull()) + { + if (USkeletalMesh* DefaultSourceMesh = Settings->DefaultSourceLocomotionMesh.LoadSynchronous()) + { + SourceMeshComponent->SetSkeletalMesh(DefaultSourceMesh); + } + } + + if (!Settings->DefaultSourceAnimClass.IsNull()) + { + if (UClass* DefaultSourceAnimClass = Settings->DefaultSourceAnimClass.LoadSynchronous()) + { + SourceMeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint); + SourceMeshComponent->SetAnimInstanceClass(DefaultSourceAnimClass); + } + } +} + +bool UPHYCharacterMeshBridgeComponent::CanUseLeaderPose() const +{ + const UPHYAnimationSettings* Settings = GetDefault(); + if (Settings && !Settings->bPreferLeaderPoseForSameSkeleton) + { + return false; + } + + const USkeletalMesh* SourceMesh = SourceMeshComponent ? SourceMeshComponent->GetSkeletalMeshAsset() : nullptr; + const USkeletalMesh* DisplayMesh = DisplayMeshComponent ? DisplayMeshComponent->GetSkeletalMeshAsset() : nullptr; + return SourceMesh && DisplayMesh && SourceMesh->GetSkeleton() && SourceMesh->GetSkeleton() == DisplayMesh->GetSkeleton(); +} diff --git a/Source/PHY/Private/Characters/PHYCharacterBase.cpp b/Source/PHY/Private/Characters/PHYCharacterBase.cpp index a1aedd9..2573cb4 100644 --- a/Source/PHY/Private/Characters/PHYCharacterBase.cpp +++ b/Source/PHY/Private/Characters/PHYCharacterBase.cpp @@ -5,6 +5,7 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(PHYCharacterBase) #include "AbilitySystemComponent.h" +#include "Animation/PHYCharacterMeshBridgeComponent.h" #include "Camera/CameraComponent.h" #include "Characters/PHYCharacterSettings.h" #include "Characters/PHYCharacterStateComponent.h" @@ -34,6 +35,11 @@ APHYCharacterBase::APHYCharacterBase(const FObjectInitializer& ObjectInitializer InteractionSystemComponent = CreateDefaultSubobject(TEXT("InteractionSystemComponent")); RagdollComponent = CreateDefaultSubobject(TEXT("RagdollComponent")); ContextEffectComponent = CreateDefaultSubobject(TEXT("ContextEffectComponent")); + MeshBridgeComponent = CreateDefaultSubobject(TEXT("MeshBridgeComponent")); + DisplayMeshComponent = CreateDefaultSubobject(TEXT("DisplayMeshComponent")); + DisplayMeshComponent->SetupAttachment(GetMesh()); + DisplayMeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); + DisplayMeshComponent->SetGenerateOverlapEvents(false); } void APHYCharacterBase::PreInitializeComponents() @@ -49,6 +55,11 @@ void APHYCharacterBase::BeginPlay() ApplyCharacterSettings(); + if (MeshBridgeComponent) + { + MeshBridgeComponent->InitializeMeshBridge(this, DisplayMeshComponent); + } + if (RagdollComponent) { RagdollComponent->SetMeshComponent(GetMesh()); diff --git a/Source/PHY/Private/Class/PHYClassComponent.cpp b/Source/PHY/Private/Class/PHYClassComponent.cpp index 0517f22..acbf701 100644 --- a/Source/PHY/Private/Class/PHYClassComponent.cpp +++ b/Source/PHY/Private/Class/PHYClassComponent.cpp @@ -9,11 +9,11 @@ #include "AbilitySystem/Attributes/PHYCombatAttributeSet.h" #include "AbilitySystem/Attributes/PHYCoreAttributeSet.h" #include "AbilitySystem/Attributes/PHYElementAttributeSet.h" +#include "Animation/PHYCharacterMeshBridgeComponent.h" #include "Characters/PHYAICharacter.h" +#include "Characters/PHYCharacterBase.h" #include "Characters/PHYPlayerCharacter.h" #include "Class/PHYClassSettings.h" -#include "Components/SkeletalMeshComponent.h" -#include "GameFramework/Character.h" #include "Net/UnrealNetwork.h" #include "PHYGameplayTags.h" #include "Player/PHYPlayerState.h" @@ -100,7 +100,7 @@ bool UPHYClassComponent::ApplyConfiguredPlayerMeshIfAllowed(AActor* AvatarActor) } AActor* MeshOwner = AvatarActor ? AvatarActor : GetOwner(); - ACharacter* Character = Cast(MeshOwner); + APHYCharacterBase* Character = Cast(MeshOwner); if (!Character) { return false; @@ -112,8 +112,8 @@ bool UPHYClassComponent::ApplyConfiguredPlayerMeshIfAllowed(AActor* AvatarActor) return false; } - Character->GetMesh()->SetSkeletalMesh(PlayerMesh); - return true; + UPHYCharacterMeshBridgeComponent* MeshBridge = Character->GetMeshBridgeComponent(); + return MeshBridge ? MeshBridge->ApplyDisplayMesh(PlayerMesh) : false; } bool UPHYClassComponent::ApplyClassAttributesToAbilitySystem(UAbilitySystemComponent* AbilitySystemComponent) const diff --git a/Source/PHY/Public/Animation/PHYAnimationSettings.h b/Source/PHY/Public/Animation/PHYAnimationSettings.h new file mode 100644 index 0000000..6993f74 --- /dev/null +++ b/Source/PHY/Public/Animation/PHYAnimationSettings.h @@ -0,0 +1,56 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "UObject/SoftObjectPath.h" +#include "UObject/SoftObjectPtr.h" +#include "PHYAnimationSettings.generated.h" + +class UAnimInstance; +class USkeletalMesh; + +/** + * @brief PHY 动画桥接配置。 + * + * 首期只保存运动源、显示层 AnimClass 与 Retargeter 软引用,不直接依赖 IKRig 模块或创建动画资产。 + */ +UCLASS(Config=PHYAnimation, DefaultConfig) +class PHY_API UPHYAnimationSettings : public UObject +{ + GENERATED_BODY() + +public: + /** @brief 默认源运动 Mesh,挂载到 ACharacter::GetMesh() 上承载统一运动动画。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation") + TSoftObjectPtr DefaultSourceLocomotionMesh; + + /** @brief 默认源运动 AnimClass,用于 ACharacter::GetMesh() 的统一 Locomotion 动画。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation") + TSoftClassPtr DefaultSourceAnimClass; + + /** @brief 默认显示层 AnimClass,异骨架或未来 Retarget Pose From Mesh 路径使用。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation") + TSoftClassPtr DefaultDisplayAnimClass; + + /** @brief 默认 IK Retargeter 资产路径,首期使用软路径避免新增 IKRig 模块依赖。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation") + FSoftObjectPath DefaultIKRetargeter; + + /** @brief 是否在游戏中隐藏源运动 Mesh。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation") + bool bHideSourceMeshInGame = false; + + /** @brief 同骨架时是否优先使用 LeaderPoseComponent 跟随源运动 Mesh。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation") + bool bPreferLeaderPoseForSameSkeleton = true; + + /** @brief 是否允许运行时切换显示层 Mesh。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation") + bool bAllowRuntimeVisualMeshSwitch = true; + + /** @brief 是否允许 AI 套用玩家职业 Mesh,默认关闭以保持 AI 外观独立。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation") + bool bApplyPlayerClassMeshToAI = false; +}; diff --git a/Source/PHY/Public/Animation/PHYCharacterMeshBridgeComponent.h b/Source/PHY/Public/Animation/PHYCharacterMeshBridgeComponent.h new file mode 100644 index 0000000..e43716c --- /dev/null +++ b/Source/PHY/Public/Animation/PHYCharacterMeshBridgeComponent.h @@ -0,0 +1,91 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "UObject/SoftObjectPath.h" +#include "PHYCharacterMeshBridgeComponent.generated.h" + +class ACharacter; +class UAnimInstance; +class UPHYClassComponent; +class USkeletalMesh; +class USkeletalMeshComponent; + +/** + * @brief 角色运动源 Mesh 与显示层 Mesh 的桥接组件。 + * + * ACharacter::GetMesh() 固定作为 SourceMeshComponent,承载统一运动动画;DisplayMeshComponent 承载职业和外观 Mesh。 + */ +UCLASS(BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent)) +class PHY_API UPHYCharacterMeshBridgeComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + /** @brief 构造 Mesh Bridge 默认状态。 */ + UPHYCharacterMeshBridgeComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** @brief 使用角色和外部创建的显示 Mesh 组件初始化桥接关系。 + * @param Character 拥有源运动 Mesh 的角色。 + * @param InDisplayMeshComponent 由角色构造阶段创建的显示层 Mesh 组件。 + */ + UFUNCTION(BlueprintCallable, Category="PHY|Animation") + void InitializeMeshBridge(ACharacter* Character, USkeletalMeshComponent* InDisplayMeshComponent); + + /** @brief 将新的显示 Mesh 应用到显示层,并按骨架关系刷新 Follow Pose 模式。 + * @param NewDisplayMesh 新显示层 Skeletal Mesh。 + * @param NewDisplayAnimClass 可选显示层 AnimClass,异骨架 Retarget 路径使用。 + * @param NewRetargeter 可选 Retargeter 软路径,首期只缓存不强制加载。 + * @return 应用成功返回 true。 + */ + UFUNCTION(BlueprintCallable, Category="PHY|Animation") + bool ApplyDisplayMesh(USkeletalMesh* NewDisplayMesh, TSubclassOf NewDisplayAnimClass = nullptr, FSoftObjectPath NewRetargeter = FSoftObjectPath()); + + /** @brief 从职业组件读取职业外观并应用到显示层。 + * @param ClassComponent 提供职业 Mesh 配置的职业组件。 + * @return 应用成功返回 true。 + */ + UFUNCTION(BlueprintCallable, Category="PHY|Animation") + bool ApplyPlayerClassVisualFromClassComponent(const UPHYClassComponent* ClassComponent); + + /** @brief 根据源 Mesh 和显示 Mesh 的骨架关系刷新 Leader Pose 或显示层 AnimClass。 */ + UFUNCTION(BlueprintCallable, Category="PHY|Animation") + void RefreshFollowPoseMode(); + + /** @brief 获取源运动 Mesh 组件。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Animation") + USkeletalMeshComponent* GetSourceMeshComponent() const { return SourceMeshComponent; } + + /** @brief 获取显示层 Mesh 组件。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Animation") + USkeletalMeshComponent* GetDisplayMeshComponent() const { return DisplayMeshComponent; } + + /** @brief 按 Socket 查询应使用的可视 Mesh 组件,优先返回拥有该 Socket 的显示层。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Animation") + USkeletalMeshComponent* GetVisualMeshComponentForSocket(FName SocketName) const; + + /** @brief 获取当前缓存的 Retargeter 软路径。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Animation") + FSoftObjectPath GetCurrentRetargeter() const { return CurrentRetargeter; } + +protected: + /** @brief 应用配置中的源运动 Mesh 和 AnimClass 默认值。 */ + void ApplySourceDefaults(); + + /** @brief 判断显示层是否可使用 Leader Pose 跟随源运动 Mesh。 */ + bool CanUseLeaderPose() const; + + /** @brief 源运动 Mesh 组件,固定来自 ACharacter::GetMesh()。 */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation") + TObjectPtr SourceMeshComponent; + + /** @brief 显示层 Mesh 组件,由 CharacterBase 创建并注入。 */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation") + TObjectPtr DisplayMeshComponent; + + /** @brief 当前显示层 Retargeter 软路径,首期只作为后续 AnimBP/Retarget 配置入口。 */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation") + FSoftObjectPath CurrentRetargeter; +}; diff --git a/Source/PHY/Public/Characters/PHYCharacterBase.h b/Source/PHY/Public/Characters/PHYCharacterBase.h index 3c73ccb..9ef1385 100644 --- a/Source/PHY/Public/Characters/PHYCharacterBase.h +++ b/Source/PHY/Public/Characters/PHYCharacterBase.h @@ -17,7 +17,9 @@ class UGCS_TargetingSystemComponent; class UGES_ContextEffectComponent; class UGGS_InteractionSystemComponent; class UGGS_RagdollComponent; +class UPHYCharacterMeshBridgeComponent; class UPHYCharacterStateComponent; +class USkeletalMeshComponent; /** * @brief PHY 玩家和 AI 共用角色基类。 @@ -118,6 +120,14 @@ public: UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Character") UGES_ContextEffectComponent* GetContextEffectComponent() const { return ContextEffectComponent; } + /** @brief 获取角色 Mesh Bridge 组件。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Character|Animation") + UPHYCharacterMeshBridgeComponent* GetMeshBridgeComponent() const { return MeshBridgeComponent; } + + /** @brief 获取显示层 Mesh 组件,职业和外观 Mesh 应应用到该组件。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Character|Animation") + USkeletalMeshComponent* GetDisplayMeshComponent() const { return DisplayMeshComponent; } + /** @brief 获取当前战斗目标 Actor。 */ virtual AActor* GetCombatTargetActor_Implementation() const override; @@ -234,6 +244,14 @@ protected: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Character") TObjectPtr ContextEffectComponent; + /** @brief 运动源 Mesh 与显示层 Mesh 的桥接组件。 */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Character|Animation") + TObjectPtr MeshBridgeComponent; + + /** @brief 显示层 Mesh 组件,承载职业和外观 Mesh。 */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Character|Animation") + TObjectPtr DisplayMeshComponent; + /** @brief 当前 Avatar 缓存的 ASC,玩家来自 PlayerState,AI 来自自身。 */ UPROPERTY(Transient) TObjectPtr CachedAbilitySystemComponent;