From a2e1bd1ccb41e1655c156ea5b4fe2d471df2cd75 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 6 May 2019 22:57:32 -0700 Subject: [PATCH] Improve OnCollide Doodad Script Handling * Events.OnCollide now receives a CollideEvent object, which makes available the .Actor who collided and the .Overlap rect which is zero-relative to the target actor. Doodad scripts can use the .Overlap to see WHERE in their own box the other actor has intruded. * Update the LockedDoor and ElectricDoor doodads to detect when the player has entered their inner rect (since their doors are narrower than their doodad size) * Update the Button doodads to only press in when the player actually touches them (because their sizes are shorter than their doodad height) * Update the Trapdoor to only trigger its animation when the board along its top has been touched, not when the empty space below was touched from the bottom. * Events.OnLeave now implemented and fires when an actor who was previously intersecting your doodad has left. * The engine detects when an event JS callback returns false. Eventually, the OnCollide can return false to signify the collision is not accepted and the actor should be bumped away as if they hit solid geometry. --- dev-assets/doodads/buttons/button.js | 15 +++- dev-assets/doodads/buttons/sticky.js | 16 +++- dev-assets/doodads/doors/electric-door.js | 25 +++++-- dev-assets/doodads/doors/locked-door.js | 16 ++++ dev-assets/doodads/trapdoors/down.js | 25 ++++--- pkg/collision/actors_test.go | 17 ++++- pkg/collision/collide_actors.go | 89 ++++++++++++++++++++++- pkg/collision/collide_level.go | 13 +--- pkg/collision/debug_box.go | 50 ++++++++++--- pkg/scripting/events.go | 31 +++++++- pkg/uix/actor.go | 5 ++ pkg/uix/actor_events.go | 9 +++ pkg/uix/canvas.go | 31 +++++--- 13 files changed, 285 insertions(+), 57 deletions(-) create mode 100644 pkg/uix/actor_events.go diff --git a/dev-assets/doodads/buttons/button.js b/dev-assets/doodads/buttons/button.js index 83318fb..4cb1d9e 100644 --- a/dev-assets/doodads/buttons/button.js +++ b/dev-assets/doodads/buttons/button.js @@ -3,7 +3,15 @@ function main() { var timer = 0; - Events.OnCollide( function() { + Events.OnCollide(function(e) { + // Verify they've touched the button. + if (e.Overlap.Y + e.Overlap.H < 24) { + Self.Canvas.SetBackground(RGBA(0, 255, 0, 153)); + return; + } + + Self.Canvas.SetBackground(RGBA(255, 255, 0, 153)); + if (timer > 0) { clearTimeout(timer); } @@ -13,5 +21,10 @@ function main() { Self.ShowLayer(0); timer = 0; }, 200); + }); + + Events.OnLeave(function(e) { + console.log("%s has stopped touching %s", e, Self.Doodad.Title) + Self.Canvas.SetBackground(RGBA(0, 0, 1, 0)); }) } diff --git a/dev-assets/doodads/buttons/sticky.js b/dev-assets/doodads/buttons/sticky.js index cf4fe74..a3e1373 100644 --- a/dev-assets/doodads/buttons/sticky.js +++ b/dev-assets/doodads/buttons/sticky.js @@ -1,7 +1,19 @@ function main() { console.log("%s initialized!", Self.Doodad.Title); - Events.OnCollide( function() { + var pressed = false; + + Events.OnCollide(function(e) { + if (pressed) { + return; + } + + // Verify they've touched the button. + if (e.Overlap.Y + e.Overlap.H < 24) { + return; + } + Self.ShowLayer(1); - }) + pressed = true; + }); } diff --git a/dev-assets/doodads/doors/electric-door.js b/dev-assets/doodads/doors/electric-door.js index b665d73..b9ae2b7 100644 --- a/dev-assets/doodads/doors/electric-door.js +++ b/dev-assets/doodads/doors/electric-door.js @@ -1,16 +1,29 @@ function main() { console.log("%s initialized!", Self.Doodad.Title); - var err = Self.AddAnimation("open", 100, [0, 1, 2, 3]); - console.error("door error: %s", err) + Self.AddAnimation("open", 100, [0, 1, 2, 3]); + Self.AddAnimation("close", 100, [3, 2, 1, 0]); var animating = false; + var opened = false; - Events.OnCollide(function() { - if (animating) { + Events.OnCollide(function(e) { + if (animating || opened) { return; } - animating = true; - Self.PlayAnimation("open", null); + if (e.Overlap.X + e.Overlap.W >= 16 && e.Overlap.X < 48) { + animating = true; + Self.PlayAnimation("open", function() { + opened = true; + animating = false; + }); + } }); + Events.OnLeave(function() { + if (opened) { + Self.PlayAnimation("close", function() { + opened = false; + }); + } + }) } diff --git a/dev-assets/doodads/doors/locked-door.js b/dev-assets/doodads/doors/locked-door.js index 75545f9..0c18d2f 100644 --- a/dev-assets/doodads/doors/locked-door.js +++ b/dev-assets/doodads/doors/locked-door.js @@ -3,6 +3,18 @@ function main() { var unlocked = false; Events.OnCollide(function(e) { + console.log("%s was touched by %s!", Self.Doodad.Title, e.Actor.ID()); + console.log("my box: %+v and theirs: %+v", Self.GetBoundingRect(), e.Actor.GetBoundingRect()); + console.warn("But the overlap is: %+v", e.Overlap); + console.log(Object.keys(e)); + + if (e.Overlap.X + e.Overlap.W >= 16 && e.Overlap.X < 48) { + Self.Canvas.SetBackground(RGBA(255, 0, 0, 153)); + } else { + Self.Canvas.SetBackground(RGBA(0, 255, 0, 153)); + return; + } + if (unlocked) { return; } @@ -10,4 +22,8 @@ function main() { unlocked = true; Self.PlayAnimation("open", null); }); + Events.OnLeave(function(e) { + console.log("%s has stopped touching %s", e, Self.Doodad.Title) + Self.Canvas.SetBackground(RGBA(0, 0, 1, 0)); + }) } diff --git a/dev-assets/doodads/trapdoors/down.js b/dev-assets/doodads/trapdoors/down.js index 209eedc..0764ccb 100644 --- a/dev-assets/doodads/trapdoors/down.js +++ b/dev-assets/doodads/trapdoors/down.js @@ -4,22 +4,27 @@ function main() { var timer = 0; var animationSpeed = 100; - var animating = false; + var opened = false; Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]); Self.AddAnimation("close", animationSpeed, ["down4", "down3", "down2", "down1"]); - Events.OnCollide( function() { - if (animating) { + Events.OnCollide( function(e) { + if (opened) { return; } - animating = true; + // Not touching the top of the door means door doesn't open. + if (e.Overlap.Y > 9) { + return; + } + + opened = true; Self.PlayAnimation("open", function() { - setTimeout(function() { - Self.PlayAnimation("close", function() { - animating = false; - }); - }, 3000); - }) + }); }); + Events.OnLeave(function() { + Self.PlayAnimation("close", function() { + opened = false; + }); + }) } diff --git a/pkg/collision/actors_test.go b/pkg/collision/actors_test.go index 63bfe10..fcac887 100644 --- a/pkg/collision/actors_test.go +++ b/pkg/collision/actors_test.go @@ -10,6 +10,9 @@ import ( func TestActorCollision(t *testing.T) { boxes := []render.Rect{ // 0: intersects with 1 + // Expected intersection rect would be + // X,Y = 90,10 + // X2,Y2 = 100,99 render.Rect{ X: 0, Y: 0, @@ -18,6 +21,9 @@ func TestActorCollision(t *testing.T) { }, // 1: intersects with 0 + // Expected intersection rect would be + // X,Y = 90,10 + // X2,Y2 = 100,99 render.Rect{ X: 90, Y: 10, @@ -34,6 +40,9 @@ func TestActorCollision(t *testing.T) { }, // 3: intersects with 4 + // Expected intersection rect would be + // X,Y = 240,200 + // X2,Y2 = 264,231 render.Rect{ X: 233, Y: 200, @@ -70,22 +79,22 @@ func TestActorCollision(t *testing.T) { }, } - assert := func(i int, result collision.IndexTuple, expectA, expectB int) { - if result[0] != expectA || result[1] != expectB { + assert := func(i int, result collision.BoxCollision, expectA, expectB int) { + if result.A != expectA || result.B != expectB { t.Errorf( "unexpected collision at index %d of BetweenBoxes() generator\n"+ "expected: (%d,%d)\n"+ " but got: (%d,%d)", i, expectA, expectB, - result[0], result[1], + result.A, result.B, ) } } var i int for overlap := range collision.BetweenBoxes(boxes) { - a, b := overlap[0], overlap[1] + a, b := overlap.A, overlap.B // Ensure expected collisions happened. switch i { diff --git a/pkg/collision/collide_actors.go b/pkg/collision/collide_actors.go index 899c57a..bb5479d 100644 --- a/pkg/collision/collide_actors.go +++ b/pkg/collision/collide_actors.go @@ -1,9 +1,21 @@ package collision import ( + "math" + "git.kirsle.net/apps/doodle/lib/render" ) +// BoxCollision holds the result of a collision BetweenBoxes. +type BoxCollision struct { + // A and B are the indexes of the boxes sent to BetweenBoxes. + A int + B int + + // Overlap is the rect of how the boxes overlap. + Overlap render.Rect +} + // IndexTuple holds two integers used as array indexes. type IndexTuple [2]int @@ -12,15 +24,30 @@ type IndexTuple [2]int // // This returns a generator that spits out indexes of the // intersecting boxes. -func BetweenBoxes(boxes []render.Rect) chan IndexTuple { - generator := make(chan IndexTuple) +func BetweenBoxes(boxes []render.Rect) chan BoxCollision { + generator := make(chan BoxCollision) go func() { // Outer loop: test each box for intersection with the others. for i, box := range boxes { for j := i + 1; j < len(boxes); j++ { - if box.Intersects(boxes[j]) { - generator <- IndexTuple{i, j} + other := boxes[j] + if box.Intersects(other) { + var ( + overlap = OverlapRelative(box, other) + topLeft = overlap.TopLeft() + bottomRight = overlap.BottomRight() + ) + generator <- BoxCollision{ + A: i, + B: j, + Overlap: render.Rect{ + X: topLeft.X, + Y: topLeft.Y, + W: bottomRight.X, + H: bottomRight.Y, + }, + } } } } @@ -30,3 +57,57 @@ func BetweenBoxes(boxes []render.Rect) chan IndexTuple { return generator } + +/* +OverlapRelative returns the Overlap box using coordinates relative +to the source rect instead of absolute coordinates. +*/ +func OverlapRelative(source, other render.Rect) CollisionBox { + var ( + // Move the source rect to 0,0 and record the distance we need + // to go to get there, so we can move the other rect the same. + deltaX = 0 - source.X + deltaY = 0 - source.Y + ) + + source.X = 0 + source.Y = 0 + other.X += deltaX + other.Y += deltaY + + return Overlap(source, other) +} + +/* +Overlap returns the overlap rectangle between two boxes. + +The two rects given have an X,Y coordinate and their W,H are their +width and heights. + +The returned CollisionBox uses absolute coordinates in the same space +as the passed-in rects. +*/ +func Overlap(a, b render.Rect) CollisionBox { + max := func(x, y int32) int32 { + return int32(math.Max(float64(x), float64(y))) + } + min := func(x, y int32) int32 { + return int32(math.Min(float64(x), float64(y))) + } + + var ( + A = GetCollisionBox(a) + B = GetCollisionBox(b) + + ATL = A.TopLeft() + ABR = A.BottomRight() + BTL = B.TopLeft() + BBR = B.BottomRight() + + // Coordinates of the intersection box. + X1, Y1 = max(ATL.X, BTL.X), max(ATL.Y, BTL.Y) + X2, Y2 = min(ABR.X, BBR.X), min(ABR.Y, BBR.Y) + ) + + return NewBox(render.NewPoint(X1, Y1), render.NewPoint(X2, Y2)) +} diff --git a/pkg/collision/collide_level.go b/pkg/collision/collide_level.go index 6e7539f..11f54df 100644 --- a/pkg/collision/collide_level.go +++ b/pkg/collision/collide_level.go @@ -206,16 +206,17 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool { // Check all four edges of the box in parallel on different CPU cores. type jobSide struct { - p1 render.Point - p2 render.Point + p1 render.Point // p2 is perpendicular to p1 along a straight edge + p2 render.Point // of the collision box. side Side } - jobs := []jobSide{ + jobs := []jobSide{ // We'll scan each side of the bounding box in parallel jobSide{col.Top[0], col.Top[1], Top}, jobSide{col.Bottom[0], col.Bottom[1], Bottom}, jobSide{col.Left[0], col.Left[1], Left}, jobSide{col.Right[0], col.Right[1], Right}, } + var wg sync.WaitGroup for _, job := range jobs { wg.Add(1) @@ -226,12 +227,6 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool { } wg.Wait() - - // TODO: the old synchronous version of the above. - // c.ScanGridLine(col.Top[0], col.Top[1], grid, Top) - // c.ScanGridLine(col.Bottom[0], col.Bottom[1], grid, Bottom) - // c.ScanGridLine(col.Left[0], col.Left[1], grid, Left) - // c.ScanGridLine(col.Right[0], col.Right[1], grid, Right) return c.IsColliding() } diff --git a/pkg/collision/debug_box.go b/pkg/collision/debug_box.go index b99aef2..8392af7 100644 --- a/pkg/collision/debug_box.go +++ b/pkg/collision/debug_box.go @@ -1,20 +1,34 @@ package collision -import "git.kirsle.net/apps/doodle/lib/render" +import ( + "fmt" + + "git.kirsle.net/apps/doodle/lib/render" +) // CollisionBox holds all of the coordinate pairs to draw the collision box // around a doodad. type CollisionBox struct { - Top []render.Point - Bottom []render.Point - Left []render.Point - Right []render.Point + Top [2]render.Point + Bottom [2]render.Point + Left [2]render.Point + Right [2]render.Point +} + +// NewBox creates a collision box from the Top Left and Bottom Right points. +func NewBox(topLeft, bottomRight render.Point) CollisionBox { + return GetCollisionBox(render.Rect{ + X: topLeft.X, + Y: topLeft.Y, + W: bottomRight.X - topLeft.X, + H: bottomRight.Y - topLeft.Y, + }) } // GetCollisionBox returns a CollisionBox with the four coordinates. func GetCollisionBox(box render.Rect) CollisionBox { return CollisionBox{ - Top: []render.Point{ + Top: [2]render.Point{ { X: box.X, Y: box.Y, @@ -24,7 +38,7 @@ func GetCollisionBox(box render.Rect) CollisionBox { Y: box.Y, }, }, - Bottom: []render.Point{ + Bottom: [2]render.Point{ { X: box.X, Y: box.Y + box.H, @@ -34,7 +48,7 @@ func GetCollisionBox(box render.Rect) CollisionBox { Y: box.Y + box.H, }, }, - Left: []render.Point{ + Left: [2]render.Point{ { X: box.X, Y: box.Y + box.H - 1, @@ -44,7 +58,7 @@ func GetCollisionBox(box render.Rect) CollisionBox { Y: box.Y + 1, }, }, - Right: []render.Point{ + Right: [2]render.Point{ { X: box.X + box.W, Y: box.Y + box.H - 1, @@ -56,3 +70,21 @@ func GetCollisionBox(box render.Rect) CollisionBox { }, } } + +// String prints the bounds of the collision box in absolute terms. +func (c CollisionBox) String() string { + return fmt.Sprintf("CollisionBox<%s:%s>", + c.TopLeft(), + c.BottomRight(), + ) +} + +// TopLeft returns the point at the top left. +func (c CollisionBox) TopLeft() render.Point { + return render.NewPoint(c.Top[0].X, c.Top[0].Y) +} + +// BottomRight returns the point at the bottom right. +func (c CollisionBox) BottomRight() render.Point { + return render.NewPoint(c.Bottom[1].X, c.Bottom[1].Y) +} diff --git a/pkg/scripting/events.go b/pkg/scripting/events.go index 8fac86b..4172010 100644 --- a/pkg/scripting/events.go +++ b/pkg/scripting/events.go @@ -1,6 +1,8 @@ package scripting import ( + "errors" + "git.kirsle.net/apps/doodle/lib/events" "github.com/robertkrimen/otto" ) @@ -15,6 +17,11 @@ const ( KeypressEvent = "OnKeypress" // i.e. arrow keys ) +// Event return errors. +var ( + ErrReturnFalse = errors.New("JS callback function returned false") +) + // Events API for Doodad scripts. type Events struct { registry map[string][]otto.Value @@ -33,8 +40,18 @@ func (e *Events) OnCollide(call otto.FunctionCall) otto.Value { } // RunCollide invokes the OnCollide handler function. -func (e *Events) RunCollide() error { - return e.run(CollideEvent) +func (e *Events) RunCollide(v interface{}) error { + return e.run(CollideEvent, v) +} + +// OnLeave fires when another actor stops colliding with yours. +func (e *Events) OnLeave(call otto.FunctionCall) otto.Value { + return e.register(LeaveEvent, call.Argument(0)) +} + +// RunLeave invokes the OnLeave handler function. +func (e *Events) RunLeave(v interface{}) error { + return e.run(LeaveEvent, v) } // OnKeypress fires when another actor collides with yours. @@ -69,10 +86,18 @@ func (e *Events) run(name string, args ...interface{}) error { } for _, callback := range e.registry[name] { - _, err := callback.Call(otto.Value{}, args...) + value, err := callback.Call(otto.Value{}, args...) if err != nil { return err } + + // If the event handler returned a boolean false, stop all other + // callbacks and return the boolean. + if value.IsBoolean() { + if b, err := value.ToBoolean(); err == nil && b == false { + return ErrReturnFalse + } + } } return nil diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 48d11d2..99728a2 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -73,6 +73,11 @@ func (a *Actor) SetGravity(v bool) { a.hasGravity = v } +// GetBoundingRect gets the bounding box of the actor's doodad. +func (a *Actor) GetBoundingRect() render.Rect { + return doodads.GetBoundingRect(a) +} + // LayerCount returns the number of layers in this actor's drawing. func (a *Actor) LayerCount() int { return len(a.Doodad.Layers) diff --git a/pkg/uix/actor_events.go b/pkg/uix/actor_events.go new file mode 100644 index 0000000..491c2c7 --- /dev/null +++ b/pkg/uix/actor_events.go @@ -0,0 +1,9 @@ +package uix + +import "git.kirsle.net/apps/doodle/lib/render" + +// CollideEvent holds data sent to an actor's Collide handler. +type CollideEvent struct { + Actor *Actor + Overlap render.Rect +} diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 3558335..30d469c 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -53,6 +53,9 @@ type Canvas struct { actor *Actor // if this canvas IS an actor actors []*Actor // if this canvas CONTAINS actors (i.e., is a level) + // Collision memory for the actors. + collidingActors map[string]string // mapping their IDs to each other + // Doodad scripting engine supervisor. // NOTE: initialized and managed by the play_scene. scripting *scripting.Supervisor @@ -248,24 +251,34 @@ func (w *Canvas) Loop(ev *events.State) error { } // Check collisions between actors. + var collidingActors = map[string]string{} for tuple := range collision.BetweenBoxes(boxes) { - log.Debug("Actor %s collides with %s", - w.actors[tuple[0]].ID(), - w.actors[tuple[1]].ID(), - ) - a, b := w.actors[tuple[0]], w.actors[tuple[1]] + a, b := w.actors[tuple.A], w.actors[tuple.B] + + collidingActors[a.ID()] = b.ID() // Call the OnCollide handler. if w.scripting != nil { - if err := w.scripting.To(a.ID()).Events.RunCollide(); err != nil { - log.Error(err.Error()) - } - if err := w.scripting.To(b.ID()).Events.RunCollide(); err != nil { + // Tell actor A about the collision with B. + if err := w.scripting.To(a.ID()).Events.RunCollide(&CollideEvent{ + Actor: b, + Overlap: tuple.Overlap, + }); err != nil { log.Error(err.Error()) } } } + // Check for lacks of collisions since last frame. + for sourceID, targetID := range w.collidingActors { + if _, ok := collidingActors[sourceID]; !ok { + w.scripting.To(sourceID).Events.RunLeave(targetID) + } + } + + // Store this frame's colliding actors for next frame. + w.collidingActors = collidingActors + // If the canvas is editable, only care if it's over our space. if w.Editable { cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)