From 843c254cecb5db8c158e3bf50b604e3f920fd0f7 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Mon, 23 Jan 2023 22:29:04 -0800 Subject: [PATCH] Add animated player sprite --- asset/asset.go | 45 ++++++++ asset/image-source/player.aseprite | Bin 0 -> 8541 bytes asset/image/player.png | Bin 0 -> 3136 bytes component/player.go | 166 ++++++++++++++++++++++++----- game/game.go | 18 ++-- system/map.go | 2 + system/player.go | 30 +++++- 7 files changed, 226 insertions(+), 35 deletions(-) create mode 100644 asset/asset.go create mode 100644 asset/image-source/player.aseprite create mode 100644 asset/image/player.png diff --git a/asset/asset.go b/asset/asset.go new file mode 100644 index 0000000..0e0c9c3 --- /dev/null +++ b/asset/asset.go @@ -0,0 +1,45 @@ +package asset + +import ( + "embed" + "image" + + "github.com/hajimehoshi/ebiten/v2" + + _ "image/png" +) + +//go:embed image +var FS embed.FS + +const tileSize = 64 + +var ImgPlayer = LoadImage("image/player.png") + +func LoadImage(p string) *ebiten.Image { + f, err := FS.Open(p) + if err != nil { + panic(err) + } + defer f.Close() + + baseImg, _, err := image.Decode(f) + if err != nil { + panic(err) + } + + return ebiten.NewImageFromImage(baseImg) +} + +func LoadBytes(p string) []byte { + b, err := FS.ReadFile(p) + if err != nil { + panic(err) + } + return b +} + +func FrameAt(img *ebiten.Image, x int, y int) *ebiten.Image { + xPos, yPos := x*tileSize, y*tileSize + return img.SubImage(image.Rect(xPos, yPos, xPos+tileSize, yPos+tileSize)).(*ebiten.Image) +} diff --git a/asset/image-source/player.aseprite b/asset/image-source/player.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..536a7478e123ff30c97f720d9fa83ffdd641e857 GIT binary patch literal 8541 zcmeI2ZA@EL7{?Ek0>WS|C8I!TZv{5+18?1?#Fp}+6DV^XXcc7&D{n)FSqo;wOcqCm zGL}#KqQ0JaPRSZNQa~<#{i=RWBIWgog)Z zH+hq}5j+p1q;wBO-`8$dLhhxuLLZKhLkr_$p>>)Ep+6tk3>|JRgwFOgK>t|S4n1{Q z4K2AC1HJ!b8C10|8k(sJgT~3@p-N>rG$!|D=+kARP;H|X`s~0s^vfSCP%9cCjqOHg z^-DF-eI+H(Y)vNgh2&ajTU;D;_z5layBE8lb%u0k?ejM4Xx9A@H=FluOT<#J4lDvI z06y3O4r(BS7qB1&CKv$-I-r3IP&Oi~6)XS&1rT5##$g#|VG{;1zu0Ds;y_oMjl!L% zOY#J@gqo7FtcdsSs_UknmCv=)Mcy;<4cQ=xjUs@m{5zgIQqOK^@7Dtju z=YBVmRbElH4V_#HGvl%%RBkjL!sX!G+T7fwONr9W0k2IY0# zT*Q#gmSd17rl1EM4z?5t>m~J)&lgBtcJ$Y&_5rShA|W>o5D*Xh0LpVECHDN_Xx6c) zN82i=N)DXb=|zo8GJD+u1=>xW%dd1A@(IaF5shEOJj?SOdobJl%hJOMw#NFNjU4O> zcGt3J4trv3gjhs9CUCTxebCJWI_pI;l zY}c~cDCQ#P|Lpr-r0o9tJZ1*0{^Ku0fyPzC=^JmT>1Yyu_|@X5p3^zOKmF!oM!r{Y zd6l-$k&>Jz0uzUZfQmnuvRNvVni{fdk?IcY5} z-Me@~8-O&r!W?krV@CK>Zp*(BwEP?paa#H62gLUz{rBaXx4b)kI5;>b<^Q*#}*SZ;9XRc}9G$ zqQ82(6r^rJQKJ>0`uJv=dq$>8&2j(+ZwT zh#XOJ=4X3b0yYpulfJ=^iwg2; zeG|0Z?d}GVRF)Ro=)X-kO;lHSb9YlT@Eo+HXNV-krw@*0StT_;R>^hMGu^FXGEvL_ z?NdhLH;sA-YPhKvIh!q%~(e;PDSK}{xb+@Xm Pa`2;oCOH|t&aeLycHYsL literal 0 HcmV?d00001 diff --git a/asset/image/player.png b/asset/image/player.png new file mode 100644 index 0000000000000000000000000000000000000000..82861b51d5a7ee67883069259f7024dec518157a GIT binary patch literal 3136 zcmdT`ZB!Fi8on{ququWFqFY%F*5l#qy47u0(G_*D(o)*;A&X$4I9pwhSPG%Q@*y8L zY8Av5YAxayCMq9Ctd3d*L}j!tn^v>|V-l7Ci6Rm*KtjnFGGS&qL)hIPWqMd8)8e=6^(v>-s`Oxd$z zF1?kN(3HIuLv|I=xh9=F2xr#VhzJGB&ZkKKRt$=1%G(4DOlb{1o@fA!+h)jI$?^Or zaCi0Az4cqrS2rk$8laW44P^y1RiCir;6%f8(A=trk@PUe_&<}Z1u3J2Gi%gPmr^uB z6pqbVND_F~!DAG42y#?`9$luxxx z(*g#LG!uoD{6=4s4nuLK`FQ!9V8Fq~;+ct4Wt7Ef|C zOGG2>>};V?D0kCl?OtD?YdOX{FTIwi##l>vVGRm<8Bq@4d}E3jvp*mS9R~M&$ILrD z#Adciv?!m99tz2Yd$zyd>DTgI5pIn)>ELNNY!+U)yTC>uZFn{*i0$>H2%B62?u~R| zeY!bC*`8mIdnS%M>4s^$$B$(lq_x(-_G4Wzv`UGWn_w8LpCKcBGKSOkiyL{*(Cqi2 zfXf1=$DazmV7%!Av69q{GdG;Ysow-~^90tx?RDbs+H~ge?7T)x{|xH1b)AB@)ENFc zO)ch{%XjJdpkIfv-?l8GbhQ=W11{WP_OV9vLD~9f0xMu^@JyI=Rl?R)vqen(Tp$1n zFR{!PTaBkc(p$zIq=S!oz3B6rCN?Pyk_Fkzg?=16tWo2sMvwAjx$(o|rt6jV&B->4 ztj!R@w%hD&NC2I0D-DAZdaHU;2p)-F@%CQnt~T7VX&UV;scQYDo#J{kn@t9~xhQ>7 z`CoER!uJ;ta)tHW*E6>5yzM*^XkdJF!$*I1@d)RCYww7UuHA-+!O=RtkXKBCvdPF1EYBtxETLW@6=CTGFVL{v{lyc)lR zT)w%S_NBJ!)+)BOlb?H*;c9q0ANI5^PvCTDeY2QOm5 z9#blWiHpjd3qw?*sTZ|Zlnv?MI`z%+o!e&qcuR>~W(CxpHP+>kl^zQ>B^jXC60@qA z3TVhUsA<{2i?j7T>1$wZ>XgL%T@4$al;n`?BmIiJfpXEl{95(-B`1I{_500|PkNr9 zc}|ZFgJlsm`8ALE1!jFBvwDm^l)5qD<=b1DwE8U$yD=m1D>L_0$hK!65Lmx#4*h}z zv$n6y>3H zwyKHQF{qdC_qx9#+QQpQ;RNXdF3cNFJL(OHL#N89r4{bs5?S22oYVIAoRYGC1@)Bb zjrRtva$mW)c1_FexZX_uBKeJVG4>~DQpc&w#ZdHz0PI!K#k=)|3#?KfT)=-NWO929 zr$fNG4WFy$Vl=Wm^YngBLIqJ5##jdy6Ywp$rl@9ULqe< VXc{fiQ}|Fr-wY4F@W$VE{{)>WL6`sl literal 0 HcmV?d00001 diff --git a/component/player.go b/component/player.go index 1339b96..0122b96 100644 --- a/component/player.go +++ b/component/player.go @@ -4,6 +4,10 @@ import ( "fmt" "image" "image/color" + + "code.rocketnine.space/tslocum/boxbrawl/asset" + + "github.com/hajimehoshi/ebiten/v2" ) type PlayerAction int @@ -18,7 +22,7 @@ const ( type HitboxType int const ( - HitboxInvalid HitboxType = iota + HitboxNone HitboxType = iota HitboxNormal HitboxHurt ) @@ -26,22 +30,119 @@ const ( type FrameData struct { T HitboxType R image.Rectangle + S *ebiten.Image } -const PlayerSize = 20 +const PlayerSize = 16 + +const ( + PlayerHeight = 48 + PlayerWidth = 16 +) + +// stdHit returns FrameData using a standard hitbox and the provided sprite. +func stdHit(sprite *ebiten.Image) FrameData { + return FrameData{ + T: HitboxNormal, + R: image.Rect(0, 0, PlayerWidth, PlayerHeight), + S: sprite, + } +} // AllPlayerFrames defines all frame data for the game. Frames are defined in reverse order. var AllPlayerFrames = [][][]FrameData{ { // ActionIdle { - { - T: HitboxNormal, - R: image.Rect(0, 0, PlayerSize, PlayerSize), - }, + stdHit(asset.FrameAt(asset.ImgPlayer, 0, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 1, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 2, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 3, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 4, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 5, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 6, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 7, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 8, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 9, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 10, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 11, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 12, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 13, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 14, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 15, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 16, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 17, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 18, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 19, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 20, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 21, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 22, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 23, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 24, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 25, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 26, 0)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 27, 0)), }, }, { // ActionStunned { - // No hitboxes or hurtboxes while stunned. + FrameData{ + T: HitboxNone, + R: image.Rect(0, 0, 0, 0), + S: asset.FrameAt(asset.ImgPlayer, 0, 2), + }, }, }, { // ActionBlock { @@ -52,6 +153,7 @@ var AllPlayerFrames = [][][]FrameData{ { T: HitboxNormal, R: image.Rect(0, 0, PlayerSize, PlayerSize), + S: asset.FrameAt(asset.ImgPlayer, 12, 1), }, { T: HitboxHurt, @@ -59,34 +161,40 @@ var AllPlayerFrames = [][][]FrameData{ }, }, { - { - T: HitboxNormal, - R: image.Rect(0, 0, PlayerSize, PlayerSize), - }, + stdHit(asset.FrameAt(asset.ImgPlayer, 11, 1)), }, { - { - T: HitboxNormal, - R: image.Rect(0, 0, PlayerSize, PlayerSize), - }, + stdHit(asset.FrameAt(asset.ImgPlayer, 10, 1)), }, { - { - T: HitboxNormal, - R: image.Rect(0, 0, PlayerSize, PlayerSize), - }, + stdHit(asset.FrameAt(asset.ImgPlayer, 9, 1)), }, { - { - T: HitboxNormal, - R: image.Rect(0, 0, PlayerSize, PlayerSize), - }, + stdHit(asset.FrameAt(asset.ImgPlayer, 8, 1)), }, { - { - T: HitboxNormal, - R: image.Rect(0, 0, PlayerSize, PlayerSize), - }, + stdHit(asset.FrameAt(asset.ImgPlayer, 7, 1)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 6, 1)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 5, 1)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 4, 1)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 3, 1)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 2, 1)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 1, 1)), + }, + { + stdHit(asset.FrameAt(asset.ImgPlayer, 0, 1)), }, }, } @@ -94,6 +202,10 @@ var AllPlayerFrames = [][][]FrameData{ func FrameDataForActionTick(a PlayerAction, tick int) []FrameData { actionFrames := AllPlayerFrames[a] if tick-1 >= len(actionFrames) { + // Handle + if a == ActionStunned { + return AllPlayerFrames[ActionStunned][0] + } return nil } return actionFrames[tick-1] diff --git a/game/game.go b/game/game.go index c4f18b3..20b7861 100644 --- a/game/game.go +++ b/game/game.go @@ -20,6 +20,10 @@ import ( "github.com/hajimehoshi/ebiten/v2/inpututil" ) +const ( + playerSpawnGap = 50 +) + type Game struct { Players []component.Player } @@ -31,17 +35,17 @@ func NewGame() (*Game, error) { PlayerNum: 1, Color: color.RGBA{255, 0, 0, 255}, Action: component.ActionIdle, - ActionTicksLeft: 1, - X: 50, - Y: 50, + ActionTicksLeft: len(component.AllPlayerFrames[component.ActionIdle]), + X: -playerSpawnGap / 2, + Y: 0, } player2 := component.Player{ PlayerNum: 2, Color: color.RGBA{0, 0, 255, 255}, Action: component.ActionIdle, - ActionTicksLeft: 1, - X: 150, - Y: 50, + ActionTicksLeft: len(component.AllPlayerFrames[component.ActionIdle]), + X: playerSpawnGap / 2, + Y: 0, } g := &Game{ @@ -315,7 +319,7 @@ func (g *Game) UpdateByInputs(inputs []InputBits) { // Stun the opponent. opponent.Action = component.ActionStunned - opponent.ActionTicksLeft = 7 + opponent.ActionTicksLeft = 14 } } } diff --git a/system/map.go b/system/map.go index 7e02e48..ecac063 100644 --- a/system/map.go +++ b/system/map.go @@ -37,6 +37,8 @@ func (s *MapSystem) Draw(e gohan.Entity, screen *ebiten.Image) error { return nil } + screen.Fill(color.RGBA{0, 100, 100, 255}) + groundColor := color.RGBA{50, 50, 50, 255} r := image.Rect(-world.ScreenWidth*2, -world.ScreenHeight, world.ScreenWidth*2, 0) diff --git a/system/player.go b/system/player.go index 434773b..91756c5 100644 --- a/system/player.go +++ b/system/player.go @@ -38,12 +38,40 @@ func (s *PlayerSystem) Draw(e gohan.Entity, screen *ebiten.Image) error { } size := 20 - var p *component.Player + var p, o *component.Player for i := 0; i < 2; i++ { if i == 0 { p = &world.Player1 + o = &world.Player2 } else { p = &world.Player2 + o = &world.Player1 + } + + var sprite *ebiten.Image + frameData := component.FrameDataForActionTick(p.Action, p.ActionTicksLeft) + for _, frame := range frameData { + if frame.S == nil { + continue + } + sprite = frame.S + break + } + + if sprite != nil { + x, y := world.GameCoordsToScreen(p.X-24, p.Y+64) + op := &ebiten.DrawImageOptions{} + + if component.PlayerOnRightSide(*p, *o) { + op.GeoM.Scale(-1, 1) + op.GeoM.Translate(64, 0) + } + + op.GeoM.Translate(float64(x), float64(y)) + op.ColorM.ScaleWithColor(p.Color) + + screen.DrawImage(sprite, op) + continue // TODO } r := image.Rect(int(p.X), int(p.Y), int(p.X)+size, int(p.Y)+size)