diff --git a/assets/images/penguin-right.png b/assets/images/penguin.png similarity index 100% rename from assets/images/penguin-right.png rename to assets/images/penguin.png diff --git a/internal/game/camera.go b/internal/game/camera.go new file mode 100644 index 0000000..748c1a3 --- /dev/null +++ b/internal/game/camera.go @@ -0,0 +1,15 @@ +package game + +import "github.com/veandco/go-sdl2/sdl" + +// camera represents a view of the world. It's a projection through the window looking at the world. +// Since this is only a 2D game with SDL, the projection is relatively simple: window + camera = world. +// https://gamedev.stackexchange.com/a/123844 +type camera struct { + pos *sdl.Point +} + +func NewCamera() *camera { + c := camera{} + return &c +} diff --git a/internal/game/entity/penguin.go b/internal/game/entity/penguin.go new file mode 100644 index 0000000..d9398fe --- /dev/null +++ b/internal/game/entity/penguin.go @@ -0,0 +1,79 @@ +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 +} + +func NewPenguin(renderer *sdl.Renderer) *penguin { + sprite := sprite.GetSprite("assets/images/penguin.png") + + pos := sdl.Point{X: 0, Y: 0} + + p := penguin{ + renderer: renderer, + sprite: sprite, + position: &pos, + } + + 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) + 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) + }) + + return nil +} + +func (p *penguin) MoveTo(point *sdl.Point) { + p.position = point +} + +func (p *penguin) MoveRight() { + p.position.X += 1 +} + +func (p *penguin) MoveLeft() { + p.position.X -= 1 +} + +func (p *penguin) MoveUp() { + // (0,0) is the top left, so negative y moves up + p.position.Y -= 1 +} + +func (p *penguin) MoveDown() { + // positive y moves down + p.position.Y += 1 +} diff --git a/internal/game/player.go b/internal/game/player.go new file mode 100644 index 0000000..a7d69fe --- /dev/null +++ b/internal/game/player.go @@ -0,0 +1,13 @@ +package game + +// player represents a collection of stuff controlled by the user's input. +// It contains the camera used to view the world, +// the viewport for the part of the screen to draw to (splitscreen support), +// as well as the entity the player is currently controlling. +type player struct { +} + +func NewPlayer() *player { + p := player{} + return &p +} diff --git a/internal/game/sprite/sprite.go b/internal/game/sprite/sprite.go new file mode 100644 index 0000000..520e8be --- /dev/null +++ b/internal/game/sprite/sprite.go @@ -0,0 +1,84 @@ +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 new file mode 100644 index 0000000..8f7dd1a --- /dev/null +++ b/internal/game/sprite/sprite_cache.go @@ -0,0 +1,80 @@ +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/window.go b/internal/game/window.go index 989e93b..a958137 100644 --- a/internal/game/window.go +++ b/internal/game/window.go @@ -39,7 +39,7 @@ func NewWindow(title string) (*window, error) { return gw, nil } -func (g *window) Destroy() { +func (g *window) Cleanup() { if g.SdlWindow != nil { sdl.Do(func() { err := g.SdlWindow.Destroy() diff --git a/main.go b/main.go index fb9f721..48b6339 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,9 @@ import ( "gitea.wisellama.rocks/Project-Ely/project-ely/internal/config" "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game" + "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity" + "gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/sprite" "gitea.wisellama.rocks/Wisellama/gosimpleconf" - "github.com/veandco/go-sdl2/img" "github.com/veandco/go-sdl2/sdl" ) @@ -59,7 +60,7 @@ func run(configMap gosimpleconf.ConfigMap) error { err = fmt.Errorf("failed creating GameWindow: %w", err) return err } - defer gameWindow.Destroy() + defer gameWindow.Cleanup() var renderer *sdl.Renderer sdl.Do(func() { @@ -78,43 +79,16 @@ func run(configMap gosimpleconf.ConfigMap) error { }) }() - var penguinImage *sdl.Surface - sdl.Do(func() { - penguinImage, err = img.Load("assets/images/penguin-right.png") - }) + err = sprite.InitSpriteCache(renderer) if err != nil { - err = fmt.Errorf("failed to load image: %w", err) + err = fmt.Errorf("failed in InitSpriteCache: %w", err) return err } - defer func() { - sdl.Do(func() { - penguinImage.Free() - }) - }() + defer sprite.CleanupSpriteCache() - var penguinTexture *sdl.Texture - sdl.Do(func() { - penguinTexture, err = renderer.CreateTextureFromSurface(penguinImage) - }) - if err != nil { - err = fmt.Errorf("failed to create texture: %w", err) - return err - } - defer func() { - sdl.Do(func() { - err = penguinTexture.Destroy() - if err != nil { - log.Printf("error destroying renderer: %v\n", err) - } - - }) - }() - - penguinRect := &sdl.Rect{X: 0, Y: 0, W: 100, H: 100} - - var rect *sdl.Rect - var x int32 = 0 - var y int32 = 0 + penguin := entity.NewPenguin(renderer) + p2 := entity.NewPenguin(renderer) + p2.MoveTo(&sdl.Point{X: 100, Y: 100}) keystates := make(map[sdl.Keycode]bool) @@ -148,31 +122,16 @@ func run(configMap gosimpleconf.ConfigMap) error { } if keystates[sdl.K_a] { - x -= 1 - if x < 0 { - x = 0 - } + penguin.MoveLeft() } if keystates[sdl.K_d] { - x += 1 - w, _ := gameWindow.SdlWindow.GetSize() - if x+rect.W > w { - x = w - rect.W - } + penguin.MoveRight() } - // Y is inverted, positive Y moves downward if keystates[sdl.K_w] { - y -= 1 - if y < 0 { - y = 0 - } + penguin.MoveUp() } if keystates[sdl.K_s] { - y += 1 - _, h := gameWindow.SdlWindow.GetSize() - if y+rect.H > h { - y = h - rect.H - } + penguin.MoveDown() } // Background @@ -188,17 +147,15 @@ func run(configMap gosimpleconf.ConfigMap) error { } }) - // Square - sdl.Do(func() { - //renderer.SetDrawColor(0, 0, 255, 255) - rect = &sdl.Rect{X: x, Y: y, W: 200, H: 200} - err = renderer.Copy(penguinTexture, penguinRect, rect) - if err != nil { - log.Printf("error in renderer.Copy: %v\n", err) - } - //renderer.DrawRect(rect) - //renderer.FillRect(rect) - }) + // Everything else + err = penguin.Draw() + if err != nil { + log.Printf("error drawing: %v\n", err) + } + err = p2.Draw() + if err != nil { + log.Printf("error drawing: %v\n", err) + } // Draw sdl.Do(func() {