diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index eba0dea..4890f35 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -5,6 +5,7 @@ import ( "runtime" "git.kirsle.net/apps/doodle" + "git.kirsle.net/apps/doodle/render/sdl" ) // Build number is the git commit hash. @@ -31,7 +32,14 @@ func main() { filename = args[0] } - app := doodle.New(debug) + // SDL engine. + engine := sdl.New( + "Doodle v"+doodle.Version, + 800, + 600, + ) + + app := doodle.New(debug, engine) if filename != "" { if edit { app.EditLevel(filename) diff --git a/config.go b/config.go index 6b8f001..03a38ec 100644 --- a/config.go +++ b/config.go @@ -1,11 +1,12 @@ package doodle -import "github.com/veandco/go-sdl2/sdl" +import "git.kirsle.net/apps/doodle/render" // Configuration constants. var ( DebugTextPadding int32 = 8 DebugTextSize = 24 - DebugTextColor = sdl.Color{255, 153, 255, 255} - DebugTextOutline = sdl.Color{0, 0, 0, 255} + DebugTextColor = render.SkyBlue + DebugTextStroke = render.Grey + DebugTextShadow = render.Black ) diff --git a/doodle.go b/doodle.go index 6f7beb2..67f9de6 100644 --- a/doodle.go +++ b/doodle.go @@ -7,8 +7,6 @@ import ( "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/render" "github.com/kirsle/golog" - "github.com/veandco/go-sdl2/sdl" - "github.com/veandco/go-sdl2/ttf" ) const ( @@ -24,7 +22,8 @@ const ( // Doodle is the game object. type Doodle struct { - Debug bool + Debug bool + Engine render.Engine startTime time.Time running bool @@ -34,15 +33,13 @@ type Doodle struct { height int32 scene Scene - - window *sdl.Window - renderer *sdl.Renderer } // New initializes the game object. -func New(debug bool) *Doodle { +func New(debug bool, engine render.Engine) *Doodle { d := &Doodle{ Debug: debug, + Engine: engine, startTime: time.Now(), events: events.New(), running: true, @@ -59,44 +56,10 @@ func New(debug bool) *Doodle { // Run initializes SDL and starts the main loop. func (d *Doodle) Run() error { - // Initialize SDL. - log.Info("Initializing SDL") - if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { + // Set up the render engine. + if err := d.Engine.Setup(); err != nil { return err } - defer sdl.Quit() - - // Initialize SDL_TTF. - log.Info("Initializing SDL_TTF") - if err := ttf.Init(); err != nil { - return err - } - - // Create our window. - log.Info("Creating the Main Window") - window, err := sdl.CreateWindow( - "Doodle v"+Version, - sdl.WINDOWPOS_CENTERED, - sdl.WINDOWPOS_CENTERED, - d.width, - d.height, - sdl.WINDOW_SHOWN, - ) - if err != nil { - return err - } - defer window.Destroy() - d.window = window - - // Blank out the window in white. - log.Info("Creating the SDL Renderer") - renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED) - if err != nil { - panic(err) - } - d.renderer = renderer - render.Renderer = renderer - defer renderer.Destroy() // Set up the default scene. if d.scene == nil { @@ -105,31 +68,49 @@ func (d *Doodle) Run() error { log.Info("Enter Main Loop") for d.running { + start := time.Now() // Record how long this frame took. d.ticks++ // Poll for events. - _, err := d.events.Poll(d.ticks) + ev, err := d.Engine.Poll() if err != nil { log.Error("event poll error: %s", err) - return err + d.running = false + break } - // Draw a frame and log how long it took. - start := time.Now() - err = d.scene.Loop(d) + // Global event handlers. + if ev.EscapeKey.Pressed() { + log.Error("Escape key pressed, shutting down") + d.running = false + break + } + + // Run the scene's logic. + err = d.scene.Loop(d, ev) if err != nil { return err } - elapsed := time.Now().Sub(start) + // Draw the debug overlay over all scenes. + d.DrawDebugOverlay() + + // Render the pixels to the screen. + err = d.Engine.Draw() + if err != nil { + log.Error("draw error: %s", err) + d.running = false + break + } // Delay to maintain the target frames per second. + elapsed := time.Now().Sub(start) tmp := elapsed / time.Millisecond var delay uint32 if TargetFPS-int(tmp) > 0 { // make sure it won't roll under delay = uint32(TargetFPS - int(tmp)) } - sdl.Delay(delay) + d.Engine.Delay(delay) // Track how long this frame took to measure FPS over time. d.TrackFPS(delay) diff --git a/editor_scene.go b/editor_scene.go index df14e68..84667e9 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -9,7 +9,9 @@ import ( "time" "git.kirsle.net/apps/doodle/draw" + "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/render" ) // EditorScene manages the "Edit Level" game mode. @@ -42,9 +44,7 @@ func (s *EditorScene) Setup(d *Doodle) error { } // Loop the editor scene. -func (s *EditorScene) Loop(d *Doodle) error { - ev := d.events - +func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { // Taking a screenshot? if ev.ScreenshotKey.Pressed() { log.Info("Taking a screenshot") @@ -53,8 +53,7 @@ func (s *EditorScene) Loop(d *Doodle) error { } // Clear the canvas and fill it with white. - d.renderer.SetDrawColor(255, 255, 255, 255) - d.renderer.Clear() + d.Engine.Clear(render.White) // Clicking? Log all the pixels while doing so. if ev.Button1.Now { @@ -82,29 +81,25 @@ func (s *EditorScene) Loop(d *Doodle) error { } } - d.renderer.SetDrawColor(0, 0, 0, 255) for i, pixel := range s.pixelHistory { if !pixel.start && i > 0 { prev := s.pixelHistory[i-1] if prev.x == pixel.x && prev.y == pixel.y { - d.renderer.DrawPoint(pixel.x, pixel.y) + d.Engine.DrawPoint( + render.Black, + render.Point{pixel.x, pixel.y}, + ) } else { - d.renderer.DrawLine( - pixel.x, - pixel.y, - prev.x, - prev.y, + d.Engine.DrawLine( + render.Black, + render.Point{pixel.x, pixel.y}, + render.Point{prev.x, prev.y}, ) } } - d.renderer.DrawPoint(pixel.x, pixel.y) + d.Engine.DrawPoint(render.Black, render.Point{pixel.x, pixel.y}) } - // Draw the FPS. - d.DrawDebugOverlay() - - d.renderer.Present() - return nil } diff --git a/events/events.go b/events/events.go index e98d20b..b0d3647 100644 --- a/events/events.go +++ b/events/events.go @@ -1,12 +1,6 @@ // Package events manages mouse and keyboard SDL events for Doodle. package events -import ( - "errors" - - "github.com/veandco/go-sdl2/sdl" -) - // State keeps track of event states. type State struct { // Mouse buttons. @@ -15,6 +9,7 @@ type State struct { // Screenshot key. ScreenshotKey *BoolTick + EscapeKey *BoolTick Up *BoolTick Left *BoolTick Right *BoolTick @@ -31,6 +26,7 @@ func New() *State { Button1: &BoolTick{}, Button2: &BoolTick{}, ScreenshotKey: &BoolTick{}, + EscapeKey: &BoolTick{}, Up: &BoolTick{}, Left: &BoolTick{}, Right: &BoolTick{}, @@ -39,93 +35,3 @@ func New() *State { CursorY: &Int32Tick{}, } } - -// Poll for events. -func (s *State) Poll(ticks uint64) (*State, error) { - for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { - switch t := event.(type) { - case *sdl.QuitEvent: - return s, errors.New("quit") - case *sdl.MouseMotionEvent: - if DebugMouseEvents { - log.Debug("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d", - t.Timestamp, ticks, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel, - ) - } - - // Push the cursor position. - s.CursorX.Push(t.X) - s.CursorY.Push(t.Y) - s.Button1.Push(t.State == 1) - case *sdl.MouseButtonEvent: - if DebugClickEvents { - log.Debug("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d", - t.Timestamp, ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State, - ) - } - - // Push the cursor position. - s.CursorX.Push(t.X) - s.CursorY.Push(t.Y) - - // Is a mouse button pressed down? - if t.Button == 1 { - var eventName string - if DebugClickEvents { - if t.State == 1 && s.Button1.Now == false { - eventName = "DOWN" - } else if t.State == 0 && s.Button1.Now == true { - eventName = "UP" - } - } - - if eventName != "" { - log.Debug("tick:%d Mouse Button1 %s BEFORE: %+v", - ticks, - eventName, - s.Button1, - ) - s.Button1.Push(eventName == "DOWN") - log.Debug("tick:%d Mouse Button1 %s AFTER: %+v", - ticks, - eventName, - s.Button1, - ) - - // Return the event immediately. - return s, nil - } - } - - // s.Button2.Push(t.Button == 3 && t.State == 1) - case *sdl.MouseWheelEvent: - if DebugMouseEvents { - log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d", - t.Timestamp, ticks, t.Type, t.Which, t.X, t.Y, - ) - } - case *sdl.KeyboardEvent: - log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", - t.Timestamp, ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, - ) - if t.Repeat == 1 { - continue - } - - switch t.Keysym.Scancode { - case sdl.SCANCODE_F12: - s.ScreenshotKey.Push(t.State == 1) - case sdl.SCANCODE_UP: - s.Up.Push(t.State == 1) - case sdl.SCANCODE_LEFT: - s.Left.Push(t.State == 1) - case sdl.SCANCODE_RIGHT: - s.Right.Push(t.State == 1) - case sdl.SCANCODE_DOWN: - s.Down.Push(t.State == 1) - } - } - } - - return s, nil -} diff --git a/fps.go b/fps.go index 56c04f0..5ce5e73 100644 --- a/fps.go +++ b/fps.go @@ -5,7 +5,6 @@ import ( "time" "git.kirsle.net/apps/doodle/render" - "github.com/veandco/go-sdl2/sdl" ) // Frames to cache for FPS calculation. @@ -26,7 +25,7 @@ func (d *Doodle) DrawDebugOverlay() { return } - text := fmt.Sprintf( + label := fmt.Sprintf( "FPS: %d (%dms) (%d,%d) S:%s F12=screenshot", fpsCurrent, fpsSkipped, @@ -34,20 +33,31 @@ func (d *Doodle) DrawDebugOverlay() { d.events.CursorY.Now, d.scene.Name(), ) - render.StrokedText(render.TextConfig{ - Text: text, - Size: DebugTextSize, - Color: DebugTextColor, - StrokeColor: DebugTextOutline, - X: DebugTextPadding, - Y: DebugTextPadding, - }) + + err := d.Engine.DrawText( + render.Text{ + Text: label, + Size: 24, + Color: DebugTextColor, + Stroke: DebugTextStroke, + Shadow: DebugTextShadow, + }, + render.Rect{ + X: DebugTextPadding, + Y: DebugTextPadding, + W: d.width, + H: d.height, + }, + ) + if err != nil { + log.Error("DrawDebugOverlay: text error: %s", err.Error()) + } } // TrackFPS shows the current FPS once per second. func (d *Doodle) TrackFPS(skipped uint32) { fpsFrames++ - fpsCurrentTicks = sdl.GetTicks() + fpsCurrentTicks = d.Engine.GetTicks() // Skip the first second. if fpsCurrentTicks < fpsInterval { diff --git a/play_scene.go b/play_scene.go index 799576a..157ab96 100644 --- a/play_scene.go +++ b/play_scene.go @@ -3,7 +3,7 @@ package doodle import ( "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" - "github.com/veandco/go-sdl2/sdl" + "git.kirsle.net/apps/doodle/render" ) // PlayScene manages the "Edit Level" game mode. @@ -40,43 +40,31 @@ func (s *PlayScene) Setup(d *Doodle) error { } // Loop the editor scene. -func (s *PlayScene) Loop(d *Doodle) error { - s.PollEvents(d.events) +func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { + s.movePlayer(ev) // Apply gravity. - return s.Draw(d) } // Draw the pixels on this frame. func (s *PlayScene) Draw(d *Doodle) error { // Clear the canvas and fill it with white. - d.renderer.SetDrawColor(255, 255, 255, 255) - d.renderer.Clear() + d.Engine.Clear(render.White) - d.renderer.SetDrawColor(0, 0, 0, 255) for pixel := range s.canvas { - d.renderer.DrawPoint(pixel.x, pixel.y) + d.Engine.DrawPoint(render.Black, render.Point{pixel.x, pixel.y}) } // Draw our hero. - d.renderer.SetDrawColor(0, 0, 255, 255) - d.renderer.DrawRect(&sdl.Rect{ - X: s.x, - Y: s.y, - W: 16, - H: 16, - }) - - // Draw the FPS. - d.DrawDebugOverlay() - d.renderer.Present() + log.Info("hero %s %+v", render.Magenta, render.Magenta) + d.Engine.DrawRect(render.Magenta, render.Rect{s.x, s.y, 16, 16}) return nil } -// PollEvents checks the event state and updates variables. -func (s *PlayScene) PollEvents(ev *events.State) { +// movePlayer updates the player's X,Y coordinate based on key pressed. +func (s *PlayScene) movePlayer(ev *events.State) { if ev.Down.Now { s.y += 4 } diff --git a/render/interface.go b/render/interface.go new file mode 100644 index 0000000..fe37109 --- /dev/null +++ b/render/interface.go @@ -0,0 +1,104 @@ +package render + +import ( + "fmt" + + "git.kirsle.net/apps/doodle/events" +) + +// Engine is the interface for the rendering engine, keeping SDL-specific stuff +// far away from the core of Doodle. +type Engine interface { + Setup() error + + // Poll for events like keypresses and mouse clicks. + Poll() (*events.State, error) + GetTicks() uint32 + + // Draw presents the current state to the screen. + Draw() error + + // Clear the full canvas and set this color. + Clear(Color) + DrawPoint(Color, Point) + DrawLine(Color, Point, Point) + DrawRect(Color, Rect) + DrawText(Text, Rect) error + + // Delay for a moment using the render engine's delay method, + // implemented by sdl.Delay(uint32) + Delay(uint32) + + // Tasks that the Setup function should defer until tear-down. + Teardown() + + Loop() error // maybe? +} + +// Color holds an RGBA color value. +type Color struct { + Red uint8 + Green uint8 + Blue uint8 + Alpha uint8 +} + +func (c Color) String() string { + return fmt.Sprintf( + "Color<#%02x%02x%02x>", + c.Red, c.Green, c.Blue, + ) +} + +// Point holds an X,Y coordinate value. +type Point struct { + X int32 + Y int32 +} + +func (p Point) String() string { + return fmt.Sprintf("Point<%d,%d>", p.X, p.Y) +} + +// Rect has a coordinate and a width and height. +type Rect struct { + X int32 + Y int32 + W int32 + H int32 +} + +func (r Rect) String() string { + return fmt.Sprintf("Rect<%d,%d,%d,%d>", + r.X, r.Y, r.W, r.H, + ) +} + +// Text holds information for drawing text. +type Text struct { + Text string + Size int + Color Color + Stroke Color // Stroke color (if not zero) + Shadow Color // Drop shadow color (if not zero) +} + +func (t Text) String() string { + return fmt.Sprintf("Text<%s>", t.Text) +} + +// Common color names. +var ( + Invisible = Color{} + White = Color{255, 255, 255, 255} + Grey = Color{153, 153, 153, 255} + Black = Color{0, 0, 0, 255} + SkyBlue = Color{0, 153, 255, 255} + Blue = Color{0, 0, 255, 255} + Red = Color{255, 0, 0, 255} + Green = Color{0, 255, 0, 255} + Cyan = Color{0, 255, 255, 255} + Yellow = Color{255, 255, 0, 255} + Magenta = Color{255, 0, 255, 255} + Pink = Color{255, 153, 255, 255} +) diff --git a/render/sdl/canvas.go b/render/sdl/canvas.go new file mode 100644 index 0000000..f6f45b4 --- /dev/null +++ b/render/sdl/canvas.go @@ -0,0 +1,44 @@ +// Package sdl provides an SDL2 renderer for Doodle. +package sdl + +import ( + "git.kirsle.net/apps/doodle/render" + "github.com/veandco/go-sdl2/sdl" +) + +// Clear the canvas and set this color. +func (r *Renderer) Clear(color render.Color) { + if color != r.lastColor { + r.renderer.SetDrawColor(color.Red, color.Blue, color.Green, color.Alpha) + } + r.renderer.Clear() +} + +// DrawPoint puts a color at a pixel. +func (r *Renderer) DrawPoint(color render.Color, point render.Point) { + if color != r.lastColor { + r.renderer.SetDrawColor(color.Red, color.Blue, color.Green, color.Alpha) + } + r.renderer.DrawPoint(point.X, point.Y) +} + +// DrawLine draws a line between two points. +func (r *Renderer) DrawLine(color render.Color, a, b render.Point) { + if color != r.lastColor { + r.renderer.SetDrawColor(color.Red, color.Blue, color.Green, color.Alpha) + } + r.renderer.DrawLine(a.X, a.Y, b.X, b.Y) +} + +// DrawRect draws a rectangle. +func (r *Renderer) DrawRect(color render.Color, rect render.Rect) { + if color != r.lastColor { + r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha) + } + r.renderer.DrawRect(&sdl.Rect{ + X: rect.X, + Y: rect.Y, + W: rect.W, + H: rect.H, + }) +} diff --git a/render/sdl/fps.go b/render/sdl/fps.go new file mode 100644 index 0000000..4c625c9 --- /dev/null +++ b/render/sdl/fps.go @@ -0,0 +1,22 @@ +package sdl + +import ( + "git.kirsle.net/apps/doodle/level" +) + +// Frames to cache for FPS calculation. +const ( + maxSamples = 100 + TargetFPS = 1000 / 60 +) + +var ( + fpsCurrentTicks uint32 // current time we get sdl.GetTicks() + fpsLastTime uint32 // last time we printed the fpsCurrentTicks + fpsCurrent int + fpsFrames int + fpsSkipped uint32 + fpsInterval uint32 = 1000 +) + +var pixelHistory []level.Pixel diff --git a/render/sdl/log.go b/render/sdl/log.go new file mode 100644 index 0000000..30aa217 --- /dev/null +++ b/render/sdl/log.go @@ -0,0 +1,15 @@ +package sdl + +import "github.com/kirsle/golog" + +var log *golog.Logger + +// Verbose debug logging. +var ( + DebugMouseEvents = false + DebugClickEvents = false +) + +func init() { + log = golog.GetLogger("doodle") +} diff --git a/render/sdl/sdl.go b/render/sdl/sdl.go new file mode 100644 index 0000000..ae386fd --- /dev/null +++ b/render/sdl/sdl.go @@ -0,0 +1,203 @@ +// Package sdl provides an SDL2 renderer for Doodle. +package sdl + +import ( + "errors" + "time" + + "git.kirsle.net/apps/doodle/events" + "git.kirsle.net/apps/doodle/render" + "github.com/veandco/go-sdl2/sdl" + "github.com/veandco/go-sdl2/ttf" +) + +// Renderer manages the SDL state. +type Renderer struct { + // Configurable fields. + title string + width int32 + height int32 + startTime time.Time + + // Private fields. + events *events.State + window *sdl.Window + renderer *sdl.Renderer + running bool + ticks uint64 + + // Optimizations to minimize SDL calls. + lastColor render.Color +} + +// New creates the SDL renderer. +func New(title string, width, height int32) *Renderer { + return &Renderer{ + events: events.New(), + title: title, + width: width, + height: height, + } +} + +// Teardown tasks when exiting the program. +func (r *Renderer) Teardown() { + r.renderer.Destroy() + r.window.Destroy() + sdl.Quit() +} + +// Setup the renderer. +func (r *Renderer) Setup() error { + // Initialize SDL. + log.Info("Initializing SDL") + if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { + return err + } + + // Initialize SDL_TTF. + log.Info("Initializing SDL_TTF") + if err := ttf.Init(); err != nil { + return err + } + + // Create our window. + log.Info("Creating the Main Window") + window, err := sdl.CreateWindow( + r.title, + sdl.WINDOWPOS_CENTERED, + sdl.WINDOWPOS_CENTERED, + r.width, + r.height, + sdl.WINDOW_SHOWN, + ) + if err != nil { + return err + } + r.window = window + + // Blank out the window in white. + log.Info("Creating the SDL Renderer") + renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED) + if err != nil { + panic(err) + } + r.renderer = renderer + render.Renderer = renderer + + return nil +} + +// GetTicks gets SDL's current tick count. +func (r *Renderer) GetTicks() uint32 { + return sdl.GetTicks() +} + +// Poll for events. +func (r *Renderer) Poll() (*events.State, error) { + s := r.events + for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { + switch t := event.(type) { + case *sdl.QuitEvent: + return s, errors.New("quit") + case *sdl.MouseMotionEvent: + if DebugMouseEvents { + log.Debug("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d", + t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel, + ) + } + + // Push the cursor position. + s.CursorX.Push(t.X) + s.CursorY.Push(t.Y) + s.Button1.Push(t.State == 1) + case *sdl.MouseButtonEvent: + if DebugClickEvents { + log.Debug("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d", + t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State, + ) + } + + // Push the cursor position. + s.CursorX.Push(t.X) + s.CursorY.Push(t.Y) + + // Is a mouse button pressed down? + if t.Button == 1 { + var eventName string + if DebugClickEvents { + if t.State == 1 && s.Button1.Now == false { + eventName = "DOWN" + } else if t.State == 0 && s.Button1.Now == true { + eventName = "UP" + } + } + + if eventName != "" { + log.Debug("tick:%d Mouse Button1 %s BEFORE: %+v", + r.ticks, + eventName, + s.Button1, + ) + s.Button1.Push(eventName == "DOWN") + log.Debug("tick:%d Mouse Button1 %s AFTER: %+v", + r.ticks, + eventName, + s.Button1, + ) + + // Return the event immediately. + return s, nil + } + } + + // s.Button2.Push(t.Button == 3 && t.State == 1) + case *sdl.MouseWheelEvent: + if DebugMouseEvents { + log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d", + t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, + ) + } + case *sdl.KeyboardEvent: + log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", + t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, + ) + if t.Repeat == 1 { + continue + } + + switch t.Keysym.Scancode { + case sdl.SCANCODE_ESCAPE: + s.EscapeKey.Push(t.State == 1) + case sdl.SCANCODE_F12: + s.ScreenshotKey.Push(t.State == 1) + case sdl.SCANCODE_UP: + s.Up.Push(t.State == 1) + case sdl.SCANCODE_LEFT: + s.Left.Push(t.State == 1) + case sdl.SCANCODE_RIGHT: + s.Right.Push(t.State == 1) + case sdl.SCANCODE_DOWN: + s.Down.Push(t.State == 1) + } + } + } + + return s, nil +} + +// Draw a single frame. +func (r *Renderer) Draw() error { + r.renderer.Present() + return nil +} + +// Delay using sdl.Delay +func (r *Renderer) Delay(time uint32) { + sdl.Delay(time) +} + +// Loop is the main loop. +func (r *Renderer) Loop() error { + return nil +} diff --git a/render/sdl/text.go b/render/sdl/text.go new file mode 100644 index 0000000..9740298 --- /dev/null +++ b/render/sdl/text.go @@ -0,0 +1,81 @@ +package sdl + +import ( + "git.kirsle.net/apps/doodle/render" + "github.com/veandco/go-sdl2/sdl" + "github.com/veandco/go-sdl2/ttf" +) + +var fonts map[int]*ttf.Font = map[int]*ttf.Font{} + +// LoadFont loads and caches the font at a given size. +func LoadFont(size int) (*ttf.Font, error) { + if font, ok := fonts[size]; ok { + return font, nil + } + + font, err := ttf.OpenFont("./fonts/DejaVuSansMono.ttf", size) + if err != nil { + return nil, err + } + fonts[size] = font + + return font, nil +} + +// DrawText draws text on the canvas. +func (r *Renderer) DrawText(text render.Text, rect render.Rect) error { + var ( + font *ttf.Font + surface *sdl.Surface + tex *sdl.Texture + err error + ) + + if font, err = LoadFont(text.Size); err != nil { + return err + } + + write := func(dx, dy int32, color sdl.Color) { + if surface, err = font.RenderUTF8Blended(text.Text, color); err != nil { + return + } + defer surface.Free() + + if tex, err = r.renderer.CreateTextureFromSurface(surface); err != nil { + return + } + defer tex.Destroy() + + tmp := &sdl.Rect{ + X: rect.X + dx, + Y: rect.Y + dy, + W: surface.W, + H: surface.H, + } + r.renderer.Copy(tex, nil, tmp) + } + + // Does the text have a stroke around it? + if text.Stroke != render.Invisible { + color := ColorToSDL(text.Stroke) + write(-1, -1, color) + write(-1, 0, color) + write(-1, 1, color) + write(1, -1, color) + write(1, 0, color) + write(1, 1, color) + write(0, -1, color) + write(0, 1, color) + } + + // Does it have a drop shadow? + if text.Shadow != render.Invisible { + write(1, 1, ColorToSDL(text.Shadow)) + } + + // Draw the text itself. + write(0, 0, ColorToSDL(text.Color)) + + return err +} diff --git a/render/sdl/utils.go b/render/sdl/utils.go new file mode 100644 index 0000000..9085e31 --- /dev/null +++ b/render/sdl/utils.go @@ -0,0 +1,21 @@ +package sdl + +import ( + "git.kirsle.net/apps/doodle/render" + "github.com/veandco/go-sdl2/sdl" +) + +// ColorToSDL converts Doodle's Color type to an sdl.Color. +func ColorToSDL(c render.Color) sdl.Color { + return sdl.Color{c.Red, c.Green, c.Blue, c.Alpha} +} + +// RectToSDL converts Doodle's Rect type to an sdl.Rect. +func RectToSDL(r render.Rect) sdl.Rect { + return sdl.Rect{ + X: r.X, + Y: r.Y, + W: r.W, + H: r.H, + } +} diff --git a/render/text.go b/render/text.go index 009ec9c..d07a11d 100644 --- a/render/text.go +++ b/render/text.go @@ -33,57 +33,3 @@ type TextConfig struct { W int32 H int32 } - -// StrokedText draws text with a stroke color around it. -func StrokedText(t TextConfig) { - stroke := func(copy TextConfig, x, y int32) { - copy.Color = t.StrokeColor - copy.X += x - copy.Y += y - Text(copy) - } - - stroke(t, -1, -1) - stroke(t, -1, 0) - stroke(t, -1, 1) - - stroke(t, 1, -1) - stroke(t, 1, 0) - stroke(t, 1, 1) - - stroke(t, 0, -1) - stroke(t, 0, 1) - Text(t) -} - -// Text draws text on the renderer. -func Text(t TextConfig) error { - var ( - font *ttf.Font - surface *sdl.Surface - tex *sdl.Texture - err error - ) - - if font, err = LoadFont(t.Size); err != nil { - return err - } - - if surface, err = font.RenderUTF8Blended(t.Text, t.Color); err != nil { - return err - } - defer surface.Free() - - if tex, err = Renderer.CreateTextureFromSurface(surface); err != nil { - return err - } - defer tex.Destroy() - - Renderer.Copy(tex, nil, &sdl.Rect{ - X: int32(t.X), - Y: int32(t.Y), - W: int32(surface.W), - H: int32(surface.H), - }) - return nil -} diff --git a/scene.go b/scene.go index 6b8d179..bc7bac6 100644 --- a/scene.go +++ b/scene.go @@ -1,12 +1,14 @@ package doodle +import "git.kirsle.net/apps/doodle/events" + // Scene is an abstraction for a game mode in Doodle. The app points to one // scene at a time and that scene has control over the main loop, and its own // state information. type Scene interface { Name() string Setup(*Doodle) error - Loop(*Doodle) error + Loop(*Doodle, *events.State) error } // Goto a scene. First it unloads the current scene. diff --git a/scene/editor.go b/scene/editor.go new file mode 100644 index 0000000..11b025a --- /dev/null +++ b/scene/editor.go @@ -0,0 +1,86 @@ +package scene + +import "git.kirsle.net/apps/doodle/events" + +// Editor is the drawing mode of the game where the user is clicking and +// dragging to draw pixels. +type Editor struct{} + +func (s *Editor) String() string { + return "Editor" +} + +// Setup the scene. +func (s *Editor) Setup() error { + return nil +} + +// Loop the scene. +func (s *Editor) Loop(ev *events.State) error { + // Taking a screenshot? + if ev.ScreenshotKey.Pressed() { + log.Info("Taking a screenshot") + d.Screenshot() + d.SaveLevel() + } + + // Clear the canvas and fill it with white. + d.renderer.SetDrawColor(255, 255, 255, 255) + d.renderer.Clear() + + // Clicking? Log all the pixels while doing so. + if ev.Button1.Now { + pixel := Pixel{ + start: ev.Button1.Pressed(), + x: ev.CursorX.Now, + y: ev.CursorY.Now, + dx: ev.CursorX.Now, + dy: ev.CursorY.Now, + } + + // Append unique new pixels. + if len(pixelHistory) == 0 || pixelHistory[len(pixelHistory)-1] != pixel { + // If not a start pixel, make the delta coord the previous one. + if !pixel.start && len(pixelHistory) > 0 { + prev := pixelHistory[len(pixelHistory)-1] + pixel.dx = prev.x + pixel.dy = prev.y + } + + pixelHistory = append(pixelHistory, pixel) + + // Save in the pixel canvas map. + d.canvas[pixel] = nil + } + } + + d.renderer.SetDrawColor(0, 0, 0, 255) + for i, pixel := range pixelHistory { + if !pixel.start && i > 0 { + prev := pixelHistory[i-1] + if prev.x == pixel.x && prev.y == pixel.y { + d.renderer.DrawPoint(pixel.x, pixel.y) + } else { + d.renderer.DrawLine( + pixel.x, + pixel.y, + prev.x, + prev.y, + ) + } + } + d.renderer.DrawPoint(pixel.x, pixel.y) + } + + // Draw the FPS. + d.DrawDebugOverlay() + + d.renderer.Present() + + return nil +} + +// Destroy the scene. +func (s *Editor) Destroy() error { + return nil +} diff --git a/scene/log.go b/scene/log.go new file mode 100644 index 0000000..fadeb4b --- /dev/null +++ b/scene/log.go @@ -0,0 +1,9 @@ +package scene + +import "github.com/kirsle/golog" + +var log *golog.Logger + +func init() { + log = golog.GetLogger("doodle") +} diff --git a/scene/scene.go b/scene/scene.go new file mode 100644 index 0000000..f9bf712 --- /dev/null +++ b/scene/scene.go @@ -0,0 +1,67 @@ +package scene + +import ( + "errors" + "fmt" + + "git.kirsle.net/apps/doodle/events" +) + +// Scene is an interface for the top level of a game mode. The game points to +// one Scene at a time, and that Scene has majority control of the main loop, +// and maintains its own state local to that scene. +type Scene interface { + String() string // the scene's name + Setup() error + Loop() error + Destroy() error +} + +// Manager is a type that provides context switching features to manage scenes. +type Manager struct { + events *events.State + scene Scene + ticks uint64 +} + +// NewManager creates the new manager. +func NewManager(events *events.State) Manager { + return Manager{ + events: events, + scene: nil, + } +} + +// Go to a new scene. This tears down the existing scene, sets up the new one, +// and switches control to the new scene. +func (m *Manager) Go(scene Scene) error { + // Already running a scene? + if m.scene != nil { + if err := m.scene.Destroy(); err != nil { + return fmt.Errorf("couldn't destroy scene %s: %s", m.scene, err) + } + m.scene = nil + } + + // Initialize the new scene. + m.scene = scene + return m.scene.Setup() +} + +// Loop the scene manager. This is the game's main loop which runs all the tasks +// that fall in the realm of the scene manager. +func (m *Manager) Loop() error { + if m.scene == nil { + return errors.New("no scene loaded") + } + + // Poll for events. + ev, err := m.events.Poll(m.ticks) + if err != nil { + log.Error("event poll error: %s", err) + return err + } + _ = ev + + return m.scene.Loop() +}