From 70e3731c098ab77c0cbdbe9e31dbf5514a0dd387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E6=98=8E=E4=B8=8D=E6=83=91?= Date: Thu, 5 Mar 2026 01:34:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BF=A1=E6=81=AF=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Config/DefaultGameplayTags.ini | 3 +- .../AGame/Character/Player/Player_Base.uasset | Bin 64001 -> 73710 bytes .../Gameplay/GameInstance_Default.uasset | Bin 8074 -> 8277 bytes .../UI/Menu/Overlap/WBP_Menu_Overlap.uasset | Bin 26456 -> 28395 bytes .../Overlap/Widget/WBP_CharacterInfo.uasset | Bin 32248 -> 70979 bytes .../Private/Character/PHYPlayerCharacter.cpp | 10 +- Source/PHY/Private/Gameplay/GameLevelInfo.cpp | 90 ++++++ Source/PHY/Private/Gameplay/GameLevelInfo.h | 42 +++ Source/PHY/Private/Gameplay/PHYGameInstance.h | 8 +- .../Gameplay/Player/PHYPlayerState.cpp | 183 +++++++++++- .../Private/Gameplay/Player/PHYPlayerState.h | 77 ++++- Source/PHY/Private/GameplayTags/UITags.cpp | 9 +- Source/PHY/Private/GameplayTags/UITags.h | 12 +- Source/PHY/Private/UI/HUD/PHYGameHUD.cpp | 4 +- Source/PHY/Private/UI/Menu/Menu_Overlap.cpp | 266 +++++++++++++++--- Source/PHY/Private/UI/Menu/Menu_Overlap.h | 56 +++- Source/PHY/Private/UI/PHYUIIconSet.cpp | 4 + Source/PHY/Private/UI/PHYUIIconSet.h | 29 ++ .../Private/UI/Widget/Widget_NotifyItem.cpp | 126 +++++++++ .../PHY/Private/UI/Widget/Widget_NotifyItem.h | 74 +++++ .../Private/UI/Widget/Widget_NotifyList.cpp | 236 ++++++++++++++++ .../PHY/Private/UI/Widget/Widget_NotifyList.h | 64 +++++ 22 files changed, 1230 insertions(+), 63 deletions(-) create mode 100644 Source/PHY/Private/Gameplay/GameLevelInfo.cpp create mode 100644 Source/PHY/Private/Gameplay/GameLevelInfo.h create mode 100644 Source/PHY/Private/UI/PHYUIIconSet.cpp create mode 100644 Source/PHY/Private/UI/PHYUIIconSet.h create mode 100644 Source/PHY/Private/UI/Widget/Widget_NotifyItem.cpp create mode 100644 Source/PHY/Private/UI/Widget/Widget_NotifyItem.h create mode 100644 Source/PHY/Private/UI/Widget/Widget_NotifyList.cpp create mode 100644 Source/PHY/Private/UI/Widget/Widget_NotifyList.h diff --git a/Config/DefaultGameplayTags.ini b/Config/DefaultGameplayTags.ini index 87e9fa4..b147beb 100644 --- a/Config/DefaultGameplayTags.ini +++ b/Config/DefaultGameplayTags.ini @@ -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") + diff --git a/Content/AGame/Character/Player/Player_Base.uasset b/Content/AGame/Character/Player/Player_Base.uasset index 8c771eef9679d7236a4ad9400a4767acdb59fd9d..07c559ef597827774fed319114769ed574717655 100644 GIT binary patch delta 18695 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

JpmYzi@?^?BA>s68vmyIU+qZdGLpU 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 diff --git a/Content/AGame/UI/Menu/Overlap/Widget/WBP_CharacterInfo.uasset b/Content/AGame/UI/Menu/Overlap/Widget/WBP_CharacterInfo.uasset index 62465c6d70a14af8fc2dd29abe634ed09b9f5990..0a6fb8b07108373bb5b890b08bb84bc133d338a7 100644 GIT binary patch literal 70979 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


sh8Uq|dQ@hq~4nIJB7ZT6_3HS(ED$T*IMk11ql# zZ`8ca@>^u^aU2YUCid{X!2qTB)KJFpp}uo`!?~ehZEPiu;KO@Vcpi`ZLK;?sSRGVl9X@Al8Oh2VxV5IGzR&pMqE)Vnc|HAl8Lg4`Ne@$g-y)Vqe$>%btOV{WgcF zfY=P8JH!?cJt1OUD~PQjwuIOQVq1vlpCZnV5IaHa0I@Sf4~Xp`wuk5g5pjA!#J;c% zmUV%MYb^4FuMe0XBq{GB$#;|FQ4X*z#yClSuq5ACk`I&Q!&%-H(yo$xcS$~8k{=?; zqg_Ir*fv6vk7Ri_NO2AbgLywm9?N(+w#Piu1^2!@KU9+MFUhMUc|7k!c;pH0V|jj< zBtJlskCxS9{@2B zVh}{E`w^HN zy}^Lj=yRj)^E~Q0&!cYhJlY7JN1MR&Xajg2^_k~eO7f_$yj&s4x0d8luaRHq6P$vG zc1e1Twn%!7_D6b+Hbr{f%IX?z2Mgv_DZBXyv zt`W)}(LFi_#3jY561zwBP+})8)v8yoU9+}(ojUGHkG3AlVu$?Qq&7HxF2vQP1u0kC z#kIBzygdT+b}8>d1D}#hIajyx6)IM$T%{@iY^_DgxwyKPb8{_U-VL9QA> zzqEGU`VFtXw(<2%@4ol`)(^IQxP8aoPe0rD`Tj4yJaFXbcgK#O`2L5JKc74Q%Y}=- z{`UK&zpmf7`S-2c|J=EY^TK)ZAkNHcUbSIf<=os{-74a|T*{4t=vv#Yyt{XWI$?b( zsz=ss;WN2Xz3>HVw(P0g(zoxm`YDFPRT{MN`?>XBI5j#m$0jzVL^Cr_%rdW&q#Eq= z%GGwQ4MP2M=Gs}LU0M8pOM|(uDzC2EpuDy8*2YSH!|%qs1-Te4-;m~AP?Uj2Cafls$A{_DM_WcU6?{v$8k zi`;o8{DnKaeqEkVK8fk@V9g5+o@)C;^{|Uy<&&<>Z}=Y_@YDAXW*tkrldh}qyQ(g^ zl744KSkT$C*GIl|c&umWZhxH}C3$!*>!pT@U{1J@3EU`mMNk+~dTzX%ENd6Vrr5v9ig%!-qb0Uvvi{6VBa> z{I%BJ>rEz43(xcKziQ0Nx5?ChCLP*!^R=gw`wqRlrc=k9>eY$yhZs_Hy`DA+LZzoUf z&&by6HV@bwcYoac%cuqO(!tse8{nB(z_X*zDf7u-NtN(!^$)|r(^m=*c^YfcbU#J>h^X)b--8ymm zE&YqD-uqx#-9re2SbnS1N5ucI}qe?9%e_}|Kn^qaFM z$tQX@X{JeAn3*A@0dd_H{HY0JNPtvzD5`I&5yLfKI_Bq>X3=1cnqK12g zhQ;;DTlCib>Gyh#?So?JHNM@Ro9p|8o*aB~!@$LVpFP&4dC%M$qw7^Vak$2UTY0;- z)_$q`?lt9iS3UDg)RDJ4U!9=})C5*++4A;le?9E5d&Z@)nHQ#X-H<$WiQ6q8@A9j^ zE;`WJO}S3hJ$LeoUst^yx*>Rw%4glzr!G#+`%r)1Yv+4w4_#joAn0nYx%2y~lii-m zC#!p(x&8O9e|GFyndi|gcl2{tSA2P5zA>Mi*mODpr06??$Di}wc>ChA(+5}Gi}=*7 z?t=y0Do*r=1|xS}+ZYs*v^(_bs)ru$yydxBd-<0cLEo;Q`{9*FUA_P4{qct%?s~D8 zPxYU}FN{9e?w6t8j~_VE?~_}m(38z-1KOlR5AKKFAL@B)*)+hWn7v}+iPU_u??n3x zp|@k2A?m|l?0o0$yAL(v|Gf0VYd?4TYhB#DyMLa!Kh*uZ`yE&M+=0!54-Gs0+=B)EVqXj1Ie3+O;O5b;uFq`iV@72R zUpZsiyh(| zX;LZa!kB6|gKzDtQgvs|H#~ua-~3f!`u_U~+df(`?ugf(+jsuC|LoJxUAz2dc=XMx z8^3#HyJo=fyUkb4I?$}e4sG5R-z$b2*@3JBj+&dk) z^KNj8aLy&nCxLi!39#>Z`PKP<; zCT6r*=9TBRY>scEn15pXPn}<36{=0Y=WxcZaFVdi*`Q>Hm7ot~>J0Yvz=A)-1g?{{%xWgs__eWOH zR=?zjJH*e!+f0zWzS+ zut22=r@wFR4JVi2y3>F2;n)jv0qj~Vb%Dk_*O29+$g@OG1kw}!M!;ciE=eaNiIJA( zz@{+;%2P-tlp3Lh*sBX{D2*f=;IK3YVAF}I$l7uYqqAeE!20P2j|PaNDqP}P%DCcy z20Fmz0!K$81uZ3<=9YQYU@*ObGA7zC2ac-Jhz5GlK~FAla47cD1L&6mbkx9+62guI z4y3^E+3?&KS~&Z3C537F_Q8-8%E>;$ExBi zY}IpGS;kS7wJ0oUE?{;zWwRx7O#(T~2CATxjRc5F45~fQ2uG0)2d6IZwI{Ky1yjjp z%=^K<1v+a>-_kNZ3K1)8LM58T>w3dul)Q}`$A+7^>;ebRVow%+x|4Av3bauVbjP{l zT)9+C0BEjFT-I#Cy1>CAAEgTzJ2Br6j(1YY2$&)EE-!ni(T&XTCV(}P0WcDQT7w)o zL_{l)4hKXyHWTz?gz;j3E^x>$;t?U-Wnnn4ckof12EadG_!kM1;6p;-D9{^fy&(tR)q*P@xWZ`BcxUIz$1Syr7OvT^ zA zGNriEaSQG&HF$fZfsXnp=wnzsMC)?CLfNOlFXBPJivYiDITilx06lnBMcA?G==- zyw1*skAwRM@nAik1uCM~Z&?*BwxcMx#z1NO$NQDnX-)+%>i2TL(Tnth9Yqh&i+-?5 zTl*6(!12=2bnbWdp|eCkrf^9UhKuhM;C=@y;WLW|nalzC#lP;bGNx1emr11}9bmZ+ z?<|M7EFgvq=sg$kya9PQ1RPR)y0C@g`7C zqx|E}E)D)#=x3|Gr9<@Na-I(U0Q&YW@aTys35DgH`yq%?ly4KY40^aqf}FVDZEdqX z_jgccv#371z{4#fHv3%Wqf7&HQ?y%aYQx6k&4Mu8d zJsiTwPf-eHXko!~0qDMKf3n-u2z zGd#29o=Fb$D7D)@He2)aAaA-qaiM!)lmW*i07r}V-UXydl$_GyMIv}UsUXWDWeR&1 zw(9-aIP8z#vRdJe4{^AVv2b(23t|Wv411DJkn?6&SmQ(UEr32PXq_Ls_Gi}t^m;Ju zg~JB2x*!G{%&xr|9>jn+L|nX;w;6{wP!-36a2$^}Xe}?}{dUZ$Be_rjKjw~ z3m-EMA2SXgGY%hyqu^}tTtOVGj~O4H1Bqq6W_-S8e7+1{I#GjtGs1}l&J@plW0vT)wl zOgV2eR~`rF{V1PsZ2xu67v_tfSu$<&#gB3W>GQbx!Y#{xoi7D--jCWJQ64#0EOx+R zRnVIebsqf|(c1YHLrbl(BCdj1{mdF^!Rl|;7Jn)`Xp8;L65^O`5Z4mR$}%2Sm+^2% z7$5hwx;JiuBL1yMxVvqAU%Mc5TdNO<#t6C`PyB$1R-NrB-VygKbOYU^P)Tw`Tl%I$ zH2;K&kY@hWni0k$-r0s)y#c;eV!p9$q>l%;Yfo=T!}q%3%VBXkZ61A?UZvK;cRukR zcPxAlC0CGsb{St*;8Tpy8-yhBgNz>Zo_1Sa10T9%lP~xc!M5~8jM$v4zCt#9HVePC z!#>Wd5sdV)0WbQ%LcCgwA8addYabt7FU^tQWA6ORz~LJDi6TA(yls!)Q&Hj9-Qe5x zdGvYyupE<~aviJS(@gwx_4sVM`9t*h07Aif_N`xh+Fv9meE~&=kMyMxPQfC@hdwP} z?;#0ipGLsP>BaAq(`O{)A9=v1CS*Ql4yFH^{FJtw;Hv>Qx8?WW4hM1gNIf!=hB+)n zgczV6j+ZR~pW&y1gD(moHBnFy7+<`Y4H1L10X%{(zK!IE83^;mg#JZJQ|XJad?Yrx z2Oeumg2O{Xj%JS6yjeX)k5fX*pR@pTD&RRc8$Nr$hP;G|K; zZ4Rv|R60B~_zZQihK88w+cfw#j`S@to5R6ZaVVo8MXE_dExB=0*}<2D;On07(MMD9 z256Qre6>UY5g*@jhu8w5%|UuXrQBFbSHfBW7JPfAB}9DV2jBFuIV5kWba+T5mavY1 zr4vNdXnfho10ue>gfA!A9GVYQIy|%zOPCj6LB1d%5TniE;QCvN5~hakFf0@kYQxZ6 zVL*FkHg=Gwzuzz#YamD)z>|M8V1o)Qk4ZBANx?r5dWs4COAQ?%yLV4wg|d~X+Zbdj zEgz$)D#8>P4==;<;EQ-XL}Mm{T}dafmX%=xH1Ry3g+%3qa1GzIqA52aJRK`m6t?8p z8;Tf)LS{Y|3V9C{LQVe`B&;n?{Ee{1;Y2t#n1p{I$kOvaO>BH?u8i1^NwWJmL+q_6 zpv95waoE6)jfoFmg%A*A#NKPqBw8g?>9r;=!lq8VaDHPLmnWW<%naZ=oV*7*l;W6y zQYY5xJ$8si!fX;NHgjUQO=5+-u&9)Xg(kSf#4208fO^+LIsJHQQ*6rtxXslo&KXXW ztzLz^0lUN=Tt;l5KIQjZguT_v_Q_%CZQSh=E8>YF z)+D>c`q!8G$R~Yd=7MU#qm(Hab~4?l6mM; zI|lLEo=J3sGHh~KY&lCDVbd(cHiDs)HdxEbaP~#4TW3R^yccuU@zX53e8z3t6T|08 z2kr6M%eFeU@r#SJsgCPEPBfdR$HnKfp`t8YD=D8X9dPdO)aI$BT|T2c@*e2Wilfh^ zZdw21cR$gn+g#%ko#8~;CaTaT$}X{omJu5$P{d+XjN<{j#P075u}ggu_3xh_+a;Fj zi6Yj&e|~J2SW(Upt8DcG>J>xB@i8Wwt5=dUoG4qp3awsti9M{0*g%2OTfOWOdw?^< z{tvERc8P^Y6(!zIlsd8g{ng7ZvGDY$#KijldG`~aowd1!D4n54sSD4)fA`Zav3fW| ztTNXJs8hmjNUQvf^>zDaMYJq{b5E~Zg zZ6+(j-cX`>7>@;L$}qGP6N-}EoThnc1EeA8&H&LV#2{@LCKkyz3}=;ikcL4zVIQ$b zwjp4h6an=S=?qBPke6A{t*Ce3NT{7BMo8!+hDzqa9ujKvNdjnOe(Yi*>p?0Qcn6+- z!jiMBBLkAoD26F5UAR)YZay%c2P*`Z-RBsWR)XX ziU~Db<5>Y$K6iF)tsb>PSOva-ilH3DE;J`vWa)Ug^}}k4r7Huh&C)`>*RfS$U={OO ziU}RUd6-q&h$&OHXTLFXa$dWfYWWq0RF7>O_r^@uIqh=Fc~v0?W9uh3o_u=V-9Gh` zcTEs78caMqdsW{Dlga_JjI28;b?vS%A70!)szp+-UXxZg`leySL8~~uupe%a0w7;8 zMmKeFl?uJzj5{7TX7cG8a%Fi8IVSn^56lHq7jOIvLE5`m0)LT@oL%NZ2Qsv44u=L5 zqpg7B9lQAUx#@=^V;AmyCHeW4d)0DfFG)HmIENmrMs^NZXR|`mrFFL0b2$H5_0$<- zFGeo^ti$<}wSAi59IVTRO0bycfFBu-2Ki9HZ&3=6YTySTba0gjxhyEDLR^Q{8v;bc z#RP-R>Da6g+tI)1NTkzW&$8rrb+P759+SPmpFzREC+99+7V_X~nOFso${eVT_!chz z$BjwNp))^v>vnk5+}G-dj9+|w4ksoO3hgQebdjqK?L2I1&6~a6suSBvGv?+JSiKjpUb%$)#9S~lGG1XWa3E7`9%lUfNzJ3G%MoucuJ=;;y*_?8 zf9tZr62#{DpQK(2`X%V4XkqSZ$Xk3AUDo#OO8H3%pNxBTSmk`ALX}we5hn0q@Vjp{ z5)Vz-KW|#@%E8K|w**R$2@DH;okoJuoOOgAsA66oUR2dE<)K(~A+ND!g^w8~B}VqO zSxLSKZ_X+UZ7-VHswnFB59#g&>?(Zj&d$%M6`;)%Is7gT4}UBp)hL!?LQQ8dWNhAr zL5KoWu-Of|em1*7au?_ZRF#IMEX5?*oX36UVBav6Ur<0mkZ)LoU%1lO%O}`J8RQ$N z3=50!2@moO8cKiQFx)#*>7@z|@C)$@Sl){%l2epIT9U$9a&gmmIa$Etp+{Fv9v07 zjrS6I#6LoQdU+{>0wROM{FK3ds^Fl=us~ICgnxLXQsw0r5EK?k9smUr;$DXJqazLo zf>gu)zv(ufQmm%4c=B``Z|cYi0{sW(a!ykW1&<6;2KxjD2K$96 z1C#+i!QO$9k>0RIc=-o}d3yy0k^4ZxAo#iW*TD7pFNnmGZ0_sX5DBxn?=9pC+DB*} z%FI1A7Ibkg9dlpX220K6KIT!H`@rDHNR_vDq+hVVk1{aOH$oNY7ZDz$3m8)Yz5X|OY6a>f3eVIRpOiRT-HhufY;LkM#{Bp~JHcNJ#^B7FNI}B8g zrDl^~wuLV}em==tczmo)P7urnzibQtzjk8V-iAn+&3)Mx9w=0L=HW4z`!+UMYBu*} zTlmuB=aX#i+u9HWv$-$Z!k7N-xyPX3_<)z(GHzDzvMqdRn(QZe3-4h=6wE4aZwv1W zjWKi+6Z(u|0L$aOV7$A>1Ky(HfgU&G1AiFmi%Fp;Md|k%vCjIbje<`C^k>aqU@0c_ zxdeP*ayBb0bPv2TR5W0fh5DCMjd}=-H|SM;!;)17b(R3%%nVl>N@FnSDiv!m6d2D} ztp=0qT^hqdhLp~5d>kAO%1S?o7^EBGwPF!{uE3S$F<}o%U^cru{>8MTm?_YK-6t)M zZGOH>{M=<-$o`f17>w=LgU~D%iy%+oP*8)AKm9ZrI{XH4vE=0mrwB5jSQNpNLaOP7 zDdMouKH)S$rk5H`9Mv4VMh>5DE7ph}rIADXU#!7U;74ZUI+WUQ9A(uLF20K5668N` z#v6Y{}2#-I=^J|{U6n+RwX9b|ENac*05v} zX01`LLQfb3F`*|O(r4(Uw!9C6LB~(Jd-VdRot}W82Q>pC9-|C+<7#5|I}Lhn?Kx)f z_VO$86QB&k7va?pw(PnvdQQJPL+U(SJZ+FzOjdM`?(pYt4D^;qQ#z3}!7hAXBYN1g5XQQYezb%!Gou8d9srs( z)h-Wy{HE3PL{{EOw=m)TH*3UxydZtX+~@pe;5`8hKmC^Y#i+l|MJ-*k`E~t~lT|i~ z#iVzKz#T6RPV2S2{0Hw}Ke681gO!J>Ph|FQ6cn-K$Bykhe=>ZHvWsEI*9#xKKIoO$ zsV|zwFV5*{#PMV3>AAmN;OjkNw@fR)>Sh>e8Y32yNWEs)+fQp^x70fkb9~tGAxW%! z z!w|9YsnfHR;nDBj{&ZpeE1&h%i^Zhx@4<<;T70E^VcW5g@rn^f><9T09rs0d(=l)5 zs?lTb&3Jv>k=Gggz8krd56%6n*BrNX$;sjA-kVr?{U#Msmc9__n5b$jP3^-;f3OY_KL}h3P5rY(1s$~=d4*V?&M#xr{zRA*e zSo$_g-)HH2EZxG=cUiibrEjowI7_7ikg9eNtH%M;DY9$orD9<)E08vt%gV4GMS=(H zfKGo3E0H$xVP)8;0?T894QM00n2LWO^b`~Nmkn#uu3T0a$5JWPDUcc}o??|vSZYoh zvYd2GwhS9RmWy85zViCxOH{Kfx_sJxTZ+KZ(|&9US1MFva}3sFs3q+J1x*CA`de!~ zeYW=*Ryp;n6S#Det9#!?6MecTT0YVQSg7!dyd00V}&DF6Tf literal 32248 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 %0=u; zlv-sm_)B=?GV`b0ioJ>YM;bY=&*ouh89w}}e6UB3gG^5dTSq_Rjx`(@EK?RF~m?6{XPmO z0dL#m#~dQ@(^BxoRTF(!JH%+vQmtbZd>4j)WECI&Hh(@9A3!Ks&pwcdk9~{cq>naO z(IedZd=B%WPYYD^ki^SAjet)oh@Tv#&q!E*!SFP=a`m1DwIQb2qphq|Bibp?D4024m$Um^vuEAcTEv9~U) ztOQL^hCSD?slHNGYue(tcNM|gQggn~?8ksXH3H-aUsI62I*|_7 zn4}C*&XI7mIZUOQs8@o9R1v6b7x>%|J|TpuO#k?PLKSp_uQ*geHyHh^pc@QARnQH} zy$ZTP^C{7dFBD*s+FrC+M6DVX##1DoM6s(f+PedOK_LJjjsamt&3*tY;J8son97(@ zN3k46bul4+FLsW;Af-q8-U`J6kGIi;0RmH*-ti3^Ol2zK#d28CT2c=@#dm0wxf3Sr z$low^ys4eBc5(F~j`6Mu8>ZU6(avb&<3NtYH%xOKe|F}>z75lMjTN#R6&-thX}>4q zY=DIX)|*ti6?*peO9u*Dro_gMTh`<&7neaxxxBC+^jAz6Q|#k36X!Wp@3S%CWWvbt zXX{uiOQz7`4}Av>BNtxETC*By@2rRr)FW4yxv&x^v}+DmI&ATBo+G`Z=G{9#`B-@T zoW1X+OO*Sw;Xm*1jtozFG&Me$#9kjB@V=PnDY?O6}|w5sa$r-tq|MMUkvYA zbk1iv=wNjePX3Pvle$Bv{`38Np;0r| zHVhs!@8la?m?$G2NAZ2e+Cx2uO{{x6_Wk;M4GWten_{i(C4vdpC8DitdM=|xdhg6R z(%=32&9QT?IsNv2&G22;%Knps$=~HzDy9K$3(xOZ%VhuO9qYpU$A>>t7DUYrjZduQ z{d5=?Cd!ruAINcWb-e$px5u?ItF>k`pcHa(YPA-(%&+-UNl z`+>*>?ic%~hh(0#R`#FlSV-^r*S_)o!rw7;$%*@}lUjYz&std@i3F*TaJFO>CO7sBUXj!zDgrir;YxcRFb&p403;V}B=eMiv^$W08MrXUM zy_x^hKyD13m*6Z#4ZJHr7M3}RCT~Z6jpCG~ouk(bt67XvaEN!yHNYGOpH{Au{Poxa zGba@+862_Tu0Z86kYNa9d6|q0s!WBX0A0+-%ZIue<~%)@Twu;6rcS&$Lh0Lmk}fn#W;+~O67vEbgY3?Mmo4@hFh?hO5?K{8SD`f zTfv{AD?xP&tbiol`3M(_=+KSQ;|T>+V~X3YZdY2z_SkUDwkILudcFwTH$z^GZIs%J zu?>G{?&aA=mYsiZ0dvZ6J=2*`%BTkY>&0k^}&$(Pv=eQ3T3GO zcvTYdj?sN;1Flp*}vM~68l6&j-F z&pmZDxrbY-0f8Znerv{WU0>N4^KlE=rX!svyM&3wM5v$Sx%=}436q1ObBof`wisD? zN6nn1EgS2^e>OX7%8b_nrgUfJQ(Q8~yz4P4W_!I2`T7s$FOOyAn{RYlccIpm@YRds zH$8pm(BoBBE{zI)tPJVI2@+3bq5=vEQ?8gqr(|Y8Dl{2>R(^xRPofpoLW+@7)SS-Y&IUJazz*1`&f|WSNjldH~IL%#)M^ zx-uNZYRZTLXBRkI#w69f6tE*!VhBRuMsT13;K+d+b$Ck>BPLD|Q5@)sQuL%;)Bsp{ y)CgQUp{DT32UI0@MJeh%@HgBS-d#P;tLwgFNHUTpO$BFz`q~}d)t*Nfg#RD6A2tmD diff --git a/Source/PHY/Private/Character/PHYPlayerCharacter.cpp b/Source/PHY/Private/Character/PHYPlayerCharacter.cpp index 8bc6d97..4091989 100644 --- a/Source/PHY/Private/Character/PHYPlayerCharacter.cpp +++ b/Source/PHY/Private/Character/PHYPlayerCharacter.cpp @@ -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() @@ -176,7 +176,7 @@ void APHYPlayerCharacter::InitializeGAS() ASC->SetNumericAttributeBase(UPHYAttributeSet::GetHealthAttribute(), AS->GetMaxHealth()); ASC->SetNumericAttributeBase(UPHYAttributeSet::GetInnerPowerAttribute(), AS->GetMaxInnerPower()); } - + StopRegen(); if (RegenInterval > 0.f) { @@ -191,7 +191,7 @@ void APHYPlayerCharacter::StopRegen() if (!HasAuthority()) return; GetWorldTimerManager().ClearTimer(RegenTimerHandle); } - + void APHYPlayerCharacter::RegenTick() { if (!HasAuthority()) return; diff --git a/Source/PHY/Private/Gameplay/GameLevelInfo.cpp b/Source/PHY/Private/Gameplay/GameLevelInfo.cpp new file mode 100644 index 0000000..9dbd5bd --- /dev/null +++ b/Source/PHY/Private/Gameplay/GameLevelInfo.cpp @@ -0,0 +1,90 @@ +// + + +#include "GameLevelInfo.h" + +namespace +{ + // TotalXPThreshold[Level]:到达该等级的累计经验阈值(Level=1..100)。 + // - 下标与等级一致。 + // - Threshold[1]=0。 + // - 总经验(到达100级阈值)约 45,000,020。 + static const TStaticArray 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 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); +} diff --git a/Source/PHY/Private/Gameplay/GameLevelInfo.h b/Source/PHY/Private/Gameplay/GameLevelInfo.h new file mode 100644 index 0000000..effc700 --- /dev/null +++ b/Source/PHY/Private/Gameplay/GameLevelInfo.h @@ -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 TotalXPThreshold; +}; diff --git a/Source/PHY/Private/Gameplay/PHYGameInstance.h b/Source/PHY/Private/Gameplay/PHYGameInstance.h index 7589201..59a4168 100644 --- a/Source/PHY/Private/Gameplay/PHYGameInstance.h +++ b/Source/PHY/Private/Gameplay/PHYGameInstance.h @@ -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 ClassDefaults; + /** 全局UI固定图标集合(经验/升级等) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config|UI", meta=(AllowPrivateAccess=true)) + TObjectPtr UIIconSet; + public: TOptional GetRetargetInfo(const FGameplayTag& RetargetInfoTag) const; const UPHYClassDefaults* GetClassDefaults() const { return ClassDefaults; } + const UPHYUIIconSet* GetUIIconSet() const { return UIIconSet; } }; diff --git a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp index feb1b6e..9b02c15 100644 --- a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp +++ b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp @@ -7,21 +7,27 @@ #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(TEXT("AbilitySystemComponent")); - AttributeSet = CreateDefaultSubobject(TEXT("AttributeSet")); - - // 设置AbilitySystemComponent的网络复制 - AbilitySystemComponent->SetIsReplicated(true); - + AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed); + SetNetUpdateFrequency(100.f); + // 使用Minimal复制模式(推荐用于PlayerState) AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal); - + } UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const @@ -57,6 +63,9 @@ void APHYPlayerState::GetLifetimeReplicatedProps(TArray 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 SoftIcon) +{ + OnGameplayNotifyDelegate.Broadcast(Type, Message, SoftIcon); +} + +void APHYPlayerState::PushGainedXPNotify(int32 DeltaXP) +{ + if (DeltaXP <= 0) return; + const UPHYUIIconSet* IconSet = nullptr; + if (const UPHYGameInstance* GI = GetGameInstance()) + { + 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()) + { + 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; +} diff --git a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h index 4407583..747a91d 100644 --- a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h +++ b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h @@ -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 /*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 SoftIcon = nullptr); + + /** 仅本地:推送“获得经验”的提示(并不会改动 XP,改动 XP 请走 AddToXP/ServerAddXP)。 */ + void PushGainedXPNotify(int32 DeltaXP); + + /** 仅本地:推送“升级”提示 */ + void PushLevelUpNotify(int32 NewLevel); + + /** 仅本地:推送“获得物品”提示(ItemName x Count) */ + void PushGainedItemNotify(const FText& ItemName, int32 Count); }; diff --git a/Source/PHY/Private/GameplayTags/UITags.cpp b/Source/PHY/Private/GameplayTags/UITags.cpp index 5dbd924..1a87b0f 100644 --- a/Source/PHY/Private/GameplayTags/UITags.cpp +++ b/Source/PHY/Private/GameplayTags/UITags.cpp @@ -1,16 +1,15 @@ -// - +// Copyright 2025 PHY. All Rights Reserved. #include "UITags.h" - namespace UITags { 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_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"); } diff --git a/Source/PHY/Private/GameplayTags/UITags.h b/Source/PHY/Private/GameplayTags/UITags.h index a6ba153..94e3fc4 100644 --- a/Source/PHY/Private/GameplayTags/UITags.h +++ b/Source/PHY/Private/GameplayTags/UITags.h @@ -1,20 +1,18 @@ -// +// 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); 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_Modal); - - // 拓展点 + + // 扩展点 UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_HUD_CharacterInfo); -}; + UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__UI_HUD_Notify); +} diff --git a/Source/PHY/Private/UI/HUD/PHYGameHUD.cpp b/Source/PHY/Private/UI/HUD/PHYGameHUD.cpp index 1c7aaa5..b8d19b8 100644 --- a/Source/PHY/Private/UI/HUD/PHYGameHUD.cpp +++ b/Source/PHY/Private/UI/HUD/PHYGameHUD.cpp @@ -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); } } diff --git a/Source/PHY/Private/UI/Menu/Menu_Overlap.cpp b/Source/PHY/Private/UI/Menu/Menu_Overlap.cpp index aeea063..74bca42 100644 --- a/Source/PHY/Private/UI/Menu/Menu_Overlap.cpp +++ b/Source/PHY/Private/UI/Menu/Menu_Overlap.cpp @@ -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(CurrentUserInfoWidget); + } + + static UWidget_NotifyList* ResolveNotifyListWidget(UGameUIExtensionPointWidget* ExtensionPoint, const FGUIS_GameUIExtHandle& Handle) + { + if (!ExtensionPoint) + { + return nullptr; + } + UUserWidget* W = ExtensionPoint->FindExtensionWidgetByHandle(Handle); + return Cast(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()) + { + 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(); if (Attributes == nullptr) return; - // 从拓展点获取widget - if (!UserInfoExtensionPoint) return; - UUserWidget* CurrentUserInfoWidget = UserInfoExtensionPoint->FindExtensionWidgetByHandle(CharacterInfoExtHandle); - UWidget_CharacterInfo* CharacterInfoWidget = Cast(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(PlayerState->GetXPIntoCurrentLevel()); + MaxXp = static_cast(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(BoundPlayerState->GetXPIntoCurrentLevel()); + MaxXp = static_cast(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 SoftIcon) +{ + if (UWidget_NotifyList* NotifyList = ResolveNotifyListWidget(NotifyExtensionPoint, NotifyExtHandle)) + { + NotifyList->AddNotify(Type, Message, SoftIcon); + return; + } + + UE_LOG(LogTemp, Log, TEXT("[GameplayNotify][%d] %s"), static_cast(Type), *Message.ToString()); + if (GEngine) + { + const FColor Color = (Type == EPHYNotifyType::LevelUp) ? FColor::Yellow : FColor::Green; + GEngine->AddOnScreenDebugMessage(-1, 2.0f, Color, Message.ToString()); + } +} \ No newline at end of file diff --git a/Source/PHY/Private/UI/Menu/Menu_Overlap.h b/Source/PHY/Private/UI/Menu/Menu_Overlap.h index 3656f9d..8a92ec6 100644 --- a/Source/PHY/Private/UI/Menu/Menu_Overlap.h +++ b/Source/PHY/Private/UI/Menu/Menu_Overlap.h @@ -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 CharacterInfoWidgetClass; + + /** 提示列表 Widget 类(用于挂载到 UITags::Tag__UI_HUD_Notify) */ + UPROPERTY(EditDefaultsOnly, Category="Sub Widget Class") + TSubclassOf 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 SoftIcon); + + UPROPERTY(Transient) + TObjectPtr BoundASC; + UPROPERTY(Transient) + TObjectPtr 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; +}; \ No newline at end of file diff --git a/Source/PHY/Private/UI/PHYUIIconSet.cpp b/Source/PHY/Private/UI/PHYUIIconSet.cpp new file mode 100644 index 0000000..08a40f4 --- /dev/null +++ b/Source/PHY/Private/UI/PHYUIIconSet.cpp @@ -0,0 +1,4 @@ +// Copyright 2025 PHY. All Rights Reserved. + +#include "UI/PHYUIIconSet.h" + diff --git a/Source/PHY/Private/UI/PHYUIIconSet.h b/Source/PHY/Private/UI/PHYUIIconSet.h new file mode 100644 index 0000000..b28a4dc --- /dev/null +++ b/Source/PHY/Private/UI/PHYUIIconSet.h @@ -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 XP; + + /** 升级提示图标 */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="PHY|UI") + TSoftObjectPtr LevelUp; +}; + diff --git a/Source/PHY/Private/UI/Widget/Widget_NotifyItem.cpp b/Source/PHY/Private/UI/Widget/Widget_NotifyItem.cpp new file mode 100644 index 0000000..152b82a --- /dev/null +++ b/Source/PHY/Private/UI/Widget/Widget_NotifyItem.cpp @@ -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(); +} diff --git a/Source/PHY/Private/UI/Widget/Widget_NotifyItem.h b/Source/PHY/Private/UI/Widget/Widget_NotifyItem.h new file mode 100644 index 0000000..3aebf00 --- /dev/null +++ b/Source/PHY/Private/UI/Widget/Widget_NotifyItem.h @@ -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 MessageText; + + UPROPERTY(meta=(BindWidgetOptional)) + TObjectPtr 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; } +}; diff --git a/Source/PHY/Private/UI/Widget/Widget_NotifyList.cpp b/Source/PHY/Private/UI/Widget/Widget_NotifyList.cpp new file mode 100644 index 0000000..6cbb96f --- /dev/null +++ b/Source/PHY/Private/UI/Widget/Widget_NotifyList.cpp @@ -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 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(GetWorld(), NotifyItemClass); + if (!Item) + { + return; + } + + // 异步图标:先使用缓存/空图标,加载完成后再补上。 + UTexture2D* Icon = nullptr; + if (!SoftIcon.IsNull()) + { + const FSoftObjectPath Path = SoftIcon.ToSoftObjectPath(); + if (TObjectPtr Cached = LoadedIconCache.FindRef(Path)) + { + Icon = Cached; + } + else + { + // 先不阻塞,等加载完成再 SetIcon + TWeakObjectPtr WeakItem(Item); + GetStreamable().RequestAsyncLoad(Path, FStreamableDelegate::CreateWeakLambda(this, [this, WeakItem, Path]() + { + if (!IsValid(this)) + { + return; + } + + UObject* Obj = Path.ResolveObject(); + UTexture2D* Loaded = Cast(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(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(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(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(Container->GetChildAt(OuterSpacerIndex))) + { + OuterSpacer->SetSize(FVector2D(1.f, 0.f)); + } + // 次外侧(如果存在) + if (Count >= 4) + { + const int32 SecondOuterSpacerIndex = Count - 4; + if (USpacer* SecondOuterSpacer = Cast(Container->GetChildAt(SecondOuterSpacerIndex))) + { + SecondOuterSpacer->SetSize(FVector2D(1.f, ItemSpacing)); + } + } + } + else + { + // 外侧是第一对的 spacer + if (USpacer* OuterSpacer = Cast(Container->GetChildAt(0))) + { + OuterSpacer->SetSize(FVector2D(1.f, 0.f)); + } + // 次外侧(如果存在) + if (Count >= 4) + { + if (USpacer* SecondOuterSpacer = Cast(Container->GetChildAt(2))) + { + SecondOuterSpacer->SetSize(FVector2D(1.f, ItemSpacing)); + } + } + } +} diff --git a/Source/PHY/Private/UI/Widget/Widget_NotifyList.h b/Source/PHY/Private/UI/Widget/Widget_NotifyList.h new file mode 100644 index 0000000..3c1114c --- /dev/null +++ b/Source/PHY/Private/UI/Widget/Widget_NotifyList.h @@ -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 Container; + + UPROPERTY(EditDefaultsOnly, Category="PHY|UI") + TSubclassOf 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 SoftIcon); + +private: + FLinearColor ResolveColor(EPHYNotifyType Type) const; + void TrimItems(); + + /** 已加载图标缓存,避免反复异步加载 */ + UPROPERTY(Transient) + TMap> LoadedIconCache; +};