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 0000000..432e6c6 Binary files /dev/null and b/assets/images/a-platformer-in-the-forest/characters.png differ 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 0000000..939cb28 Binary files /dev/null and b/assets/images/a-platformer-in-the-forest/sheet.png differ 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 0000000..c582c1d Binary files /dev/null and b/assets/images/a-platformer-in-the-forest/swoosh.png differ 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)