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.
physics
Noah 2019-05-28 21:43:30 -07:00
parent a2e1bd1ccb
commit 1523deeb9c
14 changed files with 314 additions and 135 deletions

View File

@ -10,6 +10,7 @@ function main() {
var animFrame = animStart; var animFrame = animStart;
Self.SetGravity(true); 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-left", 100, ["blu-wl1", "blu-wl2", "blu-wl3", "blu-wl4"]);
Self.AddAnimation("walk-right", 100, ["blu-wr1", "blu-wr2", "blu-wr3", "blu-wr4"]); Self.AddAnimation("walk-right", 100, ["blu-wr1", "blu-wr2", "blu-wr3", "blu-wr4"]);

View File

@ -1,5 +1,7 @@
function main() { function main() {
Events.OnCollide(function(e) { 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(); Self.Destroy();
}) })
} }

View File

@ -2,25 +2,33 @@ function main() {
Self.AddAnimation("open", 0, [1]); Self.AddAnimation("open", 0, [1]);
var unlocked = false; var unlocked = false;
// 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) { 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;
} }
unlocked = true; if (e.InHitbox) {
Self.PlayAnimation("open", null); 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) { Events.OnLeave(function(e) {
console.log("%s has stopped touching %s", e, Self.Doodad.Title) console.log("%s has stopped touching %s", e, Self.Doodad.Title)

View File

@ -3,6 +3,8 @@ function main() {
var timer = 0; var timer = 0;
Self.SetHitbox(0, 0, 72, 9);
var animationSpeed = 100; var animationSpeed = 100;
var opened = false; var opened = false;
Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]); Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]);
@ -13,18 +15,24 @@ function main() {
return; return;
} }
// Not touching the top of the door means door doesn't open. // Is the actor colliding our solid part?
if (e.Overlap.Y > 9) { if (e.InHitbox) {
return; // 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() {
});
}
} }
opened = true;
Self.PlayAnimation("open", function() {
});
}); });
Events.OnLeave(function() { Events.OnLeave(function() {
Self.PlayAnimation("close", function() { if (opened) {
opened = false; Self.PlayAnimation("close", function() {
}); opened = false;
});
}
}) })
} }

View File

@ -102,3 +102,10 @@ func PrintCallers() {
fmt.Printf("%d: %s\n", i, caller) 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)
}

View File

@ -54,6 +54,16 @@ func TestIntersection(t *testing.T) {
B: newRect(0, -240, 874, 490), B: newRect(0, -240, 874, 490),
Expect: false, // XXX: must be true 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 { for _, test := range tests {

View File

@ -1,6 +1,7 @@
package collision package collision
import ( import (
"errors"
"math" "math"
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
@ -32,22 +33,11 @@ func BetweenBoxes(boxes []render.Rect) chan BoxCollision {
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++ {
other := boxes[j] other := boxes[j]
if box.Intersects(other) { collision, err := CompareBoxes(box, other)
var ( if err == nil {
overlap = OverlapRelative(box, other) collision.A = i
topLeft = overlap.TopLeft() collision.B = j
bottomRight = overlap.BottomRight() generator <- collision
)
generator <- BoxCollision{
A: i,
B: j,
Overlap: render.Rect{
X: topLeft.X,
Y: topLeft.Y,
W: bottomRight.X,
H: bottomRight.Y,
},
}
} }
} }
} }
@ -58,6 +48,28 @@ func BetweenBoxes(boxes []render.Rect) chan BoxCollision {
return generator 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 OverlapRelative returns the Overlap box using coordinates relative
to the source rect instead of absolute coordinates. to the source rect instead of absolute coordinates.

View File

@ -16,6 +16,10 @@ type Actor interface {
Grounded() bool Grounded() bool
SetGrounded(bool) SetGrounded(bool)
// Actor's elected hitbox set by their script.
SetHitbox(x, y, w, h int)
Hitbox() render.Rect
// Movement commands. // Movement commands.
MoveBy(render.Point) // Add {X,Y} to current Position. MoveBy(render.Point) // Add {X,Y} to current Position.
MoveTo(render.Point) // Set current Position to {X,Y}. MoveTo(render.Point) // Set current Position to {X,Y}.

View File

@ -9,10 +9,11 @@ import (
// Doodad is a reusable component for Levels that have scripts and graphics. // Doodad is a reusable component for Levels that have scripts and graphics.
type Doodad struct { type Doodad struct {
level.Base level.Base
Filename string `json:"-"` // used internally, not saved in json Filename string `json:"-"` // used internally, not saved in json
Palette *level.Palette `json:"palette"` Palette *level.Palette `json:"palette"`
Script string `json:"script"` Script string `json:"script"`
Layers []Layer `json:"layers"` 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. // Layer holds a layer of drawing data for a Doodad.
@ -38,6 +39,7 @@ func New(size int) *Doodad {
Chunker: level.NewChunker(size), Chunker: level.NewChunker(size),
}, },
}, },
Tags: map[string]string{},
} }
} }

View File

@ -14,6 +14,7 @@ type Drawing struct {
velocity render.Point velocity render.Point
accel int accel int
size render.Rect size render.Rect
hitbox render.Rect
grounded bool grounded bool
} }
@ -75,6 +76,21 @@ func (d *Drawing) SetGrounded(v bool) {
d.grounded = v 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. // MoveBy a relative value.
func (d *Drawing) MoveBy(by render.Point) { func (d *Drawing) MoveBy(by render.Point) {
d.point.Add(by) d.point.Add(by)

View File

@ -29,6 +29,8 @@ type Actor struct {
// Actor runtime variables. // Actor runtime variables.
hasGravity bool hasGravity bool
hitbox render.Rect
data map[string]string
// Animation variables. // Animation variables.
animations map[string]*Animation animations map[string]*Animation
@ -78,6 +80,41 @@ func (a *Actor) GetBoundingRect() render.Rect {
return doodads.GetBoundingRect(a) 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. // 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)

157
pkg/uix/actor_collision.go Normal file
View 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
}

View File

@ -4,6 +4,7 @@ import "git.kirsle.net/apps/doodle/lib/render"
// CollideEvent holds data sent to an actor's Collide handler. // CollideEvent holds data sent to an actor's Collide handler.
type CollideEvent struct { type CollideEvent struct {
Actor *Actor Actor *Actor
Overlap render.Rect Overlap render.Rect
InHitbox bool // If the two elected hitboxes are overlapping
} }

View File

@ -4,20 +4,16 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"sync"
"time"
"git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/pkg/balance" "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/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/wallpaper" "git.kirsle.net/apps/doodle/pkg/wallpaper"
"github.com/robertkrimen/otto"
) )
// Canvas is a custom ui.Widget that manages a single drawing. // 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() _ = w.loopConstrainScroll()
// Current time of this loop so we can advance animations. // 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. // Remove any actors that were destroyed the previous tick.
var newActors []*Actor var newActors []*Actor
@ -192,93 +188,11 @@ func (w *Canvas) Loop(ev *events.State) error {
w.actors = newActors 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. // Check collisions between actors.
var collidingActors = map[string]string{} if err := w.loopActorCollision(); err != nil {
for tuple := range collision.BetweenBoxes(boxes) { log.Error("loopActorCollision: %s", err)
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())
}
}
} }
// 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)