repl, save

This commit is contained in:
Josh Deprez 2021-08-25 16:46:30 +10:00
parent cd4cbbdef9
commit 01468798d4
6 changed files with 87 additions and 8 deletions

View file

@ -39,8 +39,8 @@ func loadGobz(dst interface{}, assets fs.FS, path string) error {
return gob.NewDecoder(gz).Decode(dst) return gob.NewDecoder(gz).Decode(dst)
} }
// SaveGobz takes an object, gob-encodes it, gzips it, and writes to disk. // saveGobz takes an object, gob-encodes it, gzips it, and writes to disk.
func SaveGobz(src interface{}, name string) error { func saveGobz(src interface{}, name string) error {
f, err := os.CreateTemp(".", name) f, err := os.CreateTemp(".", name)
if err != nil { if err != nil {
return err return err

View file

@ -3,6 +3,7 @@ package engine
import ( import (
"encoding/gob" "encoding/gob"
"io/fs" "io/fs"
"sync"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -15,15 +16,21 @@ func init() {
// One component must be the designated root component - usually a // One component must be the designated root component - usually a
// scene of some kind. // scene of some kind.
type Game struct { type Game struct {
Disabled
Hidden
ScreenWidth int ScreenWidth int
ScreenHeight int ScreenHeight int
Root DrawUpdater // typically a *Scene or SceneRef though Root DrawUpdater // typically a *Scene or SceneRef though
componentsByID map[string]interface{} dbmu sync.RWMutex
db map[string]interface{}
} }
// Draw draws the entire thing, with default draw options. // Draw draws the entire thing, with default draw options.
func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Draw(screen *ebiten.Image) {
if g.Hidden {
return
}
g.Root.Draw(screen, ebiten.DrawImageOptions{}) g.Root.Draw(screen, ebiten.DrawImageOptions{})
} }
@ -33,7 +40,12 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (w, h int) {
} }
// Update updates the scene. // Update updates the scene.
func (g *Game) Update() error { return g.Root.Update() } func (g *Game) Update() error {
if g.Disabled {
return nil
}
return g.Root.Update()
}
// RegisterComponent tells the game there is a new component. Currently this is // RegisterComponent tells the game there is a new component. Currently this is
// only necessary for components with IDs. // only necessary for components with IDs.
@ -46,7 +58,9 @@ func (g *Game) RegisterComponent(c interface{}) {
if id == "" { if id == "" {
return return
} }
g.componentsByID[id] = c g.dbmu.Lock()
g.db[id] = c
g.dbmu.Unlock()
} }
// UnregisterComponent tells the game the component is no more. // UnregisterComponent tells the game the component is no more.
@ -60,11 +74,17 @@ func (g *Game) UnregisterComponent(c interface{}) {
if id == "" { if id == "" {
return return
} }
delete(g.componentsByID, id) g.dbmu.Lock()
delete(g.db, id)
g.dbmu.Unlock()
} }
// Component returns the component with a given ID, or nil if there is none. // Component returns the component with a given ID, or nil if there is none.
func (g *Game) Component(id string) interface{} { return g.componentsByID[id] } func (g *Game) Component(id string) interface{} {
g.dbmu.RLock()
defer g.dbmu.RUnlock()
return g.db[id]
}
// Scan implements Scanner. // Scan implements Scanner.
func (g *Game) Scan() []interface{} { return []interface{}{g.Root} } func (g *Game) Scan() []interface{} { return []interface{}{g.Root} }
@ -104,7 +124,10 @@ func (g *Game) Load(assets fs.FS) error {
// to Component. You may call Prepare again (e.g. as an alternative to // to Component. You may call Prepare again (e.g. as an alternative to
// fastidiously calling RegisterComponent/UnregisterComponent). // fastidiously calling RegisterComponent/UnregisterComponent).
func (g *Game) Prepare() { func (g *Game) Prepare() {
g.componentsByID = make(map[string]interface{}) g.dbmu.Lock()
g.db = make(map[string]interface{})
g.dbmu.Unlock()
// Moment in time where db is empty... whatev.
Walk(g.Root, func(c interface{}) error { Walk(g.Root, func(c interface{}) error {
g.RegisterComponent(c) g.RegisterComponent(c)
return nil return nil

View file

@ -113,6 +113,11 @@ type Scener interface {
Updater Updater
} }
// Saver components can be saved to disk.
type Saver interface {
Save() error
}
// Updater components can update themselves. Update is called repeatedly. // Updater components can update themselves. Update is called repeatedly.
// Each component is responsible for calling Update on its child components // Each component is responsible for calling Update on its child components
// (so that disabling the parent prevents updates to the children, etc). // (so that disabling the parent prevents updates to the children, etc).

View file

@ -4,6 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"image" "image"
"io/fs" "io/fs"
"path/filepath"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -43,6 +44,9 @@ func (r *SceneRef) Load(assets fs.FS) error {
return nil return nil
} }
// Save saves the scene to a file in the current directory.
func (r *SceneRef) Save() error { return saveGobz(r.scene, filepath.Base(r.Path)) }
// The rest of the methods forward to r.scene, as such they will // The rest of the methods forward to r.scene, as such they will
// panic if the scene isn't loaded. // panic if the scene isn't loaded.

Binary file not shown.

47
main.go
View file

@ -1,8 +1,11 @@
package main package main
import ( import (
"bufio"
_ "image/png" _ "image/png"
"log" "log"
"os"
"strings"
"drjosh.dev/gurgle/engine" "drjosh.dev/gurgle/engine"
"drjosh.dev/gurgle/game" "drjosh.dev/gurgle/game"
@ -34,7 +37,51 @@ func main() {
} }
g.Prepare() g.Prepare()
// Run a repl on the console.
go repl(g)
// ... while the game also runs
if err := ebiten.RunGame(g); err != nil { if err := ebiten.RunGame(g); err != nil {
log.Fatalf("Game error: %v", err) log.Fatalf("Game error: %v", err)
} }
} }
func repl(g *engine.Game) {
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
tok := strings.Split(sc.Text(), " ")
if len(tok) == 0 {
continue
}
switch tok[0] {
case "quit":
os.Exit(0)
case "pause":
g.Disable()
case "resume", "unpause":
g.Enable()
case "save":
if len(tok) != 2 {
log.Print("Usage: save ID")
break
}
id := tok[1]
c := g.Component(id)
if c == nil {
log.Printf("Component %q not found", id)
break
}
s, ok := c.(engine.Saver)
if !ok {
log.Printf("Component %q not a Saver (type %T)", id, c)
break
}
if err := s.Save(); err != nil {
log.Printf("Couldn't save %q: %v", id, err)
}
}
}
if err := sc.Err(); err != nil {
log.Fatalf("Couldn't scan stdin: %v", err)
}
}