error-based walk; Scener

This commit is contained in:
Josh Deprez 2021-08-20 15:01:31 +10:00
parent 91bf4939e7
commit 946b76df9a
5 changed files with 83 additions and 52 deletions

View file

@ -2,12 +2,15 @@ package engine
import ( import (
"encoding/gob" "encoding/gob"
"errors"
"image" "image"
) )
// Ensure Actor satisfies interfaces. // Ensure Actor satisfies interfaces.
var _ Prepper = &Actor{} var _ Prepper = &Actor{}
var errCollision = errors.New("collision detected")
func init() { func init() {
gob.Register(Actor{}) gob.Register(Actor{})
} }
@ -26,17 +29,16 @@ type Actor struct {
} }
func (a *Actor) CollidesAt(p image.Point) bool { func (a *Actor) CollidesAt(p image.Point) bool {
hit := false return nil != Walk(a.collisionDomain, func(c interface{}) error {
Walk(a.collisionDomain, func(c interface{}) bool { coll, ok := c.(Collider)
if coll, ok := c.(Collider); ok { if !ok {
return nil
}
if coll.CollidesWith(image.Rectangle{Min: p, Max: p.Add(a.Size)}) { if coll.CollidesWith(image.Rectangle{Min: p, Max: p.Add(a.Size)}) {
hit = true return errCollision
return false
} }
} return nil
return true
}) })
return hit
} }
func (a *Actor) MoveX(dx float64, onCollide func()) { func (a *Actor) MoveX(dx float64, onCollide func()) {

View file

@ -19,6 +19,9 @@ var (
AnimDefs map[string]*AnimDef AnimDefs map[string]*AnimDef
imageCache = make(map[string]*ebiten.Image) imageCache = make(map[string]*ebiten.Image)
// Ensure SceneRef does the same stuff as Scene.
_ Scener = &SceneRef{}
) )
// AnimRef manages an Anim using a premade AnimDef from the cache. // AnimRef manages an Anim using a premade AnimDef from the cache.
@ -78,26 +81,37 @@ func (r *ImageRef) Image() *ebiten.Image {
type SceneRef struct { type SceneRef struct {
Path string Path string
scene *Scene scene *Scene // not exported for gob reasons
} }
func (r *SceneRef) Scene() *Scene { // Load loads the scene from the file and then calls Load
if r.scene != nil { // on the freshly-loaded Scene.
return r.scene func (r *SceneRef) Load() error {
}
f, err := AssetFS.Open(r.Path) f, err := AssetFS.Open(r.Path)
if err != nil { if err != nil {
log.Fatalf("Couldn't open asset: %v", err) return err
} }
defer f.Close() defer f.Close()
gz, err := gzip.NewReader(f) gz, err := gzip.NewReader(f)
if err != nil { if err != nil {
log.Fatalf("Couldn't gunzip asset: %v", err) return err
} }
r.scene = new(Scene) sc := new(Scene)
if err := gob.NewDecoder(gz).Decode(r.scene); err != nil { if err := gob.NewDecoder(gz).Decode(sc); err != nil {
log.Fatalf("Couldn't decode asset: %v", err) return err
} }
return r.scene r.scene = sc
return sc.Load()
} }
// Implement Scener by forwarding all the other calls to r.scene
func (r SceneRef) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
r.scene.Draw(screen, opts)
}
func (r SceneRef) DrawOrder() float64 { return r.scene.DrawOrder() }
func (r SceneRef) Ident() string { return r.scene.Ident() }
func (r SceneRef) Prepare(g *Game) { r.scene.Prepare(g) }
func (r SceneRef) Scan() []interface{} { return r.scene.Scan() }
func (r SceneRef) Update() error { return r.scene.Update() }

View file

@ -10,21 +10,11 @@ func init() {
gob.Register(Game{}) gob.Register(Game{})
} }
type GameMode int
const (
GameModeMenu = GameMode(iota)
GameModePlay
GameModePause
GameModeEdit
)
// Game implements the ebiten methods using a collection of components. // Game implements the ebiten methods using a collection of components.
type Game struct { type Game struct {
Mode GameMode
ScreenWidth int ScreenWidth int
ScreenHeight int ScreenHeight int
*Scene Scene *Scene
componentsByID map[string]interface{} componentsByID map[string]interface{}
} }
@ -79,21 +69,21 @@ func (g *Game) Component(id string) interface{} { return g.componentsByID[id] }
func (g *Game) Scan() []interface{} { return []interface{}{g.Scene} } func (g *Game) Scan() []interface{} { return []interface{}{g.Scene} }
// Walk calls v with every component reachable from c via Scan, recursively, // Walk calls v with every component reachable from c via Scan, recursively,
// for as long as visit returns true. // for as long as visit returns nil.
func Walk(c interface{}, v func(interface{}) bool) { func Walk(c interface{}, v func(interface{}) error) error {
if !v(c) { if err := v(c); err != nil {
return return err
} }
sc, ok := c.(Scanner) sc, ok := c.(Scanner)
if !ok { if !ok {
return return nil
} }
for _, c := range sc.Scan() { for _, c := range sc.Scan() {
if !v(c) { if err := Walk(c, v); err != nil {
return return err
} }
Walk(c, v)
} }
return nil
} }
// PrepareToRun builds the component database (using Walk) and then calls // PrepareToRun builds the component database (using Walk) and then calls
@ -101,14 +91,14 @@ func Walk(c interface{}, v func(interface{}) bool) {
// ebiten.RunGame. // ebiten.RunGame.
func (g *Game) PrepareToRun() { func (g *Game) PrepareToRun() {
g.componentsByID = make(map[string]interface{}) g.componentsByID = make(map[string]interface{})
Walk(g, func(c interface{}) bool { Walk(g, func(c interface{}) error {
g.RegisterComponent(c) g.RegisterComponent(c)
return true return nil
}) })
Walk(g, func(c interface{}) bool { Walk(g, func(c interface{}) error {
if p, ok := c.(Prepper); ok { if p, ok := c.(Prepper); ok {
p.Prepare(g) p.Prepare(g)
} }
return true return nil
}) })
} }

View file

@ -29,6 +29,12 @@ type Identifier interface {
Ident() string Ident() string
} }
// Loader components get the chance to load themselves. This happens
// before preparation.
type Loader interface {
Load() error
}
// ParallaxScaler components have a scaling factor. This is used for // ParallaxScaler components have a scaling factor. This is used for
// parallax layers in a scene, and can be thought of as 1/distance. // parallax layers in a scene, and can be thought of as 1/distance.
type ParallaxScaler interface { type ParallaxScaler interface {
@ -49,6 +55,18 @@ type Scanner interface {
Scan() []interface{} Scan() []interface{}
} }
// Scener components do everything that a Scene can. The other implementation
// is SceneRef.
type Scener interface {
Drawer
DrawOrderer
Identifier
Loader
Prepper
Scanner
Updater
}
// 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

@ -9,15 +9,8 @@ import (
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
// Ensure Scene satisfies interfaces. // Ensure Scene satisfies Scener.
var ( var _ Scener = &Scene{}
_ Identifier = &Scene{}
_ Drawer = &Scene{}
_ DrawOrderer = &Scene{}
_ Prepper = &Scene{}
_ Scanner = &Scene{}
_ Updater = &Scene{}
)
func init() { func init() {
gob.Register(Scene{}) gob.Register(Scene{})
@ -46,6 +39,20 @@ func (s *Scene) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
} }
} }
// Load loads any components that need loading.
func (s *Scene) Load() error {
for _, i := range s.Components {
l, ok := i.(Loader)
if !ok {
continue
}
if err := l.Load(); err != nil {
return err
}
}
return nil
}
// Prepare does an initial Z-order sort. // Prepare does an initial Z-order sort.
func (s *Scene) Prepare(game *Game) { s.sortByDrawOrder() } func (s *Scene) Prepare(game *Game) { s.sortByDrawOrder() }