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.
physics
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;
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));
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
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()
}

View File

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

View File

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

View File

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

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
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)