abolish transform

This commit is contained in:
Josh Deprez 2021-09-07 14:00:50 +10:00
parent 02d91f9d23
commit 024cf2cafe
13 changed files with 102 additions and 96 deletions

View file

@ -16,13 +16,15 @@ func init() {
// Actor handles basic movement.
type Actor struct {
CollisionDomain string // id of component to look for colliders inside of
Pos, Size Int3
xRem, yRem, zRem float64
CollisionDomain string // id of component to look for colliders inside of
Pos, Size Int3 // in voxels; multiply by game.VoxelScale for regular Euclidean space
rem Float3
game *Game
}
// CollidesAt runs a collision test of the actor, supposing the actor is at a
// given position (not necessarily a.Pos).
func (a *Actor) CollidesAt(p Int3) bool {
bounds := Box{Min: p, Max: p.Add(a.Size)}
for c := range a.game.Query(a.CollisionDomain, ColliderType) {
@ -33,13 +35,17 @@ func (a *Actor) CollidesAt(p Int3) bool {
return false
}
// MoveX moves the actor x units in world space. It takes Game.VoxelScale into
// account (so MoveX(x) moves the actor x/VoxelScale.X voxel units). onCollide
// is called if a collision occurs, and the actor wil be in the colliding
// position during the call.
func (a *Actor) MoveX(x float64, onCollide func()) {
a.xRem += x
move := int(a.xRem + 0.5) // Note: math.Round can lead to vibration
a.rem.X += x / a.game.VoxelScale.X
move := int(a.rem.X + 0.5) // Note: math.Round can lead to vibration
if move == 0 {
return
}
a.xRem -= float64(move)
a.rem.X -= float64(move)
sign := sign(move)
for move != 0 {
a.Pos.X += sign
@ -51,18 +57,19 @@ func (a *Actor) MoveX(x float64, onCollide func()) {
onCollide()
}
a.Pos.X -= sign
a.xRem = 0
a.rem.X = 0
return
}
}
// MoveY is like MoveX but in the Y dimension. See MoveX for more information.
func (a *Actor) MoveY(y float64, onCollide func()) {
a.yRem += y
move := int(a.yRem + 0.5)
a.rem.Y += y / a.game.VoxelScale.Y
move := int(a.rem.Y + 0.5)
if move == 0 {
return
}
a.yRem -= float64(move)
a.rem.Y -= float64(move)
sign := sign(move)
for move != 0 {
a.Pos.Y += sign
@ -74,18 +81,19 @@ func (a *Actor) MoveY(y float64, onCollide func()) {
onCollide()
}
a.Pos.Y -= sign
a.yRem = 0
a.rem.Y = 0
return
}
}
// MoveZ is like MoveX but in the Y dimension. See MoveX for more information.
func (a *Actor) MoveZ(z float64, onCollide func()) {
a.zRem += z
move := int(a.zRem + 0.5)
a.rem.Z += z / a.game.VoxelScale.Z
move := int(a.rem.Z + 0.5)
if move == 0 {
return
}
a.zRem -= float64(move)
a.rem.Z -= float64(move)
sign := sign(move)
for move != 0 {
a.Pos.Z += sign
@ -97,11 +105,12 @@ func (a *Actor) MoveZ(z float64, onCollide func()) {
onCollide()
}
a.Pos.Z -= sign
a.zRem = 0
a.rem.Z = 0
return
}
}
// Prepare stores a reference to the game.
func (a *Actor) Prepare(g *Game) error {
a.game = g
return nil

View file

@ -36,7 +36,7 @@ func (b *Billboard) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
// Scan returns a slice containing Src.
func (b *Billboard) Scan() []interface{} { return []interface{}{&b.Src} }
func (b *Billboard) Transform(pt Transform) (tf Transform) {
tf.Opts.GeoM.Translate(cfloat(b.Pos))
return tf.Concat(pt)
func (b *Billboard) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(b.Pos))
return opts
}

View file

@ -3,6 +3,8 @@ package engine
import (
"encoding/gob"
"image"
"github.com/hajimehoshi/ebiten/v2"
)
// Ensure Camera satisfies interfaces.
@ -87,11 +89,10 @@ func (c *Camera) Prepare(game *Game) error {
func (c *Camera) Scan() []interface{} { return []interface{}{c.Child} }
// Transform returns the camera transform.
func (c *Camera) Transform(pt Transform) (tf Transform) {
tf.Projection = c.Projection
tf.Opts.GeoM.Translate(cfloat(c.Centre.Mul(-1)))
tf.Opts.GeoM.Scale(c.Zoom, c.Zoom)
tf.Opts.GeoM.Rotate(c.Rotation)
tf.Opts.GeoM.Translate(cfloat(c.game.ScreenSize.Div(2)))
return tf.Concat(pt)
func (c *Camera) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(c.Centre.Mul(-1)))
opts.GeoM.Scale(c.Zoom, c.Zoom)
opts.GeoM.Rotate(c.Rotation)
opts.GeoM.Translate(cfloat(c.game.ScreenSize.Div(2)))
return opts
}

View file

@ -41,6 +41,7 @@ type Game struct {
Hidden
ScreenSize image.Point
Root interface{} // typically a *Scene or SceneRef though
Projection IntProjection
VoxelScale Float3
dbmu sync.RWMutex
@ -61,8 +62,8 @@ func (g *Game) Draw(screen *ebiten.Image) {
// parents as well.
// accum memoises the results for each component.
type state struct {
hidden bool
transform Transform
hidden bool
opts ebiten.DrawImageOptions
}
accum := map[interface{}]state{
g: {hidden: false},
@ -96,9 +97,9 @@ func (g *Game) Draw(screen *ebiten.Image) {
accum[p] = state{hidden: true}
continue
}
// p is not hidden, so compute its cumulative transform.
// p is not hidden, so compute its cumulative opts.
if tf, ok := p.(Transformer); ok {
st.transform = tf.Transform(st.transform)
st.opts = ConcatOpts(tf.Transform(), st.opts)
}
accum[p] = st
}
@ -107,7 +108,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
if st.hidden {
continue
}
d.Draw(screen, &st.transform.Opts)
d.Draw(screen, &st.opts)
}
}
@ -265,6 +266,10 @@ func postorderWalk(component, parent interface{}, visit func(component, parent i
// builds the component databases and then calls Prepare on every Preparer.
// LoadAndPrepare must be called before any calls to Component or Query.
func (g *Game) LoadAndPrepare(assets fs.FS) error {
if g.VoxelScale == (Float3{}) {
g.VoxelScale = Float3{1, 1, 1}
}
// Load all the Loaders.
startLoad := time.Now()
if err := PreorderWalk(g, func(c, _ interface{}) error {
@ -440,3 +445,17 @@ func (d drawList) Less(i, j int) bool {
func (d drawList) Len() int { return len(d) }
func (d drawList) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
// ConcatOpts returns the combined options (as though a was applied and then
// b).
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

@ -105,17 +105,11 @@ type Saver interface {
Save() error
}
// Transformer components can provide a transform to apply to themselves and any
// child components, based on the cumulative parent transform. An
// example implementation:
//
// func (f Foo) Transform(pt Transform) Transform {
// var tf Transform
// tf.Opts.GeoM.Translate(-2, 3) // or your own transform
// return tf.Concat(pt)
// }
// Transformer components can provide draw options to apply to themselves and
// any child components. The opts passed to Draw of a component c will be the
// cumulative opts of all parents of c plus the value returned from c.Transform.
type Transformer interface {
Transform(Transform) Transform
Transform() ebiten.DrawImageOptions
}
// Updater components can update themselves. Update is called repeatedly. Each

View file

@ -3,6 +3,8 @@ package engine
import (
"encoding/gob"
"fmt"
"github.com/hajimehoshi/ebiten/v2"
)
var _ interface {
@ -39,8 +41,8 @@ func (p *Parallax) Prepare(game *Game) error {
func (p *Parallax) Scan() []interface{} { return []interface{}{p.Child} }
// Transform returns a GeoM translation of Factor * camera.Centre.
func (p *Parallax) Transform(pt Transform) (tf Transform) {
func (p *Parallax) Transform() (opts ebiten.DrawImageOptions) {
x, y := cfloat(p.camera.Centre)
tf.Opts.GeoM.Translate(x*p.Factor, y*p.Factor)
return tf.Concat(pt)
opts.GeoM.Translate(x*p.Factor, y*p.Factor)
return opts
}

View file

@ -57,9 +57,9 @@ func (m *PrismMap) Prepare(*Game) error {
return nil
}
func (m *PrismMap) Transform(pt Transform) (tf Transform) {
tf.Opts.GeoM.Translate(cfloat(m.DrawOffset))
return tf.Concat(pt)
func (m *PrismMap) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(m.DrawOffset))
return opts
}
type Prism struct {
@ -78,9 +78,9 @@ func (p *Prism) DrawOrder() (int, int) {
dot(p.pos.XY(), p.pm.DrawOrderBias)
}
func (p *Prism) Transform(pt Transform) (tf Transform) {
tf.Opts.GeoM.Translate(cfloat(
func (p *Prism) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(
p.pm.PosToDraw.Apply(p.pos),
))
return tf.Concat(pt)
return opts
}

View file

@ -2,10 +2,12 @@ package engine
import "image"
// IntProjection holds an integer projection definition.
// It is designed for projecting Z onto X and Y with integer fractions as would
// be used in e.g. a diametric projection (IntProjection{X:0, Y:-2}).
type IntProjection image.Point
// Project performs an integer parallel projection of a 3D coordinate into 2D.
//
// If π.X = 0, the x returned is p.X; similarly for π.Y and y.
// Otherwise, x projects to x + z/π.X and y projects to y + z/π.Y.
func (π IntProjection) Project(p Int3) image.Point {

View file

@ -56,12 +56,15 @@ func (s *Sprite) SetAnim(a *Anim) {
s.anim = a
}
// Transform returns a translation by the DrawOffset and the iso-projected Pos.
func (s *Sprite) Transform(pt Transform) (tf Transform) {
tf.Opts.GeoM.Translate(cfloat(
pt.Projection.Project(s.Actor.Pos).Add(s.DrawOffset),
// Transform returns a translation by the DrawOffset and Actor.Pos projected
func (s *Sprite) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(
// Reaching into Actor for a reference to Game so I don't have to
// implement Prepare in this file, but writing this long comment
// providing exposition...
s.Actor.game.Projection.Project(s.Actor.Pos).Add(s.DrawOffset),
))
return tf.Concat(pt)
return opts
}
// Update updates the Sprite's anim. anim can change a bit so we don't tell Game

View file

@ -110,9 +110,9 @@ func (t *Tilemap) Scan() []interface{} {
}
// Transform returns a translation by t.Offset.
func (t *Tilemap) Transform(pt Transform) (tf Transform) {
tf.Opts.GeoM.Translate(cfloat(t.Offset))
return tf.Concat(pt)
func (t *Tilemap) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(t.Offset))
return opts
}
// TileAt returns the tile present at the given world coordinate.

View file

@ -1,32 +0,0 @@
package engine
import "github.com/hajimehoshi/ebiten/v2"
// Transform is a bucket of things that affect drawing.
type Transform struct {
// Projection is used by isometric 3D components to project their
// coordinates into 2D. There's usually only one component in the tree that
// sets this field, but it would apply to all descendants.
Projection IntProjection
// Opts contains the 2D geometry matrix, the colour matrix, filter mode, and
// composition mode.
Opts ebiten.DrawImageOptions
}
// Concat returns the combined transform (a transform equivalent to applying t
// and then u).
func (t Transform) Concat(u Transform) Transform {
if u.Projection != (IntProjection{}) {
t.Projection = u.Projection
}
t.Opts.ColorM.Concat(u.Opts.ColorM)
t.Opts.GeoM.Concat(u.Opts.GeoM)
if u.Opts.CompositeMode != 0 {
t.Opts.CompositeMode = u.Opts.CompositeMode
}
if u.Opts.Filter != 0 {
t.Opts.Filter = u.Opts.Filter
}
return t
}

View file

@ -80,9 +80,9 @@ func (w *Wall) Prepare(*Game) error {
}
// Transform returns a GeoM translation by Offset.
func (w *Wall) Transform(pt Transform) (tf Transform) {
tf.Opts.GeoM.Translate(cfloat(w.Offset))
return tf.Concat(pt)
func (w *Wall) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(w.Offset))
return opts
}
// WallUnit is a unit in a wall. Unlike a tile in a tilemap, WallUnit is
@ -105,9 +105,9 @@ func (u *WallUnit) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
// Scan returns the Tile.
func (u *WallUnit) Scan() []interface{} { return []interface{}{u.Tile} }
func (u *WallUnit) Transform(pt Transform) (tf Transform) {
tf.Opts.GeoM.Translate(cfloat(
func (u *WallUnit) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(
cmul(u.pos, u.wall.UnitSize).Add(u.wall.UnitOffset),
))
return tf.Concat(pt)
return opts
}

View file

@ -5,6 +5,7 @@ import (
"image/color"
_ "image/png"
"log"
"math"
"os"
"runtime"
"runtime/pprof"
@ -51,6 +52,13 @@ func main() {
g := &engine.Game{
ScreenSize: image.Pt(320, 240),
Projection: engine.IntProjection{X: 0, Y: 1},
VoxelScale: engine.Float3{
// Each voxel counts for this much Eucliden space:
X: 1,
Y: 1,
Z: math.Sqrt(3),
},
Root: &engine.Scene{
ID: "root",
Components: []interface{}{