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:
Noah 2024-02-07 22:14:48 -08:00
parent 85523d8311
commit 6def8f7625
11 changed files with 123 additions and 28 deletions

View File

@ -32,8 +32,15 @@ Some minor changes:
the ground in order to overcome the constant (fast) gravity.
* Coyote time where the player can still jump a few frames late when they
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
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.
## v0.13.2 (Dec 2 2023)

View File

@ -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
// hitbox width smaller than its Canvas size.
S = SizePlusHitbox(GetBoundingRect(d), hitbox)
actorHeight := P.Y + S.H
// Test if we are ALREADY colliding with level geometry and try and wiggle
// 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.
if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) {
// If the step is short enough, try and jump up.
height := P.Y + S.H
if result.Left { // && target.X < P.X {
height -= result.LeftPoint.Y
} else {
height -= 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
// Handle walking up slopes, if the step is short enough.
var slopeHeight int
if result.Left {
slopeHeight = result.LeftPoint.Y
} else if result.Right {
slopeHeight = result.RightPoint.Y
}
if offset, ok := CanStepUp(actorHeight, slopeHeight, target.X > P.X); ok {
target.Add(offset)
} else {
// Not a slope.. may be a solid wall. If the wall is a SemiSolid though,
// 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
// 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.
// 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 _, ok := CanStepUp(actorHeight, result.LeftPoint.Y, false); !ok {
hitLeft = true
capLeft = result.LeftPoint.X
}
}
if result.Right && !hitRight && !result.RightPixel.SemiSolid {
if _, ok := CanStepUp(actorHeight, result.RightPoint.Y, false); !ok {
hitRight = true
capRight = result.RightPoint.X - S.W
}
}
}
// So far so good, keep following the MoveTo to
// 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()
}
/*
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.
func (c *Collide) IsColliding() bool {
return c.Top || c.Bottom || (c.Left && !c.LeftPixel.SemiSolid) || (c.Right && !c.RightPixel.SemiSolid) ||

View File

@ -14,6 +14,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
"github.com/dop251/goja"
)
@ -112,6 +113,10 @@ func (c Command) Run(d *Doodle) error {
"Filename: trapdoor-down.doodad\n" +
"Position: 643,266",
)
case "flush-textures":
// Flush all textures.
native.FreeTextures(d.Engine)
d.Flash("All textures freed.")
default:
return c.Default(d)
}

View File

@ -191,8 +191,8 @@ func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point {
size = int(c.Size)
)
for x := viewport.X; x < viewport.W; x += (size / 4) {
for y := viewport.Y; y < viewport.H; y += (size / 4) {
for x := viewport.X; x < viewport.W+size; x += (size / 4) {
for y := viewport.Y; y < viewport.H+size; y += (size / 4) {
// Constrain this chunksize step to a point within the bounds
// of the viewport. This can yield partial chunks on the edges

View File

@ -8,6 +8,7 @@ import (
"fmt"
"image"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/sdl"
@ -43,6 +44,16 @@ func CountTextures(e render.Engine) string {
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.

View File

@ -26,4 +26,6 @@ func CountTextures(e render.Engine) string {
return "n/a"
}
func FreeTextures() {}
func MaximizeWindow(e render.Engine) {}

View File

@ -3,6 +3,7 @@ package doodle
import (
"git.kirsle.net/SketchyMaze/doodle/pkg/gamepad"
"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/go/render/event"
)
@ -39,6 +40,9 @@ func (d *Doodle) Goto(scene Scene) error {
// Teardown exceptions modal (singleton windows so it can clean up).
exceptions.Teardown()
// Flush all SDL2 textures between scenes.
native.FreeTextures(d.Engine)
log.Info("Goto Scene: %s", scene.Name())
d.Scene = scene
return d.Scene.Setup(d)

View File

@ -5,7 +5,6 @@ import (
"sync"
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
"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]"
// from an OnCollide handler between azu-white and thief that was crashing
// 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
// `ReferenceError: playerSpeed is not defined at <eval>:173:9(93)`
// 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",
name,
e.vm.Name,
err,
)
log.Error("Scripting error on %s: %s", name, err)
return err
}

View File

@ -6,6 +6,7 @@ import (
"strings"
"sync"
"git.kirsle.net/SketchyMaze/doodle/lib/debugging"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
@ -13,6 +14,7 @@ import (
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui"
"github.com/dop251/goja"
)
// 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)
}
// 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,
// e.g., when you want the window to show up the first time.
func Setup() {

View File

@ -1,7 +1,6 @@
package scripting
import (
"git.kirsle.net/SketchyMaze/doodle/lib/debugging"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
"github.com/dop251/goja"
@ -28,9 +27,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
// Catch any exceptions raised by the JavaScript VM.
defer func() {
if err := recover(); err != nil {
// TODO EXCEPTIONS
exceptions.Catch("RegisterPublishHooks(%s): %s", vm.Name, err)
debugging.PrintCallers()
exceptions.FormatAndCatch(vm.vm, "RegisterPublishHooks(%s): %s: %s", vm.Name, err)
}
}()

View File

@ -127,7 +127,8 @@ func (w *Canvas) InstallScripts() error {
// Call the main() function.
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",
actor.Actor.Filename,
err,