Abstract away all SDL logic into isolated package

chunks
Noah 2018-07-21 17:12:22 -07:00
parent cf6d5d999c
commit 30be42c343
19 changed files with 744 additions and 255 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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