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;
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"]);

View File

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

View File

@ -2,25 +2,33 @@ function main() {
Self.AddAnimation("open", 0, [1]);
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) {
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;
}
unlocked = true;
Self.PlayAnimation("open", null);
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)

View File

@ -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() {
});
}
}
opened = true;
Self.PlayAnimation("open", function() {
});
});
Events.OnLeave(function() {
Self.PlayAnimation("close", function() {
opened = false;
});
if (opened) {
Self.PlayAnimation("close", function() {
opened = false;
});
}
})
}

View File

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

View File

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

View File

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

View File

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

View File

@ -9,10 +9,11 @@ import (
// Doodad is a reusable component for Levels that have scripts and graphics.
type Doodad struct {
level.Base
Filename string `json:"-"` // used internally, not saved in json
Palette *level.Palette `json:"palette"`
Script string `json:"script"`
Layers []Layer `json:"layers"`
Filename string `json:"-"` // used internally, not saved in json
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{},
}
}

View File

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

View File

@ -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
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.
type CollideEvent struct {
Actor *Actor
Overlap render.Rect
Actor *Actor
Overlap render.Rect
InHitbox bool // If the two elected hitboxes are overlapping
}

View File

@ -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,93 +188,11 @@ 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 {
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)