添加信息系统
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
;METADATA=(Diff=true, UseCommands=true)
|
||||
[/Script/GameplayTags.GameplayTagsSettings]
|
||||
ImportTagsFromConfig=True
|
||||
WarnOnInvalidTags=True
|
||||
ClearInvalidTags=False
|
||||
@@ -12,3 +11,5 @@ NumBitsForContainerSize=6
|
||||
NetIndexFirstBitSegment=16
|
||||
+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 "Components/RetargeterComponent.h"
|
||||
#include "AbilitySystemComponent.h"
|
||||
#include "AbilitySystem/PHYClassDefaults.h"
|
||||
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
|
||||
#include "AbilitySystem/Effects/PHYGE_DerivedAttributes.h"
|
||||
#include "AbilitySystem/Effects/PHYGE_InitPrimary.h"
|
||||
#include "AbilitySystem/Effects/PHYGE_RegenTick.h"
|
||||
#include "AbilitySystem/PHYClassDefaults.h"
|
||||
#include "GameplayTags/InitAttributeTags.h"
|
||||
#include "GameplayTags/RegenTags.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "UI/HUD/PHYGameHUD.h"
|
||||
#include "GameplayTags/InitAttributeTags.h"
|
||||
|
||||
|
||||
|
||||
APHYPlayerCharacter::APHYPlayerCharacter()
|
||||
|
||||
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
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
class UIKRetargeter;
|
||||
class UPHYClassDefaults;
|
||||
class UPHYUIIconSet;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -35,8 +36,13 @@ class PHY_API UPHYGameInstance : public UGameInstance
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config|Attributes", meta=(AllowPrivateAccess=true))
|
||||
TObjectPtr<UPHYClassDefaults> ClassDefaults;
|
||||
|
||||
/** 全局UI固定图标集合(经验/升级等) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config|UI", meta=(AllowPrivateAccess=true))
|
||||
TObjectPtr<UPHYUIIconSet> UIIconSet;
|
||||
|
||||
public:
|
||||
TOptional<FRetargetInfo> GetRetargetInfo(const FGameplayTag& RetargetInfoTag) const;
|
||||
const UPHYClassDefaults* GetClassDefaults() const { return ClassDefaults; }
|
||||
const UPHYUIIconSet* GetUIIconSet() const { return UIIconSet; }
|
||||
|
||||
};
|
||||
|
||||
@@ -7,17 +7,23 @@
|
||||
#include "Character/PHYPlayerCharacter.h"
|
||||
#include "Components/RetargeterComponent.h"
|
||||
#include "Gameplay/PHYGameInstance.h"
|
||||
#include "UI/PHYUIIconSet.h"
|
||||
#include "Gameplay/GameLevelInfo.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "Net/Core/PushModel/PushModel.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// 你可以把这些做成配置/曲线/数据表;这里先给一个默认规则:每升1级给 1 点属性点
|
||||
constexpr int32 AttributePointsPerLevel = 4;
|
||||
}
|
||||
|
||||
APHYPlayerState::APHYPlayerState()
|
||||
{
|
||||
// 创建AbilitySystemComponent
|
||||
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
|
||||
AttributeSet = CreateDefaultSubobject<UPHYAttributeSet>(TEXT("AttributeSet"));
|
||||
|
||||
// 设置AbilitySystemComponent的网络复制
|
||||
AbilitySystemComponent->SetIsReplicated(true);
|
||||
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
|
||||
SetNetUpdateFrequency(100.f);
|
||||
|
||||
// 使用Minimal复制模式(推荐用于PlayerState)
|
||||
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
|
||||
@@ -57,6 +63,9 @@ void APHYPlayerState::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>
|
||||
SharedParams.bIsPushBased = true;
|
||||
|
||||
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)
|
||||
@@ -70,6 +79,141 @@ void APHYPlayerState::SetReTargeterTag(const FGameplayTag& NewReTargeterTag)
|
||||
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)
|
||||
{
|
||||
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 "AbilitySystemInterface.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "UObject/SoftObjectPtr.h"
|
||||
#include "GameFramework/PlayerState.h"
|
||||
#include "PHYPlayerState.generated.h"
|
||||
|
||||
@@ -15,6 +16,20 @@ class UPHYAttributeSet;
|
||||
* 玩家状态类,包含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()
|
||||
class PHY_API APHYPlayerState : public APlayerState, public IAbilitySystemInterface
|
||||
{
|
||||
@@ -45,7 +60,13 @@ public:
|
||||
const UPHYAttributeSet* GetAttributeSet() const { return AttributeSet; }
|
||||
|
||||
// 原有的ReTargeterTag相关代码
|
||||
UPROPERTY(VisibleAnywhere,Category = "Config|Character",ReplicatedUsing=OnRep_ReTargeterTagChanged)
|
||||
FOnPlayerStatChanged OnXPChangedDelegate;
|
||||
FOnLevelChanged OnLevelChangedDelegate;
|
||||
FOnPlayerStatChanged OnAttributePointsChangedDelegate;
|
||||
|
||||
FOnGameplayNotify OnGameplayNotifyDelegate;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly,ReplicatedUsing=OnRep_ReTargeterTagChanged)
|
||||
FGameplayTag ReTargeterTag;
|
||||
|
||||
UFUNCTION()
|
||||
@@ -58,5 +79,59 @@ public:
|
||||
public:
|
||||
UFUNCTION(Server, Reliable,BlueprintCallable)
|
||||
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; }
|
||||
|
||||
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,9 +1,7 @@
|
||||
//
|
||||
|
||||
// Copyright 2025 PHY. All Rights Reserved.
|
||||
|
||||
#include "UITags.h"
|
||||
|
||||
|
||||
namespace UITags
|
||||
{
|
||||
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_Layer_Game, "UI.Layer.Game");
|
||||
@@ -11,6 +9,7 @@ namespace UITags
|
||||
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_HUD_CharacterInfo, "UI.HUD.CharacterInfo");
|
||||
UE_DEFINE_GAMEPLAY_TAG(Tag__UI_HUD_Notify, "UI.HUD.Notify");
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
//
|
||||
// Copyright 2025 PHY. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "NativeGameplayTags.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
namespace UITags
|
||||
{
|
||||
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_Layer_Game);
|
||||
@@ -15,6 +12,7 @@ namespace UITags
|
||||
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_HUD_CharacterInfo);
|
||||
};
|
||||
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_HUD_Notify);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//
|
||||
|
||||
// Copyright 2025 PHY. All Rights Reserved.
|
||||
|
||||
#include "PHYGameHUD.h"
|
||||
|
||||
@@ -68,6 +67,7 @@ void APHYGameHUD::OnAfterPushOverlapWidget(UCommonActivatableWidget* UserWidget)
|
||||
{
|
||||
OverlapWidgetInstance = OverlapWidget;
|
||||
OverlapWidgetInstance->BindAscEvents(BoundAbilitySystemComponent);
|
||||
OverlapWidgetInstance->BindPlayerStateEvents(BoundPlayerState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,101 @@
|
||||
//
|
||||
|
||||
// Copyright 2025 PHY. All Rights Reserved.
|
||||
|
||||
#include "Menu_Overlap.h"
|
||||
|
||||
#include "AbilitySystemComponent.h"
|
||||
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
|
||||
#include "Gameplay/Player/PHYPlayerState.h"
|
||||
#include "GameplayTags/UITags.h"
|
||||
#include "UI/GameUIExtensionPointWidget.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()
|
||||
{
|
||||
@@ -29,7 +117,11 @@ void UMenu_Overlap::NativeOnInitialized()
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (AbilitySystemComponent == nullptr) return;
|
||||
|
||||
// 防重复绑定:如果换了 ASC 或已经绑定过,先解绑
|
||||
if (BoundASC.Get() != AbilitySystemComponent)
|
||||
{
|
||||
UnbindAsc();
|
||||
}
|
||||
if (bAscBound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BoundASC = AbilitySystemComponent;
|
||||
|
||||
const UPHYAttributeSet* Attributes = AbilitySystemComponent->GetSet<UPHYAttributeSet>();
|
||||
if (Attributes == nullptr) return;
|
||||
// 从拓展点获取widget
|
||||
if (!UserInfoExtensionPoint) return;
|
||||
UUserWidget* CurrentUserInfoWidget = UserInfoExtensionPoint->FindExtensionWidgetByHandle(CharacterInfoExtHandle);
|
||||
UWidget_CharacterInfo* CharacterInfoWidget = Cast<UWidget_CharacterInfo>(CurrentUserInfoWidget);
|
||||
|
||||
UWidget_CharacterInfo* CharacterInfoWidget = ResolveCharacterInfoWidget(UserInfoExtensionPoint, CharacterInfoExtHandle);
|
||||
if (CharacterInfoWidget == nullptr) return;
|
||||
// 同步初始属性值
|
||||
|
||||
CurrentHp = Attributes->GetHealth();
|
||||
MaxHp = Attributes->GetMaxHealth();
|
||||
CurrentMp = Attributes->GetInnerPower();
|
||||
MaxMp = Attributes->GetMaxInnerPower();
|
||||
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetHealthAttribute()).AddLambda(
|
||||
[this, CharacterInfoWidget](const FOnAttributeChangeData& Data)
|
||||
{
|
||||
CurrentHp = Data.NewValue;
|
||||
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
||||
});
|
||||
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxHealthAttribute()).AddLambda(
|
||||
[this, CharacterInfoWidget](const FOnAttributeChangeData& Data)
|
||||
{
|
||||
MaxHp = Data.NewValue;
|
||||
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
HealthChangedHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetHealthAttribute()).AddUObject(
|
||||
this, &UMenu_Overlap::OnHealthChanged);
|
||||
MaxHealthChangedHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxHealthAttribute()).AddUObject(
|
||||
this, &UMenu_Overlap::OnMaxHealthChanged);
|
||||
InnerPowerChangedHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetInnerPowerAttribute()).AddUObject(
|
||||
this, &UMenu_Overlap::OnInnerPowerChanged);
|
||||
MaxInnerPowerChangedHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attributes->GetMaxInnerPowerAttribute()).AddUObject(
|
||||
this, &UMenu_Overlap::OnMaxInnerPowerChanged);
|
||||
|
||||
bAscBound = true;
|
||||
|
||||
// 初次同步
|
||||
CharacterInfoWidget->SetHp(CurrentHp, MaxHp);
|
||||
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
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
|
||||
#include "Gameplay/Player/PHYPlayerState.h"
|
||||
#include "UI/GUIS_ActivatableWidget.h"
|
||||
#include "UIExtension/GUIS_GameUIExtensionSubsystem.h"
|
||||
#include "Menu_Overlap.generated.h"
|
||||
|
||||
class UAbilitySystemComponent;
|
||||
class UGameUIExtensionPointWidget;
|
||||
class APHYPlayerState;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -19,23 +22,70 @@ class PHY_API UMenu_Overlap : public UGUIS_ActivatableWidget
|
||||
|
||||
UPROPERTY(meta=(BindWidget))
|
||||
UGameUIExtensionPointWidget* UserInfoExtensionPoint;
|
||||
|
||||
/** 古风 HUD 提示/飘字区域扩展点(例如右侧竖排提示) */
|
||||
UPROPERTY(meta=(BindWidgetOptional))
|
||||
UGameUIExtensionPointWidget* NotifyExtensionPoint;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category="Sub Widget Class")
|
||||
TSubclassOf<UUserWidget> CharacterInfoWidgetClass;
|
||||
|
||||
/** 提示列表 Widget 类(用于挂载到 UITags::Tag__UI_HUD_Notify) */
|
||||
UPROPERTY(EditDefaultsOnly, Category="Sub Widget Class")
|
||||
TSubclassOf<UUserWidget> NotifyListWidgetClass;
|
||||
|
||||
FGUIS_GameUIExtHandle CharacterInfoExtHandle;
|
||||
FGUIS_GameUIExtHandle NotifyExtHandle;
|
||||
|
||||
protected:
|
||||
virtual void NativePreConstruct() override;
|
||||
virtual void NativeOnActivated() override;
|
||||
virtual void NativeOnInitialized() override;
|
||||
virtual void NativeDestruct() override;
|
||||
|
||||
public:
|
||||
void BindAscEvents(UAbilitySystemComponent* AbilitySystemComponent);
|
||||
void BindPlayerStateEvents(APHYPlayerState* PlayerState);
|
||||
|
||||
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 MaxHp = 0.f;
|
||||
float CurrentMp = 0.f;
|
||||
float MaxMp = 0.f;
|
||||
float CurrentXp = 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