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:
parent
61af068b80
commit
a2e1bd1ccb
|
@ -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));
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
9
pkg/uix/actor_events.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user