Level Collision and Scrolling Fixes

* Fix the level collision bug that allowed clipping thru a ceiling while
  climbing up a wall.
* Fix the scrolling behavior to keep the character on-screen no matter
  how fast the character is moving, especially downwards.
* Increase player speed and gravity.
* New cheat: "ghost mode" disables clipping for the player character.
* Mark an actor as "grounded" if they fall and are stopped by the lower
  level border, so they may jump again.
This commit is contained in:
Noah 2020-01-02 20:23:27 -08:00
parent 7b3aec0fef
commit a43e45fad0
9 changed files with 77 additions and 40 deletions

View File

@ -9,6 +9,7 @@
* `import antigravity` - during Play Mode, disables gravity for the player * `import antigravity` - during Play Mode, disables gravity for the player
character and allows free movement in all directions with the arrow keys. character and allows free movement in all directions with the arrow keys.
Enter the cheat again to restore gravity to normal. Enter the cheat again to restore gravity to normal.
* `ghost mode` - during Play Mode, toggles noclip for the player character.
## Bool Props ## Bool Props

View File

@ -10,14 +10,13 @@ var (
CanvasScrollSpeed = 8 CanvasScrollSpeed = 8
// Window scrolling behavior in Play Mode. // Window scrolling behavior in Play Mode.
ScrollboxHoz = 256 // horizontal px from window border to start scrol ScrollboxHoz = 256 // horizontal px from window border to start scrol
ScrollboxVert = 128 ScrollboxVert = 128
ScrollMaxVelocity = 8 // 24
// Player speeds // Player speeds
PlayerMaxVelocity = 8 PlayerMaxVelocity = 10
PlayerAcceleration = 2 PlayerAcceleration = 2
Gravity = 2 Gravity = 6
// Default chunk size for canvases. // Default chunk size for canvases.
ChunkSize = 128 ChunkSize = 128

View File

@ -84,7 +84,8 @@ func CollidesWithGrid(d doodads.Actor, grid *level.Chunker, target render.Point)
d.SetGrounded(false) d.SetGrounded(false)
} }
if result.Top { if result.Top {
// Never seen it touch the top. ceiling = true
P.Y++
} }
if result.Left { if result.Left {
P.X++ P.X++
@ -141,6 +142,13 @@ func CollidesWithGrid(d doodads.Actor, grid *level.Chunker, target render.Point)
result.Reset() result.Reset()
result.MoveTo = P result.MoveTo = P
for point := range render.IterLine(P, target) { for point := range render.IterLine(P, target) {
// Before we compute their next move, if we're already capping their
// height make sure the new point stays capped too. This prevents them
// clipping thru a ceiling if they were also holding right/left too.
if capHeight != 0 && point.Y < capHeight {
point.Y = capHeight
}
if has := result.ScanBoundingBox(render.Rect{ if has := result.ScanBoundingBox(render.Rect{
X: point.X, X: point.X,
Y: point.Y, Y: point.Y,
@ -174,7 +182,6 @@ func CollidesWithGrid(d doodads.Actor, grid *level.Chunker, target render.Point)
// So far so good, keep following the MoveTo to // So far so good, keep following the MoveTo to
// the last good point before a collision. // the last good point before a collision.
result.MoveTo = point result.MoveTo = point
} }
// If they hit the roof, cap them to the roof. // If they hit the roof, cap them to the roof.
@ -225,10 +232,11 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
var wg sync.WaitGroup var wg sync.WaitGroup
for _, job := range jobs { for _, job := range jobs {
wg.Add(1) wg.Add(1)
go func(job jobSide) { job := job
go func() {
defer wg.Done() defer wg.Done()
c.ScanGridLine(job.p1, job.p2, grid, job.side) c.ScanGridLine(job.p1, job.p2, grid, job.side)
}(job) }()
} }
wg.Wait() wg.Wait()
@ -239,6 +247,15 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
// for any pixels to be set, implying a collision between level geometry and the // for any pixels to be set, implying a collision between level geometry and the
// bounding boxes of the doodad. // bounding boxes of the doodad.
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) { func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) {
// If scanning the top or bottom line, offset the X coordinate by 1 pixel.
// This is because the 4 corners of the bounding box share their corner
// pixel with each side, so the Left and Right edges will check the
// left- and right-most point.
if side == Top || side == Bottom {
p1.X += 1
p2.X -= 1
}
for point := range render.IterLine(p1, p2) { for point := range render.IterLine(p1, p2) {
if swatch, err := grid.Get(point); err == nil { if swatch, err := grid.Get(point); err == nil {
// We're intersecting a pixel! If it's a solid one we'll return it // We're intersecting a pixel! If it's a solid one we'll return it

View File

@ -63,6 +63,23 @@ func (c Command) Run(d *Doodle) error {
d.Flash("Use this cheat in Play Mode to disable gravity for the player character.") d.Flash("Use this cheat in Play Mode to disable gravity for the player character.")
} }
return nil return nil
} else if c.Raw == "ghost mode" {
if playScene, ok := d.Scene.(*PlayScene); ok {
playScene.noclip = !playScene.noclip
playScene.Player.SetNoclip(playScene.noclip)
playScene.antigravity = playScene.noclip
playScene.Player.SetGravity(!playScene.antigravity)
if playScene.noclip {
d.Flash("Clipping disabled for player character.")
} else {
d.Flash("Clipping and gravity restored for player character.")
}
} else {
d.Flash("Use this cheat in Play Mode to disable clipping for the player character.")
}
return nil
} }
switch c.Command { switch c.Command {

View File

@ -51,6 +51,7 @@ type PlayScene struct {
// Player character // Player character
Player *uix.Actor Player *uix.Actor
antigravity bool // Cheat: disable player gravity antigravity bool // Cheat: disable player gravity
noclip bool // Cheat: disable player clipping
playerJumpCounter int // limit jump length playerJumpCounter int // limit jump length
} }
@ -438,7 +439,7 @@ func (s *PlayScene) movePlayer(ev *event.State) {
velocity.Y = -playerSpeed velocity.Y = -playerSpeed
if s.Player.Grounded() { if s.Player.Grounded() {
s.playerJumpCounter = 20 s.playerJumpCounter = 12
} }
} }
if ev.Down && s.antigravity { if ev.Down && s.antigravity {

View File

@ -31,6 +31,7 @@ type Actor struct {
// Actor runtime variables. // Actor runtime variables.
hasGravity bool hasGravity bool
isMobile bool // Mobile character, such as the player or an enemy isMobile bool // Mobile character, such as the player or an enemy
noclip bool // Disable collision detection
hitbox render.Rect hitbox render.Rect
data map[string]string data map[string]string
@ -89,6 +90,12 @@ func (a *Actor) IsMobile() bool {
return a.isMobile return a.isMobile
} }
// SetNoclip sets the noclip setting for an actor. If true, the actor can
// clip through level geometry.
func (a *Actor) SetNoclip(v bool) {
a.noclip = v
}
// GetBoundingRect gets the bounding box of the actor's doodad. // GetBoundingRect gets the bounding box of the actor's doodad.
func (a *Actor) GetBoundingRect() render.Rect { func (a *Actor) GetBoundingRect() render.Rect {
return doodads.GetBoundingRect(a) return doodads.GetBoundingRect(a)

View File

@ -63,7 +63,7 @@ func (w *Canvas) loopActorCollision() error {
// Get the actor's velocity to see if it's moving this tick. // Get the actor's velocity to see if it's moving this tick.
v := a.Velocity() v := a.Velocity()
if a.hasGravity { if a.hasGravity && v.Y >= 0 {
v.Y += balance.Gravity v.Y += balance.Gravity
} }
@ -86,7 +86,11 @@ func (w *Canvas) loopActorCollision() error {
w.OnLevelCollision(a, info) w.OnLevelCollision(a, info)
} }
} }
delta = info.MoveTo // Move us back where the collision check put us
// Move us back where the collision check put us
if !a.noclip {
delta = info.MoveTo
}
// Move the actor's World Position to the new location. // Move the actor's World Position to the new location.
a.MoveTo(delta) a.MoveTo(delta)
@ -121,7 +125,6 @@ func (w *Canvas) loopActorCollision() error {
W: boxes[tuple.B].W, W: boxes[tuple.B].W,
H: boxes[tuple.B].H, H: boxes[tuple.B].H,
} }
// lastGoodBox = originalPositions[b.ID()] // boxes[tuple.B] // worst case scenario we get blocked right away
) )
// Firstly we want to make sure B isn't able to clip through A's // Firstly we want to make sure B isn't able to clip through A's
@ -206,7 +209,9 @@ func (w *Canvas) loopActorCollision() error {
} }
} }
b.MoveTo(lastGoodBox.Point()) if !b.noclip {
b.MoveTo(lastGoodBox.Point())
}
} else { } else {
log.Error( log.Error(
"ERROR: Actors %s and %s overlap and the script returned false,"+ "ERROR: Actors %s and %s overlap and the script returned false,"+

View File

@ -126,14 +126,11 @@ func (w *Canvas) loopFollowActor(ev *event.State) error {
) )
// Scroll left // Scroll left
if APosition.X-VP.X <= balance.ScrollboxHoz { if APosition.X <= VP.X+balance.ScrollboxHoz {
var delta = APosition.X - VP.X var delta = VP.X + balance.ScrollboxHoz - APosition.X
if delta > balance.ScrollMaxVelocity {
delta = balance.ScrollMaxVelocity
}
// constrain in case they're FAR OFF SCREEN so we don't flip back around
if delta < 0 { if delta < 0 {
// constrain in case they're FAR OFF SCREEN so we don't flip back around
delta = -delta delta = -delta
} }
scrollBy.X = delta scrollBy.X = delta
@ -141,22 +138,13 @@ func (w *Canvas) loopFollowActor(ev *event.State) error {
// Scroll right // Scroll right
if APosition.X >= VP.W-ASize.W-balance.ScrollboxHoz { if APosition.X >= VP.W-ASize.W-balance.ScrollboxHoz {
var delta = VP.W - ASize.W - balance.ScrollboxHoz var delta = VP.W - ASize.W - APosition.X - balance.ScrollboxHoz
if delta > balance.ScrollMaxVelocity { scrollBy.X = delta
delta = balance.ScrollMaxVelocity
}
scrollBy.X = -delta
} }
// Scroll up // Scroll up
if APosition.Y-VP.Y <= balance.ScrollboxVert { if APosition.Y <= VP.Y+balance.ScrollboxVert {
var delta = APosition.Y - VP.Y var delta = VP.Y + balance.ScrollboxVert - APosition.Y
if delta > balance.ScrollMaxVelocity {
delta = balance.ScrollMaxVelocity
}
// TODO: add gravity to counteract jitters on scrolling vertically
scrollBy.Y -= balance.Gravity
if delta < 0 { if delta < 0 {
delta = -delta delta = -delta
@ -166,14 +154,13 @@ func (w *Canvas) loopFollowActor(ev *event.State) error {
// Scroll down // Scroll down
if APosition.Y >= VP.H-ASize.H-balance.ScrollboxVert { if APosition.Y >= VP.H-ASize.H-balance.ScrollboxVert {
var delta = VP.H - ASize.H - balance.ScrollboxVert var delta = VP.H - ASize.H - APosition.Y - balance.ScrollboxVert
if delta > balance.ScrollMaxVelocity { if delta > 300 {
delta = balance.ScrollMaxVelocity delta = 300
} else if delta < -300 {
delta = -300
} }
scrollBy.Y = -delta scrollBy.Y = delta
// TODO: add gravity to counteract jitters on scrolling vertically
scrollBy.Y += balance.Gravity * 3
} }
if scrollBy != render.Origin { if scrollBy != render.Origin {

View File

@ -57,6 +57,9 @@ func (w *Canvas) loopContainActorsInsideLevel(a *Actor) {
if int64(orig.Y+size.H) > w.wallpaper.maxHeight { if int64(orig.Y+size.H) > w.wallpaper.maxHeight {
var delta = w.wallpaper.maxHeight - int64(orig.Y+size.H) var delta = w.wallpaper.maxHeight - int64(orig.Y+size.H)
moveBy.Y = int(delta) moveBy.Y = int(delta)
// Allow them to jump from the bottom by marking them as grounded.
a.SetGrounded(true)
} }
} }
} }