Stabilize frame rate, add debug overlay

This commit is contained in:
Noah 2018-06-16 19:59:23 -07:00
parent a8e82f4dd2
commit b7751507e4
14 changed files with 440 additions and 150 deletions

1
.gitignore vendored
View File

@ -0,0 +1 @@
fonts/

View File

@ -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
```

View File

@ -4,7 +4,7 @@ import (
"flag"
"runtime"
"github.com/kirsle/doodle"
"git.kirsle.net/apps/doodle"
)
// Build number is the git commit hash.

11
config.go Normal file
View File

@ -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}
)

252
doodle.go
View File

@ -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,
)
}
}
}

7
events/debug.go Normal file
View File

@ -0,0 +1,7 @@
package events
// Debug constants, toggle on or off for SUPER VERBOSE debugging.
var (
DebugMouseEvents = false
DebugClickEvents = true
)

81
events/events.go Normal file
View File

@ -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
}

9
events/log.go Normal file
View File

@ -0,0 +1,9 @@
package events
import "github.com/kirsle/golog"
var log *golog.Logger
func init() {
log = golog.GetLogger("doodle")
}

25
events/types.go Normal file
View File

@ -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
}

70
fps.go Normal file
View File

@ -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
}
}

14
log.go Normal file
View File

@ -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,
})
}

14
render/log.go Normal file
View File

@ -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,
})
}

7
render/render.go Normal file
View File

@ -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

89
render/text.go Normal file
View File

@ -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
}