game does everything: updates

This commit is contained in:
Josh Deprez 2021-09-01 09:17:08 +10:00
parent 46c6a72fdd
commit 3a9109ae20
13 changed files with 163 additions and 220 deletions

Binary file not shown.

View file

@ -31,9 +31,6 @@ type Billboard struct {
// Draw draws the image. // Draw draws the image.
func (b *Billboard) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) { func (b *Billboard) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if b.Hidden {
return
}
var geom ebiten.GeoM var geom ebiten.GeoM
geom.Translate(float64(b.Pos.X), float64(b.Pos.Y)) geom.Translate(float64(b.Pos.X), float64(b.Pos.Y))
geom.Concat(opts.GeoM) geom.Concat(opts.GeoM)

View file

@ -29,8 +29,5 @@ type Fill struct {
} }
func (f *Fill) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) { func (f *Fill) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if f.Hidden {
return
}
screen.Fill(opts.ColorM.Apply(f.Color)) screen.Fill(opts.ColorM.Apply(f.Color))
} }

View file

@ -13,8 +13,6 @@ import (
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
const gameDoesEverything = true
var _ interface { var _ interface {
Disabler Disabler
Hider Hider
@ -40,99 +38,73 @@ type Game struct {
Hidden Hidden
ScreenWidth int ScreenWidth int
ScreenHeight int ScreenHeight int
Root DrawUpdater // typically a *Scene or SceneRef though Root interface{} // typically a *Scene or SceneRef though
dbmu sync.RWMutex dbmu sync.RWMutex
byID map[string]Identifier // Named components by ID byID map[string]Identifier // Named components by ID
byAB map[abKey]map[interface{}]struct{} // Ancestor/behaviour index byAB map[abKey]map[interface{}]struct{} // Ancestor/behaviour index
drawList drawers // draw list :| drawList drawList // draw list :|
par map[interface{}]interface{} // par[x] is parent of x par map[interface{}]interface{} // par[x] is parent of x
} }
type abKey struct { // Draw draws everything.
ancestor string
behaviour reflect.Type
}
var _ Drawer = tombstone{}
type tombstone struct{}
func (tombstone) Draw(*ebiten.Image, ebiten.DrawImageOptions) {}
func (tombstone) DrawOrder() float64 { return math.Inf(1) }
type drawers []Drawer
func (d drawers) Less(i, j int) bool { return d[i].DrawOrder() < d[j].DrawOrder() }
func (d drawers) Len() int { return len(d) }
func (d drawers) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func concatOpts(a, b ebiten.DrawImageOptions) ebiten.DrawImageOptions {
a.ColorM.Concat(b.ColorM)
a.GeoM.Concat(b.GeoM)
a.CompositeMode = b.CompositeMode
a.Filter = b.Filter
return a
}
// 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 { if g.Hidden {
return return
} }
if gameDoesEverything { // Hiding a parent component should hide the child objects, and the
type state struct { // transform applied to a child should be the cumulative transform of all
hidden bool // parents as well.
opts ebiten.DrawImageOptions // accum memoises the results for each component.
type state struct {
hidden bool
opts ebiten.DrawImageOptions
}
accum := map[interface{}]state{
g: {hidden: false},
}
// Draw everything in g.drawList, where not hidden (itself or any parent)
for _, d := range g.drawList {
// Is d hidden itself?
if h, ok := d.(Hider); ok && h.IsHidden() {
accum[d] = state{hidden: true}
continue // skip drawing
} }
accum := map[interface{}]state{ // Walk up g.par to find the nearest state in accum.
g: {hidden: false}, var st state
stack := []interface{}{d}
for p := g.par[d]; ; p = g.par[p] {
if s, found := accum[p]; found {
st = s
break
}
stack = append(stack, p)
} }
// draw everything // Unwind the stack, accumulating state along the way.
for _, d := range g.drawList { for len(stack) > 0 {
// is d directly hidden? l1 := len(stack) - 1
if h, ok := d.(Hider); ok && h.IsHidden() { p := stack[l1]
accum[d] = state{hidden: true} stack = stack[:l1]
continue // skip drawing if h, ok := p.(Hider); ok {
st.hidden = st.hidden || h.IsHidden()
} }
// walk up g.par to find the nearest parent state in accum
var st state
stack := []interface{}{d}
for p := g.par[d]; ; p = g.par[p] {
if s, found := accum[p]; found {
st = s
break
}
stack = append(stack, p)
}
// unwind the stack, accumulating state along the way
for len(stack) > 0 {
l1 := len(stack) - 1
p := stack[l1]
stack = stack[:l1]
if h, ok := p.(Hider); ok {
st.hidden = st.hidden || h.IsHidden()
}
if st.hidden {
accum[p] = state{hidden: true}
continue
}
if t, ok := p.(Transformer); ok {
st.opts = concatOpts(t.Transform(), st.opts)
}
accum[p] = st
}
// now...skip drawing if hidden :P
if st.hidden { if st.hidden {
accum[p] = state{hidden: true}
continue continue
} }
d.Draw(screen, st.opts) // p is not hidden, so compute its cumulative transform.
if t, ok := p.(Transformer); ok {
st.opts = concatOpts(t.Transform(), st.opts)
}
accum[p] = st
} }
} else { // !gameDoesEverything
g.Root.Draw(screen, ebiten.DrawImageOptions{}) // Skip drawing if hidden.
if st.hidden {
continue
}
d.Draw(screen, st.opts)
} }
} }
@ -141,23 +113,69 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (w, h int) {
return g.ScreenWidth, g.ScreenHeight return g.ScreenWidth, g.ScreenHeight
} }
// Update updates the scene. // Update updates everything.
func (g *Game) Update() error { func (g *Game) Update() error {
if g.Disabled { if g.Disabled {
return nil return nil
} }
if err := g.Root.Update(); err != nil { // Need to do a similar trick for Draw: disabling a parent object should
return err // disable the child objects.
// accum memoises the disabled state for each component.
accum := map[interface{}]bool{
g: false,
} }
if gameDoesEverything {
// sort the draw list (yes, on every frame) // Update everything that is not disabled.
sort.Stable(g.drawList) for u := range g.Query(g.Ident(), UpdaterType) {
// slice out any tombstones // Skip g (note g satisfies Updater, so this would infinitely recurse)
for i := len(g.drawList) - 1; i >= 0; i-- { if u == g {
if g.drawList[i] == (tombstone{}) { continue
g.drawList = g.drawList[:i] }
// Is u disabled itself?
if d, ok := u.(Disabler); ok && d.IsDisabled() {
accum[u] = true
continue
}
// Walk up g.par to find the nearest state in accum.
var st bool
stack := []interface{}{u}
for p := g.par[u]; ; p = g.par[p] {
if s, found := accum[p]; found {
st = s
break
} }
stack = append(stack, p)
}
// Unwind the stack, accumulating state along the way.
for len(stack) > 0 {
l1 := len(stack) - 1
p := stack[l1]
stack = stack[:l1]
if d, ok := p.(Disabler); ok {
st = st || d.IsDisabled()
}
accum[p] = st
}
// Skip updating if disabled.
if st {
continue
}
if err := u.(Updater).Update(); err != nil {
return err
}
}
// Sort the draw list (on every frame - this isn't as bad as it sounds)
sort.Stable(g.drawList)
// Truncate tombstones from the end.
for i := len(g.drawList) - 1; i >= 0; i-- {
if g.drawList[i] == (tombstone{}) {
g.drawList = g.drawList[:i]
} }
} }
return nil return nil
@ -360,3 +378,36 @@ func (g *Game) unregister(component interface{}) {
} }
delete(g.byID, i.Ident()) delete(g.byID, i.Ident())
} }
// --------- Helper types ---------
type abKey struct {
ancestor string
behaviour reflect.Type
}
var _ Drawer = tombstone{}
type tombstone struct{}
func (tombstone) Draw(*ebiten.Image, ebiten.DrawImageOptions) {}
func (tombstone) DrawOrder() float64 { return math.Inf(1) }
type drawList []Drawer
func (d drawList) Less(i, j int) bool { return d[i].DrawOrder() < d[j].DrawOrder() }
func (d drawList) Len() int { return len(d) }
func (d drawList) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func concatOpts(a, b ebiten.DrawImageOptions) ebiten.DrawImageOptions {
a.ColorM.Concat(b.ColorM)
a.GeoM.Concat(b.GeoM)
if b.CompositeMode != 0 {
a.CompositeMode = b.CompositeMode
}
if b.Filter != 0 {
a.Filter = b.Filter
}
return a
}

View file

@ -151,13 +151,10 @@ type Scener interface {
Bounder Bounder
Disabler Disabler
Drawer
Hider Hider
Identifier Identifier
Prepper
Scanner Scanner
Transformer Transformer
Updater
} }
// Saver components can be saved to disk. // Saver components can be saved to disk.

View file

@ -50,7 +50,7 @@ type ZOrder float64
// DrawOrder returns z as a float64. // DrawOrder returns z as a float64.
func (z ZOrder) DrawOrder() float64 { return float64(z) } func (z ZOrder) DrawOrder() float64 { return float64(z) }
// Some math helpers // ---------- Some math helpers ----------
func mul2(p, q image.Point) image.Point { func mul2(p, q image.Point) image.Point {
p.X *= q.X p.X *= q.X

View file

@ -2,8 +2,6 @@ package engine
import ( import (
"encoding/gob" "encoding/gob"
"math"
"sort"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -26,9 +24,10 @@ type Scene struct {
ZOrder ZOrder
} }
/*
// Draw draws all components in order. // Draw draws all components in order.
func (s *Scene) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) { func (s *Scene) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if s.Hidden || gameDoesEverything { if s.Hidden {
return return
} }
if s.Camera == nil { if s.Camera == nil {
@ -105,6 +104,7 @@ func (s *Scene) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
d.Draw(screen, opts) d.Draw(screen, opts)
} }
} }
*/
// Transform returns the camera transform // Transform returns the camera transform
func (s *Scene) Transform() ebiten.DrawImageOptions { func (s *Scene) Transform() ebiten.DrawImageOptions {
@ -114,26 +114,6 @@ func (s *Scene) Transform() ebiten.DrawImageOptions {
return s.Camera.Transform() return s.Camera.Transform()
} }
// Prepare does an initial Z-order sort.
func (s *Scene) Prepare(game *Game) error {
s.sortByDrawOrder()
return nil
}
// sortByDrawOrder sorts the components by Z position.
// Everything without a Z sorts first. Stable sort is used to avoid Z-fighting
// (among layers without a Z, or those with equal Z).
func (s *Scene) sortByDrawOrder() {
sort.SliceStable(s.Components, func(i, j int) bool {
a, aok := s.Components[i].(Drawer)
b, bok := s.Components[j].(Drawer)
if aok && bok {
return a.DrawOrder() < b.DrawOrder()
}
return !aok && bok
})
}
// Scan returns all immediate subcomponents (including the camera, if not nil). // Scan returns all immediate subcomponents (including the camera, if not nil).
func (s *Scene) Scan() []interface{} { func (s *Scene) Scan() []interface{} {
if s.Camera != nil { if s.Camera != nil {
@ -141,34 +121,3 @@ func (s *Scene) Scan() []interface{} {
} }
return s.Components return s.Components
} }
// Update calls Update on all Updater components.
func (s *Scene) Update() error {
if s.Disabled {
return nil
}
for _, c := range s.Components {
// Update each updater in turn
if u, ok := c.(Updater); ok {
if err := u.Update(); err != nil {
return err
}
}
}
// Check if the updates put the components out of order; if so, sort
cz := -math.MaxFloat64 // fun fact: this is min float64
for _, c := range s.Components {
z, ok := c.(Drawer)
if !ok {
continue
}
if t := z.DrawOrder(); t >= cz {
cz = t
continue
}
s.sortByDrawOrder()
return nil
}
return nil
}

View file

@ -62,11 +62,6 @@ func (r SceneRef) Disable() { r.scene.Disable() }
// Enable calls Enable on the scene. // Enable calls Enable on the scene.
func (r SceneRef) Enable() { r.scene.Enable() } func (r SceneRef) Enable() { r.scene.Enable() }
// Draw draws the scene.
func (r SceneRef) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
r.scene.Draw(screen, opts)
}
// DrawOrder returns the value of DrawOrder from the scene. // DrawOrder returns the value of DrawOrder from the scene.
func (r SceneRef) DrawOrder() float64 { return r.scene.DrawOrder() } func (r SceneRef) DrawOrder() float64 { return r.scene.DrawOrder() }
@ -82,14 +77,8 @@ func (r SceneRef) Show() { r.scene.Show() }
// Ident returns the value of Ident from the scene. // Ident returns the value of Ident from the scene.
func (r SceneRef) Ident() string { return r.scene.Ident() } func (r SceneRef) Ident() string { return r.scene.Ident() }
// Prepare prepares the scene.
func (r SceneRef) Prepare(g *Game) error { return r.scene.Prepare(g) }
// Scan returns the components in the scene. // Scan returns the components in the scene.
func (r SceneRef) Scan() []interface{} { return r.scene.Scan() } func (r SceneRef) Scan() []interface{} { return r.scene.Scan() }
// Transform returns the value of Transform from the scene. // Transform returns the value of Transform from the scene.
func (r SceneRef) Transform() ebiten.DrawImageOptions { return r.scene.Transform() } func (r SceneRef) Transform() ebiten.DrawImageOptions { return r.scene.Transform() }
// Update updates the scene.
func (r SceneRef) Update() error { return r.scene.Update() }

View file

@ -16,4 +16,6 @@ type SolidRect struct {
Bounds Bounds
} }
func (s SolidRect) CollidesWith(r image.Rectangle) bool { return s.BoundingRect().Overlaps(r) } func (s SolidRect) CollidesWith(r image.Rectangle) bool {
return s.BoundingRect().Overlaps(r)
}

View file

@ -20,7 +20,7 @@ func init() {
// Sprite combines an Actor with the ability to Draw from a single spritesheet. // Sprite combines an Actor with the ability to Draw from a single spritesheet.
type Sprite struct { type Sprite struct {
Actor Actor Actor
FrameOffset image.Point FrameOffset image.Point
Hidden Hidden
Sheet Sheet Sheet Sheet
@ -30,10 +30,7 @@ type Sprite struct {
} }
func (s *Sprite) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) { func (s *Sprite) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if s.Hidden { dp := s.Actor.Pos.Add(s.FrameOffset)
return
}
dp := s.Pos.Add(s.FrameOffset)
var geom ebiten.GeoM var geom ebiten.GeoM
geom.Translate(float64(dp.X), float64(dp.Y)) geom.Translate(float64(dp.X), float64(dp.Y))
geom.Concat(opts.GeoM) geom.Concat(opts.GeoM)
@ -57,4 +54,5 @@ func (s *Sprite) SetAnim(a *Anim) {
s.anim = a s.anim = a
} }
// anim isn't returned from Scan so we must update it ourselves
func (s *Sprite) Update() error { return s.anim.Update() } func (s *Sprite) Update() error { return s.anim.Update() }

View file

@ -14,7 +14,6 @@ var _ interface {
Drawer Drawer
Hider Hider
Scanner Scanner
Updater
} = &Tilemap{} } = &Tilemap{}
// Ensure StaticTile and AnimatedTile satisfy Tile. // Ensure StaticTile and AnimatedTile satisfy Tile.
@ -67,9 +66,6 @@ func (t *Tilemap) CollidesWith(r image.Rectangle) bool {
// Draw draws the tilemap. // Draw draws the tilemap.
func (t *Tilemap) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) { func (t *Tilemap) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if t.Hidden {
return
}
og := opts.GeoM og := opts.GeoM
var geom ebiten.GeoM var geom ebiten.GeoM
for p, tile := range t.Map { for p, tile := range t.Map {
@ -96,21 +92,6 @@ func (t *Tilemap) Scan() []interface{} {
return c return c
} }
// Update calls Update on any tiles that are Updaters, e.g. AnimatedTile.
func (t *Tilemap) Update() error {
if t.Disabled {
return nil
}
for _, tile := range t.Map {
if u, ok := tile.(Updater); ok {
if err := u.Update(); err != nil {
return err
}
}
}
return nil
}
// TileAt returns the tile present at the given world coordinate. // TileAt returns the tile present at the given world coordinate.
func (t *Tilemap) TileAt(wc image.Point) Tile { func (t *Tilemap) TileAt(wc image.Point) Tile {
return t.Map[div2(wc.Sub(t.Offset), t.Sheet.CellSize)] return t.Map[div2(wc.Sub(t.Offset), t.Sheet.CellSize)]

View file

@ -18,7 +18,6 @@ var (
Disabler Disabler
Hider Hider
Prepper Prepper
Updater
} = &WallUnit{} } = &WallUnit{}
) )
@ -80,9 +79,6 @@ type WallUnit struct {
} }
func (u *WallUnit) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) { func (u *WallUnit) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if u.Hidden {
return
}
var geom ebiten.GeoM var geom ebiten.GeoM
geom.Translate(float2(mul2(u.Pos, u.wall.UnitSize).Add(u.wall.UnitOffset).Add(u.wall.Offset))) geom.Translate(float2(mul2(u.Pos, u.wall.UnitSize).Add(u.wall.UnitOffset).Add(u.wall.Offset)))
geom.Concat(opts.GeoM) geom.Concat(opts.GeoM)
@ -98,12 +94,4 @@ func (u *WallUnit) Prepare(g *Game) error {
return nil return nil
} }
func (u *WallUnit) Update() error { func (u *WallUnit) Scan() []interface{} { return []interface{}{u.Tile} }
if u.Disabled {
return nil
}
if up, ok := u.Tile.(Updater); ok {
return up.Update()
}
return nil
}

View file

@ -12,9 +12,7 @@ import (
var _ interface { var _ interface {
engine.Identifier engine.Identifier
engine.Drawer // provided by Sprite
engine.Disabler engine.Disabler
engine.Hider // provided by Sprite
engine.Prepper engine.Prepper
engine.Scanner engine.Scanner
engine.Updater engine.Updater
@ -24,10 +22,10 @@ func init() {
gob.Register(&Awakeman{}) gob.Register(&Awakeman{})
} }
// Awakeman is a bit of a god object for now...
type Awakeman struct { type Awakeman struct {
engine.Disabled engine.Disabled
engine.Sprite Sprite engine.Sprite
CameraID string CameraID string
ToastID string ToastID string
@ -46,10 +44,6 @@ type Awakeman struct {
func (aw *Awakeman) Ident() string { return "awakeman" } func (aw *Awakeman) Ident() string { return "awakeman" }
func (aw *Awakeman) Update() error { func (aw *Awakeman) Update() error {
if aw.Disabled {
return nil
}
// TODO: better cheat for noclip // TODO: better cheat for noclip
if inpututil.IsKeyJustPressed(ebiten.KeyN) { if inpututil.IsKeyJustPressed(ebiten.KeyN) {
aw.noclip = !aw.noclip aw.noclip = !aw.noclip
@ -75,22 +69,22 @@ func (aw *Awakeman) Update() error {
aw.camera.Zoom = 2 aw.camera.Zoom = 2
} }
// aw.Pos is top-left corner, so add half size to get centre // aw.Pos is top-left corner, so add half size to get centre
aw.camera.Centre = aw.Pos.Add(aw.Size.Div(2)) aw.camera.Centre = aw.Sprite.Actor.Pos.Add(aw.Sprite.Actor.Size.Div(2))
return aw.Sprite.Update() return nil
} }
func (aw *Awakeman) noclipUpdate() error { func (aw *Awakeman) noclipUpdate() error {
if ebiten.IsKeyPressed(ebiten.KeyUp) { if ebiten.IsKeyPressed(ebiten.KeyUp) {
aw.Pos.Y-- aw.Sprite.Actor.Pos.Y--
} }
if ebiten.IsKeyPressed(ebiten.KeyDown) { if ebiten.IsKeyPressed(ebiten.KeyDown) {
aw.Pos.Y++ aw.Sprite.Actor.Pos.Y++
} }
if ebiten.IsKeyPressed(ebiten.KeyLeft) { if ebiten.IsKeyPressed(ebiten.KeyLeft) {
aw.Pos.X-- aw.Sprite.Actor.Pos.X--
} }
if ebiten.IsKeyPressed(ebiten.KeyRight) { if ebiten.IsKeyPressed(ebiten.KeyRight) {
aw.Pos.X++ aw.Sprite.Actor.Pos.X++
} }
return nil return nil
} }
@ -120,7 +114,7 @@ func (aw *Awakeman) realUpdate() error {
ux, uy := aw.vx, aw.vy ux, uy := aw.vx, aw.vy
// Has traction? // Has traction?
if aw.CollidesAt(aw.Pos.Add(image.Pt(0, 1))) { if aw.Sprite.Actor.CollidesAt(aw.Sprite.Actor.Pos.Add(image.Pt(0, 1))) {
// Not falling. // Not falling.
// Instantly decelerate (AW absorbs all kinetic E in legs, or something) // Instantly decelerate (AW absorbs all kinetic E in legs, or something)
if aw.jumpBuffer > 0 { if aw.jumpBuffer > 0 {
@ -160,25 +154,25 @@ func (aw *Awakeman) realUpdate() error {
switch { switch {
case ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA): case ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA):
aw.vx = -runVelocity aw.vx = -runVelocity
aw.SetAnim(aw.animRunLeft) aw.Sprite.SetAnim(aw.animRunLeft)
aw.facingLeft = true aw.facingLeft = true
case ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD): case ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD):
aw.vx = runVelocity aw.vx = runVelocity
aw.SetAnim(aw.animRunRight) aw.Sprite.SetAnim(aw.animRunRight)
aw.facingLeft = false aw.facingLeft = false
default: default:
aw.vx = 0 aw.vx = 0
aw.SetAnim(aw.animIdleRight) aw.Sprite.SetAnim(aw.animIdleRight)
if aw.facingLeft { if aw.facingLeft {
aw.SetAnim(aw.animIdleLeft) aw.Sprite.SetAnim(aw.animIdleLeft)
} }
} }
// s = (v_0 + v) / 2. // s = (v_0 + v) / 2.
aw.MoveX((ux+aw.vx)/2, nil) aw.Sprite.Actor.MoveX((ux+aw.vx)/2, nil)
// For Y, on collision, bounce a little bit. // For Y, on collision, bounce a little bit.
// Does not apply to X because controls override it anyway. // Does not apply to X because controls override it anyway.
aw.MoveY((uy+aw.vy)/2, func() { aw.Sprite.Actor.MoveY((uy+aw.vy)/2, func() {
aw.vy *= restitution aw.vy *= restitution
if math.Abs(aw.vy) < ε { if math.Abs(aw.vy) < ε {
aw.vy = 0 aw.vy = 0