From 019b35dee4573fa6d6730aeb5ce922738f66c509 Mon Sep 17 00:00:00 2001 From: "J. M. Ellis" <75238062+Othelas@users.noreply.github.com> Date: Wed, 21 May 2025 16:21:37 -0700 Subject: [PATCH] Initil Sprite Animation with the player class. (#4) * Added RoSpriteSheet and defined animation states. * Added methods o handle sprite animation states. * Figured out how to flip sprites when moving left. * Bug Fix: Ready state wasn't animating, now it is ;) --------- Co-authored-by: jme9 --- Content/Content.mgcb | 36 ++-- Content/Ro_Sprites/RoSpriteSheet.png | Bin 0 -> 8034 bytes Content/ro_emoji.png | Bin 0 -> 716 bytes Content/sprRo_Wall.png | Bin 527 -> 0 bytes LunaLightGame.cs | 8 +- LunaLightXMG.csproj | 5 +- Player.cs | 283 +++++++++++++++++++++++++-- UpdateNotes/Player.md | 196 +++++++++++++++++++ 8 files changed, 494 insertions(+), 34 deletions(-) create mode 100644 Content/Ro_Sprites/RoSpriteSheet.png create mode 100644 Content/ro_emoji.png delete mode 100644 Content/sprRo_Wall.png create mode 100644 UpdateNotes/Player.md diff --git a/Content/Content.mgcb b/Content/Content.mgcb index 1215a8b..6f06e07 100644 --- a/Content/Content.mgcb +++ b/Content/Content.mgcb @@ -98,6 +98,30 @@ /processorParam:TextureFormat=Color /build:player.png +#begin ro_emoji.png +/importer:TextureImporter +/processor:TextureProcessor +/processorParam:ColorKeyColor=255,0,255,255 +/processorParam:ColorKeyEnabled=True +/processorParam:GenerateMipmaps=False +/processorParam:PremultiplyAlpha=True +/processorParam:ResizeToPowerOfTwo=False +/processorParam:MakeSquare=False +/processorParam:TextureFormat=Color +/build:ro_emoji.png + +#begin Ro_Sprites/RoSpriteSheet.png +/importer:TextureImporter +/processor:TextureProcessor +/processorParam:ColorKeyColor=255,0,255,255 +/processorParam:ColorKeyEnabled=True +/processorParam:GenerateMipmaps=False +/processorParam:PremultiplyAlpha=True +/processorParam:ResizeToPowerOfTwo=False +/processorParam:MakeSquare=False +/processorParam:TextureFormat=Color +/build:Ro_Sprites/RoSpriteSheet.png + #begin Sink area.png /importer:TextureImporter /processor:TextureProcessor @@ -146,15 +170,3 @@ /processorParam:TextureFormat=Color /build:spr_Cave1_walls.png -#begin sprRo_Wall.png -/importer:TextureImporter -/processor:TextureProcessor -/processorParam:ColorKeyColor=255,0,255,255 -/processorParam:ColorKeyEnabled=True -/processorParam:GenerateMipmaps=False -/processorParam:PremultiplyAlpha=True -/processorParam:ResizeToPowerOfTwo=False -/processorParam:MakeSquare=False -/processorParam:TextureFormat=Color -/build:sprRo_Wall.png - diff --git a/Content/Ro_Sprites/RoSpriteSheet.png b/Content/Ro_Sprites/RoSpriteSheet.png new file mode 100644 index 0000000000000000000000000000000000000000..b4033d00bcfd8280a1e560058486aad115fd8cc5 GIT binary patch literal 8034 zcmV-oAD!TdP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91ke~wq1ONa40RR91kN^Mx06#wphyVZ{%t=H+RCodHoqfzDRei^2%dWWF zWtU|O2m(f=DB|)`DAv>lRBA%nhLk3@Zm226v{JW-Rhk-;zK{|d#ae|35;2W!*4Csp zq<@fDXlW#EwFOpr>&pt^wSf(+yK8opUHWuBzdOJ4{N~I#XU@Dl&vT#K`%CVfnKS2n z&+l!{%-s3jGi!==Suj=BnDwr!bDXrtR;7R15@O+(k3Yqk{nvY*(e<`=T2GgA9Q8za zxoZ9MBVl`_RE&D@#9 z<~ZpLTJ8QxC4!u$E?B=wJaFe7@0aB%+y9hhs-4s`UvnJejKJ!u_D^&KDh-t+*;0-~ zXKdV5-1gYU;v*mWYQd?klrGmmbDTidn4;C@pIYowBZ-`!8Dft*<=Eox4aXI0*R3ty z{NO{y-TU@nf2ytV&*VHmjW}H6edjG_&7574c;&_{823x|y&y6D{%!_0?C~L zLD##mOAo#z04JP&qD_vr^(Di(`Pv(y}T5_BthmS1w*1i_h=fk(&=2~V3zJ`Nu)@|BcC{8T!u9L+ThjZ$i zPc81o!uB#0k zP8Aq-$4dvL4ntu^A_j5L<$Y@pYhO9bAWlw0uTBuh>XUhZ1#xU&8G&asY2aM=2#J3kvvAg#XG-#L>eA%l+UL-VFBVrHx1qS{pZ*0LznzcWz*)a_ zYrzqEc>lg)??d0V^_60e(*i70M!y!;4PtJkL*`X+}QSaNlp%Eud z9u5o==NHd-ovPSa=HPI^-&C~(>u+~I$*3hZHWG*<{hU&_C)OY3Izyogo_elW-{At< z$59L}Tz5L;^onnsbe1yVi4YFXexV!JI7wX^Bal^w>=x;sJQk)9oGHLSsWEh!5*!&l zYK~sqNCthenyd}NiFL$9k}t8kem%SGZgnt3u~Hi zrb!n+!H2{v%dWN#>}DZhr2OuJ9{z`YYrNSHI`0e;EaJWtG>Z55gx8Bl!UT{J8!&d;o>*_ge=QF z_S{%iZC#9oHBC6vqzeI95q(;(fh;5O=vS{Vj=$nYlx=HA9bZUBDTCcO0Bf3X+H~Py z%kfnkj_=FZ_Oj!|u7NOIhG~949UZmvJH7D$lBelNM@HgUSh+Z&WBC=bi+x!*AHDc(1(b%! zQ~o3d%sn_cNaP8)>?^;LIO^vXUBJr1(N4?dAm8}y{SinW?{l%shX2GuM2ecf>kL{MKj@_&?(H0E(7-fpbHh?SQW`rt7R+(d4qKJ}iQa`7TRBqf(PDP>TE$Ug$Q&o_#bxnNPGVR^ za_Wc7l8Z*&F@V`P$5AJSmt*)x;+@MLZ5x3^A;2m?z->Z>&8U~wjA9p9edk)V&~qH? zjm3wmTH3=uF5Zg4GKCYmh}O2jz{5-|r@14Eaz-PE!4HB1n2mEBjY@bqmVX?2ZoFb6 zP)#9FNu0j4R_0mcccme7EuzC52QS2B1pn|KvC;etyG=d=D6M+Rb66$a0-}|c$Gx$* zN9)Q`by^{unl7b^eeD_xN4@0mmfJtEQy2;r_aE9{KtsLZkN>97Nj6pEmZh}k+==M( zte?8}n)1Qutuh+5v}dBWB=u`q8IRC#8?*URDX6=7p&^0uCyS;b+0o zSh3N?&p`tf%mV%E=O%$CsY(g8qa2abA5)l||D%(+rN8##ff^+Q)}BA>!=$PEc21^* z$0$ewNA{OrzFwo@kjP4PQBqwWhJ^Id89Jmw(?1kD zol^S5;?;0Cj?%z0wVm>S=!q%rkcw@`YOp;t1Gp}T2A|n5e&xnb6@T^d>t*^89Gzh8 zNfMI4`bn>Cv4C+f48(D|(2wfFIyzZ>n6oIsZyF1St_6#g96wX9%|Dh4 z^u{-xCNr`4eh$UUCV(iV3c=x7w`pB*(lHx52kg_IeNUlu2b9OrLc^$d)hB)P-{m1~ z$|%~|z{hZ^5s}?U!yXF9zySmC6UetKF+wL3r~?ja$fyCCtj*5}|8&CPp?WA@>TstJ zoDg+VDDVk|+)-fzrWj~zM>DpcyZ0Yhs1B?nop2%;W8yfh9J>@LjV`9NhhqEG|KH83kSn{C4A zlyHfu?g$wFv=p={z(J2S<@JpOn5ZK2EmjOv zYZv=CP|LXz8?o0!kWom_^A?E4R;n3RwVmC?x*J~kLCQMXOdKhbK4whgCV1L`Vy!Ta zsB+G#uKtdqg& zf}{P@CS+GvBB0_)tc^}m-iK6Z6>#9z!i;q>tWqYUda{b@kgSxkb1a;Uu9mfmZ&zR6 zhuVe8T8)!Nj6h{gK7D0K+rE9y+f6*s*Y}}zp|Vy#)2LU&R^)kwneagDVt6KuYcawe z7(@IpZ8$AvkGng^ant6YG4yeFF}SlQLsImxx4|onqnR;=f56JZX-l9n94*R>;OMb% zYBm9iR75#e+k8TuAO<^D2#2nbnqEB5stGB zy^!UIS1f^?G6NknZ8+lCDPr!)Ba#vHnMyx9w%q_mA_1Ew9M0?VS#IW@WN!V->0*7< zgK4R#WB8dw>@-kRj>xAZhg3P=Wy%@+%mK=raP&1g*kjV?aXFyjR?P zLu>TLo(>XaQq0B-N-G$M7^RfWm1IaNrL`|}Z(`0OeRLpB1-BSK!X6lsFf93igQM?Vf2_RO_?xG^LCR$%Mz%K_wGR3$njdG_P zCEH=B+Qhp0XPI#55^<*-5-1g~5SMuZQO6;Hi2W^3l&?26v%pn$*<5kWYa^;7S5T(yLOPT@76WEu0>=exxfqfaa}O1RPO`fD>zX{2FE@^k8nJlcXZE!TI4UI1 z?mwBl4O`1nBeOcpWiweA8Z{Iwm5pT`7kui+aB?w1N4yOfeI&4=Hqp+P9HN{K@hQR` zxCyM9e}>}F*BJ3q9G0q+^>f0<^3vU!QGnRdN2!e#kU9Zz0n>dU9Nm{#cA{G5;P9V( z`Mb?i`->kGPZpM~S6$P&*IGyLL?`yaAiP4}iadE!b9H?n%`&jYz;S-ogzo9*2972V z#%SEdu*6qw1kz_GKFv}c4o>QVs@yF~DGv257Tad7MeUp7kTfoc(S!e&DV3B)E$4Tt zI2?5GUx`gwCcPFc#NkagYo|Tfd^tJ|#i{w(w@VY5PFnm^!=b*#V!P%4dsJ~^9mBeQ zs9@QNu0dqQ;z(QC^XWs2G=13is+-@JsUBPnwd;xz1g;8uG4}$1IXG5t-!4l_4Zzom zEDk#yR+)&pUTa`C>_xO45m?uc)aS@2J4^99l>O$*;!x~xI$#*M#Z}Y3hPzm!bxn1# zI@Y5peORm6EFHsFtr#|o zm)eaeqcWu5IgWZUyj)iQL8pmD(VTdo{ zAc9r}jv(5E^6Q7TYfh#DSRILJP7m9`3gV!~7Bue}0x=D^y zyx4=7Hq{J3>|+$NwsD%RZW3a?mHOsOM*G!@6Ed=K`sx^XY0nd0p6-W;PS8TkTtmi} zwN>X>9;!0!T2I2%SU9R&!#_jcYOTpL29Cu*VSW2^xf+VK+N4Y8t6Uua8c2?K{v|-* z0pSgWU;4&=@&NYcRS4J0q&Q&HCb^+jrUBp>;Qm+R-OO zbmB>Kg6uRFj_T-qM6i?|yX)wUH6QW1?i|se55#eaNS_?6J@x60Ljt+R1Wbd*^#(y_ zMzC-F#(6nDN!GD^qP}zYZTrMMjy_6f;Iy$LLrV3)aXtc1S$-TGA<_MzePx-$(t15{ z1YX&MV66lWrz0^)PdjR~jvO}d>`SoI1cy3`r5Kd?l3?vo)#=2BB|5R~O9E?2aOj@{ z&Dv-El<*NvYdQ_UX<+H~nB_B5sYRbbIQ$a5j=dmuzx}9z1RhR^viK-(VI)$;;lM{x z_MiryvW}b?hX;5mHJwSw44r5<296s&7&-W90a043J2)PEm}09Z+g6qohif73nDq`U z!#_cNdf-?a3(IQ5`TD6Hh6ZurFrY_9OhmtNLir^j@SfSFTmIfmBsoW4W}F# zrC+0uU}Kjli|L`|B+(c+iZ^j;Ibx_XZpW0Zpx+qM1t*qpvGSOHvHG#{)q>;noc82# zimt#)Ea771F%@I=W93tYlR8hUqSla{`r6K4&T)K?)VEiqf7%j4F0Rzho=ifT&vDd= z;pH^>M-rJkPL)K__D0Tqakx59iISyWk%QX3Cf%w|wLG5bWj*!lp7$eac z#gBpmn1pj2=j!0fwE0Jrggl7+3cHSm^#HOKM1vbZ@-{t;c6lZnq|AH7au4=a_oOot?PJ`*4s zeBeQFHHx`c71^$z<-$aS~1*$hyh$LBmfV8GUHTMU>A~QNSD);7T*)R99Wj^-)iRmp*sg66Cmy@h@c*#XqOH z^E(yvI$=2(ZmL+doa>YR;chhl=seiO+{T;4ac|iU@SI}5MAwpsV8$}g3wUGGCgB{X z;hw4Sv;5nnRPXKk4i@*DSAWBaM<5o4B$_~H@7Y^1?p~2=m>R<^gcK>8<3t=&6;G3Y zbn*)B2$9owKT+INP8G~elQ2tnKUry=UN9_KuI1>lO{17W&4nNbk zeq#MexqlbtSGGp9)%Dxj?KzJ1Mst1D`X`kXa*L?8&e@3S3AbdJmkl$e3)?B5y2fT@ z>Tr07uF@9w#t(^A!&>rC2IwrX5eYgEcUAh@1rCYB2kDDLyD@Oa)TfJoLbvgfJPLww z%1~3MXD&y>u|XfAk2+cPA7Yncu$&bf^|KxnId%m{Ad-iFUh)adYiNDfx}Lzv9_le@ zEF9)O;uEkdTOW*G2Ao#k+6rUnqkbO4KXL`GzX!lZhF{>W?#y#CG~S~1S%Diqmf^q&)B$0*oZt_r47C1lHg#|`_>+ou3|n4jX+Kru7!9Kd-JtBil2V| zNqHvbDW2Ml;?SsrgXChIWLAB)FC3CduizPV2z(6(9e(n+J}eIjfA>>g?{cKp>;mWV z-}`;>59=Kyh{HomW-aR=4ZF|>oL#$icYv92xYW-yBzeGzB@cD{kwlDo_Qc7|TM)~j zdU8~J`N+Cr?Ygz{R6a4G6PSn?r5qfK!_JgK>^IWDf%B^)VuvHXVb@aNFhXaJh)KJ! z|M32OsaI#Ji;_4GeDK2D11#yEa~^(d^2Wzzz7&lSh~(kXK0(LHyh9uz8=2}(AMdLL zu_ejl$PUgE^x?$m^2cIpre!uN4o|d=frp+T3?qT9QL6Q+@dNGTMdihfCWyK zh=Xt*KjZblWl2mQwJSI9DI65x<$5_1!x0Qa25Z9#;iyf-D(2@#0%=Ao5llM<49T5c zEQF&N;N&JEIE;o?A8T0G5ix{=KGk1ZS`#s57s<@2JvZ<%9CSg3`dJUsA^IFSd}N|b z`5ikj8VN)u`l11gf%x^=Cg>r-FmQ4m1P&K=A>jqSxrrFkN1aR(>fKMMA6pjkkK%Cq z-Ni18t3FmNyDzow>!Ub&VD;69J~@?M@YIBf7)!)P0{P0SLd+1~Mf{*Ejw+Ok<>Sx} zCxe4gxeoGia1O$q3U`FLi71T%UwX$w5|z?AlKJ{5WS4_f+k}>?PiSAL%-2W#LtT6m z^Ru5o%xWt5mmhk?O`8w3@xY$u$P`kfDswn+Z1qob;2^PVuG%&h+`cOghAB?Ws0KiJ z_n>wu4mv3g^)s;?j{3*y(;*QZj?!%=-WxYVu=2Zh6N`c(em?F1>` z__y(@nA{!`I2C|8&am3R0IPak0;{pU*{2f3U}w&27Zjvg2C;hJ0N21WIE+5MZ3CyH zpHupJ%VgcLg20N$<=Qt_vO!kfCqxPOgtdU9z8K$)_y@n{aQ*E@m zT`na~S6$F++VqL()W6nv*FU!njk=v2Y+d{qyJK1u*ugwcW(lm&C3C z`k?0mwpj8OzNVb7X_wIN_~K-}fadnj8rQ)f7c8)b;6O`2w!Q25;@q=NEw*mi)Ikkb zb=vfCim`%CSMJzfeEYGdi-+zxQK%T@1uRK4)gw`G1R~&LFaU83?6wHAG1oEjre$>6 zw(ZK|rmq}Zoc3eyEI#)6pz3rP6S-AY+XgTa$I{2?i7nWKZPGyt$c|`DP}f zbfW)BL5Y z)<_^u4Iy$71TF%fOLzju7fnQ7;+u=37x{L*l5s1EV zv@+9_ai>46r`Tym?*_xo!^veO3J%H02i~$>H~qD(&MHou-AlgTKYHa_L6211yC#q= zYgrFs8JsQ;3gwZAVd}Y&Ff*Z5)mg<+`<8v_ClCsVlLMNB>ISOSib7a@a1<0gZLiq6 kt#xvivx<{r`>1vP58zL)RH9%fy8r+H07*qoM6N<$f-*~QbpQYW literal 0 HcmV?d00001 diff --git a/Content/ro_emoji.png b/Content/ro_emoji.png new file mode 100644 index 0000000000000000000000000000000000000000..caffe07c90f11c4c86a16b00f1e48b8cf7bc20f1 GIT binary patch literal 716 zcmV;-0yF)IP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0%1u+K~y+TjZ@!C zQ(+i>&Nkc5ocra@=BBW$9|l2j2?K-kN5lD=e+Opy$6i3mQ)l)2Ed52EGy0S zoCwrdOAcGw*&V5ebEL!7R7{}Dair5}`N_jaik4K3G!Q*&z>d9$l5ae%eJDjS17A4? z?MvzDXHQdK@;Q`_D%ew1o_v4u6fRyfRd+SeRHTMo^$<3H$5on7$iF&||Ic)Io0$ndx*m*yUiD8#&(TXV)DP zo49n9rEcA3S32A**k(ClY`v12nWH8v+!YnZ9w&Z#?f5J?@&51tueUZ)h{xdbcUChR z;36dOcxp;UIRKuLvVJCBeENIJbd;E-zL-eUPbLuAre_iCas$ zHyFf?L_a>xe8sn=H7p;L@#f$NFBX^ad^U@$AmZ20EliBX(aV+_LgWJt6!lL%4~cfp zuuEdLXfJcc&oOEE0t<|eF=wow4;xYMOUolBn9OFh*=+pT>JLB@;&Y@wVq+$gnX~$& zwU?Pp^0e3vqo7TsXu+z>jyBPXqe=y8u~?^IG$A`bKeV#2(49;sUy%qOKT@VU^%%0- zFyiULfW@puDp=giVdncPEn~p%MWQc)`BI+0*P%aeB6&pN8uh)?nkK{&ec{%gP*vIj y{g|Bk-U++=1#E2ZRNtzosr@gh8hsUvG5iHg(V(JAj2sRC0000U}LhYzb4 zkPTu48Dg(t#BlJ&Ik2I|ih>MF>l_&TljndHoc;cZVdLBshLur$cnty@1~LR>*oP;t z7!ooUg9SZH3K&jIn$BR%z{fCQ=5c~9f_v{X12B+f1Q?FI{mj70FTil^@DT{i88HTdTm-V$=HoYp zvrw;r3`rK^0<-TveMX#%Kme2#Kw0AZ#}8n>(w+MZJ;20RVsFWyEDa2$I}ZsO#0bhq zupF=b`aJ^!2m^r&zbH8B16AaSb`gqUFtF$2Yp~q WinExe - net6.0 + net8.0 Major false false @@ -19,7 +19,8 @@ - + + diff --git a/Player.cs b/Player.cs index 18a2fe6..a00b544 100644 --- a/Player.cs +++ b/Player.cs @@ -1,20 +1,26 @@ using System; +using System.Reflection.Metadata; +using System.Transactions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Animations; +using MonoGame.Extended.Graphics; namespace LunaLightXMG { public class Player { - private float hsp; // Horizontal speed + public float hsp; // Horizontal speed public float vsp; // Vertical speed private float hsp_frac; // Hold fractional position data private float vsp_frac; // Hold fractional position data private float wallJumpDelay; // Delay for wall jumping public bool grounded; // Is the player grounded? - public bool walled; // Is the player wall sliding? - private bool wasWalled; // Remember is the player was on a wall or ground. + public bool walled; // Is the player wall sliding + public bool stopped; // animation control for when player has stopped moving. + private bool spriteFacingLeft; + private bool wasWalled; // Remember if the player was on a wall or ground. private float hspAcc; // Horizontal acceleration private float hspFrictionGround; // Ground friction private float hspFrictionAir; // Air friction @@ -24,11 +30,32 @@ namespace LunaLightXMG private float grvWall; // Gravity when wall sliding private float vspMaxWall; // Maximum vertical speed when wall sliding private float vspJump; // Jump speed - private Texture2D texture; public Vector2 position; public BoundingBox boundingBox { get; private set; } public CollisionManager.CollisionSide collisionSide { get; private set; } private CollisionManager collisionManager; + + // Sprites + private Sprite wallSlide; + private AnimatedSprite animatedSprite; + + public enum PlayerState + { + Idle, + Run, + Jump, + WallSlide, + Climb, + ClimbLedge, + Edge, + Stop, + Ready, + Attack1, + Attack2, + Attack3 + } + + public PlayerState currentState; // make private before release ;) @@ -42,10 +69,12 @@ namespace LunaLightXMG grounded = false; walled = false; wasWalled = false; + spriteFacingLeft = false; + stopped = true; hspAcc = 0.2f; hspFrictionGround = 0.3f; hspFrictionAir = 0.3f; - hspWalk = 3f; + hspWalk = 2.5f; grv = 0.2f; vspMax = 10f; grvWall = 0.1f; @@ -58,30 +87,240 @@ namespace LunaLightXMG public void LoadContent(ContentManager content) { - texture = content.Load("player"); - } + // Load Player SpriteSheet + Texture2D texture2D = content.Load("Ro_Sprites/RoSpriteSheet"); + Texture2DAtlas atlas = Texture2DAtlas.Create("Atlas/RoSpriteSheet", texture2D, 18, 18); + SpriteSheet spriteSheet = new SpriteSheet("SpriteSheet/RoSpriteSheet", atlas); + + // Single Sprites + wallSlide = atlas.CreateSprite(regionIndex: 47); + // Animated Sprites + spriteSheet.DefineAnimation("climbLedge", builder => + { + builder.IsLooping(true) + .AddFrame(regionIndex: 0, duration: TimeSpan.FromSeconds(0.05)) + .AddFrame(1, TimeSpan.FromSeconds(0.05)) + .AddFrame(2, TimeSpan.FromSeconds(0.05)) + .AddFrame(3, TimeSpan.FromSeconds(0.05)) + .AddFrame(4, TimeSpan.FromSeconds(0.05)) + .AddFrame(5, TimeSpan.FromSeconds(0.05)) + .AddFrame(6, TimeSpan.FromSeconds(0.05)) + .AddFrame(7, TimeSpan.FromSeconds(0.05)); + }); + + spriteSheet.DefineAnimation("edge", builder => + { + builder.IsLooping(true) + .AddFrame(8, TimeSpan.FromSeconds(0.05)) + .AddFrame(9, TimeSpan.FromSeconds(0.05)) + .AddFrame(10, TimeSpan.FromSeconds(0.05)) + .AddFrame(11, TimeSpan.FromSeconds(0.05)) + .AddFrame(12, TimeSpan.FromSeconds(0.05)) + .AddFrame(13, TimeSpan.FromSeconds(0.05)) + .AddFrame(14, TimeSpan.FromSeconds(0.05)) + .AddFrame(15, TimeSpan.FromSeconds(0.05)); + }); + + spriteSheet.DefineAnimation("jump", builder => + { + builder.IsLooping(false) + .AddFrame(16, TimeSpan.FromSeconds(0.05)) + .AddFrame(17, TimeSpan.FromSeconds(0.05)) + .AddFrame(18, TimeSpan.FromSeconds(0.05)) + .AddFrame(19, TimeSpan.FromSeconds(0.05)) + .AddFrame(20, TimeSpan.FromSeconds(0.05)) + .AddFrame(21, TimeSpan.FromSeconds(0.05)) + .AddFrame(22, TimeSpan.FromSeconds(0.05)) + .AddFrame(23, TimeSpan.FromSeconds(0.05)); + }); + + spriteSheet.DefineAnimation("run", builder => + { + builder.IsLooping(true) + .AddFrame(24, TimeSpan.FromSeconds(0.05)) + .AddFrame(25, TimeSpan.FromSeconds(0.05)) + .AddFrame(26, TimeSpan.FromSeconds(0.05)) + .AddFrame(27, TimeSpan.FromSeconds(0.05)) + .AddFrame(28, TimeSpan.FromSeconds(0.05)) + .AddFrame(29, TimeSpan.FromSeconds(0.05)) + .AddFrame(30, TimeSpan.FromSeconds(0.05)) + .AddFrame(31, TimeSpan.FromSeconds(0.05)) + .AddFrame(32, TimeSpan.FromSeconds(0.05)) + .AddFrame(33, TimeSpan.FromSeconds(0.05)) + .AddFrame(34, TimeSpan.FromSeconds(0.05)) + .AddFrame(35, TimeSpan.FromSeconds(0.05)); + }); + + spriteSheet.DefineAnimation("stop", builder => + { + builder.IsLooping(false) + .AddFrame(36, TimeSpan.FromSeconds(0.05)) + .AddFrame(37, TimeSpan.FromSeconds(0.05)) + .AddFrame(38, TimeSpan.FromSeconds(0.05)) + .AddFrame(39, TimeSpan.FromSeconds(0.05)); + }); + + spriteSheet.DefineAnimation("ready", builder => + { + builder.IsLooping(true) + .AddFrame(40, TimeSpan.FromSeconds(0.2)) + .AddFrame(41, TimeSpan.FromSeconds(0.2)) + .AddFrame(42, TimeSpan.FromSeconds(0.2)) + .AddFrame(43, TimeSpan.FromSeconds(0.2)) + .AddFrame(44, TimeSpan.FromSeconds(0.2)) + .AddFrame(45, TimeSpan.FromSeconds(0.2)) + .AddFrame(46, TimeSpan.FromSeconds(0.2)); + }); + + spriteSheet.DefineAnimation("climb", builder => + { + builder.IsLooping(true) + .AddFrame(48, TimeSpan.FromSeconds(0.2)) + .AddFrame(49, TimeSpan.FromSeconds(0.2)) + .AddFrame(50, TimeSpan.FromSeconds(0.2)) + .AddFrame(51, TimeSpan.FromSeconds(0.2)) + .AddFrame(52, TimeSpan.FromSeconds(0.2)) + .AddFrame(53, TimeSpan.FromSeconds(0.2)) + .AddFrame(54, TimeSpan.FromSeconds(0.2)) + .AddFrame(55, TimeSpan.FromSeconds(0.2)) + .AddFrame(56, TimeSpan.FromSeconds(0.2)) + .AddFrame(57, TimeSpan.FromSeconds(0.2)); + }); + + spriteSheet.DefineAnimation("idle", builder => + { + builder.IsLooping(true) + .AddFrame(58, TimeSpan.FromSeconds(0.2)) + .AddFrame(59, TimeSpan.FromSeconds(0.2)) + .AddFrame(60, TimeSpan.FromSeconds(0.2)) + .AddFrame(61, TimeSpan.FromSeconds(0.2)); + }); + + animatedSprite = new AnimatedSprite(spriteSheet, "idle"); + } + public void Update(GameTime gameTime, PlayerInput input, Platform[] platforms) { + animatedSprite.Update(gameTime); Movement(input); boundingBox.Update(position, boundingBox.Bounds.Width, boundingBox.Bounds.Height); DumpFractions(); CollisionChecks(platforms); + UpdateAnimation(); } public void Draw(SpriteBatch spriteBatch) { + // Adjust sprite draw position to align with bounding box + Vector2 drawOffset = new Vector2(-5, -1); + // Snap position to integer values - Vector2 drawPosition = new Vector2((int)position.X, (int)position.Y); - spriteBatch.Draw(texture, drawPosition, Color.White); + Vector2 drawPosition = new Vector2((int)position.X, (int)position.Y) + drawOffset; + + // Flip sprites when moving left. + SpriteEffects flipEffect = spriteFacingLeft ? SpriteEffects.FlipHorizontally : SpriteEffects.None; + + // Draw sprite + if (currentState == PlayerState.WallSlide) + { + spriteBatch.Draw(wallSlide.TextureRegion.Texture, + drawPosition, + wallSlide.TextureRegion.Bounds, + Color.White, + 0f, + Vector2.Zero, + 1f, + flipEffect, + 0f); + } + else + { + animatedSprite.Effect = flipEffect; + spriteBatch.Draw(animatedSprite, drawPosition); + } } - - // Method Implementation ------------------------------------------------------------------------------- + public void Reset(Vector2 initialPosition) { position = initialPosition; // Reset other player-specific states later } + + private void UpdateAnimation() + { + if (walled && !grounded) + { + currentState = PlayerState.WallSlide; + stopped = false; + } + else if (grounded) + { + if (hsp == 0.0f) + { + if (!stopped) + { + stopped = true; + HandleSetAnimation(PlayerState.Stop); + } + } + else + { + HandleSetAnimation(PlayerState.Run); + stopped = false; + } + } + else + { + HandleSetAnimation(PlayerState.Jump); + stopped = false; + } + + // TODO: Need logic for climbing, climgLedge, edge, attacks + + // TODO: Need timer to track idle time, then move to idle state after some amount of time + } + + private void HandleSetAnimation(PlayerState newState) + { + if (currentState == newState) return; + + currentState = newState; + + switch (currentState) + { + case PlayerState.Idle: + animatedSprite.SetAnimation("idle"); + break; + case PlayerState.Run: + animatedSprite.SetAnimation("run"); + break; + case PlayerState.Jump: + animatedSprite.SetAnimation("jump"); + break; + case PlayerState.Stop: + animatedSprite.SetAnimation("stop").OnAnimationEvent += (sender, trigger) => + { + if (trigger == AnimationEventTrigger.AnimationCompleted) + { + HandleSetAnimation(PlayerState.Ready); + } + }; + break; + case PlayerState.Ready: + animatedSprite.SetAnimation("ready"); + break; + case PlayerState.WallSlide: + // not sure yet + break; + case PlayerState.Climb: + animatedSprite.SetAnimation("climb"); + break; + case PlayerState.ClimbLedge: + animatedSprite.SetAnimation("climbLedge"); + break; + } + } private void Movement(PlayerInput input) { @@ -89,10 +328,10 @@ namespace LunaLightXMG { vsp = vspJump; } - + // Horizontal movement with acceleration + wall jump delay wallJumpDelay = Math.Max(wallJumpDelay - 1, 0); // counts down every frame - + if (wallJumpDelay == 0) // no left-right control until delay is 0, prevents rapid wall climb { float moveH = input.KeyRight - input.KeyLeft; @@ -103,14 +342,22 @@ namespace LunaLightXMG float hspFrictionFinal = grounded ? hspFrictionGround : hspFrictionAir; hsp = Approach(hsp, 0, hspFrictionFinal); } - + hsp = MathHelper.Clamp(hsp, -hspWalk, hspWalk); + if (hsp < 0) + { + spriteFacingLeft = true; + } + else if (hsp > 0) + { + spriteFacingLeft = false; + } } - + // Calculate vertical movement float grvFinal = grv; float vspMaxFinal = vspMax; - + if (walled && vsp > 0) { grvFinal = grvWall; @@ -156,8 +403,10 @@ namespace LunaLightXMG collisionSide = CollisionManager.GetCollisionSide(position, boundingBox, platforms); } + - private float Approach(float start, float end, float shift) + // TODO: Move below to utilities class + private static float Approach(float start, float end, float shift) { if (start < end) { diff --git a/UpdateNotes/Player.md b/UpdateNotes/Player.md new file mode 100644 index 0000000..f89851c --- /dev/null +++ b/UpdateNotes/Player.md @@ -0,0 +1,196 @@ +## March 31st, 2025 - JME + + **Animated Sprites** + +We are going to use the MonoGame.Extended framework for sprite animation. I have set up the project to use the extended frmework and implemented a simple test in the player class. + +I learned that we should be creating sprite sheets for each character that include the animations for each state of the character, e.g. "run", "jump", "attack", etc. +The sprite sheet can be thought of as a single dimension array represented as a matrix. Each index in the array represents a frame. Lets say our run animation is 6 frames, our jump animation is 4 frames and our attack animation is 10 frames and we draw each frame in the sprite sheet whereeach frame is 18px by 18px and our sprite sheet is 90px by 72px. Then our frames can be defined as follows + + [00][01][02][03][04] + [05][06][07][08][09] + [10][11][12][13][14] + [15][16][17][18][19] + +Where "run" is index 00 to 05, "jump" is index 06 to 09 and "attack" is index 10 to 19. + +Then from our single sprite sheet we can define the different animations for each state. + + Texture2D texture2D = content.Load("Path/To/SpriteSheet"); + Texture2DAtlas atlas = Texture2DAtlas.Create("Atlas/SpriteSheet", texture2D, 18, 18); + SpriteSheet spriteSheet = new SpriteSheet("SpriteSheet/SpriteSheet", atlas); + + spriteSheet.DefineAnimation("run", builder => + { + builder.IsLooping(true) + .AddFrame(regionIndex: 0, duration: TimeSpan.FromSeconds(0.05)) + .AddFrame(1, TimeSpan.FromSeconds(0.05)) + .AddFrame(2, TimeSpan.FromSeconds(0.05)) + .AddFrame(3, TimeSpan.FromSeconds(0.05)) + .AddFrame(4, TimeSpan.FromSeconds(0.05)) + .AddFrame(5, TimeSpan.FromSeconds(0.05)); + }); + + spriteSheet.DefineAnimation("jump", builder => + { + builder.IsLooping(true) + .AddFrame(regionIndex: 0, duration: TimeSpan.FromSeconds(0.05)) + .AddFrame(6, TimeSpan.FromSeconds(0.05)) + .AddFrame(7, TimeSpan.FromSeconds(0.05)) + .AddFrame(8, TimeSpan.FromSeconds(0.05)) + .AddFrame(9, TimeSpan.FromSeconds(0.05)); + }); + + spriteSheet.DefineAnimation("attack", builder => + { + builder.IsLooping(true) + .AddFrame(regionIndex: 0, duration: TimeSpan.FromSeconds(0.05)) + .AddFrame(10, TimeSpan.FromSeconds(0.05)) + .AddFrame(11, TimeSpan.FromSeconds(0.05)) + .AddFrame(12, TimeSpan.FromSeconds(0.05)) + .AddFrame(13, TimeSpan.FromSeconds(0.05)) + .AddFrame(14, TimeSpan.FromSeconds(0.05)) + .AddFrame(15, TimeSpan.FromSeconds(0.05)) + .AddFrame(16, TimeSpan.FromSeconds(0.05)) + .AddFrame(17, TimeSpan.FromSeconds(0.05)) + .AddFrame(18, TimeSpan.FromSeconds(0.05)) + .AddFrame(19, TimeSpan.FromSeconds(0.05)); + }); + +## April 1st, 2025 - JME + +**More sprite animation** + +Added RoSpriteSheet containing animations for run. jump, climbLedge, climb, wallStatic, edge and idle states. + +Added a drawOffset to align the 18px by 18px player sprites with the 8px by 16px bounding box. + + public void Draw(SpriteBatch spriteBatch) + { + // Adjust sprite draw position to align with bounding box + Vector2 drawOffset = new Vector2(-4, -1); + // Snap position to integer values + Vector2 drawPosition = new Vector2((int)position.X, (int)position.Y) + drawOffset; + //spriteBatch.Draw(texture, drawPosition, Color.White); + spriteBatch.Draw(animatedSprite, drawPosition); + } + +## April 2nd, 2025 - JME + +**Handling player states and animations** + +Created initial sprite animation handling. We have an UpdateAnimation method that detects player states and updates the animation based on the state. + + private void UpdateAnimation() + { + if (grounded) + { + if (Math.Abs(hsp) > 0.1f) + { + HandleSetAnimation(PlayerState.Run); + } + else + { + HandleSetAnimation(PlayerState.Idle); + } + } + else if (vsp < 0) + { + HandleSetAnimation(PlayerState.Jump); + } + } + +We still need logic for stop, ready, wallSlide, climbing, climgLedge, edge, attacks + +We will need a timer to track idle time, as well as some other things probably. + +The HandleSetAnimation method switches the animation based on state as well as other variables that might need to be set during an animation switch. + + private void HandleSetAnimation(PlayerState newState) + { + if (currentState == newState) return; + + currentState = newState; + + switch (currentState) + { + case PlayerState.Idle: + animatedSprite.SetAnimation("idle"); + break; + case PlayerState.Run: + animatedSprite.SetAnimation("run"); + break; + case PlayerState.Jump: + animatedSprite.SetAnimation("jump"); + break; + case PlayerState.Stop: + animatedSprite.SetAnimation("stop"); + break; + case PlayerState.Ready: + animatedSprite.SetAnimation("ready"); + break; + case PlayerState.WallSlide: + animatedSprite.SetAnimation("wallSlide"); + break; + case PlayerState.Climb: + animatedSprite.SetAnimation("climb"); + break; + case PlayerState.ClimbLedge: + animatedSprite.SetAnimation("climbLedge"); + break; + } + } + +## April 3rd, 2025 - JME + +Figured out how to flip our player sprites when moving left by creating a boolean **spriteFacingLeft** that ses to true when player.hsp is less than zero and false when player.hp is greater than zero. + +Then we can create a sprite efect using SpriteEffects that changes fom FlipHorizontally to None depending on spriteFacingLeft. + +The correct overload for a static sprite with a sprite effect was tricky to figure out, the best I could find is the following, + + spriteBatch.Draw(wallSlide.TextureRegion.Texture, + drawPosition, + wallSlide.TextureRegion.Bounds, + Color.White, + 0f, + Vector2.Zero, + 1f, + flipEffect, + 0f); + +So for further static sprite effects we'll probably want to use this overload. + +Here is the full updated draw method for the player class + + public void Draw(SpriteBatch spriteBatch) + { + // Adjust sprite draw position to align with bounding box + Vector2 drawOffset = new Vector2(-5, -1); + + // Snap position to integer values + Vector2 drawPosition = new Vector2((int)position.X, (int)position.Y) + drawOffset; + + // Flip sprites when moving left. + SpriteEffects flipEffect = spriteFacingLeft ? SpriteEffects.FlipHorizontally : SpriteEffects.None; + + // Draw sprite + if (currentState == PlayerState.WallSlide) + { + spriteBatch.Draw(wallSlide.TextureRegion.Texture, + drawPosition, + wallSlide.TextureRegion.Bounds, + Color.White, + 0f, + Vector2.Zero, + 1f, + flipEffect, + 0f); + } + else + { + animatedSprite.Effect = flipEffect; + spriteBatch.Draw(animatedSprite, drawPosition); + } + } + \ No newline at end of file