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.
func (b *Billboard) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if b.Hidden {
return
}
var geom ebiten.GeoM
geom.Translate(float64(b.Pos.X), float64(b.Pos.Y))
geom.Concat(opts.GeoM)

View file

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

View file

@ -13,8 +13,6 @@ import (
"github.com/hajimehoshi/ebiten/v2"
)
const gameDoesEverything = true
var _ interface {
Disabler
Hider
@ -40,99 +38,73 @@ type Game struct {
Hidden
ScreenWidth int
ScreenHeight int
Root DrawUpdater // typically a *Scene or SceneRef though
Root interface{} // typically a *Scene or SceneRef though
dbmu sync.RWMutex
byID map[string]Identifier // Named components by ID
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
}
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 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.
// Draw draws everything.
func (g *Game) Draw(screen *ebiten.Image) {
if g.Hidden {
return
}
if gameDoesEverything {
type state struct {
hidden bool
opts ebiten.DrawImageOptions
// Hiding a parent component should hide the child objects, and the
// transform applied to a child should be the cumulative transform of all
// parents as well.
// 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{
g: {hidden: false},
// Walk up g.par to find the nearest 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)
}
// draw everything
for _, d := range g.drawList {
// is d directly hidden?
if h, ok := d.(Hider); ok && h.IsHidden() {
accum[d] = state{hidden: true}
continue // skip drawing
// 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()
}
// 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 {
accum[p] = state{hidden: true}
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
}
// Update updates the scene.
// Update updates everything.
func (g *Game) Update() error {
if g.Disabled {
return nil
}
if err := g.Root.Update(); err != nil {
return err
// Need to do a similar trick for Draw: disabling a parent object should
// 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)
sort.Stable(g.drawList)
// slice out any tombstones
for i := len(g.drawList) - 1; i >= 0; i-- {
if g.drawList[i] == (tombstone{}) {
g.drawList = g.drawList[:i]
// Update everything that is not disabled.
for u := range g.Query(g.Ident(), UpdaterType) {
// Skip g (note g satisfies Updater, so this would infinitely recurse)
if u == g {
continue
}
// 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
@ -360,3 +378,36 @@ func (g *Game) unregister(component interface{}) {
}
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
Disabler
Drawer
Hider
Identifier
Prepper
Scanner
Transformer
Updater
}
// Saver components can be saved to disk.

View file

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

View file

@ -2,8 +2,6 @@ package engine
import (
"encoding/gob"
"math"
"sort"
"github.com/hajimehoshi/ebiten/v2"
)
@ -26,9 +24,10 @@ type Scene struct {
ZOrder
}
/*
// Draw draws all components in order.
func (s *Scene) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if s.Hidden || gameDoesEverything {
if s.Hidden {
return
}
if s.Camera == nil {
@ -105,6 +104,7 @@ func (s *Scene) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
d.Draw(screen, opts)
}
}
*/
// Transform returns the camera transform
func (s *Scene) Transform() ebiten.DrawImageOptions {
@ -114,26 +114,6 @@ func (s *Scene) Transform() ebiten.DrawImageOptions {
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).
func (s *Scene) Scan() []interface{} {
if s.Camera != nil {
@ -141,34 +121,3 @@ func (s *Scene) Scan() []interface{} {
}
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.
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.
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.
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.
func (r SceneRef) Scan() []interface{} { return r.scene.Scan() }
// Transform returns the value of Transform from the scene.
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
}
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.
type Sprite struct {
Actor
Actor Actor
FrameOffset image.Point
Hidden
Sheet Sheet
@ -30,10 +30,7 @@ type Sprite struct {
}
func (s *Sprite) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if s.Hidden {
return
}
dp := s.Pos.Add(s.FrameOffset)
dp := s.Actor.Pos.Add(s.FrameOffset)
var geom ebiten.GeoM
geom.Translate(float64(dp.X), float64(dp.Y))
geom.Concat(opts.GeoM)
@ -57,4 +54,5 @@ func (s *Sprite) SetAnim(a *Anim) {
s.anim = a
}
// anim isn't returned from Scan so we must update it ourselves
func (s *Sprite) Update() error { return s.anim.Update() }

View file

@ -14,7 +14,6 @@ var _ interface {
Drawer
Hider
Scanner
Updater
} = &Tilemap{}
// Ensure StaticTile and AnimatedTile satisfy Tile.
@ -67,9 +66,6 @@ func (t *Tilemap) CollidesWith(r image.Rectangle) bool {
// Draw draws the tilemap.
func (t *Tilemap) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if t.Hidden {
return
}
og := opts.GeoM
var geom ebiten.GeoM
for p, tile := range t.Map {
@ -96,21 +92,6 @@ func (t *Tilemap) Scan() []interface{} {
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.
func (t *Tilemap) TileAt(wc image.Point) Tile {
return t.Map[div2(wc.Sub(t.Offset), t.Sheet.CellSize)]

View file

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

View file

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