reimplement camera constraint

This commit is contained in:
Josh Deprez 2021-09-01 12:07:49 +10:00
parent 8f495888d9
commit 7c7fd781d2
5 changed files with 58 additions and 98 deletions

View file

@ -25,14 +25,57 @@ type Camera struct {
Child interface{} Child interface{}
// Camera controls // Camera controls
// These directly manipulate the camera. If you want to restrict the camera
// view area to the child's bounding rectangle, use PointAt.
Centre image.Point // world coordinates Centre image.Point // world coordinates
Filter ebiten.Filter
Rotation float64 // radians Rotation float64 // radians
Zoom float64 // unitless Zoom float64 // unitless
game *Game game *Game
} }
// PointAt points the camera at a particular centre point and zoom, but adjusts
// for the bounds of the child component (if available).
func (c *Camera) PointAt(centre image.Point, zoom float64) {
// Special sauce: if Child has a BoundingRect, make some adjustments
bnd, ok := c.Child.(Bounder)
if !ok {
c.Centre, c.Zoom = centre, zoom
return
}
// The child has boundaries; respect them.
br := bnd.BoundingRect()
// The lower bound on zoom is the larger of
// { (ScreenWidth / BoundsWidth), (ScreenHeight / BoundsHeight) }
sz := br.Size()
if z := float64(c.game.ScreenSize.X) / float64(sz.X); zoom < z {
zoom = z
}
if z := float64(c.game.ScreenSize.Y) / float64(sz.Y); zoom < z {
zoom = z
}
// If the configured centre puts the camera out of bounds, move it.
// Camera frame currently Rectangle{ centre ± (screen/(2*zoom)) }.
sw2, sh2 := float2(c.game.ScreenSize.Div(2))
swz, shz := int(sw2/zoom), int(sh2/zoom)
if centre.X-swz < br.Min.X {
centre.X = br.Min.X + swz
}
if centre.Y-shz < br.Min.Y {
centre.Y = br.Min.Y + shz
}
if centre.X+swz > br.Max.X {
centre.X = br.Max.X - swz
}
if centre.Y+shz > br.Max.Y {
centre.Y = br.Max.Y - shz
}
c.Centre, c.Zoom = centre, zoom
}
// Prepare grabs a copy of game (needed for screen dimensions) // Prepare grabs a copy of game (needed for screen dimensions)
func (c *Camera) Prepare(game *Game) error { func (c *Camera) Prepare(game *Game) error {
c.game = game c.game = game
@ -47,6 +90,6 @@ func (c *Camera) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(float2(c.Centre.Mul(-1))) opts.GeoM.Translate(float2(c.Centre.Mul(-1)))
opts.GeoM.Scale(c.Zoom, c.Zoom) opts.GeoM.Scale(c.Zoom, c.Zoom)
opts.GeoM.Rotate(c.Rotation) opts.GeoM.Rotate(c.Rotation)
opts.GeoM.Translate(float64(c.game.ScreenWidth/2), float64(c.game.ScreenHeight/2)) opts.GeoM.Translate(float2(c.game.ScreenSize.Div(2)))
return opts return opts
} }

View file

@ -4,6 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"fmt" "fmt"
"image"
"io/fs" "io/fs"
"math" "math"
"reflect" "reflect"
@ -36,8 +37,7 @@ func init() {
type Game struct { type Game struct {
Disabled Disabled
Hidden Hidden
ScreenWidth int ScreenSize image.Point
ScreenHeight int
Root interface{} // typically a *Scene or SceneRef though Root interface{} // typically a *Scene or SceneRef though
dbmu sync.RWMutex dbmu sync.RWMutex
@ -110,7 +110,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
// Layout returns the configured screen width/height. // Layout returns the configured screen width/height.
func (g *Game) Layout(outsideWidth, outsideHeight int) (w, h int) { func (g *Game) Layout(outsideWidth, outsideHeight int) (w, h int) {
return g.ScreenWidth, g.ScreenHeight return g.ScreenSize.X, g.ScreenSize.Y
} }
// Update updates everything. // Update updates everything.

View file

@ -11,7 +11,7 @@ func init() {
gob.Register(&Scene{}) gob.Register(&Scene{})
} }
// Scene manages drawing and updating a bunch of components. // Scene just contains a bunch of components.
type Scene struct { type Scene struct {
ID ID
Bounds // world coordinates Bounds // world coordinates
@ -20,87 +20,5 @@ type Scene struct {
Hidden Hidden
} }
/*
// Draw draws all components in order.
func (s *Scene) Draw(screen *ebiten.Image, opts ebiten.DrawImageOptions) {
if s.Hidden {
return
}
if s.Camera == nil {
// Draw everything, no camera transforms.
for _, i := range s.Components {
if d, ok := i.(Drawer); ok {
d.Draw(screen, opts)
}
}
return
}
// There is a camera; apply camera transforms.
br := s.BoundingRect()
// The lower bound on zoom is the larger of
// { (ScreenWidth / BoundsWidth), (ScreenHeight / BoundsHeight) }
zoom := s.Camera.Zoom
sz := br.Size()
if z := float64(s.Camera.game.ScreenWidth) / float64(sz.X); zoom < z {
zoom = z
}
if z := float64(s.Camera.game.ScreenHeight) / float64(sz.Y); zoom < z {
zoom = z
}
// If the configured centre puts the camera out of bounds, move it.
centre := s.Camera.Centre
// Camera frame currently Rectangle{ centre ± (screen/(2*zoom)) }.
sw2, sh2 := float64(s.Camera.game.ScreenWidth/2), float64(s.Camera.game.ScreenHeight/2)
swz, shz := int(sw2/zoom), int(sh2/zoom)
if centre.X-swz < br.Min.X {
centre.X = br.Min.X + swz
}
if centre.Y-shz < br.Min.Y {
centre.Y = br.Min.Y + shz
}
if centre.X+swz > br.Max.X {
centre.X = br.Max.X - swz
}
if centre.Y+shz > br.Max.Y {
centre.Y = br.Max.Y - shz
}
// Apply other options
opts.Filter = s.Camera.Filter
// Compute common matrix (parts independent of parallax, which is step 1).
// Moving centre to the origin happens per component.
var comm ebiten.GeoM
// 2. Zoom and rotate
comm.Scale(zoom, zoom)
comm.Rotate(s.Camera.Rotation)
// 3. Move the origin to the centre of screen space.
comm.Translate(sw2, sh2)
// 4. Apply transforms from the caller.
comm.Concat(opts.GeoM)
// Draw everything.
for _, i := range s.Components {
d, ok := i.(Drawer)
if !ok {
continue
}
pf := 1.0
if s, ok := i.(ParallaxScaler); ok {
pf = s.ParallaxFactor()
}
var geom ebiten.GeoM
// 1. Move centre to the origin, subject to parallax factor
geom.Translate(-float64(centre.X)*pf, -float64(centre.Y)*pf)
geom.Concat(comm)
opts.GeoM = geom
d.Draw(screen, opts)
}
}
*/
// Scan returns all immediate subcomponents (including the camera, if not nil). // Scan returns all immediate subcomponents (including the camera, if not nil).
func (s *Scene) Scan() []interface{} { return s.Components } func (s *Scene) Scan() []interface{} { return s.Components }

View file

@ -64,12 +64,12 @@ func (aw *Awakeman) Update() error {
} }
// Update the camera // Update the camera
aw.camera.Zoom = 1
if ebiten.IsKeyPressed(ebiten.KeyShift) {
aw.camera.Zoom = 2
}
// aw.Pos is top-left corner, so add half size to get centre // aw.Pos is top-left corner, so add half size to get centre
aw.camera.Centre = aw.Sprite.Actor.Pos.Add(aw.Sprite.Actor.Size.Div(2)) z := 1.0
if ebiten.IsKeyPressed(ebiten.KeyShift) {
z = 2.0
}
aw.camera.PointAt(aw.Sprite.Actor.Pos.Add(aw.Sprite.Actor.Size.Div(2)), z)
return nil return nil
} }

View file

@ -38,8 +38,7 @@ func main() {
} }
g := &engine.Game{ g := &engine.Game{
ScreenHeight: 240, ScreenSize: image.Pt(320, 240),
ScreenWidth: 320,
Root: &engine.Scene{ Root: &engine.Scene{
ID: "root", ID: "root",
Components: []interface{}{ Components: []interface{}{