添加信息系统
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
;METADATA=(Diff=true, UseCommands=true)
|
;METADATA=(Diff=true, UseCommands=true)
|
||||||
[/Script/GameplayTags.GameplayTagsSettings]
|
|
||||||
ImportTagsFromConfig=True
|
ImportTagsFromConfig=True
|
||||||
WarnOnInvalidTags=True
|
WarnOnInvalidTags=True
|
||||||
ClearInvalidTags=False
|
ClearInvalidTags=False
|
||||||
@@ -12,3 +11,5 @@ NumBitsForContainerSize=6
|
|||||||
NetIndexFirstBitSegment=16
|
NetIndexFirstBitSegment=16
|
||||||
+GameplayTagList=(Tag="Data.Regen.InnerPower",DevComment="SetByCaller: InnerPower regen per tick")
|
+GameplayTagList=(Tag="Data.Regen.InnerPower",DevComment="SetByCaller: InnerPower regen per tick")
|
||||||
|
|
||||||
|
+GameplayTagList=(Tag="Data.Init.Primary.Agility",DevComment="Init Primary Agility")
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,15 +12,15 @@
|
|||||||
#include "GameplayTags/InputTags.h"
|
#include "GameplayTags/InputTags.h"
|
||||||
#include "Components/RetargeterComponent.h"
|
#include "Components/RetargeterComponent.h"
|
||||||
#include "AbilitySystemComponent.h"
|
#include "AbilitySystemComponent.h"
|
||||||
|
#include "AbilitySystem/PHYClassDefaults.h"
|
||||||
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
|
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
|
||||||
#include "AbilitySystem/Effects/PHYGE_DerivedAttributes.h"
|
#include "AbilitySystem/Effects/PHYGE_DerivedAttributes.h"
|
||||||
#include "AbilitySystem/Effects/PHYGE_InitPrimary.h"
|
#include "AbilitySystem/Effects/PHYGE_InitPrimary.h"
|
||||||
#include "AbilitySystem/Effects/PHYGE_RegenTick.h"
|
#include "AbilitySystem/Effects/PHYGE_RegenTick.h"
|
||||||
#include "AbilitySystem/PHYClassDefaults.h"
|
|
||||||
#include "GameplayTags/InitAttributeTags.h"
|
|
||||||
#include "GameplayTags/RegenTags.h"
|
#include "GameplayTags/RegenTags.h"
|
||||||
#include "GameFramework/CharacterMovementComponent.h"
|
|
||||||
#include "UI/HUD/PHYGameHUD.h"
|
#include "UI/HUD/PHYGameHUD.h"
|
||||||
|
#include "GameplayTags/InitAttributeTags.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
APHYPlayerCharacter::APHYPlayerCharacter()
|
APHYPlayerCharacter::APHYPlayerCharacter()
|
||||||
@@ -176,7 +176,7 @@ void APHYPlayerCharacter::InitializeGAS()
|
|||||||
ASC->SetNumericAttributeBase(UPHYAttributeSet::GetHealthAttribute(), AS->GetMaxHealth());
|
ASC->SetNumericAttributeBase(UPHYAttributeSet::GetHealthAttribute(), AS->GetMaxHealth());
|
||||||
ASC->SetNumericAttributeBase(UPHYAttributeSet::GetInnerPowerAttribute(), AS->GetMaxInnerPower());
|
ASC->SetNumericAttributeBase(UPHYAttributeSet::GetInnerPowerAttribute(), AS->GetMaxInnerPower());
|
||||||
}
|
}
|
||||||
|
|
||||||
StopRegen();
|
StopRegen();
|
||||||
if (RegenInterval > 0.f)
|
if (RegenInterval > 0.f)
|
||||||
{
|
{
|
||||||
@@ -191,7 +191,7 @@ void APHYPlayerCharacter::StopRegen()
|
|||||||
if (!HasAuthority()) return;
|
if (!HasAuthority()) return;
|
||||||
GetWorldTimerManager().ClearTimer(RegenTimerHandle);
|
GetWorldTimerManager().ClearTimer(RegenTimerHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APHYPlayerCharacter::RegenTick()
|
void APHYPlayerCharacter::RegenTick()
|
||||||
{
|
{
|
||||||
if (!HasAuthority()) return;
|
if (!HasAuthority()) return;
|
||||||
|
|||||||
90
Source/PHY/Private/Gameplay/GameLevelInfo.cpp
Normal file
90
Source/PHY/Private/Gameplay/GameLevelInfo.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
#include "GameLevelInfo.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// TotalXPThreshold[Level]:到达该等级的累计经验阈值(Level=1..100)。
|
||||||
|
// - 下标与等级一致。
|
||||||
|
// - Threshold[1]=0。
|
||||||
|
// - 总经验(到达100级阈值)约 45,000,020。
|
||||||
|
static const TStaticArray<int32, FGameLevelInfo::MaxLevel + 1> ThresholdConst = {
|
||||||
|
0,0,
|
||||||
|
28500,59630,93570,130520,170660,214180,261260,312090,
|
||||||
|
366860,425760,488980,556700,629110,706400,788760,876370,969430,
|
||||||
|
1068120,1172620,1283130,1399830,1522910,1652560,1788970,1932320,
|
||||||
|
2082800,2240600,2405900,2578900,2759780,2948730,3145940,3351590,
|
||||||
|
3565870,3788970,4021080,4262390,4513080,4773340,5043360,5323320,
|
||||||
|
5613420,5913840,6224770,6546390,6878900,7222480,7577320,7943600,
|
||||||
|
8321520,8711260,9113010,9526960,9953290,10392200,10843870,11308490,
|
||||||
|
11786240,12277320,12781910,13300200,13832370,14378620,14939130,15514090,
|
||||||
|
16103690,16708110,17327540,17962170,18612190,19277790,19959150,20656460,
|
||||||
|
21369910,22099680,22845970,23608960,24388840,25185800,26000020,26831690,
|
||||||
|
27681000,28548140,29433290,30336650,31258400,32198730,33157820,34135860,
|
||||||
|
35133050,36149560,37185590,38241320,39316950,40412650,41528620,42665050,
|
||||||
|
43822120,45000020
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const TStaticArray<int32, FGameLevelInfo::MaxLevel + 1> FGameLevelInfo::TotalXPThreshold = ThresholdConst;
|
||||||
|
|
||||||
|
int32 FGameLevelInfo::GetXPForNextLevel(int32 Level)
|
||||||
|
{
|
||||||
|
Level = FMath::Clamp(Level, MinLevel, MaxLevel);
|
||||||
|
if (Level >= MaxLevel)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 差分:到达(Level+1)的阈值 - 到达(Level)的阈值
|
||||||
|
return TotalXPThreshold[Level + 1] - TotalXPThreshold[Level];
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FGameLevelInfo::GetTotalXPToReachLevel(int32 Level)
|
||||||
|
{
|
||||||
|
Level = FMath::Clamp(Level, MinLevel, MaxLevel);
|
||||||
|
return TotalXPThreshold[Level];
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FGameLevelInfo::GetLevelForTotalXP(int32 TotalXP)
|
||||||
|
{
|
||||||
|
TotalXP = FMath::Max(0, TotalXP);
|
||||||
|
|
||||||
|
// 阈值单调递增:TotalXPThreshold[L] 是到达 L 的最小总经验。
|
||||||
|
// 找到最大的 L,使得 Threshold[L] <= TotalXP。
|
||||||
|
int32 Low = MinLevel;
|
||||||
|
int32 High = MaxLevel;
|
||||||
|
while (Low < High)
|
||||||
|
{
|
||||||
|
// 上中位数,避免死循环
|
||||||
|
const int32 Mid = (Low + High + 1) / 2;
|
||||||
|
if (TotalXPThreshold[Mid] <= TotalXP)
|
||||||
|
{
|
||||||
|
Low = Mid;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
High = Mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Low;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGameLevelInfo::GetProgressForTotalXP(int32 TotalXP, int32& OutLevel, int32& OutXPIntoLevel, int32& OutXPToNext)
|
||||||
|
{
|
||||||
|
TotalXP = FMath::Max(0, TotalXP);
|
||||||
|
OutLevel = GetLevelForTotalXP(TotalXP);
|
||||||
|
|
||||||
|
const int32 CurrentLevelThreshold = GetTotalXPToReachLevel(OutLevel);
|
||||||
|
OutXPToNext = GetXPForNextLevel(OutLevel);
|
||||||
|
|
||||||
|
OutXPIntoLevel = TotalXP - CurrentLevelThreshold;
|
||||||
|
if (OutXPToNext <= 0)
|
||||||
|
{
|
||||||
|
OutXPIntoLevel = 0;
|
||||||
|
OutXPToNext = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutXPIntoLevel = FMath::Clamp(OutXPIntoLevel, 0, OutXPToNext);
|
||||||
|
}
|
||||||
42
Source/PHY/Private/Gameplay/GameLevelInfo.h
Normal file
42
Source/PHY/Private/Gameplay/GameLevelInfo.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1-100级经验表(常量内置,无需配置)。
|
||||||
|
* - 等级从 1 开始,最高 100。
|
||||||
|
* - XP 表示“累计经验值”(总经验),即从1级开始累计获得的经验。
|
||||||
|
* - LevelXP[i] 表示从 i 级升到 i+1 级需要的经验(i=1..99)。
|
||||||
|
*/
|
||||||
|
struct FGameLevelInfo
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr int32 MinLevel = 1;
|
||||||
|
static constexpr int32 MaxLevel = 100;
|
||||||
|
|
||||||
|
/** 从当前 Level 升到 Level+1 需要的经验(Level=1..99)。Level=100 返回 0。 */
|
||||||
|
static int32 GetXPForNextLevel(int32 Level);
|
||||||
|
|
||||||
|
/** 返回达到指定等级所需的“累计经验阈值”。
|
||||||
|
* - Level=1 -> 0
|
||||||
|
* - Level=2 -> 从1升2的经验
|
||||||
|
*/
|
||||||
|
static int32 GetTotalXPToReachLevel(int32 Level);
|
||||||
|
|
||||||
|
/** 通过当前累计经验反查等级(1..100)。 */
|
||||||
|
static int32 GetLevelForTotalXP(int32 TotalXP);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param OutLevel 当前等级(1..100)
|
||||||
|
* @param OutXPToNext 从当前等级升到下一级所需经验(100级为0)
|
||||||
|
*/
|
||||||
|
static void GetProgressForTotalXP(int32 TotalXP, int32& OutLevel, int32& OutXPIntoLevel, int32& OutXPToNext);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** 累计阈值表:TotalXPThreshold[Level] for Level=1..100。0 号位不用。
|
||||||
|
* TotalXPThreshold[1] 必须为 0。
|
||||||
|
*/
|
||||||
|
static const TStaticArray<int32, MaxLevel + 1> TotalXPThreshold;
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
class UIKRetargeter;
|
class UIKRetargeter;
|
||||||
class UPHYClassDefaults;
|
class UPHYClassDefaults;
|
||||||
|
class UPHYUIIconSet;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -35,8 +36,13 @@ class PHY_API UPHYGameInstance : public UGameInstance
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config|Attributes", meta=(AllowPrivateAccess=true))
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config|Attributes", meta=(AllowPrivateAccess=true))
|
||||||
TObjectPtr<UPHYClassDefaults> ClassDefaults;
|
TObjectPtr<UPHYClassDefaults> ClassDefaults;
|
||||||
|
|
||||||
|
/** 全局UI固定图标集合(经验/升级等) */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config|UI", meta=(AllowPrivateAccess=true))
|
||||||
|
TObjectPtr<UPHYUIIconSet> UIIconSet;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TOptional<FRetargetInfo> GetRetargetInfo(const FGameplayTag& RetargetInfoTag) const;
|
TOptional<FRetargetInfo> GetRetargetInfo(const FGameplayTag& RetargetInfoTag) const;
|
||||||
const UPHYClassDefaults* GetClassDefaults() const { return ClassDefaults; }
|
const UPHYClassDefaults* GetClassDefaults() const { return ClassDefaults; }
|
||||||
|
const UPHYUIIconSet* GetUIIconSet() const { return UIIconSet; }
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,21 +7,27 @@
|
|||||||
#include "Character/PHYPlayerCharacter.h"
|
#include "Character/PHYPlayerCharacter.h"
|
||||||
#include "Components/RetargeterComponent.h"
|
#include "Components/RetargeterComponent.h"
|
||||||
#include "Gameplay/PHYGameInstance.h"
|
#include "Gameplay/PHYGameInstance.h"
|
||||||
|
#include "UI/PHYUIIconSet.h"
|
||||||
|
#include "Gameplay/GameLevelInfo.h"
|
||||||
#include "Net/UnrealNetwork.h"
|
#include "Net/UnrealNetwork.h"
|
||||||
#include "Net/Core/PushModel/PushModel.h"
|
#include "Net/Core/PushModel/PushModel.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// 你可以把这些做成配置/曲线/数据表;这里先给一个默认规则:每升1级给 1 点属性点
|
||||||
|
constexpr int32 AttributePointsPerLevel = 4;
|
||||||
|
}
|
||||||
|
|
||||||
APHYPlayerState::APHYPlayerState()
|
APHYPlayerState::APHYPlayerState()
|
||||||
{
|
{
|
||||||
// 创建AbilitySystemComponent
|
// 创建AbilitySystemComponent
|
||||||
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
|
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
|
||||||
AttributeSet = CreateDefaultSubobject<UPHYAttributeSet>(TEXT("AttributeSet"));
|
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
|
||||||
|
SetNetUpdateFrequency(100.f);
|
||||||
// 设置AbilitySystemComponent的网络复制
|
|
||||||
AbilitySystemComponent->SetIsReplicated(true);
|
|
||||||
|
|
||||||
// 使用Minimal复制模式(推荐用于PlayerState)
|
// 使用Minimal复制模式(推荐用于PlayerState)
|
||||||
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
|
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const
|
UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const
|
||||||
@@ -57,6 +63,9 @@ void APHYPlayerState::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>
|
|||||||
SharedParams.bIsPushBased = true;
|
SharedParams.bIsPushBased = true;
|
||||||
|
|
||||||
DOREPLIFETIME_WITH_PARAMS_FAST(APHYPlayerState, ReTargeterTag, SharedParams);
|
DOREPLIFETIME_WITH_PARAMS_FAST(APHYPlayerState, ReTargeterTag, SharedParams);
|
||||||
|
DOREPLIFETIME_WITH_PARAMS_FAST(APHYPlayerState, XP, SharedParams);
|
||||||
|
DOREPLIFETIME_WITH_PARAMS_FAST(APHYPlayerState, Level, SharedParams);
|
||||||
|
DOREPLIFETIME_WITH_PARAMS_FAST(APHYPlayerState, AttributePoints, SharedParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APHYPlayerState::SetReTargeterTag(const FGameplayTag& NewReTargeterTag)
|
void APHYPlayerState::SetReTargeterTag(const FGameplayTag& NewReTargeterTag)
|
||||||
@@ -70,6 +79,141 @@ void APHYPlayerState::SetReTargeterTag(const FGameplayTag& NewReTargeterTag)
|
|||||||
ForceNetUpdate();
|
ForceNetUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::AddToXP(const int32 InXP)
|
||||||
|
{
|
||||||
|
if (InXP <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地提示:获得经验(只在本地玩家的 PlayerState 上显示即可)
|
||||||
|
if (IsOwnedBy(GetWorld() ? GetWorld()->GetFirstPlayerController() : nullptr))
|
||||||
|
{
|
||||||
|
PushGainedXPNotify(InXP);
|
||||||
|
}
|
||||||
|
|
||||||
|
MARK_PROPERTY_DIRTY_FROM_NAME(APHYPlayerState, XP, this);
|
||||||
|
XP += InXP;
|
||||||
|
|
||||||
|
// 只在权威端(Server/Standalone)做升级计算,客户端靠复制拿到最终 Level/XP
|
||||||
|
if (HasAuthority())
|
||||||
|
{
|
||||||
|
const int32 NewLevel = FMath::Clamp(FGameLevelInfo::GetLevelForTotalXP(XP), FGameLevelInfo::MinLevel, FGameLevelInfo::MaxLevel);
|
||||||
|
if (NewLevel > Level)
|
||||||
|
{
|
||||||
|
const int32 Delta = NewLevel - Level;
|
||||||
|
AddToLevel(Delta);
|
||||||
|
AddToAttributePoints(Delta * AttributePointsPerLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnXPChangedDelegate.Broadcast(XP);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::AddToLevel(const int32 InLevel)
|
||||||
|
{
|
||||||
|
MARK_PROPERTY_DIRTY_FROM_NAME(APHYPlayerState, Level, this);
|
||||||
|
Level += InLevel;
|
||||||
|
OnLevelChangedDelegate.Broadcast(Level,true);
|
||||||
|
|
||||||
|
// 本地提示:升级
|
||||||
|
if (IsOwnedBy(GetWorld() ? GetWorld()->GetFirstPlayerController() : nullptr))
|
||||||
|
{
|
||||||
|
PushLevelUpNotify(Level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::PushGameplayNotify(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon)
|
||||||
|
{
|
||||||
|
OnGameplayNotifyDelegate.Broadcast(Type, Message, SoftIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::PushGainedXPNotify(int32 DeltaXP)
|
||||||
|
{
|
||||||
|
if (DeltaXP <= 0) return;
|
||||||
|
const UPHYUIIconSet* IconSet = nullptr;
|
||||||
|
if (const UPHYGameInstance* GI = GetGameInstance<UPHYGameInstance>())
|
||||||
|
{
|
||||||
|
IconSet = GI->GetUIIconSet();
|
||||||
|
}
|
||||||
|
PushGameplayNotify(EPHYNotifyType::Reward,
|
||||||
|
FText::Format(NSLOCTEXT("PHY", "Notify_GainXP", "获得经验 {0}"), FText::AsNumber(DeltaXP)),
|
||||||
|
IconSet ? IconSet->XP : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::PushLevelUpNotify(int32 NewLevel)
|
||||||
|
{
|
||||||
|
const UPHYUIIconSet* IconSet = nullptr;
|
||||||
|
if (const UPHYGameInstance* GI = GetGameInstance<UPHYGameInstance>())
|
||||||
|
{
|
||||||
|
IconSet = GI->GetUIIconSet();
|
||||||
|
}
|
||||||
|
PushGameplayNotify(EPHYNotifyType::LevelUp,
|
||||||
|
FText::Format(NSLOCTEXT("PHY", "Notify_LevelUp", "升级到 {0} 级"), FText::AsNumber(NewLevel)),
|
||||||
|
IconSet ? IconSet->LevelUp : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::PushGainedItemNotify(const FText& ItemName, int32 Count)
|
||||||
|
{
|
||||||
|
PushGameplayNotify(EPHYNotifyType::Reward,
|
||||||
|
FText::Format(NSLOCTEXT("PHY", "Notify_GainItem", "获得 {0} x{1}"), ItemName, FText::AsNumber(Count)),
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::SetXP(const int32 InXP)
|
||||||
|
{
|
||||||
|
MARK_PROPERTY_DIRTY_FROM_NAME(APHYPlayerState, XP, this);
|
||||||
|
XP = FMath::Max(0, InXP);
|
||||||
|
|
||||||
|
if (HasAuthority())
|
||||||
|
{
|
||||||
|
const int32 NewLevel = FMath::Clamp(FGameLevelInfo::GetLevelForTotalXP(XP), FGameLevelInfo::MinLevel, FGameLevelInfo::MaxLevel);
|
||||||
|
if (NewLevel != Level)
|
||||||
|
{
|
||||||
|
// 这里用 SetLevel 以避免把它当“连升触发”重复发奖励(奖励统一走 AddToXP 的路径)
|
||||||
|
SetLevel(NewLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnXPChangedDelegate.Broadcast(XP);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::AddToAttributePoints(const int32 InPoints)
|
||||||
|
{
|
||||||
|
MARK_PROPERTY_DIRTY_FROM_NAME(APHYPlayerState, AttributePoints, this);
|
||||||
|
AttributePoints += InPoints;
|
||||||
|
OnAttributePointsChangedDelegate.Broadcast(AttributePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::SetLevel(const int32 InLevel)
|
||||||
|
{
|
||||||
|
MARK_PROPERTY_DIRTY_FROM_NAME(APHYPlayerState, Level, this);
|
||||||
|
Level = InLevel;
|
||||||
|
OnLevelChangedDelegate.Broadcast(Level,false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::SetAttributePoints(const int32 InPoints)
|
||||||
|
{
|
||||||
|
MARK_PROPERTY_DIRTY_FROM_NAME(APHYPlayerState, AttributePoints, this);
|
||||||
|
AttributePoints = InPoints;
|
||||||
|
OnAttributePointsChangedDelegate.Broadcast(AttributePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::OnRep_Level(int32 OldLevel)
|
||||||
|
{
|
||||||
|
OnLevelChangedDelegate.Broadcast(Level, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::OnRep_XP(int32 OldXP)
|
||||||
|
{
|
||||||
|
OnXPChangedDelegate.Broadcast(XP);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::OnRep_AttributePoints(int32 OldAttributePoints)
|
||||||
|
{
|
||||||
|
OnAttributePointsChangedDelegate.Broadcast(AttributePoints);
|
||||||
|
}
|
||||||
|
|
||||||
void APHYPlayerState::ServerSetReTargeterTag_Implementation(const FGameplayTag& NewReTargeterTag)
|
void APHYPlayerState::ServerSetReTargeterTag_Implementation(const FGameplayTag& NewReTargeterTag)
|
||||||
{
|
{
|
||||||
if (!NewReTargeterTag.IsValid() || NewReTargeterTag == ReTargeterTag) return;
|
if (!NewReTargeterTag.IsValid() || NewReTargeterTag == ReTargeterTag) return;
|
||||||
@@ -81,3 +225,30 @@ void APHYPlayerState::ServerSetReTargeterTag_Implementation(const FGameplayTag&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APHYPlayerState::ServerAddXP_Implementation(int32 DeltaXP)
|
||||||
|
{
|
||||||
|
if (DeltaXP <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddToXP(DeltaXP);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 APHYPlayerState::GetXPIntoCurrentLevel() const
|
||||||
|
{
|
||||||
|
int32 L = 0;
|
||||||
|
int32 Into = 0;
|
||||||
|
int32 ToNext = 0;
|
||||||
|
FGameLevelInfo::GetProgressForTotalXP(XP, L, Into, ToNext);
|
||||||
|
return Into;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 APHYPlayerState::GetXPRequirementForNextLevel() const
|
||||||
|
{
|
||||||
|
int32 L = 0;
|
||||||
|
int32 Into = 0;
|
||||||
|
int32 ToNext = 0;
|
||||||
|
FGameLevelInfo::GetProgressForTotalXP(XP, L, Into, ToNext);
|
||||||
|
return ToNext;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "AbilitySystemInterface.h"
|
#include "AbilitySystemInterface.h"
|
||||||
#include "GameplayTagContainer.h"
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "UObject/SoftObjectPtr.h"
|
||||||
#include "GameFramework/PlayerState.h"
|
#include "GameFramework/PlayerState.h"
|
||||||
#include "PHYPlayerState.generated.h"
|
#include "PHYPlayerState.generated.h"
|
||||||
|
|
||||||
@@ -15,6 +16,20 @@ class UPHYAttributeSet;
|
|||||||
* 玩家状态类,包含GAS支持
|
* 玩家状态类,包含GAS支持
|
||||||
* 用于管理玩家属性、技能等游戏玩法系统
|
* 用于管理玩家属性、技能等游戏玩法系统
|
||||||
*/
|
*/
|
||||||
|
DECLARE_MULTICAST_DELEGATE_OneParam(FOnPlayerStatChanged, int32 /*StatValue*/)
|
||||||
|
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnLevelChanged, int32 /*StatValue*/, bool /*bLevelUp*/)
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EPHYNotifyType : uint8
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Reward,
|
||||||
|
LevelUp,
|
||||||
|
Warning
|
||||||
|
};
|
||||||
|
|
||||||
|
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnGameplayNotify, EPHYNotifyType /*Type*/, const FText& /*Message*/, TSoftObjectPtr<UTexture2D> /*SoftIcon*/)
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class PHY_API APHYPlayerState : public APlayerState, public IAbilitySystemInterface
|
class PHY_API APHYPlayerState : public APlayerState, public IAbilitySystemInterface
|
||||||
{
|
{
|
||||||
@@ -45,7 +60,13 @@ public:
|
|||||||
const UPHYAttributeSet* GetAttributeSet() const { return AttributeSet; }
|
const UPHYAttributeSet* GetAttributeSet() const { return AttributeSet; }
|
||||||
|
|
||||||
// 原有的ReTargeterTag相关代码
|
// 原有的ReTargeterTag相关代码
|
||||||
UPROPERTY(VisibleAnywhere,Category = "Config|Character",ReplicatedUsing=OnRep_ReTargeterTagChanged)
|
FOnPlayerStatChanged OnXPChangedDelegate;
|
||||||
|
FOnLevelChanged OnLevelChangedDelegate;
|
||||||
|
FOnPlayerStatChanged OnAttributePointsChangedDelegate;
|
||||||
|
|
||||||
|
FOnGameplayNotify OnGameplayNotifyDelegate;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly,ReplicatedUsing=OnRep_ReTargeterTagChanged)
|
||||||
FGameplayTag ReTargeterTag;
|
FGameplayTag ReTargeterTag;
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
@@ -58,5 +79,59 @@ public:
|
|||||||
public:
|
public:
|
||||||
UFUNCTION(Server, Reliable,BlueprintCallable)
|
UFUNCTION(Server, Reliable,BlueprintCallable)
|
||||||
void ServerSetReTargeterTag(const FGameplayTag& NewReTargeterTag);
|
void ServerSetReTargeterTag(const FGameplayTag& NewReTargeterTag);
|
||||||
|
|
||||||
|
/** Debug/Dev: 请求服务器增加累计经验(DeltaXP>0 时生效) */
|
||||||
|
UFUNCTION(Server, Reliable, BlueprintCallable, Category="Player|Progress")
|
||||||
|
void ServerAddXP(int32 DeltaXP);
|
||||||
|
|
||||||
FGameplayTag GetReTargeterTag() const { return ReTargeterTag; }
|
FGameplayTag GetReTargeterTag() const { return ReTargeterTag; }
|
||||||
|
|
||||||
|
FORCEINLINE int32 GetPlayerLevel() const { return Level; }
|
||||||
|
FORCEINLINE int32 GetXP() const { return XP; }
|
||||||
|
FORCEINLINE int32 GetAttributePoints() const { return AttributePoints; }
|
||||||
|
|
||||||
|
/** 当前等级内经验值(0..XPToNext),用于UI经验条 */
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Player|Progress")
|
||||||
|
int32 GetXPIntoCurrentLevel() const;
|
||||||
|
|
||||||
|
/** 当前等级升下一级所需经验(满级返回0),用于UI经验条 */
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Player|Progress")
|
||||||
|
int32 GetXPRequirementForNextLevel() const;
|
||||||
|
|
||||||
|
void AddToXP(int32 InXP);
|
||||||
|
void AddToLevel(int32 InLevel);
|
||||||
|
void AddToAttributePoints(int32 InPoints);
|
||||||
|
void SetXP(int32 InXP);
|
||||||
|
void SetLevel(int32 InLevel);
|
||||||
|
void SetAttributePoints(int32 InPoints);
|
||||||
|
private:
|
||||||
|
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_Level)
|
||||||
|
int32 Level = 1;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_XP)
|
||||||
|
int32 XP = 0;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_AttributePoints)
|
||||||
|
int32 AttributePoints = 0;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_Level(int32 OldLevel);
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_XP(int32 OldXP);
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_AttributePoints(int32 OldAttributePoints);
|
||||||
|
|
||||||
|
/** 仅本地:派发一条提示消息(UI toast / 日志)。不会复制。 */
|
||||||
|
void PushGameplayNotify(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon = nullptr);
|
||||||
|
|
||||||
|
/** 仅本地:推送“获得经验”的提示(并不会改动 XP,改动 XP 请走 AddToXP/ServerAddXP)。 */
|
||||||
|
void PushGainedXPNotify(int32 DeltaXP);
|
||||||
|
|
||||||
|
/** 仅本地:推送“升级”提示 */
|
||||||
|
void PushLevelUpNotify(int32 NewLevel);
|
||||||
|
|
||||||
|
/** 仅本地:推送“获得物品”提示(ItemName x Count) */
|
||||||
|
void PushGainedItemNotify(const FText& ItemName, int32 Count);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
//
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
#include "UITags.h"
|
#include "UITags.h"
|
||||||
|
|
||||||
|
|
||||||
namespace UITags
|
namespace UITags
|
||||||
{
|
{
|
||||||
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Game, "UI.Layer.Game");
|
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Game, "UI.Layer.Game");
|
||||||
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_GameMenu, "UI.Layer.GameMenu");
|
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_GameMenu, "UI.Layer.GameMenu");
|
||||||
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Menu, "UI.Layer.Menu");
|
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Menu, "UI.Layer.Menu");
|
||||||
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Modal, "UI.Layer.Modal");
|
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Modal, "UI.Layer.Modal");
|
||||||
|
|
||||||
// 拓展点
|
// 扩展点
|
||||||
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_HUD_CharacterInfo, "UI.HUD.CharacterInfo");
|
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_HUD_CharacterInfo, "UI.HUD.CharacterInfo");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_HUD_Notify, "UI.HUD.Notify");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
//
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "NativeGameplayTags.h"
|
#include "NativeGameplayTags.h"
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
namespace UITags
|
namespace UITags
|
||||||
{
|
{
|
||||||
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Game);
|
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Game);
|
||||||
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_GameMenu);
|
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_GameMenu);
|
||||||
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Menu);
|
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Menu);
|
||||||
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Modal);
|
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Modal);
|
||||||
|
|
||||||
// 拓展点
|
// 扩展点
|
||||||
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_HUD_CharacterInfo);
|
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_HUD_CharacterInfo);
|
||||||
};
|
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_HUD_Notify);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
#include "PHYGameHUD.h"
|
#include "PHYGameHUD.h"
|
||||||
|
|
||||||
@@ -68,6 +67,7 @@ void APHYGameHUD::OnAfterPushOverlapWidget(UCommonActivatableWidget* UserWidget)
|
|||||||
{
|
{
|
||||||
OverlapWidgetInstance = OverlapWidget;
|
OverlapWidgetInstance = OverlapWidget;
|
||||||
OverlapWidgetInstance->BindAscEvents(BoundAbilitySystemComponent);
|
OverlapWidgetInstance->BindAscEvents(BoundAbilitySystemComponent);
|
||||||
|
OverlapWidgetInstance->BindPlayerStateEvents(BoundPlayerState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,101 @@
|
|||||||
//
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
#include "Menu_Overlap.h"
|
#include "Menu_Overlap.h"
|
||||||
|
|
||||||
#include "AbilitySystemComponent.h"
|
#include "AbilitySystemComponent.h"
|
||||||
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
|
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
|
||||||
|
#include "Gameplay/Player/PHYPlayerState.h"
|
||||||
#include "GameplayTags/UITags.h"
|
#include "GameplayTags/UITags.h"
|
||||||
#include "UI/GameUIExtensionPointWidget.h"
|
#include "UI/GameUIExtensionPointWidget.h"
|
||||||
#include "UI/Widget/Widget_CharacterInfo.h"
|
#include "UI/Widget/Widget_CharacterInfo.h"
|
||||||
|
#include "UI/Widget/Widget_NotifyList.h"
|
||||||
|
|
||||||
|
class UWidget_NotifyList;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static UWidget_CharacterInfo* ResolveCharacterInfoWidget(UGameUIExtensionPointWidget* ExtensionPoint, const FGUIS_GameUIExtHandle& Handle)
|
||||||
|
{
|
||||||
|
if (!ExtensionPoint)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
UUserWidget* CurrentUserInfoWidget = ExtensionPoint->FindExtensionWidgetByHandle(Handle);
|
||||||
|
return Cast<UWidget_CharacterInfo>(CurrentUserInfoWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
static UWidget_NotifyList* ResolveNotifyListWidget(UGameUIExtensionPointWidget* ExtensionPoint, const FGUIS_GameUIExtHandle& Handle)
|
||||||
|
{
|
||||||
|
if (!ExtensionPoint)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
UUserWidget* W = ExtensionPoint->FindExtensionWidgetByHandle(Handle);
|
||||||
|
return Cast<UWidget_NotifyList>(W);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::NativeDestruct()
|
||||||
|
{
|
||||||
|
UnbindAll();
|
||||||
|
Super::NativeDestruct();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::UnbindAll()
|
||||||
|
{
|
||||||
|
UnbindAsc();
|
||||||
|
UnbindPlayerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::UnbindAsc()
|
||||||
|
{
|
||||||
|
if (!bAscBound || !BoundASC)
|
||||||
|
{
|
||||||
|
bAscBound = false;
|
||||||
|
BoundASC = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const UPHYAttributeSet* Attributes = BoundASC->GetSet<UPHYAttributeSet>())
|
||||||
|
{
|
||||||
|
BoundASC->GetGameplayAttributeValueChangeDelegate(Attributes->GetHealthAttribute()).Remove(HealthChangedHandle);
|
||||||
|
BoundASC->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxHealthAttribute()).Remove(MaxHealthChangedHandle);
|
||||||
|
BoundASC->GetGameplayAttributeValueChangeDelegate(Attributes->GetInnerPowerAttribute()).Remove(InnerPowerChangedHandle);
|
||||||
|
BoundASC->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxInnerPowerAttribute()).Remove(MaxInnerPowerChangedHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
HealthChangedHandle.Reset();
|
||||||
|
MaxHealthChangedHandle.Reset();
|
||||||
|
InnerPowerChangedHandle.Reset();
|
||||||
|
MaxInnerPowerChangedHandle.Reset();
|
||||||
|
|
||||||
|
bAscBound = false;
|
||||||
|
BoundASC = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::UnbindPlayerState()
|
||||||
|
{
|
||||||
|
if (!bPlayerStateBound || !BoundPlayerState)
|
||||||
|
{
|
||||||
|
bPlayerStateBound = false;
|
||||||
|
BoundPlayerState = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BoundPlayerState)
|
||||||
|
{
|
||||||
|
BoundPlayerState->OnXPChangedDelegate.Remove(XPChangedHandle);
|
||||||
|
BoundPlayerState->OnLevelChangedDelegate.Remove(LevelChangedHandle);
|
||||||
|
BoundPlayerState->OnGameplayNotifyDelegate.Remove(GameplayNotifyHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
XPChangedHandle.Reset();
|
||||||
|
LevelChangedHandle.Reset();
|
||||||
|
GameplayNotifyHandle.Reset();
|
||||||
|
|
||||||
|
bPlayerStateBound = false;
|
||||||
|
BoundPlayerState = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void UMenu_Overlap::NativePreConstruct()
|
void UMenu_Overlap::NativePreConstruct()
|
||||||
{
|
{
|
||||||
@@ -29,7 +117,11 @@ void UMenu_Overlap::NativeOnInitialized()
|
|||||||
{
|
{
|
||||||
if (CharacterInfoWidgetClass)
|
if (CharacterInfoWidgetClass)
|
||||||
{
|
{
|
||||||
CharacterInfoExtHandle = ExtensionSubsystem->RegisterExtensionAsWidget(UITags::Tag__UI_HUD_CharacterInfo,CharacterInfoWidgetClass, 0);
|
CharacterInfoExtHandle = ExtensionSubsystem->RegisterExtensionAsWidget(UITags::Tag__UI_HUD_CharacterInfo, CharacterInfoWidgetClass, 0);
|
||||||
|
}
|
||||||
|
if (NotifyListWidgetClass)
|
||||||
|
{
|
||||||
|
NotifyExtHandle = ExtensionSubsystem->RegisterExtensionAsWidget(UITags::Tag__UI_HUD_Notify, NotifyListWidgetClass, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,42 +129,152 @@ void UMenu_Overlap::NativeOnInitialized()
|
|||||||
void UMenu_Overlap::BindAscEvents(UAbilitySystemComponent* AbilitySystemComponent)
|
void UMenu_Overlap::BindAscEvents(UAbilitySystemComponent* AbilitySystemComponent)
|
||||||
{
|
{
|
||||||
if (AbilitySystemComponent == nullptr) return;
|
if (AbilitySystemComponent == nullptr) return;
|
||||||
|
|
||||||
|
// 防重复绑定:如果换了 ASC 或已经绑定过,先解绑
|
||||||
|
if (BoundASC.Get() != AbilitySystemComponent)
|
||||||
|
{
|
||||||
|
UnbindAsc();
|
||||||
|
}
|
||||||
|
if (bAscBound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BoundASC = AbilitySystemComponent;
|
||||||
|
|
||||||
const UPHYAttributeSet* Attributes = AbilitySystemComponent->GetSet<UPHYAttributeSet>();
|
const UPHYAttributeSet* Attributes = AbilitySystemComponent->GetSet<UPHYAttributeSet>();
|
||||||
if (Attributes == nullptr) return;
|
if (Attributes == nullptr) return;
|
||||||
// 从拓展点获取widget
|
|
||||||
if (!UserInfoExtensionPoint) return;
|
UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle);
|
||||||
UUserWidget* CurrentUserInfoWidget = UserInfoExtensionPoint->FindExtensionWidgetByHandle(CharacterInfoExtHandle);
|
|
||||||
UWidget_CharacterInfo* CharacterInfoWidget = Cast<UWidget_CharacterInfo>(CurrentUserInfoWidget);
|
|
||||||
if (CharacterInfoWidget == nullptr) return;
|
if (CharacterInfoWidget == nullptr) return;
|
||||||
// 同步初始属性值
|
|
||||||
CurrentHp = Attributes->GetHealth();
|
CurrentHp = Attributes->GetHealth();
|
||||||
MaxHp = Attributes->GetMaxHealth();
|
MaxHp = Attributes->GetMaxHealth();
|
||||||
CurrentMp = Attributes->GetInnerPower();
|
CurrentMp = Attributes->GetInnerPower();
|
||||||
MaxMp = Attributes->GetMaxInnerPower();
|
MaxMp = Attributes->GetMaxInnerPower();
|
||||||
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetHealthAttribute()).AddLambda(
|
|
||||||
[this, CharacterInfoWidget](const FOnAttributeChangeData& Data)
|
HealthChangedHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetHealthAttribute()).AddUObject(
|
||||||
{
|
this, &UMenu_Overlap::OnHealthChanged);
|
||||||
CurrentHp = Data.NewValue;
|
MaxHealthChangedHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxHealthAttribute()).AddUObject(
|
||||||
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
this, &UMenu_Overlap::OnMaxHealthChanged);
|
||||||
});
|
InnerPowerChangedHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetInnerPowerAttribute()).AddUObject(
|
||||||
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxHealthAttribute()).AddLambda(
|
this, &UMenu_Overlap::OnInnerPowerChanged);
|
||||||
[this, CharacterInfoWidget](const FOnAttributeChangeData& Data)
|
MaxInnerPowerChangedHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxInnerPowerAttribute()).AddUObject(
|
||||||
{
|
this, &UMenu_Overlap::OnMaxInnerPowerChanged);
|
||||||
MaxHp = Data.NewValue;
|
|
||||||
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
bAscBound = true;
|
||||||
});
|
|
||||||
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetInnerPowerAttribute()).AddLambda(
|
// 初次同步
|
||||||
[this, CharacterInfoWidget](const FOnAttributeChangeData& Data)
|
|
||||||
{
|
|
||||||
CurrentMp = Data.NewValue;
|
|
||||||
CharacterInfoWidget->SetMp(CurrentMp, MaxMp);
|
|
||||||
});
|
|
||||||
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxInnerPowerAttribute()).AddLambda(
|
|
||||||
[this, CharacterInfoWidget](const FOnAttributeChangeData& Data)
|
|
||||||
{
|
|
||||||
MaxMp = Data.NewValue;
|
|
||||||
CharacterInfoWidget->SetMp(CurrentMp, MaxMp);
|
|
||||||
});
|
|
||||||
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
||||||
CharacterInfoWidget->SetMp(CurrentMp, MaxMp);
|
CharacterInfoWidget->SetMp(CurrentMp, MaxMp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::BindPlayerStateEvents(APHYPlayerState* PlayerState)
|
||||||
|
{
|
||||||
|
if (!PlayerState) return;
|
||||||
|
|
||||||
|
if (BoundPlayerState.Get() != PlayerState)
|
||||||
|
{
|
||||||
|
UnbindPlayerState();
|
||||||
|
}
|
||||||
|
if (bPlayerStateBound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BoundPlayerState = PlayerState;
|
||||||
|
|
||||||
|
UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle);
|
||||||
|
if (CharacterInfoWidget == nullptr) return;
|
||||||
|
|
||||||
|
CachedLevel = PlayerState->GetPlayerLevel();
|
||||||
|
CachedTotalXP = PlayerState->GetXP();
|
||||||
|
CurrentXp = static_cast<float>(PlayerState->GetXPIntoCurrentLevel());
|
||||||
|
MaxXp = static_cast<float>(PlayerState->GetXPRequirementForNextLevel());
|
||||||
|
|
||||||
|
CharacterInfoWidget->SetLevel(CachedLevel);
|
||||||
|
CharacterInfoWidget->SetXp(CurrentXp, MaxXp);
|
||||||
|
|
||||||
|
XPChangedHandle = PlayerState->OnXPChangedDelegate.AddUObject(this, &UMenu_Overlap::OnTotalXPChanged);
|
||||||
|
LevelChangedHandle = PlayerState->OnLevelChangedDelegate.AddUObject(this, &UMenu_Overlap::OnLevelChanged);
|
||||||
|
GameplayNotifyHandle = PlayerState->OnGameplayNotifyDelegate.AddUObject(this, &UMenu_Overlap::OnGameplayNotify);
|
||||||
|
|
||||||
|
bPlayerStateBound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== handlers (use handles, no lambdas) =====
|
||||||
|
|
||||||
|
void UMenu_Overlap::OnHealthChanged(const FOnAttributeChangeData& Data)
|
||||||
|
{
|
||||||
|
CurrentHp = Data.NewValue;
|
||||||
|
if (UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle))
|
||||||
|
{
|
||||||
|
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::OnMaxHealthChanged(const FOnAttributeChangeData& Data)
|
||||||
|
{
|
||||||
|
MaxHp = Data.NewValue;
|
||||||
|
if (UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle))
|
||||||
|
{
|
||||||
|
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::OnInnerPowerChanged(const FOnAttributeChangeData& Data)
|
||||||
|
{
|
||||||
|
CurrentMp = Data.NewValue;
|
||||||
|
if (UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle))
|
||||||
|
{
|
||||||
|
CharacterInfoWidget->SetMp(CurrentMp, MaxMp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::OnMaxInnerPowerChanged(const FOnAttributeChangeData& Data)
|
||||||
|
{
|
||||||
|
MaxMp = Data.NewValue;
|
||||||
|
if (UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle))
|
||||||
|
{
|
||||||
|
CharacterInfoWidget->SetMp(CurrentMp, MaxMp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::OnTotalXPChanged(int32 NewTotalXP)
|
||||||
|
{
|
||||||
|
CachedTotalXP = NewTotalXP;
|
||||||
|
if (!BoundPlayerState) return;
|
||||||
|
|
||||||
|
CurrentXp = static_cast<float>(BoundPlayerState->GetXPIntoCurrentLevel());
|
||||||
|
MaxXp = static_cast<float>(BoundPlayerState->GetXPRequirementForNextLevel());
|
||||||
|
|
||||||
|
if (UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle))
|
||||||
|
{
|
||||||
|
CharacterInfoWidget->SetXp(CurrentXp, MaxXp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::OnLevelChanged(int32 NewLevel, bool bLevelUp)
|
||||||
|
{
|
||||||
|
CachedLevel = NewLevel;
|
||||||
|
if (UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle))
|
||||||
|
{
|
||||||
|
CharacterInfoWidget->SetLevel(NewLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMenu_Overlap::OnGameplayNotify(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon)
|
||||||
|
{
|
||||||
|
if (UWidget_NotifyList* NotifyList = ResolveNotifyListWidget(NotifyExtensionPoint, NotifyExtHandle))
|
||||||
|
{
|
||||||
|
NotifyList->AddNotify(Type, Message, SoftIcon);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("[GameplayNotify][%d] %s"), static_cast<int32>(Type), *Message.ToString());
|
||||||
|
if (GEngine)
|
||||||
|
{
|
||||||
|
const FColor Color = (Type == EPHYNotifyType::LevelUp) ? FColor::Yellow : FColor::Green;
|
||||||
|
GEngine->AddOnScreenDebugMessage(-1, 2.0f, Color, Message.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
|
||||||
|
#include "Gameplay/Player/PHYPlayerState.h"
|
||||||
#include "UI/GUIS_ActivatableWidget.h"
|
#include "UI/GUIS_ActivatableWidget.h"
|
||||||
#include "UIExtension/GUIS_GameUIExtensionSubsystem.h"
|
#include "UIExtension/GUIS_GameUIExtensionSubsystem.h"
|
||||||
#include "Menu_Overlap.generated.h"
|
#include "Menu_Overlap.generated.h"
|
||||||
|
|
||||||
class UAbilitySystemComponent;
|
class UAbilitySystemComponent;
|
||||||
class UGameUIExtensionPointWidget;
|
class UGameUIExtensionPointWidget;
|
||||||
|
class APHYPlayerState;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -19,23 +22,70 @@ class PHY_API UMenu_Overlap : public UGUIS_ActivatableWidget
|
|||||||
|
|
||||||
UPROPERTY(meta=(BindWidget))
|
UPROPERTY(meta=(BindWidget))
|
||||||
UGameUIExtensionPointWidget* UserInfoExtensionPoint;
|
UGameUIExtensionPointWidget* UserInfoExtensionPoint;
|
||||||
|
|
||||||
|
/** 古风 HUD 提示/飘字区域扩展点(例如右侧竖排提示) */
|
||||||
|
UPROPERTY(meta=(BindWidgetOptional))
|
||||||
|
UGameUIExtensionPointWidget* NotifyExtensionPoint;
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, Category="Sub Widget Class")
|
UPROPERTY(EditDefaultsOnly, Category="Sub Widget Class")
|
||||||
TSubclassOf<UUserWidget> CharacterInfoWidgetClass;
|
TSubclassOf<UUserWidget> CharacterInfoWidgetClass;
|
||||||
|
|
||||||
|
/** 提示列表 Widget 类(用于挂载到 UITags::Tag__UI_HUD_Notify) */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="Sub Widget Class")
|
||||||
|
TSubclassOf<UUserWidget> NotifyListWidgetClass;
|
||||||
|
|
||||||
FGUIS_GameUIExtHandle CharacterInfoExtHandle;
|
FGUIS_GameUIExtHandle CharacterInfoExtHandle;
|
||||||
|
FGUIS_GameUIExtHandle NotifyExtHandle;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void NativePreConstruct() override;
|
virtual void NativePreConstruct() override;
|
||||||
virtual void NativeOnActivated() override;
|
virtual void NativeOnActivated() override;
|
||||||
virtual void NativeOnInitialized() override;
|
virtual void NativeOnInitialized() override;
|
||||||
|
virtual void NativeDestruct() override;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void BindAscEvents(UAbilitySystemComponent* AbilitySystemComponent);
|
void BindAscEvents(UAbilitySystemComponent* AbilitySystemComponent);
|
||||||
|
void BindPlayerStateEvents(APHYPlayerState* PlayerState);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void UnbindAll();
|
||||||
|
void UnbindAsc();
|
||||||
|
void UnbindPlayerState();
|
||||||
|
|
||||||
|
// ASC handlers
|
||||||
|
void OnHealthChanged(const FOnAttributeChangeData& Data);
|
||||||
|
void OnMaxHealthChanged(const FOnAttributeChangeData& Data);
|
||||||
|
void OnInnerPowerChanged(const FOnAttributeChangeData& Data);
|
||||||
|
void OnMaxInnerPowerChanged(const FOnAttributeChangeData& Data);
|
||||||
|
|
||||||
|
// PlayerState handlers
|
||||||
|
void OnTotalXPChanged(int32 NewTotalXP);
|
||||||
|
void OnLevelChanged(int32 NewLevel, bool bLevelUp);
|
||||||
|
void OnGameplayNotify(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon);
|
||||||
|
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
TObjectPtr<UAbilitySystemComponent> BoundASC;
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
TObjectPtr<APHYPlayerState> BoundPlayerState;
|
||||||
|
|
||||||
|
bool bAscBound = false;
|
||||||
|
bool bPlayerStateBound = false;
|
||||||
|
|
||||||
|
FDelegateHandle HealthChangedHandle;
|
||||||
|
FDelegateHandle MaxHealthChangedHandle;
|
||||||
|
FDelegateHandle InnerPowerChangedHandle;
|
||||||
|
FDelegateHandle MaxInnerPowerChangedHandle;
|
||||||
|
|
||||||
|
FDelegateHandle XPChangedHandle;
|
||||||
|
FDelegateHandle LevelChangedHandle;
|
||||||
|
FDelegateHandle GameplayNotifyHandle;
|
||||||
|
|
||||||
float CurrentHp = 0.f;
|
float CurrentHp = 0.f;
|
||||||
float MaxHp = 0.f;
|
float MaxHp = 0.f;
|
||||||
float CurrentMp = 0.f;
|
float CurrentMp = 0.f;
|
||||||
float MaxMp = 0.f;
|
float MaxMp = 0.f;
|
||||||
float CurrentXp = 0.f;
|
float CurrentXp = 0.f;
|
||||||
float MaxXp = 0.f;
|
float MaxXp = 0.f;
|
||||||
};
|
int32 CachedLevel = 1;
|
||||||
|
int32 CachedTotalXP = 0;
|
||||||
|
};
|
||||||
4
Source/PHY/Private/UI/PHYUIIconSet.cpp
Normal file
4
Source/PHY/Private/UI/PHYUIIconSet.cpp
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "UI/PHYUIIconSet.h"
|
||||||
|
|
||||||
29
Source/PHY/Private/UI/PHYUIIconSet.h
Normal file
29
Source/PHY/Private/UI/PHYUIIconSet.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataAsset.h"
|
||||||
|
#include "PHYUIIconSet.generated.h"
|
||||||
|
|
||||||
|
class UTexture2D;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI 图标集合(全局固定图标)。
|
||||||
|
* 例如:经验、升级、金币、提示等。
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class PHY_API UPHYUIIconSet : public UDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** 获得经验提示图标 */
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="PHY|UI")
|
||||||
|
TSoftObjectPtr<UTexture2D> XP;
|
||||||
|
|
||||||
|
/** 升级提示图标 */
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="PHY|UI")
|
||||||
|
TSoftObjectPtr<UTexture2D> LevelUp;
|
||||||
|
};
|
||||||
|
|
||||||
126
Source/PHY/Private/UI/Widget/Widget_NotifyItem.cpp
Normal file
126
Source/PHY/Private/UI/Widget/Widget_NotifyItem.cpp
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "UI/Widget/Widget_NotifyItem.h"
|
||||||
|
|
||||||
|
#include "Components/Image.h"
|
||||||
|
#include "Components/TextBlock.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static float EaseOut(float T)
|
||||||
|
{
|
||||||
|
// 轻量 ease-out
|
||||||
|
return 1.f - FMath::Square(1.f - T);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyItem::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||||
|
{
|
||||||
|
Super::NativeTick(MyGeometry, InDeltaTime);
|
||||||
|
|
||||||
|
if (!bAnimating)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimTime += InDeltaTime;
|
||||||
|
|
||||||
|
if (!bFadingOut)
|
||||||
|
{
|
||||||
|
const float D = FMath::Max(0.001f, AppearDuration);
|
||||||
|
const float Alpha = FMath::Clamp(AnimTime / D, 0.f, 1.f);
|
||||||
|
const float E = EaseOut(Alpha);
|
||||||
|
|
||||||
|
// 从下往上浮:Y 从 FloatDistance -> 0
|
||||||
|
SetRenderTranslation(FVector2D(0.f, FMath::Lerp(FloatDistance, 0.f, E)));
|
||||||
|
SetRenderOpacity(E);
|
||||||
|
SetRenderScale(FVector2D(FMath::Lerp(AppearStartScale, 1.f, E), FMath::Lerp(AppearStartScale, 1.f, E)));
|
||||||
|
|
||||||
|
if (Alpha >= 1.f)
|
||||||
|
{
|
||||||
|
bAnimating = false;
|
||||||
|
AnimTime = 0.f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const float D = FMath::Max(0.001f, FadeOutDuration);
|
||||||
|
const float Alpha = FMath::Clamp(AnimTime / D, 0.f, 1.f);
|
||||||
|
const float E = EaseOut(Alpha);
|
||||||
|
|
||||||
|
SetRenderOpacity(1.f - E);
|
||||||
|
// 退出时再轻微上浮一点
|
||||||
|
SetRenderTranslation(FVector2D(0.f, FMath::Lerp(0.f, -FadeOutExtraFloat, E)));
|
||||||
|
// 缩放恢复到 1
|
||||||
|
SetRenderScale(FVector2D(1.f, 1.f));
|
||||||
|
|
||||||
|
if (Alpha >= 1.f)
|
||||||
|
{
|
||||||
|
bAnimating = false;
|
||||||
|
AnimTime = 0.f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyItem::PlayAppearAnim()
|
||||||
|
{
|
||||||
|
bAnimating = true;
|
||||||
|
bFadingOut = false;
|
||||||
|
AnimTime = 0.f;
|
||||||
|
|
||||||
|
// 初值
|
||||||
|
SetRenderTranslation(FVector2D(0.f, FloatDistance));
|
||||||
|
SetRenderOpacity(0.f);
|
||||||
|
SetRenderScale(FVector2D(AppearStartScale, AppearStartScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyItem::PlayExitAnim()
|
||||||
|
{
|
||||||
|
bAnimating = true;
|
||||||
|
bFadingOut = true;
|
||||||
|
AnimTime = 0.f;
|
||||||
|
|
||||||
|
// 退出从当前位置开始即可
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyItem::SetMessage(const FText& InText)
|
||||||
|
{
|
||||||
|
if (MessageText)
|
||||||
|
{
|
||||||
|
MessageText->SetText(InText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyItem::SetColor(const FLinearColor& InColor)
|
||||||
|
{
|
||||||
|
if (MessageText)
|
||||||
|
{
|
||||||
|
MessageText->SetColorAndOpacity(FSlateColor(InColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyItem::SetIcon(UTexture2D* InTexture)
|
||||||
|
{
|
||||||
|
if (!IconImage)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InTexture)
|
||||||
|
{
|
||||||
|
IconImage->SetBrushFromTexture(InTexture, false);
|
||||||
|
IconImage->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IconImage->SetVisibility(ESlateVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyItem::SetupNotify(const FText& InText, const FLinearColor& InColor, UTexture2D* InIcon)
|
||||||
|
{
|
||||||
|
SetMessage(InText);
|
||||||
|
SetColor(InColor);
|
||||||
|
SetIcon(InIcon);
|
||||||
|
PlayAppearAnim();
|
||||||
|
}
|
||||||
74
Source/PHY/Private/UI/Widget/Widget_NotifyItem.h
Normal file
74
Source/PHY/Private/UI/Widget/Widget_NotifyItem.h
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "Widget_NotifyItem.generated.h"
|
||||||
|
|
||||||
|
class UTextBlock;
|
||||||
|
class UImage;
|
||||||
|
class UTexture2D;
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class PHY_API UWidget_NotifyItem : public UUserWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(meta=(BindWidgetOptional))
|
||||||
|
TObjectPtr<UTextBlock> MessageText;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(BindWidgetOptional))
|
||||||
|
TObjectPtr<UImage> IconImage;
|
||||||
|
|
||||||
|
/** 上浮距离(像素) */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI|Anim")
|
||||||
|
float FloatDistance = 18.0f;
|
||||||
|
|
||||||
|
/** 出现动画时长(秒) */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI|Anim")
|
||||||
|
float AppearDuration = 0.12f;
|
||||||
|
|
||||||
|
/** 淡出动画时长(秒) */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI|Anim")
|
||||||
|
float FadeOutDuration = 0.20f;
|
||||||
|
|
||||||
|
/** 出现时的起始缩放(1.0 为不缩放),建议 0.96~1.0 */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI|Anim")
|
||||||
|
float AppearStartScale = 0.98f;
|
||||||
|
|
||||||
|
/** 淡出阶段额外上浮距离(像素) */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI|Anim")
|
||||||
|
float FadeOutExtraFloat = 6.0f;
|
||||||
|
|
||||||
|
bool bAnimating = false;
|
||||||
|
bool bFadingOut = false;
|
||||||
|
float AnimTime = 0.f;
|
||||||
|
|
||||||
|
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void SetMessage(const FText& InText);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void SetColor(const FLinearColor& InColor);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void SetIcon(UTexture2D* InTexture);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void SetupNotify(const FText& InText, const FLinearColor& InColor, UTexture2D* InIcon);
|
||||||
|
|
||||||
|
/** 播放出现动画(上浮+从透明到不透明) */
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void PlayAppearAnim();
|
||||||
|
|
||||||
|
/** 播放淡出动画(淡出后自动移除由容器负责) */
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void PlayExitAnim();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure)
|
||||||
|
float GetFadeOutDuration() const { return FadeOutDuration; }
|
||||||
|
};
|
||||||
236
Source/PHY/Private/UI/Widget/Widget_NotifyList.cpp
Normal file
236
Source/PHY/Private/UI/Widget/Widget_NotifyList.cpp
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "UI/Widget/Widget_NotifyList.h"
|
||||||
|
|
||||||
|
#include "Engine/Texture2D.h"
|
||||||
|
#include "Components/VerticalBox.h"
|
||||||
|
#include "Components/Spacer.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "TimerManager.h"
|
||||||
|
#include "UI/Widget/Widget_NotifyItem.h"
|
||||||
|
#include "Engine/AssetManager.h"
|
||||||
|
#include "Engine/StreamableManager.h"
|
||||||
|
#include "Gameplay/Player/PHYPlayerState.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static FStreamableManager& GetStreamable()
|
||||||
|
{
|
||||||
|
return UAssetManager::GetStreamableManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoftIcon notifications: no GameplayTag mapping
|
||||||
|
|
||||||
|
FLinearColor UWidget_NotifyList::ResolveColor(EPHYNotifyType Type) const
|
||||||
|
{
|
||||||
|
switch (Type)
|
||||||
|
{
|
||||||
|
case EPHYNotifyType::Info:
|
||||||
|
return Color_Info;
|
||||||
|
case EPHYNotifyType::Reward:
|
||||||
|
return Color_Reward;
|
||||||
|
case EPHYNotifyType::LevelUp:
|
||||||
|
return Color_LevelUp;
|
||||||
|
case EPHYNotifyType::Warning:
|
||||||
|
return Color_Warning;
|
||||||
|
default:
|
||||||
|
return Color_Info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyList::AddNotify(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon)
|
||||||
|
{
|
||||||
|
if (!Container)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("NotifyList: Container is null. Check WBP_NotifyList has a VerticalBox named 'Container'."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!NotifyItemClass)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UWidget_NotifyItem* Item = CreateWidget<UWidget_NotifyItem>(GetWorld(), NotifyItemClass);
|
||||||
|
if (!Item)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步图标:先使用缓存/空图标,加载完成后再补上。
|
||||||
|
UTexture2D* Icon = nullptr;
|
||||||
|
if (!SoftIcon.IsNull())
|
||||||
|
{
|
||||||
|
const FSoftObjectPath Path = SoftIcon.ToSoftObjectPath();
|
||||||
|
if (TObjectPtr<UTexture2D> Cached = LoadedIconCache.FindRef(Path))
|
||||||
|
{
|
||||||
|
Icon = Cached;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 先不阻塞,等加载完成再 SetIcon
|
||||||
|
TWeakObjectPtr<UWidget_NotifyItem> WeakItem(Item);
|
||||||
|
GetStreamable().RequestAsyncLoad(Path, FStreamableDelegate::CreateWeakLambda(this, [this, WeakItem, Path]()
|
||||||
|
{
|
||||||
|
if (!IsValid(this))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* Obj = Path.ResolveObject();
|
||||||
|
UTexture2D* Loaded = Cast<UTexture2D>(Obj);
|
||||||
|
if (Loaded)
|
||||||
|
{
|
||||||
|
LoadedIconCache.Add(Path, Loaded);
|
||||||
|
if (WeakItem.IsValid())
|
||||||
|
{
|
||||||
|
WeakItem->SetIcon(Loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item->SetupNotify(Message, ResolveColor(Type), Icon);
|
||||||
|
|
||||||
|
// 约定:Container 内部严格按 [Spacer, Item] 成对存放。
|
||||||
|
USpacer* Spacer = NewObject<USpacer>(this);
|
||||||
|
Spacer->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
|
||||||
|
Spacer->SetSize(FVector2D(1.f, 0.f)); // 新消息永远是最外侧,因此前导 Spacer=0
|
||||||
|
|
||||||
|
const int32 OldCount = Container->GetChildrenCount();
|
||||||
|
|
||||||
|
if (bStackFromBottom)
|
||||||
|
{
|
||||||
|
// 追加到末尾
|
||||||
|
Container->AddChildToVerticalBox(Spacer);
|
||||||
|
Container->AddChildToVerticalBox(Item);
|
||||||
|
|
||||||
|
// 原来的“最外侧”(旧的最后一对)的 Spacer 需要从 0 变为 ItemSpacing
|
||||||
|
if (OldCount >= 2)
|
||||||
|
{
|
||||||
|
const int32 PrevOuterSpacerIndex = OldCount - 2;
|
||||||
|
if (USpacer* PrevOuterSpacer = Cast<USpacer>(Container->GetChildAt(PrevOuterSpacerIndex)))
|
||||||
|
{
|
||||||
|
PrevOuterSpacer->SetSize(FVector2D(1.f, ItemSpacing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 插到开头
|
||||||
|
Container->InsertChildAt(0, Item);
|
||||||
|
Container->InsertChildAt(0, Spacer);
|
||||||
|
|
||||||
|
// 原来的“最外侧”(旧的第一对)的 Spacer 需要从 0 变为 ItemSpacing(它现在变成第二对了)
|
||||||
|
if (OldCount >= 2)
|
||||||
|
{
|
||||||
|
const int32 PrevOuterSpacerIndex = 2; // 插入了两项,新外侧在 0,原外侧 spacer 被挤到 index=2
|
||||||
|
if (USpacer* PrevOuterSpacer = Cast<USpacer>(Container->GetChildAt(PrevOuterSpacerIndex)))
|
||||||
|
{
|
||||||
|
PrevOuterSpacer->SetSize(FVector2D(1.f, ItemSpacing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrimItems();
|
||||||
|
|
||||||
|
FTimerHandle Tmp;
|
||||||
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
World->GetTimerManager().SetTimer(Tmp, FTimerDelegate::CreateWeakLambda(this, [this, Item, Spacer]()
|
||||||
|
{
|
||||||
|
if (!Container || !Item || !Spacer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Item->PlayExitAnim();
|
||||||
|
const float RemoveDelay = FMath::Max(0.01f, Item->GetFadeOutDuration());
|
||||||
|
|
||||||
|
FTimerHandle RemoveHandle;
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
W->GetTimerManager().SetTimer(RemoveHandle, FTimerDelegate::CreateWeakLambda(this, [this, Item, Spacer]()
|
||||||
|
{
|
||||||
|
if (!Container) return;
|
||||||
|
|
||||||
|
Container->RemoveChild(Item);
|
||||||
|
Container->RemoveChild(Spacer);
|
||||||
|
|
||||||
|
TrimItems();
|
||||||
|
}), RemoveDelay, false);
|
||||||
|
}
|
||||||
|
}), ItemLifeSeconds, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWidget_NotifyList::TrimItems()
|
||||||
|
{
|
||||||
|
if (!Container) return;
|
||||||
|
|
||||||
|
// 容错:如果不成对,丢掉最后一个
|
||||||
|
if (Container->GetChildrenCount() % 2 != 0)
|
||||||
|
{
|
||||||
|
Container->RemoveChildAt(Container->GetChildrenCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除多余的最旧 pair
|
||||||
|
while (Container->GetChildrenCount() / 2 > MaxItems)
|
||||||
|
{
|
||||||
|
if (bStackFromBottom)
|
||||||
|
{
|
||||||
|
Container->RemoveChildAt(0);
|
||||||
|
Container->RemoveChildAt(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const int32 Last = Container->GetChildrenCount() - 1;
|
||||||
|
Container->RemoveChildAt(Last);
|
||||||
|
Container->RemoveChildAt(Last - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新校正外侧 / 次外侧的 Spacer:
|
||||||
|
// 外侧 Spacer = 0;次外侧 Spacer = ItemSpacing。
|
||||||
|
const int32 Count = Container->GetChildrenCount();
|
||||||
|
if (Count < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bStackFromBottom)
|
||||||
|
{
|
||||||
|
// 外侧是最后一对的 spacer
|
||||||
|
const int32 OuterSpacerIndex = Count - 2;
|
||||||
|
if (USpacer* OuterSpacer = Cast<USpacer>(Container->GetChildAt(OuterSpacerIndex)))
|
||||||
|
{
|
||||||
|
OuterSpacer->SetSize(FVector2D(1.f, 0.f));
|
||||||
|
}
|
||||||
|
// 次外侧(如果存在)
|
||||||
|
if (Count >= 4)
|
||||||
|
{
|
||||||
|
const int32 SecondOuterSpacerIndex = Count - 4;
|
||||||
|
if (USpacer* SecondOuterSpacer = Cast<USpacer>(Container->GetChildAt(SecondOuterSpacerIndex)))
|
||||||
|
{
|
||||||
|
SecondOuterSpacer->SetSize(FVector2D(1.f, ItemSpacing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 外侧是第一对的 spacer
|
||||||
|
if (USpacer* OuterSpacer = Cast<USpacer>(Container->GetChildAt(0)))
|
||||||
|
{
|
||||||
|
OuterSpacer->SetSize(FVector2D(1.f, 0.f));
|
||||||
|
}
|
||||||
|
// 次外侧(如果存在)
|
||||||
|
if (Count >= 4)
|
||||||
|
{
|
||||||
|
if (USpacer* SecondOuterSpacer = Cast<USpacer>(Container->GetChildAt(2)))
|
||||||
|
{
|
||||||
|
SecondOuterSpacer->SetSize(FVector2D(1.f, ItemSpacing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Source/PHY/Private/UI/Widget/Widget_NotifyList.h
Normal file
64
Source/PHY/Private/UI/Widget/Widget_NotifyList.h
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright 2025 PHY. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "UObject/SoftObjectPtr.h"
|
||||||
|
#include "Widget_NotifyList.generated.h"
|
||||||
|
|
||||||
|
class UWidget_NotifyItem;
|
||||||
|
class UVerticalBox;
|
||||||
|
enum class EPHYNotifyType : uint8;
|
||||||
|
|
||||||
|
class UTexture2D;
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class PHY_API UWidget_NotifyList : public UUserWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(meta=(BindWidgetOptional))
|
||||||
|
TObjectPtr<UVerticalBox> Container;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
TSubclassOf<UWidget_NotifyItem> NotifyItemClass;
|
||||||
|
|
||||||
|
// 古风HUD通常右侧竖排提示,保留 5 条比较舒服
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
int32 MaxItems = 5;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
float ItemLifeSeconds = 2.0f;
|
||||||
|
|
||||||
|
/** 从下往上冒:true=新消息插到末尾(底部),列表向上堆叠;false=插到顶部 */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
bool bStackFromBottom = true;
|
||||||
|
|
||||||
|
/** 不同类型的颜色 */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
FLinearColor Color_Info = FLinearColor(0.85f, 0.85f, 0.85f);
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
FLinearColor Color_Reward = FLinearColor(0.25f, 0.95f, 0.25f);
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
FLinearColor Color_LevelUp = FLinearColor(1.0f, 0.85f, 0.2f);
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
FLinearColor Color_Warning = FLinearColor(1.0f, 0.25f, 0.25f);
|
||||||
|
|
||||||
|
/** 两个提示条目之间的间隙(像素)。建议 4~12。 */
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category="PHY|UI")
|
||||||
|
float ItemSpacing = 6.0f;
|
||||||
|
|
||||||
|
public:
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void AddNotify(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon);
|
||||||
|
|
||||||
|
private:
|
||||||
|
FLinearColor ResolveColor(EPHYNotifyType Type) const;
|
||||||
|
void TrimItems();
|
||||||
|
|
||||||
|
/** 已加载图标缓存,避免反复异步加载 */
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
TMap<FSoftObjectPath, TObjectPtr<UTexture2D>> LoadedIconCache;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user