Play Mode: Fix Level Collision w/ Scrolling

Fixes:
* Move the call to CollidesWithGrid() inside the Canvas instead of
  outside in the PlayScene.movePlayer() so it can apply to all Actors
  in motion.
* PlayScene.movePlayer() in turn just sets the player's Velocity so the
  Canvas.Loop() can move the actor itself.
* When keeping the player inside the level boundaries: previously it was
  assuming the player Position was relative to the window, and was
  checking the WorldIndexAt and getting wrong results.
* Canvas scrolling (loopFollowActor): check that the actor is getting
  close to the screen edge using the Viewport into the world, NOT the
  screen-relative coordinates of the Canvas bounding boxes.
This commit is contained in:
Noah 2019-04-14 15:25:03 -07:00
parent 5c08577214
commit 241186209c
15 changed files with 109 additions and 136 deletions

View File

@ -135,6 +135,19 @@ func (r Rect) AddPoint(other Point) Rect {
} }
} }
// SubtractPoint is the inverse of AddPoint. Use this only if you need to invert
// the Point being added.
//
// This does r.X - other.X, r.Y - other.Y and keeps the width/height the same.
func (r Rect) SubtractPoint(other Point) Rect {
return Rect{
X: r.X - other.X,
Y: r.Y - other.Y,
W: r.W,
H: r.H,
}
}
// Text holds information for drawing text. // Text holds information for drawing text.
type Text struct { type Text struct {
Text string Text string

View File

@ -75,6 +75,12 @@ func (p *Point) Add(other Point) {
p.Y += other.Y p.Y += other.Y
} }
// Subtract the other point from your current point.
func (p *Point) Subtract(other Point) {
p.X -= other.X
p.Y -= other.Y
}
// MarshalText to convert the point into text so that a render.Point may be used // MarshalText to convert the point into text so that a render.Point may be used
// as a map key and serialized to JSON. // as a map key and serialized to JSON.
func (p *Point) MarshalText() ([]byte, error) { func (p *Point) MarshalText() ([]byte, error) {

View File

@ -68,11 +68,14 @@ func (w *Label) Compute(e render.Engine) {
// Max rect to encompass all lines of text. // Max rect to encompass all lines of text.
var maxRect = render.Rect{} var maxRect = render.Rect{}
for _, line := range lines { for _, line := range lines {
if line == "" {
line = "<empty>"
}
text.Text = line // only this line at this time. text.Text = line // only this line at this time.
rect, err := e.ComputeTextRect(text) rect, err := e.ComputeTextRect(text)
if err != nil { if err != nil {
panic(fmt.Sprintf("%s: failed to compute text rect: %s", w, err)) // TODO return an error panic(fmt.Sprintf("%s: failed to compute text rect: %s", w, err)) // TODO return an error
return
} }
if rect.W > maxRect.W { if rect.W > maxRect.W {

View File

@ -29,7 +29,7 @@ var (
// Put a border around all Canvas widgets. // Put a border around all Canvas widgets.
DebugCanvasBorder = render.Invisible DebugCanvasBorder = render.Invisible
DebugCanvasLabel = false // Tag the canvas with a label. DebugCanvasLabel = true // Tag the canvas with a label.
) )
func init() { func init() {

View File

@ -12,7 +12,7 @@ var (
// Window scrolling behavior in Play Mode. // Window scrolling behavior in Play Mode.
ScrollboxHoz = 64 // horizontal px from window border to start scrol ScrollboxHoz = 64 // horizontal px from window border to start scrol
ScrollboxVert = 128 ScrollboxVert = 128
ScrollMaxVelocity = 24 ScrollMaxVelocity = 8 // 24
// Player speeds // Player speeds
PlayerMaxVelocity = 8 PlayerMaxVelocity = 8

View File

@ -96,7 +96,11 @@ const (
Right Right
) )
// CollidesWithGrid checks if a Doodad collides with level geometry. /*
CollidesWithGrid checks if a Doodad collides with level geometry.
The `target` is the point the actor wants to move to on this tick.
*/
func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Collide, bool) { func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Collide, bool) {
var ( var (
P = d.Position() P = d.Position()
@ -109,10 +113,10 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
capHeight int32 // Stop vertical movement thru a ceiling capHeight int32 // Stop vertical movement thru a ceiling
capLeft int32 // Stop movement thru a wall capLeft int32 // Stop movement thru a wall
capRight int32 capRight int32
capFloor int32 // Stop movement thru the floor
hitLeft bool // Has hit an obstacle on the left hitLeft bool // Has hit an obstacle on the left
hitRight bool // or right hitRight bool // or right
hitFloor bool hitFloor bool
capFloor int32
) )
// Test all of the bounding boxes for a collision with level geometry. // Test all of the bounding boxes for a collision with level geometry.

View File

@ -1,79 +0,0 @@
package doodads
import "git.kirsle.net/apps/doodle/lib/render"
// PlayerID is the Doodad ID for the player character.
const PlayerID = "PLAYER"
// Player is a special doodad for the player character.
type Player struct {
point render.Point
velocity render.Point
size render.Rect
grounded bool
}
// NewPlayer creates the special Player Character doodad.
func NewPlayer() *Player {
return &Player{
point: render.Point{
X: 100,
Y: 100,
},
size: render.Rect{
W: 32,
H: 32,
},
}
}
// ID of the Player singleton.
func (p *Player) ID() string {
return PlayerID
}
// Position of the player.
func (p *Player) Position() render.Point {
return p.point
}
// MoveBy a relative delta position.
func (p *Player) MoveBy(by render.Point) {
p.point.X += by.X
p.point.Y += by.Y
}
// MoveTo an absolute position.
func (p *Player) MoveTo(to render.Point) {
p.point = to
}
// Velocity returns the player's current velocity.
func (p *Player) Velocity() render.Point {
return p.velocity
}
// Size returns the player's size.
func (p *Player) Size() render.Rect {
return p.size
}
// Grounded returns if the player is grounded.
func (p *Player) Grounded() bool {
return p.grounded
}
// SetGrounded sets if the player is grounded.
func (p *Player) SetGrounded(v bool) {
p.grounded = v
}
// Draw the player sprite.
func (p *Player) Draw(e render.Engine) {
e.DrawBox(render.RGBA(255, 255, 153, 255), render.Rect{
X: p.point.X,
Y: p.point.Y,
W: p.size.W,
H: p.size.H,
})
}

View File

@ -76,10 +76,13 @@ func (s *EditorScene) Setup(d *Doodle) error {
if s.Level != nil { if s.Level != nil {
log.Debug("EditorScene.Setup: received level from scene caller") log.Debug("EditorScene.Setup: received level from scene caller")
s.UI.Canvas.LoadLevel(d.Engine, s.Level) s.UI.Canvas.LoadLevel(d.Engine, s.Level)
s.UI.Canvas.InstallActors(s.Level.Actors)
} else if s.filename != "" && s.OpenFile { } else if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
if err := s.LoadLevel(s.filename); err != nil { if err := s.LoadLevel(s.filename); err != nil {
d.Flash("LoadLevel error: %s", err) d.Flash("LoadLevel error: %s", err)
} else {
s.UI.Canvas.InstallActors(s.Level.Actors)
} }
} }

View File

@ -131,6 +131,10 @@ func (d *Doodle) DrawDebugOverlay() {
} }
// DrawCollisionBox draws the collision box around a Doodad. // DrawCollisionBox draws the collision box around a Doodad.
//
// TODO: move inside the Canvas. Currently it takes an actor's World Position
// and draws the box as if it were a relative (to the window) position, so the
// hitbox drifts off when the level scrolls away from 0,0
func (d *Doodle) DrawCollisionBox(actor doodads.Actor) { func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
if !DebugCollision { if !DebugCollision {
return return

View File

@ -6,7 +6,6 @@ import (
"git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/doodads/dummy" "git.kirsle.net/apps/doodle/pkg/doodads/dummy"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
@ -56,6 +55,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
// Initialize the drawing canvas. // Initialize the drawing canvas.
s.drawing = uix.NewCanvas(balance.ChunkSize, false) s.drawing = uix.NewCanvas(balance.ChunkSize, false)
s.drawing.Name = "play-canvas"
s.drawing.MoveTo(render.Origin) s.drawing.MoveTo(render.Origin)
s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height))) s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height)))
s.drawing.Compute(d.Engine) s.drawing.Compute(d.Engine)
@ -92,7 +92,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
// Update debug overlay values. // Update debug overlay values.
*s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)).String() *s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)).String()
*s.debPosition = s.Player.Position().String() *s.debPosition = s.Player.Position().String() + " vel " + s.Player.Velocity().String()
*s.debViewport = s.drawing.Viewport().String() *s.debViewport = s.drawing.Viewport().String()
*s.debScroll = s.drawing.Scroll.String() *s.debScroll = s.drawing.Scroll.String()
@ -134,8 +134,10 @@ func (s *PlayScene) Draw(d *Doodle) error {
// Draw the level. // Draw the level.
s.drawing.Present(d.Engine, s.drawing.Point()) s.drawing.Present(d.Engine, s.drawing.Point())
// Draw our hero. // Draw our hero. TODO: this draws a yellow box using the player's World
d.Engine.DrawBox(render.RGBA(255, 255, 153, 255), render.Rect{ // Position as tho it were Screen Position. The player has its own canvas
// currently drawn in red
d.Engine.DrawBox(render.RGBA(255, 255, 153, 64), render.Rect{
X: s.Player.Position().X, X: s.Player.Position().X,
Y: s.Player.Position().Y, Y: s.Player.Position().Y,
W: s.Player.Size().W, W: s.Player.Size().W,
@ -150,48 +152,37 @@ func (s *PlayScene) Draw(d *Doodle) error {
// movePlayer updates the player's X,Y coordinate based on key pressed. // movePlayer updates the player's X,Y coordinate based on key pressed.
func (s *PlayScene) movePlayer(ev *events.State) { func (s *PlayScene) movePlayer(ev *events.State) {
delta := s.Player.Position()
var playerSpeed = int32(balance.PlayerMaxVelocity) var playerSpeed = int32(balance.PlayerMaxVelocity)
var gravity = int32(balance.Gravity) var gravity = int32(balance.Gravity)
var velocity render.Point var velocity render.Point
if ev.Down.Now { if ev.Down.Now {
delta.Y += playerSpeed
velocity.Y = playerSpeed velocity.Y = playerSpeed
} }
if ev.Left.Now { if ev.Left.Now {
delta.X -= playerSpeed
velocity.X = -playerSpeed velocity.X = -playerSpeed
} }
if ev.Right.Now { if ev.Right.Now {
delta.X += playerSpeed
velocity.X = playerSpeed velocity.X = playerSpeed
} }
if ev.Up.Now { if ev.Up.Now {
delta.Y -= playerSpeed
velocity.Y = -playerSpeed velocity.Y = -playerSpeed
} }
// Apply gravity.
// var onFloor bool
info, ok := doodads.CollidesWithGrid(s.Player, s.Level.Chunker, delta)
if ok {
// Collision happened with world.
}
delta = info.MoveTo
// Apply gravity if not grounded. // Apply gravity if not grounded.
if !s.Player.Grounded() { if !s.Player.Grounded() {
// Gravity has to pipe through the collision checker, too, so it // Gravity has to pipe through the collision checker, too, so it
// can't give us a cheated downward boost. // can't give us a cheated downward boost.
delta.Y += gravity
velocity.Y += gravity velocity.Y += gravity
} }
// s.Player.SetVelocity(velocity) s.Player.SetVelocity(velocity)
s.Player.MoveTo(delta) }
// Drawing returns the private world drawing, for debugging with the console.
func (s *PlayScene) Drawing() *uix.Canvas {
return s.drawing
} }
// LoadLevel loads a level from disk. // LoadLevel loads a level from disk.

View File

@ -31,7 +31,6 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor
size := int32(doodad.Layers[0].Chunker.Size) size := int32(doodad.Layers[0].Chunker.Size)
can := NewCanvas(int(size), false) can := NewCanvas(int(size), false)
can.Name = id can.Name = id
can.actor = levelActor
// TODO: if the Background is render.Invisible it gets defaulted to // TODO: if the Background is render.Invisible it gets defaulted to
// White somewhere and the Doodad masks the level drawing behind it. // White somewhere and the Doodad masks the level drawing behind it.
@ -40,9 +39,15 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor
can.LoadDoodad(doodad) can.LoadDoodad(doodad)
can.Resize(render.NewRect(size, size)) can.Resize(render.NewRect(size, size))
return &Actor{ actor := &Actor{
Drawing: doodads.NewDrawing(id, doodad), Drawing: doodads.NewDrawing(id, doodad),
Actor: levelActor, Actor: levelActor,
Canvas: can, Canvas: can,
} }
// Give the Canvas a pointer to its (parent) Actor so it can draw its debug
// label and show the World Position of the actor within the world.
can.actor = actor
return actor
} }

View File

@ -45,8 +45,8 @@ type Canvas struct {
chunks *level.Chunker chunks *level.Chunker
// Actors to superimpose on top of the drawing. // Actors to superimpose on top of the drawing.
actor *level.Actor // if this canvas IS an actor actor *Actor // if this canvas IS an actor
actors []*Actor actors []*Actor // if this canvas CONTAINS actors (i.e., is a level)
// Wallpaper settings. // Wallpaper settings.
wallpaper *Wallpaper wallpaper *Wallpaper
@ -163,20 +163,35 @@ func (w *Canvas) Loop(ev *events.State) error {
if err := w.loopFollowActor(ev); err != nil { if err := w.loopFollowActor(ev); err != nil {
log.Error("Follow actor: %s", err) // not fatal but nice to know log.Error("Follow actor: %s", err) // not fatal but nice to know
} }
w.loopConstrainScroll() if err := w.loopConstrainScroll(); err != nil {
log.Debug("loopConstrainScroll: %s", err)
}
// Move any actors. // Move any actors.
for _, a := range w.actors { for _, a := range w.actors {
if v := a.Velocity(); v != render.Origin { if v := a.Velocity(); v != render.Origin {
// orig := a.Drawing.Position() // Create a delta point from their current location to where they
a.MoveBy(v) // want to move to this tick.
delta := a.Position()
delta.Add(v)
// Check collision with level geometry.
info, ok := doodads.CollidesWithGrid(a, w.chunks, delta)
if ok {
// Collision happened with world.
log.Error("COLLIDE %+v", info)
}
delta = info.MoveTo // Move us back where the collision check put us
// Move the actor's World Position to the new location.
a.MoveTo(delta)
// Keep them contained inside the level. // Keep them contained inside the level.
if w.wallpaper.pageType > level.Unbounded { if w.wallpaper.pageType > level.Unbounded {
var ( var (
orig = w.WorldIndexAt(a.Drawing.Position()) orig = a.Position() // Actor's World Position
moveBy render.Point moveBy render.Point
size = a.Canvas.Size() size = a.Size()
) )
// Bound it on the top left edges. // Bound it on the top left edges.

View File

@ -20,7 +20,12 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error {
return fmt.Errorf("InstallActors: %s", err) return fmt.Errorf("InstallActors: %s", err)
} }
w.actors = append(w.actors, NewActor(id, actor, doodad)) // Create the "live" Actor to exist in the world, and set its world
// position to the Point defined in the level data.
liveActor := NewActor(id, actor, doodad)
liveActor.MoveTo(actor.Point)
w.actors = append(w.actors, liveActor)
} }
return nil return nil
} }
@ -46,10 +51,9 @@ func (w *Canvas) drawActors(e render.Engine, p render.Point) {
continue continue
} }
var ( var (
actor = a.Actor // Static Actor instance from Level file, DO NOT CHANGE
can = a.Canvas // Canvas widget that draws the actor can = a.Canvas // Canvas widget that draws the actor
actorPoint = actor.Point // XXX TODO: DO NOT CHANGE actorPoint = a.Position()
actorSize = can.Size() actorSize = a.Size()
) )
// Create a box of World Coordinates that this actor occupies. The // Create a box of World Coordinates that this actor occupies. The
@ -87,7 +91,7 @@ func (w *Canvas) drawActors(e render.Engine, p render.Point) {
// Hitting the left edge. Cap the X coord and shrink the width. // Hitting the left edge. Cap the X coord and shrink the width.
delta := p.X - drawAt.X // positive number delta := p.X - drawAt.X // positive number
drawAt.X = p.X drawAt.X = p.X
// scrollTo.X -= delta // TODO // scrollTo.X -= delta / 2 // TODO
resizeTo.W -= delta resizeTo.W -= delta
} }

View File

@ -149,11 +149,17 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
// Viewport.W, Viewport.H, // Viewport.W, Viewport.H,
// ), // ),
} }
// Draw the actor's position details.
// LP = Level Position, where the Actor starts at in the level data
// WP = World Position, the Actor's current position in the level
if w.actor != nil { if w.actor != nil {
rows = append(rows, rows = append(rows,
fmt.Sprintf("WP=%s", w.actor.Point), fmt.Sprintf("LP=%s", w.actor.Actor.Point),
fmt.Sprintf("WP=%s", w.actor.Position()),
) )
} }
label := ui.NewLabel(ui.Label{ label := ui.NewLabel(ui.Label{
Text: strings.Join(rows, "\n"), Text: strings.Join(rows, "\n"),
Font: render.Text{ Font: render.Text{

View File

@ -110,8 +110,7 @@ func (w *Canvas) loopFollowActor(ev *events.State) error {
} }
var ( var (
P = w.Point() VP = w.Viewport()
S = w.Size()
) )
// Find the actor. // Find the actor.
@ -121,19 +120,18 @@ func (w *Canvas) loopFollowActor(ev *events.State) error {
} }
actor.Canvas.SetBorderSize(2) actor.Canvas.SetBorderSize(2)
actor.Canvas.SetBorderColor(render.Cyan) actor.Canvas.SetBorderColor(render.Red)
actor.Canvas.SetBorderStyle(ui.BorderSolid) actor.Canvas.SetBorderStyle(ui.BorderSolid)
var ( var (
APosition = actor.Position() // relative to screen APosition = actor.Position() // absolute world position
APoint = actor.Drawing.Position()
ASize = actor.Drawing.Size() ASize = actor.Drawing.Size()
scrollBy render.Point scrollBy render.Point
) )
// Scroll left // Scroll left
if APosition.X-P.X <= int32(balance.ScrollboxHoz) { if APosition.X-VP.X <= int32(balance.ScrollboxHoz) {
var delta = APoint.X - P.X var delta = APosition.X - VP.X
if delta > int32(balance.ScrollMaxVelocity) { if delta > int32(balance.ScrollMaxVelocity) {
delta = int32(balance.ScrollMaxVelocity) delta = int32(balance.ScrollMaxVelocity)
} }
@ -146,8 +144,8 @@ func (w *Canvas) loopFollowActor(ev *events.State) error {
} }
// Scroll right // Scroll right
if APosition.X >= S.W-ASize.W-int32(balance.ScrollboxHoz) { if APosition.X >= VP.W-ASize.W-int32(balance.ScrollboxHoz) {
var delta = S.W - ASize.W - int32(balance.ScrollboxHoz) var delta = VP.W - ASize.W - int32(balance.ScrollboxHoz)
if delta > int32(balance.ScrollMaxVelocity) { if delta > int32(balance.ScrollMaxVelocity) {
delta = int32(balance.ScrollMaxVelocity) delta = int32(balance.ScrollMaxVelocity)
} }
@ -155,8 +153,8 @@ func (w *Canvas) loopFollowActor(ev *events.State) error {
} }
// Scroll up // Scroll up
if APosition.Y-P.Y <= int32(balance.ScrollboxVert) { if APosition.Y-VP.Y <= int32(balance.ScrollboxVert) {
var delta = APoint.Y - P.Y var delta = APosition.Y - VP.Y
if delta > int32(balance.ScrollMaxVelocity) { if delta > int32(balance.ScrollMaxVelocity) {
delta = int32(balance.ScrollMaxVelocity) delta = int32(balance.ScrollMaxVelocity)
} }
@ -168,8 +166,8 @@ func (w *Canvas) loopFollowActor(ev *events.State) error {
} }
// Scroll down // Scroll down
if APosition.Y >= S.H-ASize.H-int32(balance.ScrollboxVert) { if APosition.Y >= VP.H-ASize.H-int32(balance.ScrollboxVert) {
var delta = S.H - ASize.H - int32(balance.ScrollboxVert) var delta = VP.H - ASize.H - int32(balance.ScrollboxVert)
if delta > int32(balance.ScrollMaxVelocity) { if delta > int32(balance.ScrollMaxVelocity) {
delta = int32(balance.ScrollMaxVelocity) delta = int32(balance.ScrollMaxVelocity)
} }