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.
|
// Actor handles basic movement.
|
||||||
type Actor struct {
|
type Actor struct {
|
||||||
Position image.Point
|
CollisionDomain string
|
||||||
Size image.Point
|
Pos 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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
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{})
|
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{}
|
||||||
|
|
|
@ -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
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"
|
"log"
|
||||||
|
|
||||||
"drjosh.dev/gurgle/engine"
|
"drjosh.dev/gurgle/engine"
|
||||||
|
"drjosh.dev/gurgle/game"
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -124,18 +125,17 @@ 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",
|
||||||
},*/
|
Actor: engine.Actor{
|
||||||
&engine.Sprite{
|
CollisionDomain: "level_1",
|
||||||
ID: "protagonist",
|
Pos: image.Pt(100, 100),
|
||||||
Actor: engine.Actor{
|
Size: image.Pt(10, 16),
|
||||||
Position: 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 {
|
if err := ebiten.RunGame(game); err != nil {
|
||||||
log.Fatalf("Game error: %v", err)
|
log.Fatalf("Game error: %v", err)
|
||||||
|
|
Loading…
Reference in a new issue