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.
This commit is contained in:
Noah 2019-05-06 22:57:32 -07:00
parent 61af068b80
commit a2e1bd1ccb
13 changed files with 285 additions and 57 deletions

View File

@ -3,7 +3,15 @@ function main() {
var timer = 0; 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) { if (timer > 0) {
clearTimeout(timer); clearTimeout(timer);
} }
@ -13,5 +21,10 @@ function main() {
Self.ShowLayer(0); Self.ShowLayer(0);
timer = 0; timer = 0;
}, 200); }, 200);
});
Events.OnLeave(function(e) {
console.log("%s has stopped touching %s", e, Self.Doodad.Title)
Self.Canvas.SetBackground(RGBA(0, 0, 1, 0));
}) })
} }

View File

@ -1,7 +1,19 @@
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); 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); Self.ShowLayer(1);
}) pressed = true;
});
} }

View File

@ -1,16 +1,29 @@
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); console.log("%s initialized!", Self.Doodad.Title);
var err = Self.AddAnimation("open", 100, [0, 1, 2, 3]); Self.AddAnimation("open", 100, [0, 1, 2, 3]);
console.error("door error: %s", err) Self.AddAnimation("close", 100, [3, 2, 1, 0]);
var animating = false; var animating = false;
var opened = false;
Events.OnCollide(function() { Events.OnCollide(function(e) {
if (animating) { if (animating || opened) {
return; return;
} }
if (e.Overlap.X + e.Overlap.W >= 16 && e.Overlap.X < 48) {
animating = true; animating = true;
Self.PlayAnimation("open", null); Self.PlayAnimation("open", function() {
opened = true;
animating = false;
}); });
}
});
Events.OnLeave(function() {
if (opened) {
Self.PlayAnimation("close", function() {
opened = false;
});
}
})
} }

View File

@ -3,6 +3,18 @@ function main() {
var unlocked = false; var unlocked = false;
Events.OnCollide(function(e) { 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) { if (unlocked) {
return; return;
} }
@ -10,4 +22,8 @@ function main() {
unlocked = true; unlocked = true;
Self.PlayAnimation("open", null); 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));
})
} }

View File

@ -4,22 +4,27 @@ function main() {
var timer = 0; var timer = 0;
var animationSpeed = 100; var animationSpeed = 100;
var animating = false; var opened = false;
Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]); Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]);
Self.AddAnimation("close", animationSpeed, ["down4", "down3", "down2", "down1"]); Self.AddAnimation("close", animationSpeed, ["down4", "down3", "down2", "down1"]);
Events.OnCollide( function() { Events.OnCollide( function(e) {
if (animating) { if (opened) {
return; 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() { Self.PlayAnimation("open", function() {
setTimeout(function() { });
});
Events.OnLeave(function() {
Self.PlayAnimation("close", function() { Self.PlayAnimation("close", function() {
animating = false; opened = false;
}); });
}, 3000);
}) })
});
} }

View File

@ -10,6 +10,9 @@ import (
func TestActorCollision(t *testing.T) { func TestActorCollision(t *testing.T) {
boxes := []render.Rect{ boxes := []render.Rect{
// 0: intersects with 1 // 0: intersects with 1
// Expected intersection rect would be
// X,Y = 90,10
// X2,Y2 = 100,99
render.Rect{ render.Rect{
X: 0, X: 0,
Y: 0, Y: 0,
@ -18,6 +21,9 @@ func TestActorCollision(t *testing.T) {
}, },
// 1: intersects with 0 // 1: intersects with 0
// Expected intersection rect would be
// X,Y = 90,10
// X2,Y2 = 100,99
render.Rect{ render.Rect{
X: 90, X: 90,
Y: 10, Y: 10,
@ -34,6 +40,9 @@ func TestActorCollision(t *testing.T) {
}, },
// 3: intersects with 4 // 3: intersects with 4
// Expected intersection rect would be
// X,Y = 240,200
// X2,Y2 = 264,231
render.Rect{ render.Rect{
X: 233, X: 233,
Y: 200, Y: 200,
@ -70,22 +79,22 @@ func TestActorCollision(t *testing.T) {
}, },
} }
assert := func(i int, result collision.IndexTuple, expectA, expectB int) { assert := func(i int, result collision.BoxCollision, expectA, expectB int) {
if result[0] != expectA || result[1] != expectB { if result.A != expectA || result.B != expectB {
t.Errorf( t.Errorf(
"unexpected collision at index %d of BetweenBoxes() generator\n"+ "unexpected collision at index %d of BetweenBoxes() generator\n"+
"expected: (%d,%d)\n"+ "expected: (%d,%d)\n"+
" but got: (%d,%d)", " but got: (%d,%d)",
i, i,
expectA, expectB, expectA, expectB,
result[0], result[1], result.A, result.B,
) )
} }
} }
var i int var i int
for overlap := range collision.BetweenBoxes(boxes) { for overlap := range collision.BetweenBoxes(boxes) {
a, b := overlap[0], overlap[1] a, b := overlap.A, overlap.B
// Ensure expected collisions happened. // Ensure expected collisions happened.
switch i { switch i {

View File

@ -1,9 +1,21 @@
package collision package collision
import ( import (
"math"
"git.kirsle.net/apps/doodle/lib/render" "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. // IndexTuple holds two integers used as array indexes.
type IndexTuple [2]int type IndexTuple [2]int
@ -12,15 +24,30 @@ type IndexTuple [2]int
// //
// This returns a generator that spits out indexes of the // This returns a generator that spits out indexes of the
// intersecting boxes. // intersecting boxes.
func BetweenBoxes(boxes []render.Rect) chan IndexTuple { func BetweenBoxes(boxes []render.Rect) chan BoxCollision {
generator := make(chan IndexTuple) generator := make(chan BoxCollision)
go func() { go func() {
// Outer loop: test each box for intersection with the others. // Outer loop: test each box for intersection with the others.
for i, box := range boxes { for i, box := range boxes {
for j := i + 1; j < len(boxes); j++ { for j := i + 1; j < len(boxes); j++ {
if box.Intersects(boxes[j]) { other := boxes[j]
generator <- IndexTuple{i, 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 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))
}

View File

@ -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. // Check all four edges of the box in parallel on different CPU cores.
type jobSide struct { type jobSide struct {
p1 render.Point p1 render.Point // p2 is perpendicular to p1 along a straight edge
p2 render.Point p2 render.Point // of the collision box.
side Side 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.Top[0], col.Top[1], Top},
jobSide{col.Bottom[0], col.Bottom[1], Bottom}, jobSide{col.Bottom[0], col.Bottom[1], Bottom},
jobSide{col.Left[0], col.Left[1], Left}, jobSide{col.Left[0], col.Left[1], Left},
jobSide{col.Right[0], col.Right[1], Right}, jobSide{col.Right[0], col.Right[1], Right},
} }
var wg sync.WaitGroup var wg sync.WaitGroup
for _, job := range jobs { for _, job := range jobs {
wg.Add(1) wg.Add(1)
@ -226,12 +227,6 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
} }
wg.Wait() 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() return c.IsColliding()
} }

View File

@ -1,20 +1,34 @@
package collision 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 // CollisionBox holds all of the coordinate pairs to draw the collision box
// around a doodad. // around a doodad.
type CollisionBox struct { type CollisionBox struct {
Top []render.Point Top [2]render.Point
Bottom []render.Point Bottom [2]render.Point
Left []render.Point Left [2]render.Point
Right []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. // GetCollisionBox returns a CollisionBox with the four coordinates.
func GetCollisionBox(box render.Rect) CollisionBox { func GetCollisionBox(box render.Rect) CollisionBox {
return CollisionBox{ return CollisionBox{
Top: []render.Point{ Top: [2]render.Point{
{ {
X: box.X, X: box.X,
Y: box.Y, Y: box.Y,
@ -24,7 +38,7 @@ func GetCollisionBox(box render.Rect) CollisionBox {
Y: box.Y, Y: box.Y,
}, },
}, },
Bottom: []render.Point{ Bottom: [2]render.Point{
{ {
X: box.X, X: box.X,
Y: box.Y + box.H, Y: box.Y + box.H,
@ -34,7 +48,7 @@ func GetCollisionBox(box render.Rect) CollisionBox {
Y: box.Y + box.H, Y: box.Y + box.H,
}, },
}, },
Left: []render.Point{ Left: [2]render.Point{
{ {
X: box.X, X: box.X,
Y: box.Y + box.H - 1, Y: box.Y + box.H - 1,
@ -44,7 +58,7 @@ func GetCollisionBox(box render.Rect) CollisionBox {
Y: box.Y + 1, Y: box.Y + 1,
}, },
}, },
Right: []render.Point{ Right: [2]render.Point{
{ {
X: box.X + box.W, X: box.X + box.W,
Y: box.Y + box.H - 1, 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)
}

View File

@ -1,6 +1,8 @@
package scripting package scripting
import ( import (
"errors"
"git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/events"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
) )
@ -15,6 +17,11 @@ const (
KeypressEvent = "OnKeypress" // i.e. arrow keys KeypressEvent = "OnKeypress" // i.e. arrow keys
) )
// Event return errors.
var (
ErrReturnFalse = errors.New("JS callback function returned false")
)
// Events API for Doodad scripts. // Events API for Doodad scripts.
type Events struct { type Events struct {
registry map[string][]otto.Value registry map[string][]otto.Value
@ -33,8 +40,18 @@ func (e *Events) OnCollide(call otto.FunctionCall) otto.Value {
} }
// RunCollide invokes the OnCollide handler function. // RunCollide invokes the OnCollide handler function.
func (e *Events) RunCollide() error { func (e *Events) RunCollide(v interface{}) error {
return e.run(CollideEvent) 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. // 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] { for _, callback := range e.registry[name] {
_, err := callback.Call(otto.Value{}, args...) value, err := callback.Call(otto.Value{}, args...)
if err != nil { if err != nil {
return err 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 return nil

View File

@ -73,6 +73,11 @@ func (a *Actor) SetGravity(v bool) {
a.hasGravity = v 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. // LayerCount returns the number of layers in this actor's drawing.
func (a *Actor) LayerCount() int { func (a *Actor) LayerCount() int {
return len(a.Doodad.Layers) return len(a.Doodad.Layers)

9
pkg/uix/actor_events.go Normal file
View File

@ -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
}

View File

@ -53,6 +53,9 @@ type Canvas struct {
actor *Actor // if this canvas IS an actor actor *Actor // if this canvas IS an actor
actors []*Actor // if this canvas CONTAINS actors (i.e., is a level) 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. // Doodad scripting engine supervisor.
// NOTE: initialized and managed by the play_scene. // NOTE: initialized and managed by the play_scene.
scripting *scripting.Supervisor scripting *scripting.Supervisor
@ -248,24 +251,34 @@ func (w *Canvas) Loop(ev *events.State) error {
} }
// Check collisions between actors. // Check collisions between actors.
var collidingActors = map[string]string{}
for tuple := range collision.BetweenBoxes(boxes) { for tuple := range collision.BetweenBoxes(boxes) {
log.Debug("Actor %s collides with %s", a, b := w.actors[tuple.A], w.actors[tuple.B]
w.actors[tuple[0]].ID(),
w.actors[tuple[1]].ID(), collidingActors[a.ID()] = b.ID()
)
a, b := w.actors[tuple[0]], w.actors[tuple[1]]
// Call the OnCollide handler. // Call the OnCollide handler.
if w.scripting != nil { if w.scripting != nil {
if err := w.scripting.To(a.ID()).Events.RunCollide(); err != nil { // Tell actor A about the collision with B.
log.Error(err.Error()) if err := w.scripting.To(a.ID()).Events.RunCollide(&CollideEvent{
} Actor: b,
if err := w.scripting.To(b.ID()).Events.RunCollide(); err != nil { Overlap: tuple.Overlap,
}); err != nil {
log.Error(err.Error()) 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 the canvas is editable, only care if it's over our space.
if w.Editable { if w.Editable {
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now) cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)