doodle/pkg/uix/actor_collision.go
Noah Petherbridge dc2695cfc9 Add More Trapdoor Doodads
* Add the other trapdoor directions: Left, Right and Up.
* UI: Show a color square in each Palette Swatch button in Edit Mode.
  * Instead of just the label like "solid", "fire", "decoration" it also
    shows a square box colored as the swatch color. The label and box
    are left-aligned in the button.
* Minor Play Mode physics update:
  * The player jump is now limited: they may only continue to move
    upwards for 20 ticks, after which they must touch ground before
    jumping again.
  * Remove the "press Down to move down" button. Only gravity moves you
    down.
* Fix a crash in the Editor Mode when you dragged doodads on top of each
  other. Source of bug was the loopActorCollision() function, which only
  should be useful to Play Mode, and it expected the scripting engine to
  be attached to the Canvas. In EditorMode there is no scripting engine.
2019-07-05 15:02:22 -07:00

161 lines
4.6 KiB
Go

package uix
import (
"errors"
"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/log"
"git.kirsle.net/apps/doodle/pkg/scripting"
"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.
func (w *Canvas) loopActorCollision() error {
if w.scripting == nil {
return errors.New("Canvas.loopActorCollision: scripting engine not attached to Canvas")
}
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
}