Simplify game preparation
This commit is contained in:
parent
b8a3040019
commit
66e45e495a
8 changed files with 154 additions and 124 deletions
|
@ -20,17 +20,18 @@ type Collider interface {
|
|||
|
||||
// Actor handles basic movement.
|
||||
type Actor struct {
|
||||
Position image.Point
|
||||
Size image.Point
|
||||
CollisionDomain string
|
||||
Pos image.Point
|
||||
Size image.Point
|
||||
|
||||
game *Game
|
||||
xRem, yRem float64
|
||||
collisionDomain interface{}
|
||||
xRem, yRem float64
|
||||
}
|
||||
|
||||
func (a *Actor) CollidesAt(p image.Point) bool {
|
||||
// TODO: more efficient test?
|
||||
hit := false
|
||||
Walk(a.game, func(c interface{}) bool {
|
||||
Walk(a.collisionDomain, func(c interface{}) bool {
|
||||
if coll, ok := c.(Collider); ok {
|
||||
if coll.CollidesWith(image.Rectangle{Min: p, Max: p.Add(a.Size)}) {
|
||||
hit = true
|
||||
|
@ -51,13 +52,13 @@ func (a *Actor) MoveX(dx float64, onCollide func()) {
|
|||
a.xRem -= float64(move)
|
||||
sign := sign(move)
|
||||
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 {
|
||||
onCollide()
|
||||
}
|
||||
return
|
||||
}
|
||||
a.Position.X += sign
|
||||
a.Pos.X += sign
|
||||
move -= sign
|
||||
}
|
||||
}
|
||||
|
@ -71,19 +72,19 @@ func (a *Actor) MoveY(dy float64, onCollide func()) {
|
|||
a.yRem -= float64(move)
|
||||
sign := sign(move)
|
||||
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 {
|
||||
onCollide()
|
||||
}
|
||||
return
|
||||
}
|
||||
a.Position.Y += sign
|
||||
a.Pos.Y += sign
|
||||
move -= sign
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Actor) Build(g *Game) {
|
||||
a.game = g
|
||||
func (a *Actor) Prepare(g *Game) {
|
||||
a.collisionDomain = g.Component(a.CollisionDomain)
|
||||
}
|
||||
|
||||
func sign(m int) int {
|
||||
|
|
|
@ -43,8 +43,8 @@ type GobDumper struct {
|
|||
game *Game
|
||||
}
|
||||
|
||||
// Build simply stores the reference to the Game.
|
||||
func (d *GobDumper) Build(g *Game) { d.game = g }
|
||||
// Prepare simply stores the reference to the Game.
|
||||
func (d *GobDumper) Prepare(g *Game) { d.game = g }
|
||||
|
||||
// Update waits for the key combo, then dumps the game state into a gzipped gob.
|
||||
func (d *GobDumper) Update() error {
|
||||
|
|
|
@ -10,26 +10,6 @@ func init() {
|
|||
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.
|
||||
type Game struct {
|
||||
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
|
||||
// reachable via Scan.
|
||||
func (g *Game) Build() {
|
||||
// PrepareToRun builds the component database (using Walk) and then calls
|
||||
// Prepare on every Preparer. You must call PrepareToRun before passing to
|
||||
// ebiten.RunGame.
|
||||
func (g *Game) PrepareToRun() {
|
||||
g.componentsByID = make(map[string]interface{})
|
||||
Walk(g.Scene, func(c interface{}) bool {
|
||||
if b, ok := c.(Builder); ok {
|
||||
b.Build(g)
|
||||
}
|
||||
Walk(g, func(c interface{}) bool {
|
||||
g.RegisterComponent(c)
|
||||
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
40
engine/interface.go
Normal 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
|
||||
}
|
|
@ -12,21 +12,6 @@ func init() {
|
|||
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.
|
||||
type Scene struct {
|
||||
Components []interface{}
|
||||
|
|
|
@ -4,25 +4,17 @@ import (
|
|||
"image"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
)
|
||||
|
||||
const dampen = 0.5
|
||||
const gravity = 0.2
|
||||
|
||||
// Sprite combines an Actor with the ability to Draw...
|
||||
// Sprite combines an Actor with the ability to Draw from a single spritesheet.
|
||||
type Sprite struct {
|
||||
Actor
|
||||
*Anim // TODO: better
|
||||
Hidden bool
|
||||
ID
|
||||
Src ImageRef
|
||||
ZPos
|
||||
|
||||
vx, vy float64 // TODO: refactor
|
||||
facingLeft bool
|
||||
|
||||
animIdleLeft, animIdleRight, animRunLeft, animRunRight *Anim
|
||||
anim *Anim
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
frame := s.Anim.CurrentFrame()
|
||||
frame := s.anim.CurrentFrame()
|
||||
src := s.Src.Image()
|
||||
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{} {
|
||||
return []interface{}{&s.Actor}
|
||||
}
|
||||
func (s *Sprite) Scan() []interface{} { return []interface{}{&s.Actor} }
|
||||
|
||||
func (s *Sprite) Update() error {
|
||||
// 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) SetAnim(a *Anim) { s.anim = a }
|
||||
|
||||
func (s *Sprite) Build(g *Game) {
|
||||
// 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"]}
|
||||
}
|
||||
func (s *Sprite) Update() error { return s.anim.Update() }
|
||||
|
|
68
game/aw.go
Normal file
68
game/aw.go
Normal 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} }
|
24
main.go
24
main.go
|
@ -8,6 +8,7 @@ import (
|
|||
"log"
|
||||
|
||||
"drjosh.dev/gurgle/engine"
|
||||
"drjosh.dev/gurgle/game"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
|
@ -124,18 +125,17 @@ func main() {
|
|||
ID: "right_wall",
|
||||
Rect: image.Rect(320, 0, 321, 240),
|
||||
},
|
||||
/*&engine.SolidRect{
|
||||
ID: "ground",
|
||||
Rect: image.Rect(0, 192, 320, 240),
|
||||
},*/
|
||||
&engine.Sprite{
|
||||
ID: "protagonist",
|
||||
Actor: engine.Actor{
|
||||
Position: image.Pt(100, 100),
|
||||
Size: image.Pt(10, 16),
|
||||
&game.Awakeman{
|
||||
Sprite: engine.Sprite{
|
||||
ID: "awakeman",
|
||||
Actor: engine.Actor{
|
||||
CollisionDomain: "level_1",
|
||||
Pos: image.Pt(100, 100),
|
||||
Size: image.Pt(10, 16),
|
||||
},
|
||||
Src: engine.ImageRef{Path: "assets/aw.png"},
|
||||
ZPos: 1,
|
||||
},
|
||||
Src: engine.ImageRef{Path: "assets/aw.png"},
|
||||
ZPos: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ func main() {
|
|||
},
|
||||
},
|
||||
}
|
||||
game.Build()
|
||||
game.PrepareToRun()
|
||||
|
||||
if err := ebiten.RunGame(game); err != nil {
|
||||
log.Fatalf("Game error: %v", err)
|
||||
|
|
Loading…
Reference in a new issue