Add character mesh bridge

This commit is contained in:
2026-04-26 10:35:30 +08:00
parent 349d603775
commit f8169a63e9
8 changed files with 379 additions and 5 deletions

View File

@@ -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 在角色初始化时读取。

View File

@@ -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<UPHYAnimationSettings>();
CurrentRetargeter = Settings ? Settings->DefaultIKRetargeter : FSoftObjectPath();
if (Settings)
{
SourceMeshComponent->SetHiddenInGame(Settings->bHideSourceMeshInGame);
}
RefreshFollowPoseMode();
}
bool UPHYCharacterMeshBridgeComponent::ApplyDisplayMesh(USkeletalMesh* NewDisplayMesh, TSubclassOf<UAnimInstance> NewDisplayAnimClass, FSoftObjectPath NewRetargeter)
{
if (!DisplayMeshComponent || !NewDisplayMesh)
{
return false;
}
const UPHYAnimationSettings* Settings = GetDefault<UPHYAnimationSettings>();
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<UPHYAnimationSettings>();
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<UPHYAnimationSettings>();
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<UPHYAnimationSettings>();
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();
}

View File

@@ -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<UGGS_InteractionSystemComponent>(TEXT("InteractionSystemComponent"));
RagdollComponent = CreateDefaultSubobject<UGGS_RagdollComponent>(TEXT("RagdollComponent"));
ContextEffectComponent = CreateDefaultSubobject<UGES_ContextEffectComponent>(TEXT("ContextEffectComponent"));
MeshBridgeComponent = CreateDefaultSubobject<UPHYCharacterMeshBridgeComponent>(TEXT("MeshBridgeComponent"));
DisplayMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>(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());

View File

@@ -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<ACharacter>(MeshOwner);
APHYCharacterBase* Character = Cast<APHYCharacterBase>(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

View File

@@ -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<USkeletalMesh> DefaultSourceLocomotionMesh;
/** @brief 默认源运动 AnimClass用于 ACharacter::GetMesh() 的统一 Locomotion 动画。 */
UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation")
TSoftClassPtr<UAnimInstance> DefaultSourceAnimClass;
/** @brief 默认显示层 AnimClass异骨架或未来 Retarget Pose From Mesh 路径使用。 */
UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Animation")
TSoftClassPtr<UAnimInstance> 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;
};

View File

@@ -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<UAnimInstance> 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<USkeletalMeshComponent> SourceMeshComponent;
/** @brief 显示层 Mesh 组件,由 CharacterBase 创建并注入。 */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation")
TObjectPtr<USkeletalMeshComponent> DisplayMeshComponent;
/** @brief 当前显示层 Retargeter 软路径,首期只作为后续 AnimBP/Retarget 配置入口。 */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation")
FSoftObjectPath CurrentRetargeter;
};

View File

@@ -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<UGES_ContextEffectComponent> ContextEffectComponent;
/** @brief 运动源 Mesh 与显示层 Mesh 的桥接组件。 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Character|Animation")
TObjectPtr<UPHYCharacterMeshBridgeComponent> MeshBridgeComponent;
/** @brief 显示层 Mesh 组件,承载职业和外观 Mesh。 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Character|Animation")
TObjectPtr<USkeletalMeshComponent> DisplayMeshComponent;
/** @brief 当前 Avatar 缓存的 ASC玩家来自 PlayerStateAI 来自自身。 */
UPROPERTY(Transient)
TObjectPtr<UAbilitySystemComponent> CachedAbilitySystemComponent;