Simplify game preparation

This commit is contained in:
Josh Deprez 2021-08-05 12:26:41 +10:00 committed by Josh Deprez
parent b8a3040019
commit 66e45e495a
8 changed files with 154 additions and 124 deletions

View file

@ -20,17 +20,18 @@ type Collider interface {
// Actor handles basic movement. // Actor handles basic movement.
type Actor struct { type Actor struct {
Position image.Point CollisionDomain string
Pos image.Point
Size image.Point Size image.Point
game *Game collisionDomain interface{}
xRem, yRem float64 xRem, yRem float64
} }
func (a *Actor) CollidesAt(p image.Point) bool { func (a *Actor) CollidesAt(p image.Point) bool {
// TODO: more efficient test? // TODO: more efficient test?
hit := false hit := false
Walk(a.game, func(c interface{}) bool { Walk(a.collisionDomain, func(c interface{}) bool {
if coll, ok := c.(Collider); ok { if coll, ok := c.(Collider); ok {
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 hit = true
@ -51,13 +52,13 @@ func (a *Actor) MoveX(dx float64, onCollide func()) {
a.xRem -= float64(move) a.xRem -= float64(move)
sign := sign(move) sign := sign(move)
for move != 0 { for move != 0 {
if a.CollidesAt(a.Position.Add(image.Pt(sign, 0))) { if a.CollidesAt(a.Pos.Add(image.Pt(sign, 0))) {
if onCollide != nil { if onCollide != nil {
onCollide() onCollide()
} }
return return
} }
a.Position.X += sign a.Pos.X += sign
move -= sign move -= sign
} }
} }
@ -71,19 +72,19 @@ func (a *Actor) MoveY(dy float64, onCollide func()) {
a.yRem -= float64(move) a.yRem -= float64(move)
sign := sign(move) sign := sign(move)
for move != 0 { for move != 0 {
if a.CollidesAt(a.Position.Add(image.Pt(0, sign))) { if a.CollidesAt(a.Pos.Add(image.Pt(0, sign))) {
if onCollide != nil { if onCollide != nil {
onCollide() onCollide()
} }
return return
} }
a.Position.Y += sign a.Pos.Y += sign
move -= sign move -= sign
} }
} }
func (a *Actor) Build(g *Game) { func (a *Actor) Prepare(g *Game) {
a.game = g a.collisionDomain = g.Component(a.CollisionDomain)
} }
func sign(m int) int { func sign(m int) int {

View file

@ -43,8 +43,8 @@ type GobDumper struct {
game *Game game *Game
} }
// Build simply stores the reference to the Game. // Prepare simply stores the reference to the Game.
func (d *GobDumper) Build(g *Game) { d.game = g } func (d *GobDumper) Prepare(g *Game) { d.game = g }
// Update waits for the key combo, then dumps the game state into a gzipped gob. // Update waits for the key combo, then dumps the game state into a gzipped gob.
func (d *GobDumper) Update() error { func (d *GobDumper) Update() error {

View file

@ -10,26 +10,6 @@ func init() {
gob.Register(Game{}) gob.Register(Game{})
} }
// Identifier components have a sense of self. This makes it easier for
// components to find and interact with one another.
type Identifier interface {
Ident() string
}
// Builder components can be built. It is called when the game
// component database is being constructed. It should store the Game reference
// (if needed later on).
type Builder interface {
Build(game *Game)
}
// Scanner components can be scanned. It is called when the game tree is walked
// (such as when the game component database is constructed).
// Scan should return a slice containing all immediate subcomponents.
type Scanner interface {
Scan() []interface{}
}
// 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 {
ScreenWidth int ScreenWidth int
@ -103,15 +83,19 @@ func Walk(c interface{}, v func(interface{}) bool) {
} }
} }
// Build builds the component database, and calls Build, on all components // PrepareToRun builds the component database (using Walk) and then calls
// reachable via Scan. // Prepare on every Preparer. You must call PrepareToRun before passing to
func (g *Game) Build() { // ebiten.RunGame.
func (g *Game) PrepareToRun() {
g.componentsByID = make(map[string]interface{}) g.componentsByID = make(map[string]interface{})
Walk(g.Scene, func(c interface{}) bool { Walk(g, func(c interface{}) bool {
if b, ok := c.(Builder); ok {
b.Build(g)
}
g.RegisterComponent(c) g.RegisterComponent(c)
return true return true
}) })
Walk(g, func(c interface{}) bool {
if p, ok := c.(Prepper); ok {
p.Prepare(g)
}
return true
})
} }

40
engine/interface.go Normal file
View file

@ -0,0 +1,40 @@
package engine
import "github.com/hajimehoshi/ebiten/v2"
// Drawer components can draw themselves. Draw is called often.
// Each component is responsible for calling Draw on its child components.
type Drawer interface {
Draw(screen *ebiten.Image, geom ebiten.GeoM)
}
// Identifier components have a sense of self. This makes it easier for
// components to find and interact with one another.
type Identifier interface {
Ident() string
}
// Prepper components can be prepared. It is called after the component
// database has been populated but before the game is run. The component can
// store the reference to game, if needed, and also query the component database.
type Prepper interface {
Prepare(game *Game)
}
// Scanner components can be scanned. It is called when the game tree is walked
// (such as when the game component database is constructed).
// Scan should return a slice containing all immediate subcomponents.
type Scanner interface {
Scan() []interface{}
}
// Updater components can update themselves. Update is called repeatedly.
// Each component is responsible for calling Update on its child components.
type Updater interface {
Update() error
}
// ZPositioner is used to reorder layers.
type ZPositioner interface {
Z() float64
}

View file

@ -12,21 +12,6 @@ func init() {
gob.Register(Scene{}) gob.Register(Scene{})
} }
// Drawer components can draw themselves. Draw is called often.
type Drawer interface {
Draw(screen *ebiten.Image, geom ebiten.GeoM)
}
// Updater components can update themselves. Update is called repeatedly.
type Updater interface {
Update() error
}
// ZPositioner is used to reorder layers.
type ZPositioner interface {
Z() float64
}
// Scene manages drawing and updating a bunch of components. // Scene manages drawing and updating a bunch of components.
type Scene struct { type Scene struct {
Components []interface{} Components []interface{}

View file

@ -4,25 +4,17 @@ import (
"image" "image"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
) )
const dampen = 0.5 // Sprite combines an Actor with the ability to Draw from a single spritesheet.
const gravity = 0.2
// Sprite combines an Actor with the ability to Draw...
type Sprite struct { type Sprite struct {
Actor Actor
*Anim // TODO: better
Hidden bool Hidden bool
ID ID
Src ImageRef Src ImageRef
ZPos ZPos
vx, vy float64 // TODO: refactor anim *Anim
facingLeft bool
animIdleLeft, animIdleRight, animRunLeft, animRunRight *Anim
} }
func (s *Sprite) Draw(screen *ebiten.Image, geom ebiten.GeoM) { func (s *Sprite) Draw(screen *ebiten.Image, geom ebiten.GeoM) {
@ -30,59 +22,19 @@ func (s *Sprite) Draw(screen *ebiten.Image, geom ebiten.GeoM) {
return return
} }
var op ebiten.DrawImageOptions var op ebiten.DrawImageOptions
op.GeoM.Translate(float64(s.Actor.Position.X), float64(s.Actor.Position.Y)) op.GeoM.Translate(float64(s.Pos.X), float64(s.Pos.Y))
op.GeoM.Concat(geom) op.GeoM.Concat(geom)
frame := s.Anim.CurrentFrame() frame := s.anim.CurrentFrame()
src := s.Src.Image() src := s.Src.Image()
w, _ := src.Size() w, _ := src.Size()
sx, sy := (frame*s.Actor.Size.X)%w, ((frame*s.Actor.Size.X)/w)*s.Actor.Size.Y sp := image.Pt((frame*s.Size.X)%w, ((frame*s.Size.X)/w)*s.Size.Y)
screen.DrawImage(src.SubImage(image.Rect(sx, sy, sx+s.Actor.Size.X, sy+s.Actor.Size.Y)).(*ebiten.Image), &op) screen.DrawImage(src.SubImage(image.Rectangle{sp, sp.Add(s.Size)}).(*ebiten.Image), &op)
} }
func (s *Sprite) Scan() []interface{} { func (s *Sprite) Scan() []interface{} { return []interface{}{&s.Actor} }
return []interface{}{&s.Actor}
}
func (s *Sprite) Update() error { func (s *Sprite) SetAnim(a *Anim) { s.anim = a }
// TODO: delegate updating to something else
if s.Actor.CollidesAt(s.Actor.Position.Add(image.Pt(0, 1))) {
// Not falling
s.vy = 0
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
// Jump?
s.vy = -5
}
} else {
// Falling
s.vy += gravity
}
switch {
case ebiten.IsKeyPressed(ebiten.KeyLeft):
s.vx = -2
s.Anim = s.animRunLeft
s.facingLeft = true
case ebiten.IsKeyPressed(ebiten.KeyRight):
s.vx = 2
s.Anim = s.animRunRight
s.facingLeft = false
default:
s.vx = 0
s.Anim = s.animIdleRight
if s.facingLeft {
s.Anim = s.animIdleLeft
}
}
s.Actor.MoveX(s.vx, func() { s.vx = -s.vx * dampen })
s.Actor.MoveY(s.vy, func() { s.vy = -s.vy * dampen })
return s.Anim.Update()
}
func (s *Sprite) Build(g *Game) { func (s *Sprite) Update() error { return s.anim.Update() }
// TODO: better than this
s.animRunLeft = &Anim{Def: AnimDefs["aw_run_left"]}
s.animRunRight = &Anim{Def: AnimDefs["aw_run_right"]}
s.animIdleLeft = &Anim{Def: AnimDefs["aw_idle_left"]}
s.animIdleRight = &Anim{Def: AnimDefs["aw_idle_right"]}
}

68
game/aw.go Normal file
View file

@ -0,0 +1,68 @@
package game
import (
"image"
"drjosh.dev/gurgle/engine"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type Awakeman struct {
engine.Sprite
vx, vy float64
facingLeft bool
animIdleLeft, animIdleRight, animRunLeft, animRunRight *engine.Anim
}
func (aw *Awakeman) Update() error {
const (
bounceDampen = 0.5
gravity = 0.2
jumpVelocity = -5
runVelocity = 2
)
// Standing on something?
if aw.CollidesAt(aw.Pos.Add(image.Pt(0, 1))) {
// Not falling
aw.vy = 0
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
// Jump?
aw.vy = jumpVelocity
}
} else {
// Falling
aw.vy += gravity
}
switch {
case ebiten.IsKeyPressed(ebiten.KeyLeft):
aw.vx = -runVelocity
aw.SetAnim(aw.animRunLeft)
aw.facingLeft = true
case ebiten.IsKeyPressed(ebiten.KeyRight):
aw.vx = runVelocity
aw.SetAnim(aw.animRunRight)
aw.facingLeft = false
default:
aw.vx = 0
aw.SetAnim(aw.animIdleRight)
if aw.facingLeft {
aw.SetAnim(aw.animIdleLeft)
}
}
aw.MoveX(aw.vx, func() { aw.vx = -aw.vx * bounceDampen })
aw.MoveY(aw.vy, func() { aw.vy = -aw.vy * bounceDampen })
return aw.Sprite.Update()
}
func (aw *Awakeman) Prepare(*engine.Game) {
aw.animRunLeft = &engine.Anim{Def: engine.AnimDefs["aw_run_left"]}
aw.animRunRight = &engine.Anim{Def: engine.AnimDefs["aw_run_right"]}
aw.animIdleLeft = &engine.Anim{Def: engine.AnimDefs["aw_idle_left"]}
aw.animIdleRight = &engine.Anim{Def: engine.AnimDefs["aw_idle_right"]}
}
func (aw *Awakeman) Scan() []interface{} { return []interface{}{&aw.Sprite} }

16
main.go
View file

@ -8,6 +8,7 @@ import (
"log" "log"
"drjosh.dev/gurgle/engine" "drjosh.dev/gurgle/engine"
"drjosh.dev/gurgle/game"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -124,20 +125,19 @@ func main() {
ID: "right_wall", ID: "right_wall",
Rect: image.Rect(320, 0, 321, 240), Rect: image.Rect(320, 0, 321, 240),
}, },
/*&engine.SolidRect{ &game.Awakeman{
ID: "ground", Sprite: engine.Sprite{
Rect: image.Rect(0, 192, 320, 240), ID: "awakeman",
},*/
&engine.Sprite{
ID: "protagonist",
Actor: engine.Actor{ Actor: engine.Actor{
Position: image.Pt(100, 100), CollisionDomain: "level_1",
Pos: image.Pt(100, 100),
Size: image.Pt(10, 16), Size: image.Pt(10, 16),
}, },
Src: engine.ImageRef{Path: "assets/aw.png"}, Src: engine.ImageRef{Path: "assets/aw.png"},
ZPos: 1, ZPos: 1,
}, },
}, },
},
} }
game := &engine.Game{ game := &engine.Game{
@ -154,7 +154,7 @@ func main() {
}, },
}, },
} }
game.Build() game.PrepareToRun()
if err := ebiten.RunGame(game); err != nil { if err := ebiten.RunGame(game); err != nil {
log.Fatalf("Game error: %v", err) log.Fatalf("Game error: %v", err)