anim overhaul, again

This commit is contained in:
Josh Deprez 2021-09-02 11:53:04 +10:00
parent d89a1c2e71
commit df5afe11c1
9 changed files with 179 additions and 157 deletions

View file

@ -3,42 +3,51 @@ package engine
import "encoding/gob"
// Ensure Anim satisfies Animer.
var _ Animer = &Anim{}
var _ interface {
Cell() int
Reset()
Updater
} = &Anim{}
func init() {
gob.Register(&Anim{})
}
// AnimFrame describes a frame in an animation.
type AnimFrame struct {
Frame int // show this frame
// AnimDef defines an animation, as a sequence of steps and other information.
type AnimDef struct {
Steps []AnimStep
OneShot bool
}
// NewAnim spawns a new anim using this def, or nil if d is nil.
func (d *AnimDef) NewAnim() *Anim {
if d == nil {
return nil
}
return &Anim{Def: d}
}
// AnimStep describes a step in an animation.
type AnimStep struct {
Cell int // show this cell
Duration int // for this long, in ticks
}
// Anim is n animation being displayed, together with the current state.
// A nil *Anim can be used, but always returns 0 for the current frame.
// Anim is the current state of an animation being played (think of it as an
// instance of an AnimDef). nil *Anim can be used, but always returns 0 for the
// current frame.
type Anim struct {
Frames []AnimFrame
OneShot bool
Index int
Ticks int
Def *AnimDef
Index int // current step index
Ticks int // ticks spent at this step
}
// Copy makes a shallow copy of the anim.
func (a *Anim) Copy() *Anim {
if a == nil {
return nil
}
a2 := *a
return &a2
}
// CurrentFrame returns the frame number for the current index.
func (a *Anim) CurrentFrame() int {
// Cell returns the cell index for the current step.
func (a *Anim) Cell() int {
if a == nil {
return 0
}
return a.Frames[a.Index].Frame
return a.Def.Steps[a.Index].Cell
}
// Reset resets both Index and Ticks to 0.
@ -55,15 +64,15 @@ func (a *Anim) Update() error {
return nil
}
a.Ticks++
if a.OneShot && a.Index == len(a.Frames)-1 {
if a.Def.OneShot && a.Index == len(a.Def.Steps)-1 {
// on the last frame of a one shot so remain on final frame
return nil
}
if a.Ticks >= a.Frames[a.Index].Duration {
if a.Ticks >= a.Def.Steps[a.Index].Duration {
a.Ticks = 0
a.Index++
}
if !a.OneShot && a.Index >= len(a.Frames) {
if !a.Def.OneShot && a.Index >= len(a.Def.Steps) {
a.Index = 0
}
return nil

View file

@ -1,53 +0,0 @@
package engine
import (
"encoding/gob"
"io/fs"
)
// TODO: tidy this crap up
// Anims probably belong with Sheet
var (
animCache = make(map[assetKey]Anim)
_ interface {
Animer
Loader
} = &AnimRef{}
)
func init() {
gob.Register(&AnimRef{})
}
// AnimRef manages an Anim using a premade AnimDef from the cache.
type AnimRef struct {
Path string
anim Anim
}
func (r *AnimRef) Load(assets fs.FS) error {
// Fast path: set r.anim to a copy
anim, found := animCache[assetKey{assets, r.Path}]
if found {
r.anim = anim
return nil
}
// Slow path: load from gobz file
if err := LoadGobz(&r.anim, assets, r.Path); err != nil {
return err
}
animCache[assetKey{assets, r.Path}] = r.anim
return nil
}
// CurrentFrame returns the value of CurrentFrame from r.anim.
func (r *AnimRef) CurrentFrame() int { return r.anim.CurrentFrame() }
// Reset calls Reset on r.anim.
func (r *AnimRef) Reset() { r.anim.Reset() }
// Update calls Update on r.anim.
func (r *AnimRef) Update() error { return r.anim.Update() }

View file

@ -12,7 +12,6 @@ import (
var (
// TypeOf(pointer to interface).Elem() is "idiomatic" -
// see https://pkg.go.dev/reflect#example-TypeOf
AnimerType = reflect.TypeOf((*Animer)(nil)).Elem()
BounderType = reflect.TypeOf((*Bounder)(nil)).Elem()
ColliderType = reflect.TypeOf((*Collider)(nil)).Elem()
DisablerType = reflect.TypeOf((*Disabler)(nil)).Elem()
@ -28,7 +27,6 @@ var (
// Behaviours lists all the behaviours that can be queried with Game.Query.
Behaviours = []reflect.Type{
AnimerType,
BounderType,
ColliderType,
DisablerType,
@ -44,14 +42,6 @@ var (
}
)
// Animer components have a current frame index.
type Animer interface {
Updater
CurrentFrame() int
Reset()
}
// Bounder components have a bounding rectangle.
type Bounder interface {
BoundingRect() image.Rectangle

View file

@ -13,24 +13,33 @@ var _ interface {
// Sheet handles images that consist of a grid of equally sized regions
// (cells) and can produce subimages for the cell at an index. This is useful
// for various applications such as sprite animation and tile maps.
// for various applications such as sprite animation and tile maps. Additionally
// each sheet carries a collection of animations that use the sheet.
type Sheet struct {
AnimDefs map[string]*AnimDef
CellSize image.Point
Src ImageRef
w int // width as measured in number of cells
}
// NewAnim returns a new Anim for the given key, or nil if not found in
// AnimDefs.
func (s *Sheet) NewAnim(key string) *Anim {
return s.AnimDefs[key].NewAnim()
}
// Prepare computes the width of the image (in cells).
func (s *Sheet) Prepare(*Game) error {
s.w, _ = s.Src.Image().Size()
s.w /= s.CellSize.X
return nil
}
// Scan returns the Src.
func (s *Sheet) Scan() []interface{} { return []interface{}{&s.Src} }
// SubImage returns an *ebiten.Image corresponding to the cell at the given
// index.
// SubImage returns an *ebiten.Image corresponding to the given cell index.
func (s *Sheet) SubImage(i int) *ebiten.Image {
p := pmul(image.Pt(i%s.w, i/s.w), s.CellSize)
r := image.Rectangle{p, p.Add(s.CellSize)}

View file

@ -30,10 +30,12 @@ type Sprite struct {
anim *Anim
}
// Draw draws the current cell to the screen.
func (s *Sprite) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
screen.DrawImage(s.Sheet.SubImage(s.anim.CurrentFrame()), opts)
screen.DrawImage(s.Sheet.SubImage(s.anim.Cell()), opts)
}
// Scan returns the Actor and the Sheet.
func (s *Sprite) Scan() []interface{} {
return []interface{}{
&s.Actor,
@ -41,6 +43,8 @@ func (s *Sprite) Scan() []interface{} {
}
}
// SetAnim sets the Anim to use for the sprite. If it is not the same as the
// one currently set, it resets the new anim.
func (s *Sprite) SetAnim(a *Anim) {
if s.anim != a {
a.Reset()
@ -48,11 +52,12 @@ func (s *Sprite) SetAnim(a *Anim) {
s.anim = a
}
// Transform returns a translation by the FrameOffset.
func (s *Sprite) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(pfloat(s.Actor.Pos.Add(s.FrameOffset)))
return opts
}
// anim can change a bit so we don't tell Game about it, but that means it must
// be updated here.
// Update updates the Sprite's anim. anim can change a bit so we don't tell Game
// about it, but that means it must be updated manually.
func (s *Sprite) Update() error { return s.anim.Update() }

View file

@ -2,7 +2,9 @@ package engine
import (
"encoding/gob"
"fmt"
"image"
"io/fs"
"github.com/hajimehoshi/ebiten/v2"
)
@ -23,7 +25,7 @@ var (
_ interface {
Tile
Scanner
} = AnimatedTile{}
} = &AnimatedTile{}
)
func init() {
@ -77,11 +79,27 @@ func (t *Tilemap) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
geom.Concat(og)
opts.GeoM = geom
src := t.Sheet.SubImage(tile.CellIndex())
src := t.Sheet.SubImage(tile.Cell())
screen.DrawImage(src, opts)
}
}
// Load instantiates animations for all AnimatedTiles.
func (t *Tilemap) Load(fs.FS) error {
for _, tile := range t.Map {
at, ok := tile.(*AnimatedTile)
if !ok {
continue
}
ad, found := t.Sheet.AnimDefs[at.AnimKey]
if !found {
return fmt.Errorf("anim key %q not in sheet AnimDefs", at.AnimKey)
}
at.anim = ad.NewAnim()
}
return nil
}
// Scan returns a slice containing Src and all non-nil tiles.
func (t *Tilemap) Scan() []interface{} {
c := make([]interface{}, 1, len(t.Map)+1)
@ -116,20 +134,22 @@ func (t *Tilemap) TileBounds(wc image.Point) image.Rectangle {
// Tile is the interface needed by Tilemap.
type Tile interface {
CellIndex() int
Cell() int
}
// StaticTile returns a fixed tile index.
type StaticTile int
func (s StaticTile) CellIndex() int { return int(s) }
func (s StaticTile) Cell() int { return int(s) }
// AnimatedTile uses an Anim to choose a tile index.
type AnimatedTile struct {
Anim Animer
AnimKey string
anim *Anim
}
func (a AnimatedTile) CellIndex() int { return a.Anim.CurrentFrame() }
func (a *AnimatedTile) Cell() int { return a.anim.Cell() }
// Scan returns a.Anim.
func (a AnimatedTile) Scan() []interface{} { return []interface{}{a.Anim} }
// Scan returns a.anim.
func (a *AnimatedTile) Scan() []interface{} { return []interface{}{a.anim} }

View file

@ -99,7 +99,7 @@ type WallUnit struct {
// Draw draws this wall unit.
func (u *WallUnit) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
screen.DrawImage(u.wall.Sheet.SubImage(u.Tile.CellIndex()), opts)
screen.DrawImage(u.wall.Sheet.SubImage(u.Tile.Cell()), opts)
}
// Scan returns the Tile.

View file

@ -2,6 +2,8 @@ package game
import (
"encoding/gob"
"errors"
"fmt"
"image"
"math"
@ -182,39 +184,41 @@ func (aw *Awakeman) realUpdate() error {
}
func (aw *Awakeman) Prepare(game *engine.Game) error {
aw.camera = game.Component(aw.CameraID).(*engine.Camera)
aw.toast, _ = game.Component(aw.ToastID).(*engine.DebugToast)
cam, ok := game.Component(aw.CameraID).(*engine.Camera)
if !ok {
return fmt.Errorf("component %q not *engine.Camera", aw.CameraID)
}
aw.camera = cam
tst, ok := game.Component(aw.ToastID).(*engine.DebugToast)
if !ok {
return fmt.Errorf("component %q not *engine.DebugToast", aw.ToastID)
}
aw.toast = tst
aw.animIdleLeft = &engine.Anim{Frames: []engine.AnimFrame{
{Frame: 1, Duration: 60},
}}
aw.animIdleRight = &engine.Anim{Frames: []engine.AnimFrame{
{Frame: 0, Duration: 60},
}}
aw.animRunLeft = &engine.Anim{Frames: []engine.AnimFrame{
{Frame: 14, Duration: 3},
{Frame: 15, Duration: 5},
{Frame: 16, Duration: 3},
{Frame: 17, Duration: 3},
}}
aw.animRunRight = &engine.Anim{Frames: []engine.AnimFrame{
{Frame: 10, Duration: 3},
{Frame: 11, Duration: 5},
{Frame: 12, Duration: 3},
{Frame: 13, Duration: 3},
}}
aw.animWalkRight = &engine.Anim{Frames: []engine.AnimFrame{
{Frame: 2, Duration: 6},
{Frame: 3, Duration: 6},
{Frame: 4, Duration: 6},
{Frame: 5, Duration: 6},
}}
aw.animWalkLeft = &engine.Anim{Frames: []engine.AnimFrame{
{Frame: 6, Duration: 6},
{Frame: 7, Duration: 6},
{Frame: 8, Duration: 6},
{Frame: 9, Duration: 6},
}}
aw.animIdleLeft = aw.Sprite.Sheet.NewAnim("idle_left")
if aw.animIdleLeft == nil {
return errors.New("missing anim idle_left")
}
aw.animIdleRight = aw.Sprite.Sheet.NewAnim("idle_right")
if aw.animIdleRight == nil {
return errors.New("missing anim idle_right")
}
aw.animRunLeft = aw.Sprite.Sheet.NewAnim("run_left")
if aw.animRunLeft == nil {
return errors.New("missing anim run_left")
}
aw.animRunRight = aw.Sprite.Sheet.NewAnim("run_right")
if aw.animRunRight == nil {
return errors.New("missing anim run_right")
}
aw.animWalkRight = aw.Sprite.Sheet.NewAnim("walk_left")
if aw.animWalkRight == nil {
return errors.New("missing anim walk_left")
}
aw.animWalkLeft = aw.Sprite.Sheet.NewAnim("walk_right")
if aw.animWalkLeft == nil {
return errors.New("missing anim walk_right")
}
return nil
}

82
main.go
View file

@ -73,32 +73,21 @@ func main() {
// writeLevel1 dumps a test level into level1.gobz
func writeLevel1() {
redTileAnim := &engine.Anim{Frames: []engine.AnimFrame{
{Frame: 3, Duration: 12},
{Frame: 4, Duration: 12},
{Frame: 5, Duration: 12},
{Frame: 6, Duration: 12},
}}
greenTileAnim := &engine.Anim{Frames: []engine.AnimFrame{
{Frame: 0, Duration: 16},
{Frame: 1, Duration: 16},
{Frame: 2, Duration: 16},
}}
denseTiles := [][]engine.Tile{
{engine.StaticTile(9), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, engine.StaticTile(9)},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, engine.AnimatedTile{Anim: redTileAnim.Copy()}, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, engine.AnimatedTile{Anim: redTileAnim.Copy()}, nil, nil, nil},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &engine.AnimatedTile{AnimKey: "red_tile"}, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &engine.AnimatedTile{AnimKey: "red_tile"}, nil, nil, nil},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, engine.AnimatedTile{Anim: greenTileAnim.Copy()}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, engine.AnimatedTile{Anim: redTileAnim.Copy()}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, engine.AnimatedTile{Anim: greenTileAnim.Copy()}, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, engine.AnimatedTile{Anim: greenTileAnim.Copy()}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, engine.AnimatedTile{Anim: greenTileAnim.Copy()}, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, engine.AnimatedTile{Anim: redTileAnim.Copy()}, nil, nil, nil, nil, nil, nil, engine.StaticTile(9), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, &engine.AnimatedTile{AnimKey: "green_tile"}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, &engine.AnimatedTile{AnimKey: "red_tile"}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &engine.AnimatedTile{AnimKey: "green_tile"}, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, &engine.AnimatedTile{AnimKey: "green_tile"}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &engine.AnimatedTile{AnimKey: "green_tile"}, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, &engine.AnimatedTile{AnimKey: "red_tile"}, nil, nil, nil, nil, nil, nil, engine.StaticTile(9), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
{nil, nil, nil, nil, nil, engine.StaticTile(9), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, engine.StaticTile(9), nil, nil, nil},
{nil, nil, nil, nil, engine.StaticTile(9), engine.StaticTile(9), engine.StaticTile(9), nil, nil, nil, engine.StaticTile(9), nil, nil, nil, nil, nil, nil, nil, nil, nil},
{engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.AnimatedTile{Anim: redTileAnim.Copy()}, engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8)},
{engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.AnimatedTile{Anim: redTileAnim.Copy()}, engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.AnimatedTile{Anim: greenTileAnim.Copy()}, engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7)},
{engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), &engine.AnimatedTile{AnimKey: "red_tile"}, engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8), engine.StaticTile(8)},
{engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), &engine.AnimatedTile{AnimKey: "red_tile"}, engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), &engine.AnimatedTile{AnimKey: "green_tile"}, engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7)},
{engine.StaticTile(9), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(7), engine.StaticTile(9)},
}
tiles := make(map[image.Point]engine.Tile)
@ -135,6 +124,22 @@ func writeLevel1() {
ZOrder: 2,
Map: tiles,
Sheet: engine.Sheet{
AnimDefs: map[string]*engine.AnimDef{
"red_tile": {
Steps: []engine.AnimStep{
{Cell: 3, Duration: 12},
{Cell: 4, Duration: 12},
{Cell: 5, Duration: 12},
{Cell: 6, Duration: 12},
}},
"green_tile": {
Steps: []engine.AnimStep{
{Cell: 0, Duration: 16},
{Cell: 1, Duration: 16},
{Cell: 2, Duration: 16},
},
},
},
CellSize: image.Pt(16, 16),
Src: engine.ImageRef{Path: "assets/boxes.png"},
},
@ -160,12 +165,45 @@ func writeLevel1() {
Pos: image.Pt(100, 100),
Size: image.Pt(8, 16),
},
ZOrder: 3,
FrameOffset: image.Pt(-1, 0),
Sheet: engine.Sheet{
AnimDefs: map[string]*engine.AnimDef{
"idle_left": {Steps: []engine.AnimStep{
{Cell: 1, Duration: 60},
}},
"idle_right": {Steps: []engine.AnimStep{
{Cell: 0, Duration: 60},
}},
"run_left": {Steps: []engine.AnimStep{
{Cell: 14, Duration: 3},
{Cell: 15, Duration: 5},
{Cell: 16, Duration: 3},
{Cell: 17, Duration: 3},
}},
"run_right": {Steps: []engine.AnimStep{
{Cell: 10, Duration: 3},
{Cell: 11, Duration: 5},
{Cell: 12, Duration: 3},
{Cell: 13, Duration: 3},
}},
"walk_left": {Steps: []engine.AnimStep{
{Cell: 2, Duration: 6},
{Cell: 3, Duration: 6},
{Cell: 4, Duration: 6},
{Cell: 5, Duration: 6},
}},
"walk_right": {Steps: []engine.AnimStep{
{Cell: 6, Duration: 6},
{Cell: 7, Duration: 6},
{Cell: 8, Duration: 6},
{Cell: 9, Duration: 6},
}},
},
CellSize: image.Pt(10, 16),
Src: engine.ImageRef{Path: "assets/aw.png"},
},
ZOrder: 3,
},
},
},