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.
|
||||
* 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)
|
||||
|
|
|
@ -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
|
||||
// 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 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 {
|
||||
// Not a slope.. may be a solid wall. If the wall is a SemiSolid though,
|
||||
// do not cap our direction just yet.
|
||||
|
@ -198,13 +195,20 @@ 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 {
|
||||
hitLeft = true
|
||||
capLeft = result.LeftPoint.X
|
||||
if _, ok := CanStepUp(actorHeight, result.LeftPoint.Y, false); !ok {
|
||||
hitLeft = true
|
||||
capLeft = result.LeftPoint.X
|
||||
}
|
||||
}
|
||||
if result.Right && !hitRight && !result.RightPixel.SemiSolid {
|
||||
hitRight = true
|
||||
capRight = result.RightPoint.X - S.W
|
||||
if _, ok := CanStepUp(actorHeight, result.RightPoint.Y, false); !ok {
|
||||
hitRight = true
|
||||
capRight = result.RightPoint.X - S.W
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) ||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -26,4 +26,6 @@ func CountTextures(e render.Engine) string {
|
|||
return "n/a"
|
||||
}
|
||||
|
||||
func FreeTextures() {}
|
||||
|
||||
func MaximizeWindow(e render.Engine) {}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue
Block a user