Return False: Solid Collision Between Actors
* Implement the handler code for `return false` when actors are colliding with each other and wish to act like solid walls. * The locked doors will `return false` when they're closed and the colliding actor does not have the matching key. * Add arbitrary key/value storage to Actors. The colored keys will set an actor value "key:%TITLE%" on the one who touched the key before destroying itself. The colored doors check that key when touched to decide whether to open. * The trapdoor now only opens if you're touching it from the top (your overlap box Y value is 0), but if you touch it from below and the door is closed, it acts like a solid object.
This commit is contained in:
parent
a2e1bd1ccb
commit
1523deeb9c
|
@ -10,6 +10,7 @@ function main() {
|
|||
var animFrame = animStart;
|
||||
|
||||
Self.SetGravity(true);
|
||||
Self.SetHitbox(7, 4, 17, 28);
|
||||
Self.AddAnimation("walk-left", 100, ["blu-wl1", "blu-wl2", "blu-wl3", "blu-wl4"]);
|
||||
Self.AddAnimation("walk-right", 100, ["blu-wr1", "blu-wr2", "blu-wr3", "blu-wr4"]);
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
function main() {
|
||||
Events.OnCollide(function(e) {
|
||||
console.log("%s picked up by %s", Self.Doodad.Title, e.Actor.Title);
|
||||
e.Actor.SetData("key:" + Self.Doodad.Title, "true");
|
||||
Self.Destroy();
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,25 +2,33 @@ function main() {
|
|||
Self.AddAnimation("open", 0, [1]);
|
||||
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;
|
||||
// Map our door names to key names.
|
||||
var KeyMap = {
|
||||
"Blue Door": "Blue Key",
|
||||
"Red Door": "Red Key",
|
||||
"Green Door": "Green Key",
|
||||
"Yellow Door": "Yellow Key"
|
||||
}
|
||||
|
||||
log.Warn("%s loaded!", Self.Doodad.Title);
|
||||
console.log("%s Setting hitbox", Self.Doodad.Title);
|
||||
Self.SetHitbox(16, 0, 32, 64);
|
||||
|
||||
Events.OnCollide(function(e) {
|
||||
if (unlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.InHitbox) {
|
||||
var data = e.Actor.GetData("key:" + KeyMap[Self.Doodad.Title]);
|
||||
if (data === "") {
|
||||
// Door is locked.
|
||||
return false;
|
||||
}
|
||||
|
||||
unlocked = true;
|
||||
Self.PlayAnimation("open", null);
|
||||
}
|
||||
});
|
||||
Events.OnLeave(function(e) {
|
||||
console.log("%s has stopped touching %s", e, Self.Doodad.Title)
|
||||
|
|
|
@ -3,6 +3,8 @@ function main() {
|
|||
|
||||
var timer = 0;
|
||||
|
||||
Self.SetHitbox(0, 0, 72, 9);
|
||||
|
||||
var animationSpeed = 100;
|
||||
var opened = false;
|
||||
Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]);
|
||||
|
@ -13,18 +15,24 @@ function main() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Not touching the top of the door means door doesn't open.
|
||||
if (e.Overlap.Y > 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the actor colliding our solid part?
|
||||
if (e.InHitbox) {
|
||||
// Touching the top or the bottom?
|
||||
if (e.Overlap.Y > 0) {
|
||||
return false; // solid wall when touched from below
|
||||
} else {
|
||||
opened = true;
|
||||
Self.PlayAnimation("open", function() {
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.OnLeave(function() {
|
||||
if (opened) {
|
||||
Self.PlayAnimation("close", function() {
|
||||
opened = false;
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -102,3 +102,10 @@ func PrintCallers() {
|
|||
fmt.Printf("%d: %s\n", i, caller)
|
||||
}
|
||||
}
|
||||
|
||||
// Pause until the user hits enter in the console.
|
||||
func Pause() {
|
||||
var x string
|
||||
fmt.Print("Press enter to continue . . .")
|
||||
fmt.Scanf("%s", &x)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,16 @@ func TestIntersection(t *testing.T) {
|
|||
B: newRect(0, -240, 874, 490),
|
||||
Expect: false, // XXX: must be true
|
||||
},
|
||||
{
|
||||
A: newRect(0, 30, 9, 62),
|
||||
B: newRect(16, 0, 32, 64),
|
||||
Expect: false,
|
||||
},
|
||||
{
|
||||
A: newRect(0, 30, 11, 62),
|
||||
B: newRect(7, 4, 17, 28),
|
||||
Expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package collision
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
|
@ -32,22 +33,11 @@ func BetweenBoxes(boxes []render.Rect) chan BoxCollision {
|
|||
for i, box := range boxes {
|
||||
for j := i + 1; j < len(boxes); 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,
|
||||
},
|
||||
}
|
||||
collision, err := CompareBoxes(box, other)
|
||||
if err == nil {
|
||||
collision.A = i
|
||||
collision.B = j
|
||||
generator <- collision
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +48,28 @@ func BetweenBoxes(boxes []render.Rect) chan BoxCollision {
|
|||
return generator
|
||||
}
|
||||
|
||||
// CompareBoxes checks if two boxes overlaps and returns information about
|
||||
// the overlap. The boxes are bounding rectangles like those given to
|
||||
// BetweenBoxes().
|
||||
func CompareBoxes(box, other render.Rect) (BoxCollision, error) {
|
||||
if box.Intersects(other) {
|
||||
var (
|
||||
overlap = OverlapRelative(box, other)
|
||||
topLeft = overlap.TopLeft()
|
||||
bottomRight = overlap.BottomRight()
|
||||
)
|
||||
return BoxCollision{
|
||||
Overlap: render.Rect{
|
||||
X: topLeft.X,
|
||||
Y: topLeft.Y,
|
||||
W: bottomRight.X,
|
||||
H: bottomRight.Y,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return BoxCollision{}, errors.New("boxes do not intersect")
|
||||
}
|
||||
|
||||
/*
|
||||
OverlapRelative returns the Overlap box using coordinates relative
|
||||
to the source rect instead of absolute coordinates.
|
||||
|
|
|
@ -16,6 +16,10 @@ type Actor interface {
|
|||
Grounded() bool
|
||||
SetGrounded(bool)
|
||||
|
||||
// Actor's elected hitbox set by their script.
|
||||
SetHitbox(x, y, w, h int)
|
||||
Hitbox() render.Rect
|
||||
|
||||
// Movement commands.
|
||||
MoveBy(render.Point) // Add {X,Y} to current Position.
|
||||
MoveTo(render.Point) // Set current Position to {X,Y}.
|
||||
|
|
|
@ -13,6 +13,7 @@ type Doodad struct {
|
|||
Palette *level.Palette `json:"palette"`
|
||||
Script string `json:"script"`
|
||||
Layers []Layer `json:"layers"`
|
||||
Tags map[string]string `json:"data"` // arbitrary key/value data storage
|
||||
}
|
||||
|
||||
// Layer holds a layer of drawing data for a Doodad.
|
||||
|
@ -38,6 +39,7 @@ func New(size int) *Doodad {
|
|||
Chunker: level.NewChunker(size),
|
||||
},
|
||||
},
|
||||
Tags: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ type Drawing struct {
|
|||
velocity render.Point
|
||||
accel int
|
||||
size render.Rect
|
||||
hitbox render.Rect
|
||||
grounded bool
|
||||
}
|
||||
|
||||
|
@ -75,6 +76,21 @@ func (d *Drawing) SetGrounded(v bool) {
|
|||
d.grounded = v
|
||||
}
|
||||
|
||||
// SetHitbox sets the actor's elected hitbox.
|
||||
func (d *Drawing) SetHitbox(x, y, w, h int) {
|
||||
d.hitbox = render.Rect{
|
||||
X: int32(x),
|
||||
Y: int32(y),
|
||||
W: int32(w),
|
||||
H: int32(h),
|
||||
}
|
||||
}
|
||||
|
||||
// Hitbox returns the actor's elected hitbox.
|
||||
func (d *Drawing) Hitbox() render.Rect {
|
||||
return d.hitbox
|
||||
}
|
||||
|
||||
// MoveBy a relative value.
|
||||
func (d *Drawing) MoveBy(by render.Point) {
|
||||
d.point.Add(by)
|
||||
|
|
|
@ -29,6 +29,8 @@ type Actor struct {
|
|||
|
||||
// Actor runtime variables.
|
||||
hasGravity bool
|
||||
hitbox render.Rect
|
||||
data map[string]string
|
||||
|
||||
// Animation variables.
|
||||
animations map[string]*Animation
|
||||
|
@ -78,6 +80,41 @@ func (a *Actor) GetBoundingRect() render.Rect {
|
|||
return doodads.GetBoundingRect(a)
|
||||
}
|
||||
|
||||
// SetHitbox sets the actor's elected hitbox.
|
||||
func (a *Actor) SetHitbox(x, y, w, h int) {
|
||||
a.hitbox = render.Rect{
|
||||
X: int32(x),
|
||||
Y: int32(y),
|
||||
W: int32(w),
|
||||
H: int32(h),
|
||||
}
|
||||
}
|
||||
|
||||
// Hitbox returns the actor's elected hitbox.
|
||||
func (a *Actor) Hitbox() render.Rect {
|
||||
return a.hitbox
|
||||
}
|
||||
|
||||
// SetData sets an arbitrary field in the actor's K/V storage.
|
||||
func (a *Actor) SetData(key, value string) {
|
||||
if a.data == nil {
|
||||
a.data = map[string]string{}
|
||||
}
|
||||
a.data[key] = value
|
||||
}
|
||||
|
||||
// GetData gets an arbitrary field from the actor's K/V storage.
|
||||
// Missing keys just return a blank string (friendly to the JavaScript
|
||||
// environment).
|
||||
func (a *Actor) GetData(key string) string {
|
||||
if a.data == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
v, _ := a.data[key]
|
||||
return v
|
||||
}
|
||||
|
||||
// LayerCount returns the number of layers in this actor's drawing.
|
||||
func (a *Actor) LayerCount() int {
|
||||
return len(a.Doodad.Layers)
|
||||
|
|
157
pkg/uix/actor_collision.go
Normal file
157
pkg/uix/actor_collision.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package uix
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/collision"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/scripting"
|
||||
"github.com/kirsle/blog/src/log"
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
// loopActorCollision is the Loop function that checks if pairs of
|
||||
// actors are colliding with each other, and handles their scripting
|
||||
// responses to such collisions.
|
||||
//
|
||||
// boxes: array of Actor bounding box rects.
|
||||
func (w *Canvas) loopActorCollision() error {
|
||||
var (
|
||||
// Current time of this tick so we can advance animations.
|
||||
now = time.Now()
|
||||
|
||||
// As we iterate over all actors below to process their movement, track
|
||||
// their bounding rectangles so we can later see if any pair of actors
|
||||
// intersect each other. Also, in case of actor scripts protesting a
|
||||
// collision later, store each actor's original position before the move.
|
||||
boxes = make([]render.Rect, len(w.actors))
|
||||
originalPositions = map[string]render.Point{}
|
||||
)
|
||||
|
||||
// Loop over all the actors in parallel, processing their movement and
|
||||
// checking collision data against the level geometry.
|
||||
var wg sync.WaitGroup
|
||||
for i, a := range w.actors {
|
||||
wg.Add(1)
|
||||
go func(i int, a *Actor) {
|
||||
defer wg.Done()
|
||||
originalPositions[a.ID()] = a.Position()
|
||||
|
||||
// Advance any animations for this actor.
|
||||
if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) {
|
||||
if done := a.TickAnimation(a.activeAnimation); done {
|
||||
// Animation has finished, run the callback script.
|
||||
if a.animationCallback.IsFunction() {
|
||||
a.animationCallback.Call(otto.NullValue())
|
||||
}
|
||||
|
||||
// Clean up the animation state.
|
||||
a.StopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
// Get the actor's velocity to see if it's moving this tick.
|
||||
v := a.Velocity()
|
||||
if a.hasGravity {
|
||||
v.Y += int32(balance.Gravity)
|
||||
}
|
||||
|
||||
// If not moving, grab the bounding box right now.
|
||||
if v == render.Origin {
|
||||
boxes[i] = doodads.GetBoundingRect(a)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a delta point from their current location to where they
|
||||
// want to move to this tick.
|
||||
delta := a.Position()
|
||||
delta.Add(v)
|
||||
|
||||
// Check collision with level geometry.
|
||||
info, ok := collision.CollidesWithGrid(a, w.chunks, delta)
|
||||
if ok {
|
||||
// Collision happened with world.
|
||||
}
|
||||
delta = info.MoveTo // Move us back where the collision check put us
|
||||
|
||||
// Move the actor's World Position to the new location.
|
||||
a.MoveTo(delta)
|
||||
|
||||
// Keep the actor from leaving the world borders of bounded maps.
|
||||
w.loopContainActorsInsideLevel(a)
|
||||
|
||||
// Store this actor's bounding box after they've moved.
|
||||
boxes[i] = doodads.GetBoundingRect(a)
|
||||
}(i, a)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
var collidingActors = map[string]string{}
|
||||
for tuple := range collision.BetweenBoxes(boxes) {
|
||||
a, b := w.actors[tuple.A], w.actors[tuple.B]
|
||||
collidingActors[a.ID()] = b.ID()
|
||||
|
||||
// Call the OnCollide handler.
|
||||
if w.scripting != nil {
|
||||
// Tell actor A about the collision with B.
|
||||
if err := w.scripting.To(a.ID()).Events.RunCollide(&CollideEvent{
|
||||
Actor: b,
|
||||
Overlap: tuple.Overlap,
|
||||
InHitbox: tuple.Overlap.Intersects(a.Hitbox()),
|
||||
}); err != nil {
|
||||
if err == scripting.ErrReturnFalse {
|
||||
if origPoint, ok := originalPositions[b.ID()]; ok {
|
||||
// Trace a vector back from the actor's current position
|
||||
// to where they originated from and find the earliest
|
||||
// point where they are not violating the hitbox.
|
||||
var (
|
||||
rect = doodads.GetBoundingRect(b)
|
||||
hitbox = a.Hitbox()
|
||||
)
|
||||
for point := range render.IterLine2(
|
||||
b.Position(),
|
||||
origPoint,
|
||||
) {
|
||||
test := render.Rect{
|
||||
X: point.X,
|
||||
Y: point.Y,
|
||||
W: rect.W,
|
||||
H: rect.H,
|
||||
}
|
||||
info, err := collision.CompareBoxes(
|
||||
boxes[tuple.A],
|
||||
test,
|
||||
)
|
||||
if err != nil || !info.Overlap.Intersects(hitbox) {
|
||||
b.MoveTo(point)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Error(
|
||||
"ERROR: Actors %s and %s overlap and the script returned false,"+
|
||||
"but I didn't store %s original position earlier??",
|
||||
a.Doodad.Title, b.Doodad.Title, b.Doodad.Title,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
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
|
||||
return nil
|
||||
}
|
|
@ -6,4 +6,5 @@ import "git.kirsle.net/apps/doodle/lib/render"
|
|||
type CollideEvent struct {
|
||||
Actor *Actor
|
||||
Overlap render.Rect
|
||||
InHitbox bool // If the two elected hitboxes are overlapping
|
||||
}
|
||||
|
|
|
@ -4,20 +4,16 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/events"
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/lib/ui"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/collision"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/scripting"
|
||||
"git.kirsle.net/apps/doodle/pkg/wallpaper"
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
// Canvas is a custom ui.Widget that manages a single drawing.
|
||||
|
@ -178,7 +174,7 @@ func (w *Canvas) Loop(ev *events.State) error {
|
|||
_ = w.loopConstrainScroll()
|
||||
|
||||
// Current time of this loop so we can advance animations.
|
||||
now := time.Now()
|
||||
// now := time.Now()
|
||||
|
||||
// Remove any actors that were destroyed the previous tick.
|
||||
var newActors []*Actor
|
||||
|
@ -192,92 +188,10 @@ func (w *Canvas) Loop(ev *events.State) error {
|
|||
w.actors = newActors
|
||||
}
|
||||
|
||||
// Move any actors. As we iterate over all actors, track their bounding
|
||||
// rectangles so we can later see if any pair of actors intersect each other.
|
||||
boxes := make([]render.Rect, len(w.actors))
|
||||
var wg sync.WaitGroup
|
||||
for i, a := range w.actors {
|
||||
wg.Add(1)
|
||||
go func(i int, a *Actor) {
|
||||
defer wg.Done()
|
||||
|
||||
// Advance any animations for this actor.
|
||||
if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) {
|
||||
if done := a.TickAnimation(a.activeAnimation); done {
|
||||
// Animation has finished, run the callback script.
|
||||
if a.animationCallback.IsFunction() {
|
||||
a.animationCallback.Call(otto.NullValue())
|
||||
}
|
||||
|
||||
// Clean up the animation state.
|
||||
a.StopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
// Get the actor's velocity to see if it's moving this tick.
|
||||
v := a.Velocity()
|
||||
if a.hasGravity {
|
||||
v.Y += int32(balance.Gravity)
|
||||
}
|
||||
|
||||
// If not moving, grab the bounding box right now.
|
||||
if v == render.Origin {
|
||||
boxes[i] = doodads.GetBoundingRect(a)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a delta point from their current location to where they
|
||||
// want to move to this tick.
|
||||
delta := a.Position()
|
||||
delta.Add(v)
|
||||
|
||||
// Check collision with level geometry.
|
||||
info, ok := collision.CollidesWithGrid(a, w.chunks, delta)
|
||||
if ok {
|
||||
// Collision happened with world.
|
||||
}
|
||||
delta = info.MoveTo // Move us back where the collision check put us
|
||||
|
||||
// Move the actor's World Position to the new location.
|
||||
a.MoveTo(delta)
|
||||
|
||||
// Keep the actor from leaving the world borders of bounded maps.
|
||||
w.loopContainActorsInsideLevel(a)
|
||||
|
||||
// Store this actor's bounding box after they've moved.
|
||||
boxes[i] = doodads.GetBoundingRect(a)
|
||||
}(i, a)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Check collisions between actors.
|
||||
var collidingActors = map[string]string{}
|
||||
for tuple := range collision.BetweenBoxes(boxes) {
|
||||
a, b := w.actors[tuple.A], w.actors[tuple.B]
|
||||
|
||||
collidingActors[a.ID()] = b.ID()
|
||||
|
||||
// Call the OnCollide handler.
|
||||
if w.scripting != 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())
|
||||
if err := w.loopActorCollision(); err != nil {
|
||||
log.Error("loopActorCollision: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user