From 9de48f0babe14b550ce9f547239fbf62b8654b5d Mon Sep 17 00:00:00 2001 From: Sean Hickey Date: Thu, 13 Oct 2022 14:37:25 -0700 Subject: [PATCH] Add simple walk animation from opengameart --- .../a-platformer-in-the-forest/README.md | 23 ++++ .../a-platformer-in-the-forest/characters.png | Bin 0 -> 12754 bytes .../a-platformer-in-the-forest/sheet.png | Bin 0 -> 10314 bytes .../a-platformer-in-the-forest/swoosh.png | Bin 0 -> 3154 bytes internal/game/entity/penguin.go | 52 +++------- internal/game/entity/penguin_animations.go | 33 ++++++ internal/game/sprite/animation.go | 98 ++++++++++++++++++ internal/game/sprite/sprite.go | 84 --------------- internal/game/sprite/sprite_cache.go | 80 -------------- internal/game/sprite/spritesheet.go | 92 ++++++++++++++++ internal/game/sprite/spritesheet_cache.go | 85 +++++++++++++++ main.go | 4 +- 12 files changed, 348 insertions(+), 203 deletions(-) create mode 100644 assets/images/a-platformer-in-the-forest/README.md create mode 100644 assets/images/a-platformer-in-the-forest/characters.png create mode 100644 assets/images/a-platformer-in-the-forest/sheet.png create mode 100644 assets/images/a-platformer-in-the-forest/swoosh.png create mode 100644 internal/game/entity/penguin_animations.go create mode 100644 internal/game/sprite/animation.go delete mode 100644 internal/game/sprite/sprite.go delete mode 100644 internal/game/sprite/sprite_cache.go create mode 100644 internal/game/sprite/spritesheet.go create mode 100644 internal/game/sprite/spritesheet_cache.go diff --git a/assets/images/a-platformer-in-the-forest/README.md b/assets/images/a-platformer-in-the-forest/README.md new file mode 100644 index 0000000..59234b1 --- /dev/null +++ b/assets/images/a-platformer-in-the-forest/README.md @@ -0,0 +1,23 @@ +# A Platformer in the Forest + +These sprites are from opengameart.org: +https://opengameart.org/content/a-platformer-in-the-forest + +They were created by Buch: +http://blog-buch.rhcloud.com + +They are licensed as CC0 so no attribution is required, but I'd still like to give them credit. + +We're using this temporarily to test sprites and walk cycles before creating our own art. + +## Notes +Each sprite seems to be 32x32 + +* WALK: frames 1,2,3,4, cycle +* JUMP: frame 5 for "jump preparation", frame 6 for moving upwards, frame 7 for moving downards and frame 8 for landing +* HIT: frames 9,10,9 +* SLASH: frames 11,12,13 (you might use them in the order 12,11,12,13 if you want an extra "preparation" frame before the actual slash) +* PUNCH: 14,12 (again, you might use them in the order 12,14,12) +* RUN: 15-18 +* CLIMB: 19-22 +* a single frame with character's back (frame 23) diff --git a/assets/images/a-platformer-in-the-forest/characters.png b/assets/images/a-platformer-in-the-forest/characters.png new file mode 100644 index 0000000000000000000000000000000000000000..432e6c6197172d508c1e70420c94c6b7297427ca GIT binary patch literal 12754 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 literal 0 HcmV?d00001 diff --git a/assets/images/a-platformer-in-the-forest/sheet.png b/assets/images/a-platformer-in-the-forest/sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..939cb2835adae278f6ac31d8ea82e21ba3f9139a GIT binary patch literal 10314 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 z zBiT8DSUC&`u>f4{=YZT1I}^a)g`wNcHBq0?_k#4<2f-N5Fqhgi)d$lPvtB0q0^swO zf#-5MARZOl?N1ZqdP%kpw(-1j*7^XypNX`a=~d&gu}Sis5C9TB z8U}={;7;akTOhn)N(|ssKnwEE!)0Ocbqj>b5YnCeYU2FcPGAP1i_k-p0LkDXwPO4L z$3Qs@8zS!YxJ=ISNUWP(5^Gmis|P-h4n5Lqi3{>nm!F$-yhRHPX9I7T*MPCmDXXHO zSYbT1%d6F|6vu?3@1L(kd%$XQ2+T~|CEo{`^g5@3+M9zX`+lfvBI_u<6ceVSr zTW-#g7~k)q6>@a%JOH{yeX@PgzPjN^>BDsr6_cXpdnUk74v*&?-*^yhhVHQXc$WRG z%}#?(eL)Lav1VpNFX5(W6r*e8{0HErELHsyu%9UL%`G^$waMOrvV?2!RPj8ROaQd! zYtLCwi<)N!VRJ5J*>Nl9ZBR;0K{mpa(7;-EH8O{Z$l!mxPNknI_=;yY**i4(Z2na7 zDC}Uo5gxa^@~TWR^hE+UMzj9ZjBinW5pgpZixdP;VUi9M zVtxD9fUsV>iI)jCRQCPt-X-eqvjw%Ahk!R9nvtubvodbc{}T!xa&dr)KfNLbIh*2? zObg@XYwkl&v%f7Y2DGhJ?!fa@6BN{wq*~x~~ci zEEKSt^>15=9HWnk5nslZju3_&X5r|;KP4bVsEUcIPX{NtSadRjAKdN`%bFW;fY=4Q z=^~%~ZnbDsu%0_Pd$EzNV`Mpb)i2dZ^we}CLib*2`}kLW0-R}GQ`SoCa50M=R2s5l z)3r;XOKf|HIbGwb@H45HMHrJuy7z-O^S{lYij(W2@p*-&F0oAt9%e*CyLHX+2Gp$} zzw(Wy&Ds9_lB=&cB^N$vn8Nd0(~~(0pIS1Q5V>WGj}9v*kx(PPzgvTF`nuB^C{}y# z4p*Q46S|QJ2})J0I1MVbDl+5-=32_e>B&a0jLP&Vcrz@TO zG>$sHcu>eA9lprT!`O^*im8nkjO4tiMWwvo zu5y2HuUTzRRRjF#+dRi!`ol@~6b{#bCSQ}ieA}qRw#W}nQwW?y;d#6Gr;Gi@vi+3~ z|2*A$!zIH<2u|xl1ED+C=4;7H9j7q81Y6g}!r%7ttD>-*zC@!ItBU;&E)z+_{> z%asNxp2n2$arIyIN~9hes)x;y*s-EC(qPhTh$J--3e1JwQdJ*(H11q%w!BlJA6YRa z3(?~K__W1N1C+r}WxmpOh}UI2-xl7Vt@II$|J%JZVc=NemvKK?vGA^X`cYncA!7R` z5onozM`0csUDl3ZXze@*%aX51)UxrkL5MslJ4L4vg@#rlQnE!~3$^wy^i*aWq!53$ zvXy<&Tt1qeO^y(8D5<&8p3DepV#R*v9wu}FMf04v)g~FcuJwaOZUv z{G32GdqdLt;OW{utNY(IHu}&DrcuX%Z=9qwY+sE)uss}*u#2ga&TbX&XYGH!Q1j)YwAMNCNLef|Di?ju?s%DUkg0sx;E(r^iW9H9A(Iz=c`9yly&LVmkqg>_- zCFL0-qM_E~x%sUK5|wGzE7>duxnp9Fy-G?r4xx>e(Yl;RbVZlVF z(*4zqETaBNha=W{9LS~h?|wg;1C@Zcj804}^UM0|{j)F-m$1Ewd&NWoKdc30C6y#9 I#Eb*}4+*yQXaE2J literal 0 HcmV?d00001 diff --git a/assets/images/a-platformer-in-the-forest/swoosh.png b/assets/images/a-platformer-in-the-forest/swoosh.png new file mode 100644 index 0000000000000000000000000000000000000000..c582c1df39fdd98773d08da54cb9ea6a3fa3fae1 GIT binary patch literal 3154 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 literal 0 HcmV?d00001 diff --git a/internal/game/entity/penguin.go b/internal/game/entity/penguin.go index d9398fe..30a58bc 100644 --- a/internal/game/entity/penguin.go +++ b/internal/game/entity/penguin.go @@ -1,79 +1,55 @@ package entity import ( - "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/sprite" "github.com/veandco/go-sdl2/sdl" ) -type Sprite interface { - Draw(section *sdl.Rect, placement *sdl.Rect) error -} - type penguin struct { - renderer *sdl.Renderer - position *sdl.Point - sprite Sprite + worldPosition *sdl.Point + animationStep int32 } func NewPenguin(renderer *sdl.Renderer) *penguin { - sprite := sprite.GetSprite("assets/images/penguin.png") - - pos := sdl.Point{X: 0, Y: 0} + position := sdl.Point{} p := penguin{ - renderer: renderer, - sprite: sprite, - position: &pos, + worldPosition: &position, } return &p } func (p *penguin) Draw() error { - // This is where on the screen to plop that sprite sheet chunk - placement := &sdl.Rect{X: p.position.X, Y: p.position.Y, W: 200, H: 200} - - // Select the section of the spritesheet to draw - // TODO this will depend on animation cycle - section := &sdl.Rect{X: 0, Y: 0, W: 100, H: 100} - - err := p.sprite.Draw(section, placement) + a := penguinAnimations[PENGUIN_WALK_RIGHT] + step := p.animationStep / 10 + err := a.Draw(step, p.worldPosition) if err != nil { return err } - return nil -} - -func (p *penguin) SdlDrawNoTexture() error { - sdl.Do(func() { - p.renderer.SetDrawColor(0, 0, 255, 255) - rect := &sdl.Rect{X: p.position.X, Y: p.position.Y, W: 200, H: 200} - p.renderer.DrawRect(rect) - p.renderer.FillRect(rect) - }) + p.animationStep += 1 return nil } -func (p *penguin) MoveTo(point *sdl.Point) { - p.position = point +func (p *penguin) SetPosition(point *sdl.Point) { + p.worldPosition = point } func (p *penguin) MoveRight() { - p.position.X += 1 + p.worldPosition.X += 1 } func (p *penguin) MoveLeft() { - p.position.X -= 1 + p.worldPosition.X -= 1 } func (p *penguin) MoveUp() { // (0,0) is the top left, so negative y moves up - p.position.Y -= 1 + p.worldPosition.Y -= 1 } func (p *penguin) MoveDown() { // positive y moves down - p.position.Y += 1 + p.worldPosition.Y += 1 } diff --git a/internal/game/entity/penguin_animations.go b/internal/game/entity/penguin_animations.go new file mode 100644 index 0000000..20fdec9 --- /dev/null +++ b/internal/game/entity/penguin_animations.go @@ -0,0 +1,33 @@ +package entity + +import ( + "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/sprite" + "github.com/veandco/go-sdl2/sdl" +) + +type Animation interface { + Draw(frame int32, worldPosition *sdl.Point) error +} + +var penguinAnimations map[string]Animation + +const ( + PENGUIN_WALK_RIGHT = "walk-right" +) + +func DefinePenguinAnimations() { + filename := sprite.PLATFORMER_FOREST_CHARACTERS + + var ( + dimensions sdl.Point + offset sdl.Point + length int32 + ) + + dimensions = sdl.Point{X: 32, Y: 32} + penguinAnimations = make(map[string]Animation) + + offset = sdl.Point{X: 0, Y: 2} + length = 4 + penguinAnimations[PENGUIN_WALK_RIGHT] = sprite.NewAnimation(filename, dimensions, offset, length) +} diff --git a/internal/game/sprite/animation.go b/internal/game/sprite/animation.go new file mode 100644 index 0000000..713bc64 --- /dev/null +++ b/internal/game/sprite/animation.go @@ -0,0 +1,98 @@ +package sprite + +import ( + "fmt" + + "github.com/veandco/go-sdl2/sdl" +) + +// animation defines a specific for an entity that references a sequence of sections of a sprite sheet. +// For example, walking to the left could be defined by 4 subsections of a sprite sheet. +type animation struct { + spritesheet *spritesheet + dimensions sdl.Point + offset sdl.Point + length int32 +} + +func NewAnimation( + filename string, + dimensions sdl.Point, + offset sdl.Point, + length int32, +) *animation { + spritesheet := GetSpritesheet(filename) + + a := animation{ + spritesheet: spritesheet, + dimensions: dimensions, + offset: offset, + length: length, + } + + return &a +} + +func (a *animation) Draw(frame int32, worldPosition *sdl.Point) error { + width := a.dimensions.X + height := a.dimensions.Y + + base := sdl.Point{ + X: width * a.offset.X, + Y: height * a.offset.Y, + } + + // Assuming all frames are horizontal going left to right + f := frame % a.length + section := sdl.Rect{ + X: base.X + f*width, + Y: base.Y, + W: width, + H: height, + } + + err := a.checkBounds(§ion) + if err != nil { + return err + } + + // TODO convert to window position eventually + placement := sdl.Rect{ + X: worldPosition.X, + Y: worldPosition.Y, + W: width, + H: height, + } + + err = a.spritesheet.Draw(§ion, &placement) + if err != nil { + return err + } + + return nil +} + +func (a *animation) checkBounds(section *sdl.Rect) error { + width := a.spritesheet.surface.W + height := a.spritesheet.surface.H + + outOfBounds := false + if section.X < 0 { + outOfBounds = true + } + if section.Y < 0 { + outOfBounds = true + } + if section.X+section.W > width { + outOfBounds = true + } + if section.Y+section.H > height { + outOfBounds = true + } + + if outOfBounds { + return fmt.Errorf("draw section was out of bounds - section: %v, image: %v", *section, a.spritesheet.surface.Bounds()) + } + + return nil +} diff --git a/internal/game/sprite/sprite.go b/internal/game/sprite/sprite.go deleted file mode 100644 index 520e8be..0000000 --- a/internal/game/sprite/sprite.go +++ /dev/null @@ -1,84 +0,0 @@ -package sprite - -import ( - "fmt" - "log" - - "github.com/veandco/go-sdl2/img" - "github.com/veandco/go-sdl2/sdl" -) - -type sprite struct { - filename string - renderer *sdl.Renderer - image *sdl.Surface // the original png file - spriteSheet *sdl.Texture // the SDL texture created from the image -} - -func NewSprite(renderer *sdl.Renderer, filename string) (*sprite, error) { - var err error - - // Load the image file - var image *sdl.Surface - sdl.Do(func() { - image, err = img.Load(filename) - }) - if err != nil { - err = fmt.Errorf("failed to load image: %w", err) - return nil, err - } - - // Create the sprite sheet texture from the image - var spriteSheet *sdl.Texture - sdl.Do(func() { - spriteSheet, err = renderer.CreateTextureFromSurface(image) - }) - if err != nil { - err = fmt.Errorf("failed to create texture: %w", err) - return nil, err - } - - s := sprite{ - filename: filename, - renderer: renderer, - image: image, - spriteSheet: spriteSheet, - } - - return &s, nil -} - -func (s *sprite) Cleanup() { - // Clean up image - defer func() { - if s.image != nil { - sdl.Do(func() { - s.image.Free() - }) - } - }() - - // Clean up spritesheet - defer func() { - if s.spriteSheet != nil { - sdl.Do(func() { - err := s.spriteSheet.Destroy() - if err != nil { - log.Printf("error destroying spritesheet %v: %v\n", s.filename, err) - } - }) - } - }() -} - -func (s *sprite) Draw(section *sdl.Rect, placement *sdl.Rect) error { - var err error - sdl.Do(func() { - err = s.renderer.Copy(s.spriteSheet, section, placement) - }) - if err != nil { - return err - } - - return nil -} diff --git a/internal/game/sprite/sprite_cache.go b/internal/game/sprite/sprite_cache.go deleted file mode 100644 index 8f7dd1a..0000000 --- a/internal/game/sprite/sprite_cache.go +++ /dev/null @@ -1,80 +0,0 @@ -package sprite - -import ( - "log" - - "github.com/veandco/go-sdl2/sdl" -) - -var spriteFileList []string = []string{ - "assets/images/penguin.png", -} - -type Sprite interface { - Draw(section *sdl.Rect, placement *sdl.Rect) error - Cleanup() -} - -var SpriteCache map[string]Sprite -var DefaultSprite Sprite - -func InitSpriteCache(renderer *sdl.Renderer) error { - var err error - - DefaultSprite, err = createDefaultSprite(renderer) - if err != nil { - log.Printf("failed to create DefaultSprite: %v", err) - return err - } - - SpriteCache = make(map[string]Sprite) - - for _, filename := range spriteFileList { - s, err := NewSprite(renderer, filename) - if err != nil { - log.Printf("error creating sprite %v, using DefaultSprite: %v", filename, err) - SpriteCache[filename] = DefaultSprite - } else { - SpriteCache[filename] = s - } - } - - return nil -} - -func GetSprite(filename string) Sprite { - s, exists := SpriteCache[filename] - if !exists { - log.Printf("no sprite found for %v, using DefaultSprite", filename) - return DefaultSprite - } - - return s -} - -func CleanupSpriteCache() { - for _, v := range SpriteCache { - defer v.Cleanup() - } -} - -func createDefaultSprite(renderer *sdl.Renderer) (*sprite, error) { - var err error - - surface, err := sdl.CreateRGBSurface(0, 100, 100, 32, 0, 0, 0, 0) - if err != nil { - return nil, err - } - - texture, err := renderer.CreateTextureFromSurface(surface) - if err != nil { - return nil, err - } - - s := sprite{ - renderer: renderer, - spriteSheet: texture, - } - - return &s, nil -} diff --git a/internal/game/sprite/spritesheet.go b/internal/game/sprite/spritesheet.go new file mode 100644 index 0000000..b4b8f3b --- /dev/null +++ b/internal/game/sprite/spritesheet.go @@ -0,0 +1,92 @@ +package sprite + +import ( + "fmt" + "log" + + "github.com/veandco/go-sdl2/img" + "github.com/veandco/go-sdl2/sdl" +) + +type spritesheet struct { + filename string + renderer *sdl.Renderer + surface *sdl.Surface // the original png file + texture *sdl.Texture // the SDL texture created from the image +} + +func NewSprite(renderer *sdl.Renderer, filename string) (*spritesheet, error) { + var err error + + // Load the surface file + var surface *sdl.Surface + sdl.Do(func() { + surface, err = img.Load(filename) + }) + if err != nil { + err = fmt.Errorf("failed to load image: %w", err) + return nil, err + } + + // Create the sprite sheet texture from the image + var texture *sdl.Texture + sdl.Do(func() { + texture, err = renderer.CreateTextureFromSurface(surface) + }) + if err != nil { + err = fmt.Errorf("failed to create texture: %w", err) + return nil, err + } + + s := spritesheet{ + filename: filename, + renderer: renderer, + surface: surface, + texture: texture, + } + + return &s, nil +} + +func (s *spritesheet) Cleanup() { + // Clean up image + defer func() { + if s.surface != nil { + sdl.Do(func() { + s.surface.Free() + }) + } + }() + + // Clean up spritesheet + defer func() { + if s.texture != nil { + sdl.Do(func() { + err := s.texture.Destroy() + if err != nil { + log.Printf("error destroying spritesheet %v: %v\n", s.filename, err) + } + }) + } + }() +} + +func (s *spritesheet) Draw(section *sdl.Rect, placement *sdl.Rect) error { + var err error + sdl.Do(func() { + err = s.renderer.Copy(s.texture, section, placement) + }) + if err != nil { + return err + } + + return nil +} + +func (s *spritesheet) Bounds() *sdl.Point { + p := sdl.Point{ + X: s.surface.W, + Y: s.surface.H, + } + return &p +} diff --git a/internal/game/sprite/spritesheet_cache.go b/internal/game/sprite/spritesheet_cache.go new file mode 100644 index 0000000..66790ef --- /dev/null +++ b/internal/game/sprite/spritesheet_cache.go @@ -0,0 +1,85 @@ +package sprite + +import ( + "log" + + "github.com/veandco/go-sdl2/sdl" +) + +const ( + PENGUIN = "assets/images/penguin.png" + PLATFORMER_FOREST_CHARACTERS = "assets/images/a-platformer-in-the-forest/characters.png" +) + +var fileList []string = []string{ + PENGUIN, + PLATFORMER_FOREST_CHARACTERS, +} + +var ( + spriteCache map[string]*spritesheet + defaultSprite *spritesheet +) + +func InitSpriteCache(renderer *sdl.Renderer) error { + var err error + + defaultSprite, err = createDefaultSprite(renderer) + if err != nil { + log.Printf("failed to create DefaultSprite: %v", err) + return err + } + + spriteCache = make(map[string]*spritesheet) + + for _, filename := range fileList { + s, err := NewSprite(renderer, filename) + if err != nil { + log.Printf("error creating sprite %v, using DefaultSprite: %v", filename, err) + spriteCache[filename] = defaultSprite + } else { + spriteCache[filename] = s + } + } + + return nil +} + +func GetSpritesheet(filename string) *spritesheet { + s, exists := spriteCache[filename] + if !exists { + log.Printf("no sprite found for %v, using DefaultSprite", filename) + return defaultSprite + } + + return s +} + +func CleanupSpriteCache() { + for _, v := range spriteCache { + defer v.Cleanup() + } +} + +// The DefaultSprite is just a 100x100 black square +func createDefaultSprite(renderer *sdl.Renderer) (*spritesheet, error) { + var err error + + surface, err := sdl.CreateRGBSurface(0, 100, 100, 32, 0, 0, 0, 0) + if err != nil { + return nil, err + } + + texture, err := renderer.CreateTextureFromSurface(surface) + if err != nil { + return nil, err + } + + s := spritesheet{ + renderer: renderer, + texture: texture, + surface: surface, + } + + return &s, nil +} diff --git a/main.go b/main.go index 48b6339..757fb04 100644 --- a/main.go +++ b/main.go @@ -86,9 +86,11 @@ func run(configMap gosimpleconf.ConfigMap) error { } defer sprite.CleanupSpriteCache() + entity.DefinePenguinAnimations() + penguin := entity.NewPenguin(renderer) p2 := entity.NewPenguin(renderer) - p2.MoveTo(&sdl.Point{X: 100, Y: 100}) + p2.SetPosition(&sdl.Point{X: 100, Y: 100}) keystates := make(map[sdl.Keycode]bool)