diff --git a/Config/DefaultPHYClass.ini b/Config/DefaultPHYClass.ini new file mode 100644 index 0000000..b23f27c --- /dev/null +++ b/Config/DefaultPHYClass.ini @@ -0,0 +1,13 @@ +[/Script/PHY.PHYClassSettings] +DefaultPlayerClassTag=(TagName="Class.Saber") +DefaultAIClassTag=(TagName="Class.Saber") +bApplyPlayerClassMeshToPlayers=True +bApplyPlayerClassMeshToAI=False +!PlayerClassMeshes=ClearArray ++PlayerClassMeshes=(ClassTag=(TagName="Class.Saber"),PlayerMesh=None) ++PlayerClassMeshes=(ClassTag=(TagName="Class.Lancer"),PlayerMesh=None) ++PlayerClassMeshes=(ClassTag=(TagName="Class.Archer"),PlayerMesh=None) ++PlayerClassMeshes=(ClassTag=(TagName="Class.Rider"),PlayerMesh=None) ++PlayerClassMeshes=(ClassTag=(TagName="Class.Caster"),PlayerMesh=None) ++PlayerClassMeshes=(ClassTag=(TagName="Class.Assassin"),PlayerMesh=None) ++PlayerClassMeshes=(ClassTag=(TagName="Class.Berserker"),PlayerMesh=None) diff --git a/Source/PHY/Private/Characters/PHYAICharacter.cpp b/Source/PHY/Private/Characters/PHYAICharacter.cpp index f2601d0..9a0a539 100644 --- a/Source/PHY/Private/Characters/PHYAICharacter.cpp +++ b/Source/PHY/Private/Characters/PHYAICharacter.cpp @@ -4,6 +4,8 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAICharacter) +#include "Class/PHYClassComponent.h" +#include "Class/PHYClassSettings.h" #include "GGA_AbilitySystemComponent.h" APHYAICharacter::APHYAICharacter(const FObjectInitializer& ObjectInitializer) @@ -12,6 +14,9 @@ APHYAICharacter::APHYAICharacter(const FObjectInitializer& ObjectInitializer) AbilitySystemComponent = CreateDefaultSubobject(TEXT("AbilitySystemComponent")); AbilitySystemComponent->SetIsReplicated(true); AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed); + + ClassComponent = CreateDefaultSubobject(TEXT("ClassComponent")); + ClassComponent->SetClassTag(GetDefault()->DefaultAIClassTag); } void APHYAICharacter::BeginPlay() @@ -34,4 +39,9 @@ void APHYAICharacter::InitializeAIAbilitySystem() { InitializeAbilitySystem(AbilitySystemComponent, this); } + + if (ClassComponent) + { + ClassComponent->ApplyConfiguredPlayerMeshIfAllowed(this); + } } diff --git a/Source/PHY/Private/Characters/PHYPlayerCharacter.cpp b/Source/PHY/Private/Characters/PHYPlayerCharacter.cpp index ed536c2..d6f393e 100644 --- a/Source/PHY/Private/Characters/PHYPlayerCharacter.cpp +++ b/Source/PHY/Private/Characters/PHYPlayerCharacter.cpp @@ -6,6 +6,7 @@ #include "Camera/CameraComponent.h" #include "Camera/PHYUGCSpringArmComponent.h" +#include "Class/PHYClassComponent.h" #include "GameFramework/SpringArmComponent.h" #include "GIPS_InputSystemComponent.h" #include "Player/PHYPlayerState.h" @@ -51,4 +52,9 @@ void APHYPlayerCharacter::InitializeAbilitySystemFromPlayerState() { InitializeAbilitySystem(PlayerASC, PHYPlayerState); } + + if (UPHYClassComponent* ClassComponent = PHYPlayerState->GetClassComponent()) + { + ClassComponent->ApplyConfiguredPlayerMeshIfAllowed(this); + } } diff --git a/Source/PHY/Private/Class/PHYClassComponent.cpp b/Source/PHY/Private/Class/PHYClassComponent.cpp new file mode 100644 index 0000000..ea39200 --- /dev/null +++ b/Source/PHY/Private/Class/PHYClassComponent.cpp @@ -0,0 +1,112 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Class/PHYClassComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYClassComponent) + +#include "Characters/PHYAICharacter.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" + +UPHYClassComponent::UPHYClassComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SetIsReplicatedByDefault(true); + + CurrentClassTag = PHYGameplayTags::Class_Saber; +} + +void UPHYClassComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, CurrentClassTag); +} + +void UPHYClassComponent::SetClassTag(const FGameplayTag NewClassTag) +{ + if (!NewClassTag.IsValid()) + { + return; + } + + if (AActor* OwnerActor = GetOwner()) + { + if (!OwnerActor->HasAuthority()) + { + return; + } + } + + CurrentClassTag = NewClassTag; +} + +bool UPHYClassComponent::IsPlayerClassMeshAllowed() const +{ + const UPHYClassSettings* ClassSettings = GetDefault(); + if (!ClassSettings) + { + return false; + } + + const AActor* OwnerActor = GetOwner(); + if (OwnerActor && OwnerActor->IsA()) + { + return ClassSettings->bApplyPlayerClassMeshToAI; + } + + if (OwnerActor && (OwnerActor->IsA() || OwnerActor->IsA())) + { + return ClassSettings->bApplyPlayerClassMeshToPlayers; + } + + return ClassSettings->bApplyPlayerClassMeshToPlayers; +} + +USkeletalMesh* UPHYClassComponent::GetConfiguredPlayerMesh() const +{ + const UPHYClassSettings* ClassSettings = GetDefault(); + if (!ClassSettings || !CurrentClassTag.IsValid()) + { + return nullptr; + } + + for (const FPHYPlayerClassMeshConfig& MeshConfig : ClassSettings->PlayerClassMeshes) + { + if (MeshConfig.ClassTag == CurrentClassTag) + { + return MeshConfig.PlayerMesh.LoadSynchronous(); + } + } + + return nullptr; +} + +bool UPHYClassComponent::ApplyConfiguredPlayerMeshIfAllowed(AActor* AvatarActor) const +{ + if (!IsPlayerClassMeshAllowed()) + { + return false; + } + + AActor* MeshOwner = AvatarActor ? AvatarActor : GetOwner(); + ACharacter* Character = Cast(MeshOwner); + if (!Character) + { + return false; + } + + USkeletalMesh* PlayerMesh = GetConfiguredPlayerMesh(); + if (!PlayerMesh) + { + return false; + } + + Character->GetMesh()->SetSkeletalMesh(PlayerMesh); + return true; +} diff --git a/Source/PHY/Private/Class/PHYClassSettings.cpp b/Source/PHY/Private/Class/PHYClassSettings.cpp new file mode 100644 index 0000000..1ba60d8 --- /dev/null +++ b/Source/PHY/Private/Class/PHYClassSettings.cpp @@ -0,0 +1,24 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Class/PHYClassSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYClassSettings) + +#include "PHYGameplayTags.h" + +UPHYClassSettings::UPHYClassSettings() +{ + DefaultPlayerClassTag = PHYGameplayTags::Class_Saber; + DefaultAIClassTag = PHYGameplayTags::Class_Saber; + + PlayerClassMeshes = + { + { PHYGameplayTags::Class_Saber, nullptr }, + { PHYGameplayTags::Class_Lancer, nullptr }, + { PHYGameplayTags::Class_Archer, nullptr }, + { PHYGameplayTags::Class_Rider, nullptr }, + { PHYGameplayTags::Class_Caster, nullptr }, + { PHYGameplayTags::Class_Assassin, nullptr }, + { PHYGameplayTags::Class_Berserker, nullptr } + }; +} diff --git a/Source/PHY/Private/GameplayTags/PHYGameplayTags_Class.cpp b/Source/PHY/Private/GameplayTags/PHYGameplayTags_Class.cpp new file mode 100644 index 0000000..96c9184 --- /dev/null +++ b/Source/PHY/Private/GameplayTags/PHYGameplayTags_Class.cpp @@ -0,0 +1,15 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "GameplayTags/PHYGameplayTags_Class.h" + +// 职业 Tag 由职业系统维护,玩家和 AI 共享分类语义,但 Mesh 应用策略由职业配置控制。 +namespace PHYGameplayTags +{ + UE_DEFINE_GAMEPLAY_TAG(Class_Saber, "Class.Saber"); + UE_DEFINE_GAMEPLAY_TAG(Class_Lancer, "Class.Lancer"); + UE_DEFINE_GAMEPLAY_TAG(Class_Archer, "Class.Archer"); + UE_DEFINE_GAMEPLAY_TAG(Class_Rider, "Class.Rider"); + UE_DEFINE_GAMEPLAY_TAG(Class_Caster, "Class.Caster"); + UE_DEFINE_GAMEPLAY_TAG(Class_Assassin, "Class.Assassin"); + UE_DEFINE_GAMEPLAY_TAG(Class_Berserker, "Class.Berserker"); +} diff --git a/Source/PHY/Private/Player/PHYPlayerState.cpp b/Source/PHY/Private/Player/PHYPlayerState.cpp index 54c18a8..970f274 100644 --- a/Source/PHY/Private/Player/PHYPlayerState.cpp +++ b/Source/PHY/Private/Player/PHYPlayerState.cpp @@ -4,6 +4,8 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(PHYPlayerState) +#include "Class/PHYClassComponent.h" +#include "Class/PHYClassSettings.h" #include "GGA_AbilitySystemComponent.h" APHYPlayerState::APHYPlayerState(const FObjectInitializer& ObjectInitializer) @@ -14,6 +16,9 @@ APHYPlayerState::APHYPlayerState(const FObjectInitializer& ObjectInitializer) AbilitySystemComponent = CreateDefaultSubobject(TEXT("AbilitySystemComponent")); AbilitySystemComponent->SetIsReplicated(true); AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed); + + ClassComponent = CreateDefaultSubobject(TEXT("ClassComponent")); + ClassComponent->SetClassTag(GetDefault()->DefaultPlayerClassTag); } UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const diff --git a/Source/PHY/Public/Characters/PHYAICharacter.h b/Source/PHY/Public/Characters/PHYAICharacter.h index 46270a3..64d12ae 100644 --- a/Source/PHY/Public/Characters/PHYAICharacter.h +++ b/Source/PHY/Public/Characters/PHYAICharacter.h @@ -7,6 +7,7 @@ #include "PHYAICharacter.generated.h" class UGGA_AbilitySystemComponent; +class UPHYClassComponent; /** * @brief PHY AI 角色。 @@ -32,6 +33,10 @@ public: UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|AI") UGGA_AbilitySystemComponent* GetAIAbilitySystemComponent() const { return AbilitySystemComponent; } + /** @brief 获取 AI 职业分类组件。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Class") + UPHYClassComponent* GetClassComponent() const { return ClassComponent; } + protected: /** @brief 初始化 AI 自身作为 Owner 和 Avatar 的 ASC。 */ virtual void InitializeAIAbilitySystem(); @@ -39,4 +44,8 @@ protected: /** @brief AI 自身 ASC。 */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|AI") TObjectPtr AbilitySystemComponent; + + /** @brief AI 职业分类组件,默认不应用玩家职业 Mesh。 */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Class") + TObjectPtr ClassComponent; }; diff --git a/Source/PHY/Public/Class/PHYClassComponent.h b/Source/PHY/Public/Class/PHYClassComponent.h new file mode 100644 index 0000000..93ca450 --- /dev/null +++ b/Source/PHY/Public/Class/PHYClassComponent.h @@ -0,0 +1,53 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "GameplayTagContainer.h" +#include "PHYClassComponent.generated.h" + +class USkeletalMesh; + +/** + * @brief PHY 职业组件。 + * + * 组件复制当前职业分类;玩家可按配置把职业 Mesh 应用到 Avatar,AI 默认只保留分类不套用玩家 Mesh。 + */ +UCLASS(BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent)) +class PHY_API UPHYClassComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + /** @brief 构造可复制的职业组件。 */ + UPHYClassComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** @brief 注册需要网络复制的职业字段。 */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** @brief 设置当前职业 Tag,通常由服务器调用。 */ + UFUNCTION(BlueprintCallable, Category="PHY|Class") + void SetClassTag(FGameplayTag NewClassTag); + + /** @brief 获取当前职业 Tag。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Class") + FGameplayTag GetClassTag() const { return CurrentClassTag; } + + /** @brief 查询当前所有者是否允许应用玩家职业 Mesh。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Class") + bool IsPlayerClassMeshAllowed() const; + + /** @brief 按当前职业读取配置的玩家 Mesh,未配置时返回空。 */ + UFUNCTION(BlueprintCallable, Category="PHY|Class") + USkeletalMesh* GetConfiguredPlayerMesh() const; + + /** @brief 如果策略允许,则把当前职业配置的玩家 Mesh 应用到指定 Avatar 或组件所有者。 */ + UFUNCTION(BlueprintCallable, Category="PHY|Class") + bool ApplyConfiguredPlayerMeshIfAllowed(AActor* AvatarActor = nullptr) const; + +protected: + /** @brief 当前职业 Tag,由服务器复制给客户端。 */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category="PHY|Class") + FGameplayTag CurrentClassTag; +}; diff --git a/Source/PHY/Public/Class/PHYClassSettings.h b/Source/PHY/Public/Class/PHYClassSettings.h new file mode 100644 index 0000000..45a87a1 --- /dev/null +++ b/Source/PHY/Public/Class/PHYClassSettings.h @@ -0,0 +1,64 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "UObject/Object.h" +#include "UObject/SoftObjectPtr.h" +#include "PHYClassSettings.generated.h" + +class USkeletalMesh; + +/** + * @brief 玩家职业 Mesh 配置项。 + */ +USTRUCT(BlueprintType) +struct PHY_API FPHYPlayerClassMeshConfig +{ + GENERATED_BODY() + +public: + /** @brief 职业 Tag,必须来自 Class.* 原生 Gameplay Tag。 */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="PHY|Class") + FGameplayTag ClassTag; + + /** @brief 玩家使用该职业时应用的专属 Skeletal Mesh 软引用。 */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="PHY|Class") + TSoftObjectPtr PlayerMesh; +}; + +/** + * @brief PHY 职业系统配置。 + * + * 职业分类同时服务玩家和 AI;玩家 Mesh 仅表达玩家外观,AI 默认不复用这些 Mesh。 + */ +UCLASS(Config=PHYClass, DefaultConfig) +class PHY_API UPHYClassSettings : public UObject +{ + GENERATED_BODY() + +public: + /** @brief 构造默认七职阶配置。 */ + UPHYClassSettings(); + + /** @brief 玩家默认职业。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Class") + FGameplayTag DefaultPlayerClassTag; + + /** @brief AI 默认职业分类。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Class") + FGameplayTag DefaultAIClassTag; + + /** @brief 玩家职业到玩家专属 Mesh 的配置表。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Class") + TArray PlayerClassMeshes; + + /** @brief 是否允许玩家 Avatar 应用玩家职业 Mesh。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Class") + bool bApplyPlayerClassMeshToPlayers = true; + + /** @brief 是否允许 AI 应用玩家职业 Mesh,默认关闭以保持 AI 外观独立。 */ + UPROPERTY(Config, EditDefaultsOnly, Category="PHY|Class") + bool bApplyPlayerClassMeshToAI = false; +}; diff --git a/Source/PHY/Public/GameplayTags/PHYGameplayTags_Class.h b/Source/PHY/Public/GameplayTags/PHYGameplayTags_Class.h new file mode 100644 index 0000000..9d9d169 --- /dev/null +++ b/Source/PHY/Public/GameplayTags/PHYGameplayTags_Class.h @@ -0,0 +1,32 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "NativeGameplayTags.h" + +/** + * @brief PHY 职业相关原生 Gameplay Tag。 + */ +namespace PHYGameplayTags +{ + /** @brief Saber 职阶。 */ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Class_Saber); + + /** @brief Lancer 职阶。 */ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Class_Lancer); + + /** @brief Archer 职阶。 */ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Class_Archer); + + /** @brief Rider 职阶。 */ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Class_Rider); + + /** @brief Caster 职阶。 */ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Class_Caster); + + /** @brief Assassin 职阶。 */ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Class_Assassin); + + /** @brief Berserker 职阶。 */ + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Class_Berserker); +} diff --git a/Source/PHY/Public/PHYGameplayTags.h b/Source/PHY/Public/PHYGameplayTags.h index 173fa1d..b6a144e 100644 --- a/Source/PHY/Public/PHYGameplayTags.h +++ b/Source/PHY/Public/PHYGameplayTags.h @@ -3,6 +3,7 @@ #pragma once #include "GameplayTags/PHYGameplayTags_Ability.h" +#include "GameplayTags/PHYGameplayTags_Class.h" #include "GameplayTags/PHYGameplayTags_Effect.h" #include "GameplayTags/PHYGameplayTags_Event.h" #include "GameplayTags/PHYGameplayTags_Input.h" diff --git a/Source/PHY/Public/Player/PHYPlayerState.h b/Source/PHY/Public/Player/PHYPlayerState.h index d951e3a..a6067e4 100644 --- a/Source/PHY/Public/Player/PHYPlayerState.h +++ b/Source/PHY/Public/Player/PHYPlayerState.h @@ -8,6 +8,7 @@ #include "PHYPlayerState.generated.h" class UGGA_AbilitySystemComponent; +class UPHYClassComponent; /** * @brief PHY 玩家状态。 @@ -30,8 +31,16 @@ public: UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|PlayerState") UGGA_AbilitySystemComponent* GetGGAAbilitySystemComponent() const { return AbilitySystemComponent; } + /** @brief 获取玩家持久职业组件。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Class") + UPHYClassComponent* GetClassComponent() const { return ClassComponent; } + protected: /** @brief 玩家持久 ASC。 */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|PlayerState") TObjectPtr AbilitySystemComponent; + + /** @brief 玩家持久职业组件,职业不保存在 PlayerCharacter 上。 */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Class") + TObjectPtr ClassComponent; };