Walk up slopes smoothly, texture freeing improvement
* Fix collision detection to allow actors to walk up slopes smoothly, without losing any horizontal velocity. * Fix scrolling a level canvas so that chunks near the right or bottom edge of the viewpoint were getting culled prematurely. * Centralize JavaScript exception catching logic to attach Go and JS stack traces where possible to be more useful for debugging. * Performance: flush all SDL2 textures from memory between scene transitions in the app. Also add a `flush-textures` dev console command to flush the textures at any time - they all should regenerate if still needed based on underlying go.Images which can be garbage collected.
This commit is contained in:
parent
85523d8311
commit
6def8f7625
|
@ -32,8 +32,15 @@ Some minor changes:
|
||||||
the ground in order to overcome the constant (fast) gravity.
|
the ground in order to overcome the constant (fast) gravity.
|
||||||
* Coyote time where the player can still jump a few frames late when they
|
* Coyote time where the player can still jump a few frames late when they
|
||||||
walk off a cliff and jump late.
|
walk off a cliff and jump late.
|
||||||
|
* Improved character speed when walking up slopes, they will now travel
|
||||||
|
horizontally at their regular speed instead of being slowed down by level
|
||||||
|
collision every step of the way.
|
||||||
* Sound effects are now preferred to be in OGG format over MP3 as it is more
|
* Sound effects are now preferred to be in OGG format over MP3 as it is more
|
||||||
reliable to compile the game cross-platform without the dependency on mpg123.
|
reliable to compile the game cross-platform without the dependency on mpg123.
|
||||||
|
* Fix a bug where level chunks on the far right and bottom edge of the screen
|
||||||
|
would flicker out of existence while the level scrolls.
|
||||||
|
* When JavaScript exceptions are caught in doodad scripts, the error message
|
||||||
|
will now include the Go and JavaScript stack traces to help with debugging.
|
||||||
* The game window maximizes on startup to fill the screen.
|
* The game window maximizes on startup to fill the screen.
|
||||||
|
|
||||||
## v0.13.2 (Dec 2 2023)
|
## v0.13.2 (Dec 2 2023)
|
||||||
|
|
|
@ -78,6 +78,7 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
|
||||||
// e.g.: Boy's Canvas size is 56x56 but he is a narrower character with a
|
// e.g.: Boy's Canvas size is 56x56 but he is a narrower character with a
|
||||||
// hitbox width smaller than its Canvas size.
|
// hitbox width smaller than its Canvas size.
|
||||||
S = SizePlusHitbox(GetBoundingRect(d), hitbox)
|
S = SizePlusHitbox(GetBoundingRect(d), hitbox)
|
||||||
|
actorHeight := P.Y + S.H
|
||||||
|
|
||||||
// Test if we are ALREADY colliding with level geometry and try and wiggle
|
// Test if we are ALREADY colliding with level geometry and try and wiggle
|
||||||
// free. ScanBoundingBox scans level pixels along the four edges of the
|
// free. ScanBoundingBox scans level pixels along the four edges of the
|
||||||
|
@ -120,20 +121,16 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
|
||||||
|
|
||||||
// Cap our horizontal movement if we're touching walls.
|
// Cap our horizontal movement if we're touching walls.
|
||||||
if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) {
|
if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) {
|
||||||
// If the step is short enough, try and jump up.
|
// Handle walking up slopes, if the step is short enough.
|
||||||
height := P.Y + S.H
|
var slopeHeight int
|
||||||
if result.Left { // && target.X < P.X {
|
if result.Left {
|
||||||
height -= result.LeftPoint.Y
|
slopeHeight = result.LeftPoint.Y
|
||||||
} else {
|
} else if result.Right {
|
||||||
height -= result.RightPoint.Y
|
slopeHeight = result.RightPoint.Y
|
||||||
}
|
|
||||||
if height <= balance.SlopeMaxHeight {
|
|
||||||
target.Y -= height
|
|
||||||
if target.X < P.X {
|
|
||||||
target.X-- // push along to the left
|
|
||||||
} else if target.X > P.X {
|
|
||||||
target.X++ // push along to the right
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if offset, ok := CanStepUp(actorHeight, slopeHeight, target.X > P.X); ok {
|
||||||
|
target.Add(offset)
|
||||||
} else {
|
} else {
|
||||||
// Not a slope.. may be a solid wall. If the wall is a SemiSolid though,
|
// Not a slope.. may be a solid wall. If the wall is a SemiSolid though,
|
||||||
// do not cap our direction just yet.
|
// do not cap our direction just yet.
|
||||||
|
@ -198,15 +195,22 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
|
||||||
// for regular solid slopes too. But if this block of code is dummied out for
|
// for regular solid slopes too. But if this block of code is dummied out for
|
||||||
// solid walls, the player is able to clip thru thin walls (couple px thick); the
|
// solid walls, the player is able to clip thru thin walls (couple px thick); the
|
||||||
// capLeft/capRight behavior is good at stopping the player here.
|
// capLeft/capRight behavior is good at stopping the player here.
|
||||||
|
|
||||||
|
// See if they have hit a solid wall on their left or right edge. If the wall
|
||||||
|
// is short enough to step up, allow them to pass through.
|
||||||
if result.Left && !hitLeft && !result.LeftPixel.SemiSolid {
|
if result.Left && !hitLeft && !result.LeftPixel.SemiSolid {
|
||||||
|
if _, ok := CanStepUp(actorHeight, result.LeftPoint.Y, false); !ok {
|
||||||
hitLeft = true
|
hitLeft = true
|
||||||
capLeft = result.LeftPoint.X
|
capLeft = result.LeftPoint.X
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if result.Right && !hitRight && !result.RightPixel.SemiSolid {
|
if result.Right && !hitRight && !result.RightPixel.SemiSolid {
|
||||||
|
if _, ok := CanStepUp(actorHeight, result.RightPoint.Y, false); !ok {
|
||||||
hitRight = true
|
hitRight = true
|
||||||
capRight = result.RightPoint.X - S.W
|
capRight = result.RightPoint.X - S.W
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -234,6 +238,37 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
|
||||||
return result, result.IsColliding()
|
return result, result.IsColliding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CanStepUp checks whether the actor is moving left or right onto a gentle slope which
|
||||||
|
they can step on top of instead of being blocked by the solid wall.
|
||||||
|
|
||||||
|
* actorHeight is the actor's Y position + their hitbox height.
|
||||||
|
* slopeHeight is the Y position of the left or right edge of the level they collide with.
|
||||||
|
* moveRight is true if moving right, false if moving left.
|
||||||
|
|
||||||
|
If the actor can step up the slope, the return value is the Point of how to offset their
|
||||||
|
X,Y position to move up the slope and the boolean is whether they can step up.
|
||||||
|
*/
|
||||||
|
func CanStepUp(actorHeight, slopeHeight int, moveRight bool) (render.Point, bool) {
|
||||||
|
var (
|
||||||
|
height = actorHeight - slopeHeight
|
||||||
|
target render.Point
|
||||||
|
)
|
||||||
|
|
||||||
|
if height <= balance.SlopeMaxHeight {
|
||||||
|
target.Y -= height
|
||||||
|
if moveRight {
|
||||||
|
target.X++
|
||||||
|
} else {
|
||||||
|
target.X--
|
||||||
|
}
|
||||||
|
|
||||||
|
return target, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return target, false
|
||||||
|
}
|
||||||
|
|
||||||
// IsColliding returns whether any sort of collision has occurred.
|
// IsColliding returns whether any sort of collision has occurred.
|
||||||
func (c *Collide) IsColliding() bool {
|
func (c *Collide) IsColliding() bool {
|
||||||
return c.Top || c.Bottom || (c.Left && !c.LeftPixel.SemiSolid) || (c.Right && !c.RightPixel.SemiSolid) ||
|
return c.Top || c.Bottom || (c.Left && !c.LeftPixel.SemiSolid) || (c.Right && !c.RightPixel.SemiSolid) ||
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
@ -112,6 +113,10 @@ func (c Command) Run(d *Doodle) error {
|
||||||
"Filename: trapdoor-down.doodad\n" +
|
"Filename: trapdoor-down.doodad\n" +
|
||||||
"Position: 643,266",
|
"Position: 643,266",
|
||||||
)
|
)
|
||||||
|
case "flush-textures":
|
||||||
|
// Flush all textures.
|
||||||
|
native.FreeTextures(d.Engine)
|
||||||
|
d.Flash("All textures freed.")
|
||||||
default:
|
default:
|
||||||
return c.Default(d)
|
return c.Default(d)
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,8 +191,8 @@ func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point {
|
||||||
size = int(c.Size)
|
size = int(c.Size)
|
||||||
)
|
)
|
||||||
|
|
||||||
for x := viewport.X; x < viewport.W; x += (size / 4) {
|
for x := viewport.X; x < viewport.W+size; x += (size / 4) {
|
||||||
for y := viewport.Y; y < viewport.H; y += (size / 4) {
|
for y := viewport.Y; y < viewport.H+size; y += (size / 4) {
|
||||||
|
|
||||||
// Constrain this chunksize step to a point within the bounds
|
// Constrain this chunksize step to a point within the bounds
|
||||||
// of the viewport. This can yield partial chunks on the edges
|
// of the viewport. This can yield partial chunks on the edges
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/render/sdl"
|
"git.kirsle.net/go/render/sdl"
|
||||||
|
@ -43,6 +44,16 @@ func CountTextures(e render.Engine) string {
|
||||||
return texCount
|
return texCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FreeTextures will free all SDL2 textures currently in memory.
|
||||||
|
func FreeTextures(e render.Engine) {
|
||||||
|
if sdl, ok := e.(*sdl.Renderer); ok {
|
||||||
|
texCount := sdl.FreeTextures()
|
||||||
|
if texCount > 0 {
|
||||||
|
log.Info("FreeTextures: %d SDL2 textures freed", texCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TextToImage takes an SDL2_TTF texture and makes it into a Go image.
|
TextToImage takes an SDL2_TTF texture and makes it into a Go image.
|
||||||
|
|
||||||
|
|
|
@ -26,4 +26,6 @@ func CountTextures(e render.Engine) string {
|
||||||
return "n/a"
|
return "n/a"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FreeTextures() {}
|
||||||
|
|
||||||
func MaximizeWindow(e render.Engine) {}
|
func MaximizeWindow(e render.Engine) {}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package doodle
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/gamepad"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/gamepad"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
||||||
"git.kirsle.net/go/render/event"
|
"git.kirsle.net/go/render/event"
|
||||||
)
|
)
|
||||||
|
@ -39,6 +40,9 @@ func (d *Doodle) Goto(scene Scene) error {
|
||||||
// Teardown exceptions modal (singleton windows so it can clean up).
|
// Teardown exceptions modal (singleton windows so it can clean up).
|
||||||
exceptions.Teardown()
|
exceptions.Teardown()
|
||||||
|
|
||||||
|
// Flush all SDL2 textures between scenes.
|
||||||
|
native.FreeTextures(d.Engine)
|
||||||
|
|
||||||
log.Info("Goto Scene: %s", scene.Name())
|
log.Info("Goto Scene: %s", scene.Name())
|
||||||
d.Scene = scene
|
d.Scene = scene
|
||||||
return d.Scene.Setup(d)
|
return d.Scene.Setup(d)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
@ -107,7 +106,7 @@ func (e *Events) run(name string, args ...interface{}) error {
|
||||||
// TODO EXCEPTIONS: I once saw a "runtime error: index out of range [-1]"
|
// TODO EXCEPTIONS: I once saw a "runtime error: index out of range [-1]"
|
||||||
// from an OnCollide handler between azu-white and thief that was crashing
|
// from an OnCollide handler between azu-white and thief that was crashing
|
||||||
// the app, report this upstream nicely to the user.
|
// the app, report this upstream nicely to the user.
|
||||||
exceptions.Catch("PANIC: JS %s handler: %s", name, err)
|
exceptions.FormatAndCatch(e.vm.vm, "PANIC: JS %s handler (%s): %s", name, e.vm.Name, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -126,13 +125,13 @@ func (e *Events) run(name string, args ...interface{}) error {
|
||||||
// TODO EXCEPTIONS: this err is useful like
|
// TODO EXCEPTIONS: this err is useful like
|
||||||
// `ReferenceError: playerSpeed is not defined at <eval>:173:9(93)`
|
// `ReferenceError: playerSpeed is not defined at <eval>:173:9(93)`
|
||||||
// but wherever we're returning the err to isn't handling it!
|
// but wherever we're returning the err to isn't handling it!
|
||||||
exceptions.Catch(
|
exceptions.FormatAndCatch(
|
||||||
|
e.vm.vm,
|
||||||
"Scripting error in %s for %s:\n\n%s",
|
"Scripting error in %s for %s:\n\n%s",
|
||||||
name,
|
name,
|
||||||
e.vm.Name,
|
e.vm.Name,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
log.Error("Scripting error on %s: %s", name, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/lib/debugging"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/render/event"
|
"git.kirsle.net/go/render/event"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The exception catching window is a singleton and appears on top with
|
// The exception catching window is a singleton and appears on top with
|
||||||
|
@ -61,6 +63,38 @@ func Catch(exc string, args ...interface{}) {
|
||||||
*excLabel = trim(exc)
|
*excLabel = trim(exc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormatAndCatch an exception from a JavaScript VM. This is the common function called
|
||||||
|
// immediately when a scripting-related panic is recovered, and appends stack trace frames
|
||||||
|
// and formats the message for final Catch display.
|
||||||
|
func FormatAndCatch(vm *goja.Runtime, exc string, args ...interface{}) {
|
||||||
|
// Collect the JavaScript stack frame for debugging.
|
||||||
|
var (
|
||||||
|
buf [1000]goja.StackFrame
|
||||||
|
jsCallers = []string{}
|
||||||
|
sections = []string{
|
||||||
|
fmt.Sprintf(exc, args...),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if vm != nil {
|
||||||
|
frames := vm.CaptureCallStack(1000, buf[:0])
|
||||||
|
for i, frame := range frames {
|
||||||
|
var position = frame.Position()
|
||||||
|
jsCallers = append(jsCallers, fmt.Sprintf("%d. %s at %s line %d column %d", i+1, frame.FuncName(), position.Filename, position.Line, position.Column))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(jsCallers) > 0 {
|
||||||
|
sections = append(sections, fmt.Sprintf("JS stack:\n%s", strings.Join(jsCallers, "\n")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sections = append(sections, fmt.Sprintf("Go stack:\n%s", debugging.StringifyCallers()))
|
||||||
|
|
||||||
|
Catch(
|
||||||
|
strings.Join(sections, "\n\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Setup the global supervisor and window the first time - after the render engine has initialized,
|
// Setup the global supervisor and window the first time - after the render engine has initialized,
|
||||||
// e.g., when you want the window to show up the first time.
|
// e.g., when you want the window to show up the first time.
|
||||||
func Setup() {
|
func Setup() {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package scripting
|
package scripting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/SketchyMaze/doodle/lib/debugging"
|
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
@ -28,9 +27,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
|
||||||
// Catch any exceptions raised by the JavaScript VM.
|
// Catch any exceptions raised by the JavaScript VM.
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
// TODO EXCEPTIONS
|
exceptions.FormatAndCatch(vm.vm, "RegisterPublishHooks(%s): %s: %s", vm.Name, err)
|
||||||
exceptions.Catch("RegisterPublishHooks(%s): %s", vm.Name, err)
|
|
||||||
debugging.PrintCallers()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -127,7 +127,8 @@ func (w *Canvas) InstallScripts() error {
|
||||||
|
|
||||||
// Call the main() function.
|
// Call the main() function.
|
||||||
if err := vm.Main(); err != nil {
|
if err := vm.Main(); err != nil {
|
||||||
exceptions.Catch(
|
exceptions.FormatAndCatch(
|
||||||
|
nil,
|
||||||
"Error in main() for actor %s:\n\n%s\n\nActor ID: %s\nFilename: %s\nPosition: %s",
|
"Error in main() for actor %s:\n\n%s\n\nActor ID: %s\nFilename: %s\nPosition: %s",
|
||||||
actor.Actor.Filename,
|
actor.Actor.Filename,
|
||||||
err,
|
err,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user