Abstract away all SDL logic into isolated package
This commit is contained in:
parent
cf6d5d999c
commit
30be42c343
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
81
doodle.go
81
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
32
fps.go
32
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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
104
render/interface.go
Normal file
104
render/interface.go
Normal file
|
@ -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}
|
||||
)
|
44
render/sdl/canvas.go
Normal file
44
render/sdl/canvas.go
Normal file
|
@ -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,
|
||||
})
|
||||
}
|
22
render/sdl/fps.go
Normal file
22
render/sdl/fps.go
Normal file
|
@ -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
|
15
render/sdl/log.go
Normal file
15
render/sdl/log.go
Normal file
|
@ -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")
|
||||
}
|
203
render/sdl/sdl.go
Normal file
203
render/sdl/sdl.go
Normal file
|
@ -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
|
||||
}
|
81
render/sdl/text.go
Normal file
81
render/sdl/text.go
Normal file
|
@ -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
|
||||
}
|
21
render/sdl/utils.go
Normal file
21
render/sdl/utils.go
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
4
scene.go
4
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.
|
||||
|
|
86
scene/editor.go
Normal file
86
scene/editor.go
Normal file
|
@ -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
|
||||
}
|
9
scene/log.go
Normal file
9
scene/log.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package scene
|
||||
|
||||
import "github.com/kirsle/golog"
|
||||
|
||||
var log *golog.Logger
|
||||
|
||||
func init() {
|
||||
log = golog.GetLogger("doodle")
|
||||
}
|
67
scene/scene.go
Normal file
67
scene/scene.go
Normal file
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user