doodle/pkg/uix/actor_collision.go

164 lines
4.7 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.
if w.OnLevelCollision != nil {
w.OnLevelCollision(a, info)
}
}
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.IterLine(
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
}