From b7751507e4b5cd5eb0574874ac0b2abcb8e4b94a Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 16 Jun 2018 19:59:23 -0700 Subject: [PATCH] Stabilize frame rate, add debug overlay --- .gitignore | 1 + README.md | 8 ++ cmd/doodle/main.go | 2 +- config.go | 11 ++ doodle.go | 252 ++++++++++++++++++--------------------------- events/debug.go | 7 ++ events/events.go | 81 +++++++++++++++ events/log.go | 9 ++ events/types.go | 25 +++++ fps.go | 70 +++++++++++++ log.go | 14 +++ render/log.go | 14 +++ render/render.go | 7 ++ render/text.go | 89 ++++++++++++++++ 14 files changed, 440 insertions(+), 150 deletions(-) create mode 100644 config.go create mode 100644 events/debug.go create mode 100644 events/events.go create mode 100644 events/log.go create mode 100644 events/types.go create mode 100644 fps.go create mode 100644 log.go create mode 100644 render/log.go create mode 100644 render/render.go create mode 100644 render/text.go diff --git a/.gitignore b/.gitignore index e69de29..0480ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +fonts/ diff --git a/README.md b/README.md index a5e8063..91f931e 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,11 @@ As a rough idea of the milestones needed for this game to work: * [ ] Start implementing a platformer that uses the custom map format for its rendering and collision detection. * [ ] ??? + +# Building + +Fedora dependencies: + +```bash +$ sudo dnf install SDL2-devel SDL2_ttf-devel +``` diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 0487c96..56e921d 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -4,7 +4,7 @@ import ( "flag" "runtime" - "github.com/kirsle/doodle" + "git.kirsle.net/apps/doodle" ) // Build number is the git commit hash. diff --git a/config.go b/config.go new file mode 100644 index 0000000..6b8f001 --- /dev/null +++ b/config.go @@ -0,0 +1,11 @@ +package doodle + +import "github.com/veandco/go-sdl2/sdl" + +// Configuration constants. +var ( + DebugTextPadding int32 = 8 + DebugTextSize = 24 + DebugTextColor = sdl.Color{255, 153, 255, 255} + DebugTextOutline = sdl.Color{0, 0, 0, 255} +) diff --git a/doodle.go b/doodle.go index 5d57913..15d650d 100644 --- a/doodle.go +++ b/doodle.go @@ -2,59 +2,81 @@ package doodle import ( "fmt" + "time" + "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" ) -// Version number. -const Version = "0.0.0-alpha" +const ( + // Version number. + Version = "0.0.0-alpha" + + // TargetFPS is the frame rate to cap the game to. + TargetFPS = uint32(1000 / 60) // 60 FPS + + // Millisecond64 is a time.Millisecond casted to float64. + Millisecond64 = float64(time.Millisecond) +) // Doodle is the game object. type Doodle struct { Debug bool - running bool - events EventState - width int - height int + startTime time.Time + running bool + events *events.State + width int32 + height int32 + + nextSecond time.Time + canvas Grid window *sdl.Window - surface *sdl.Surface renderer *sdl.Renderer } -// EventState keeps track of important events. -type EventState struct { - CursorX int32 - CursorY int32 - LastX int32 - LastY int32 - LeftClick bool - LastLeft bool - RightClick bool - LastRight bool -} - // New initializes the game object. func New(debug bool) *Doodle { d := &Doodle{ - Debug: debug, - running: true, - width: 800, - height: 600, + Debug: debug, + startTime: time.Now(), + events: events.New(), + running: true, + width: 800, + height: 600, + canvas: Grid{}, + + nextSecond: time.Now().Add(1 * time.Second), } + + if !debug { + log.Config.Level = golog.InfoLevel + } + return d } // 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 { 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, @@ -70,56 +92,33 @@ func (d *Doodle) Run() error { 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() - for i := 0; i < 10; i++ { - d.Loop() - // renderer.Clear() - // rect := sdl.Rect{ - // X: 0, - // Y: 0, - // W: 800, - // H: 600, - // } - // renderer.SetDrawColor(0, 0, 0, 255) - // renderer.FillRect(&rect) - // - // renderer.SetDrawColor(0, 255, 0, 255) - // renderer.DrawPoint(10*i, 10*i) - // - // renderer.Present() - // - // sdl.Delay(250) - } - + log.Info("Enter Main Loop") for d.running { + // Draw a frame and log how long it took. + start := time.Now() err = d.Loop() + elapsed := time.Now().Sub(start) + + tmp := elapsed / time.Millisecond + delay := TargetFPS - uint32(tmp) + sdl.Delay(delay) + + d.TrackFPS(delay) if err != nil { return err } } - // surface, err := window.GetSurface() - // if err != nil { - // panic(err) - // } - // d.surface = surface - // - // rect := sdl.Rect{ - // X: 0, - // Y: 0, - // W: 200, - // H: 200, - // } - // surface.FillRect(&rect, 0xffff0000) - // window.UpdateSurface() - // - // sdl.Delay(2500) + log.Warn("Main Loop Exited! Shutting down...") return nil } @@ -128,117 +127,72 @@ type Pixel struct { start bool x int32 y int32 + dx int32 + dy int32 } +func (p Pixel) String() string { + return fmt.Sprintf("(%d,%d) delta (%d,%d)", + p.x, p.y, + p.dx, p.dy, + ) +} + +// Grid is a 2D grid of pixels in X,Y notation. +type Grid map[Pixel]interface{} + var pixelHistory []Pixel // Loop runs one loop of the game engine. func (d *Doodle) Loop() error { // Poll for events. - d.PollEvents() - - d.renderer.Clear() - rect := sdl.Rect{ - X: 0, - Y: 0, - W: 800, - H: 600, + ev, err := d.events.Poll() + if err != nil { + log.Error("event poll error: %s", err) + return err } + + // Clear the canvas and fill it with white. d.renderer.SetDrawColor(255, 255, 255, 255) - d.renderer.FillRect(&rect) + d.renderer.Clear() // Clicking? Log all the pixels while doing so. - if d.events.LeftClick { - fmt.Printf("Pixel at %dx%d\n", d.events.CursorX, d.events.CursorY) + if ev.Button1.Now { pixel := Pixel{ - start: d.events.LeftClick && !d.events.LastLeft, - x: d.events.CursorX, - y: d.events.CursorY, + start: ev.Button1.Now && !ev.Button1.Last, + x: ev.CursorX.Now, + y: ev.CursorY.Now, + dx: ev.CursorX.Last, + dy: ev.CursorY.Last, + } + + if len(pixelHistory) == 0 || pixelHistory[len(pixelHistory)-1] != pixel { + pixelHistory = append(pixelHistory, pixel) } - pixelHistory = append(pixelHistory, pixel) } - // Colorize all those pixels. d.renderer.SetDrawColor(0, 0, 0, 255) for i, pixel := range pixelHistory { - fmt.Printf("Draw: %v\n", pixel) - if pixel.start == false && i > 0 { - start := pixelHistory[i-1] - fmt.Printf("Line from %dx%d -> %dx%d\n", start.x, start.y, pixel.x, pixel.y) - d.renderer.DrawLine( - int(start.x), - int(start.y), - int(pixel.x), - int(pixel.y), - ) - } else { - d.renderer.DrawPoint( - int(pixel.x), int(pixel.y), - ) + 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.FillRect(&sdl.Rect{pixel.x, pixel.y, 10, 10}) + d.renderer.DrawPoint(pixel.x, pixel.y) } + // Draw the FPS. + d.DrawDebugOverlay() + d.renderer.Present() - sdl.Delay(1000 / 60) return nil } - -// PollEvents checks for keyboard/mouse/etc. events. -func (d *Doodle) PollEvents() { - for { - event := sdl.PollEvent() - if event == nil { - break - } - - // Handle the event. - switch t := event.(type) { - case *sdl.QuitEvent: - d.running = false - case *sdl.MouseMotionEvent: - fmt.Printf("[%d ms] MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d\n", - t.Timestamp, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel, - ) - d.events.LastX = d.events.CursorX - d.events.LastY = d.events.CursorY - d.events.CursorX = t.X - d.events.CursorY = t.Y - case *sdl.MouseButtonEvent: - fmt.Printf("[%d ms] MouseButton type:%d id:%d x:%d y:%d button:%d state:%d\n", - t.Timestamp, t.Type, t.Which, t.X, t.Y, t.Button, t.State, - ) - - d.events.LastX = d.events.CursorX - d.events.LastY = d.events.CursorY - d.events.CursorX = t.X - d.events.CursorY = t.Y - - d.events.LastLeft = d.events.LeftClick - d.events.LastRight = d.events.RightClick - - // Clicking? - if t.Button == 1 { - if t.State == 1 && d.events.LeftClick == false { - d.events.LeftClick = true - } else if t.State == 0 && d.events.LeftClick == true { - d.events.LeftClick = false - } - } - d.events.RightClick = t.Button == 3 && t.State == 1 - case *sdl.MouseWheelEvent: - fmt.Printf("[%d ms] MouseWheel type:%d id:%d x:%d y:%d\n", - t.Timestamp, t.Type, t.Which, t.X, t.Y, - ) - case *sdl.KeyDownEvent: - fmt.Printf("[%d ms] Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", - t.Timestamp, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, - ) - case *sdl.KeyUpEvent: - fmt.Printf("[%d ms] Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", - t.Timestamp, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, - ) - } - } -} diff --git a/events/debug.go b/events/debug.go new file mode 100644 index 0000000..e894ce2 --- /dev/null +++ b/events/debug.go @@ -0,0 +1,7 @@ +package events + +// Debug constants, toggle on or off for SUPER VERBOSE debugging. +var ( + DebugMouseEvents = false + DebugClickEvents = true +) diff --git a/events/events.go b/events/events.go new file mode 100644 index 0000000..46517f4 --- /dev/null +++ b/events/events.go @@ -0,0 +1,81 @@ +// 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. + Button1 BoolFrameState + Button2 BoolFrameState + + // Cursor positions. + CursorX Int32FrameState + CursorY Int32FrameState +} + +// New creates a new event state manager. +func New() *State { + return &State{} +} + +// Poll for events. +func (s *State) Poll() (*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] MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d", + t.Timestamp, 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] MouseButton type:%d id:%d x:%d y:%d button:%d state:%d", + t.Timestamp, 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 { + if DebugClickEvents { + if t.State == 1 && s.Button1.Now == false { + log.Debug("Mouse Button1 DOWN") + } else if t.State == 0 && s.Button1.Now == true { + log.Debug("Mouse Button1 UP") + } + } + s.Button1.Push(t.State == 1) + } + + s.Button2.Push(t.Button == 3 && t.State == 1) + case *sdl.MouseWheelEvent: + if DebugMouseEvents { + log.Debug("[%d ms] MouseWheel type:%d id:%d x:%d y:%d", + t.Timestamp, t.Type, t.Which, t.X, t.Y, + ) + } + case *sdl.KeyboardEvent: + log.Debug("[%d ms] Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", + t.Timestamp, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, + ) + } + } + + return s, nil +} diff --git a/events/log.go b/events/log.go new file mode 100644 index 0000000..925f204 --- /dev/null +++ b/events/log.go @@ -0,0 +1,9 @@ +package events + +import "github.com/kirsle/golog" + +var log *golog.Logger + +func init() { + log = golog.GetLogger("doodle") +} diff --git a/events/types.go b/events/types.go new file mode 100644 index 0000000..0bc9021 --- /dev/null +++ b/events/types.go @@ -0,0 +1,25 @@ +package events + +// BoolFrameState holds boolean state between this frame and the previous. +type BoolFrameState struct { + Now bool + Last bool +} + +// Int32FrameState manages int32 state between this frame and the previous. +type Int32FrameState struct { + Now int32 + Last int32 +} + +// Push a bool state, copying the current Now value to Last. +func (bs *BoolFrameState) Push(v bool) { + bs.Last = bs.Now + bs.Now = v +} + +// Push an int32 state, copying the current Now value to Last. +func (is *Int32FrameState) Push(v int32) { + is.Last = is.Now + is.Now = v +} diff --git a/fps.go b/fps.go new file mode 100644 index 0000000..e88e79e --- /dev/null +++ b/fps.go @@ -0,0 +1,70 @@ +package doodle + +import ( + "fmt" + "time" + + "git.kirsle.net/apps/doodle/render" + "github.com/veandco/go-sdl2/sdl" +) + +// Frames to cache for FPS calculation. +const maxSamples = 100 + +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 +) + +// DrawDebugOverlay draws the debug FPS text on the SDL canvas. +func (d *Doodle) DrawDebugOverlay() { + if !d.Debug { + return + } + + text := fmt.Sprintf( + "FPS: %d (%dms) (X,Y)=(%d,%d) canvas=%d", + fpsCurrent, + fpsSkipped, + d.events.CursorX.Now, + d.events.CursorY.Now, + len(pixelHistory), + ) + render.StrokedText(render.TextConfig{ + Text: text, + Size: DebugTextSize, + Color: DebugTextColor, + StrokeColor: DebugTextOutline, + X: DebugTextPadding, + Y: DebugTextPadding, + }) +} + +// TrackFPS shows the current FPS once per second. +func (d *Doodle) TrackFPS(skipped uint32) { + fpsFrames++ + fpsCurrentTicks = sdl.GetTicks() + + // Skip the first second. + if fpsCurrentTicks < fpsInterval { + return + } + + if fpsLastTime < fpsCurrentTicks-fpsInterval { + log.Debug("Uptime: %s FPS: %d deltaTicks: %d skipped: %dms", + time.Now().Sub(d.startTime), + fpsCurrent, + fpsCurrentTicks-fpsLastTime, + skipped, + ) + + fpsLastTime = fpsCurrentTicks + fpsCurrent = fpsFrames + fpsFrames = 0 + fpsSkipped = skipped + } +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..7eddc71 --- /dev/null +++ b/log.go @@ -0,0 +1,14 @@ +package doodle + +import "github.com/kirsle/golog" + +var log *golog.Logger + +func init() { + log = golog.GetLogger("doodle") + log.Configure(&golog.Config{ + Level: golog.DebugLevel, + Theme: golog.DarkTheme, + Colors: golog.ExtendedColor, + }) +} diff --git a/render/log.go b/render/log.go new file mode 100644 index 0000000..0891650 --- /dev/null +++ b/render/log.go @@ -0,0 +1,14 @@ +package render + +import "github.com/kirsle/golog" + +var log *golog.Logger + +func init() { + log = golog.GetLogger("doodle") + log.Configure(&golog.Config{ + Level: golog.DebugLevel, + Theme: golog.DarkTheme, + Colors: golog.ExtendedColor, + }) +} diff --git a/render/render.go b/render/render.go new file mode 100644 index 0000000..6e2147e --- /dev/null +++ b/render/render.go @@ -0,0 +1,7 @@ +// Package render manages the SDL rendering context for Doodle. +package render + +import "github.com/veandco/go-sdl2/sdl" + +// Renderer is a singleton instance of the SDL renderer. +var Renderer *sdl.Renderer diff --git a/render/text.go b/render/text.go new file mode 100644 index 0000000..009ec9c --- /dev/null +++ b/render/text.go @@ -0,0 +1,89 @@ +package render + +import ( + "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 +} + +// TextConfig are settings for rendered text. +type TextConfig struct { + Text string + Size int + Color sdl.Color + StrokeColor sdl.Color + X int32 + Y int32 + 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 +}