Skip to content

Instantly share code, notes, and snippets.

@ralt
Forked from BonsaiDen/DynamicBox.js
Last active December 10, 2015 06:18
Show Gist options
  • Save ralt/814119f0e2807d25649c to your computer and use it in GitHub Desktop.
Save ralt/814119f0e2807d25649c to your computer and use it in GitHub Desktop.
/*global graphics*/
var Class = require('Class').Class,
StaticBox = require('StaticBox').StaticBox;
// Dynamic AABB Box -----------------------------------------------------------
// ----------------------------------------------------------------------------
exports.DynamicBox = Class(function(x, y, w, h) {
StaticBox(this, x, y, w, h);
// Collision information
this.contact = {
surface: {
up: null,
right: null,
down: null,
left: null,
area: {
up: 0,
right: 0,
down: 0,
left: 0
},
previous: {
up: null,
right: null,
down: null,
left: null
}
},
area: {
up: 0,
right: 0,
down: 0,
left: 0
}
};
this.impact = {
surface: {
up: null,
right: null,
down: null,
left: null
}
};
this.inside = {
surface: null,
area: null
};
}, StaticBox, {
getType: function(type) {
return 'DynamicBox';
},
// Public API -------------------------------------------------------------
update: function(dt) {
this.updatePosition(dt);
this.updateBounds();
},
render: function(debug) {
var x = this.pos.x,
y = this.pos.y,
sx = this.size.x,
sy = this.size.y,
cx = x + sx / 2,
cy = y + sy / 2,
vx = this.vel.x,
vy = this.vel.y,
vl = Math.sqrt(vx * vx + vy * vy) || 1;
graphics.setColor(255, 255, 0);
graphics.rect(x, y, sx, sy);
graphics.setColor(0, 0, 255);
graphics.line(cx, cy, cx + vx / vl * 10, cy + vy / vl * 10);
graphics.setColor(0, 0, 255);
if (this.contact.surface.down) {
graphics.line(x, y + sy - 1, x + sx, y + sy - 1);
}
if (this.contact.surface.up) {
graphics.line(x, y, x + sx, y);
}
if (this.contact.surface.left) {
graphics.line(x, y, x, y + sy);
}
if (this.contact.surface.right) {
graphics.line(x + sx - 1, y, x + sx - 1, y + sy);
}
},
// Default behavior -------------------------------------------------------
updatePosition: function(dt) {
this.pos.x = this.pos.x + this.vel.x;
this.pos.y = this.pos.y + this.vel.y;
},
setPosition: function(x, y) {
this.pos.x = Math.round(x);
this.pos.y = Math.round(y);
this.vel.x = 0;
this.vel.y = 0;
this.updateBounds();
},
onCollision: function(other, vel, normal) {
this.vel.x = this.vel.x + vel.x;
this.vel.y = this.vel.y + vel.y;
if (this.contact.surface.down && !this.contact.surface.previous.down) {
this.impact.surface.down = other;
}
if (this.contact.surface.up && !this.contact.surface.previous.up) {
this.impact.surface.up = other;
}
if (this.contact.surface.left && !this.contact.surface.previous.left) {
this.impact.surface.left = other;
}
if (this.contact.surface.right && !this.contact.surface.previous.right) {
this.impact.surface.right = other;
}
},
// Reset the collision state
reset: function(dt) {
this.contact.surface.previous.up = this.contact.surface.up;
this.contact.surface.previous.down = this.contact.surface.down;
this.contact.surface.previous.right = this.contact.surface.right;
this.contact.surface.previous.left = this.contact.surface.left;
this.contact.surface.area.up = 0;
this.contact.surface.area.down = 0;
this.contact.surface.area.right = 0;
this.contact.surface.area.left = 0;
this.contact.surface.up = null;
this.contact.surface.down = null;
this.contact.surface.right = null;
this.contact.surface.left = null;
this.contact.area.up = 0;
this.contact.area.down = 0;
this.contact.area.right = 0;
this.contact.area.left = 0;
this.impact.surface.up = null;
this.impact.surface.down = null;
this.impact.surface.right = null;
this.impact.surface.left = null;
this.inside.surface = null;
this.inside.area = 0;
},
// Highly complicated sweeping logic --------------------------------------
// ------------------------------------------------------------------------
sweep: function(other, otherVel) {
var a = this,
b = other,
v = {
x: a.vel.x - b.vel.x,
y: a.vel.y - b.vel.y
};
if (otherVel) {
v.x = v.x + otherVel.x;
v.y = v.y + otherVel.y;
}
var hitTime = 0,
outTime = 1,
outVel = {
x: 0,
y: 0
},
overlapsTime = {
x: 0,
y: 0
};
// invert v, since we're treating b as stationary here
v.x = -v.x;
v.y = -v.y;
var xAxisOverlap = xOverlap(v, b, a, outTime, overlapsTime, hitTime);
hitTime = xAxisOverlap[0];
outTime = xAxisOverlap[1];
if (hitTime > outTime) {
return false;
}
// Y axis overlap
if (v.y < 0) {
if (b.max.y < a.min.y) {
return false;
}
if (b.max.y > a.min.y) {
outTime = Math.min((a.min.y - b.max.y) / v.y, outTime);
}
if (a.max.y < b.min.y) {
overlapsTime.y = (a.max.y - b.min.y) / v.y;
hitTime = Math.max(overlapsTime.y, hitTime);
}
} else if (v.y > 0) {
if (b.min.y > a.max.y) {
return false;
}
if (a.max.y > b.min.y) {
outTime = Math.min((a.max.y - b.min.y) / v.y, outTime);
}
if (b.max.y < a.min.y) {
overlapsTime.y = (a.min.y - b.max.y) / v.y;
hitTime = Math.max(overlapsTime.y, hitTime);
}
}
if (hitTime > outTime) {
return false;
}
// the correction to the current velocity
outVel.x = (v.x - (v.x * hitTime));
outVel.y = (v.y - (v.y * hitTime));
// IMPORTANT
// since hitTime defaults to 0, everything that's
// somehwere along an axis will eventually collide at "0"
// thus, we bail out in case of <= 0 to prevent "ghosting" collisions
// BUT: In order to stop at objects, we need to NOT bail out in the case that we actually overlap
if ((hitTime <= 0 && !a.overlaps(b)) || hitTime > 1 || hitTime > outTime) {
return false;
}
// allow for sliding on surfaces
if (a.max.y - b.min.y === 0 || a.min.y - b.max.y === 0) {
outVel.x = 0;
}
if (a.max.x - b.min.x === 0 || a.min.x - b.max.x === 0) {
outVel.y = 0;
}
// TODO cleanup below with some teneries
var hitNormal = {
x: (outVel.x < 0) && 1 || ((outVel.x > 0) && -1 || 0),
y: (outVel.y < 0) && 1 || ((outVel.y > 0) && -1 || 0)
};
// allow us to get away if (we're in contact but are moving into the opposite direction)
if (hitTime === 0) {
// return false in case we're trying to move away
if (v.x > 0 && a.max.x <= b.min.x) {
return false;
} else if (v.x < 0 && a.min.x >= b.max.x) {
return false;
}
if (v.y > 0 && a.max.y <= b.min.y) {
return false;
} else if (v.y < 0 && a.min.y >= b.max.y) {
return false;
}
// otherwise set the contact surface
var area = 0;
if (a.min.x < b.max.x && a.max.x > b.min.x) {
area = 0;
if (a.min.x >= b.min.x && a.max.x <= b.max.x) {
area = a.size.x;
} else if (a.min.x < b.min.x) {
area = a.max.x - b.min.x;
} else if (a.max.x > b.max.x) {
area = b.max.x - a.min.x;
}
area = Math.min(Math.min(a.size.x, b.size.x), area);
if (a.max.y === b.min.y && b._col.up) {
if (area > this.contact.surface.area.down) {
this.contact.surface.down = b;
this.contact.surface.area.down = area;
}
this.contact.area.down = this.contact.area.down + area;
} else if (a.min.y === b.max.y && b._col.down) {
if (area > this.contact.surface.area.up) {
this.contact.surface.up = b;
this.contact.surface.area.up = area;
}
this.contact.area.up = this.contact.area.up + area;
}
}
if (a.min.y < b.max.y && a.max.y > b.min.y) {
area = 0;
if (a.min.y >= b.min.y && a.max.y <= b.max.y) {
area = a.size.y;
} else if (a.min.y < b.min.y) {
area = a.max.y - b.min.y;
} else if (a.max.y > b.max.y) {
area = b.max.y - a.min.y;
}
area = Math.min(Math.min(a.size.y, b.size.y), area);
if (a.max.x === b.min.x && b._col.right) {
if (area > this.contact.surface.area.right) {
this.contact.surface.right = b;
this.contact.surface.area.right = area;
}
this.contact.area.right = this.contact.area.right + area;
} else if (a.min.x === b.max.x && b._col.left) {
if (area > this.contact.surface.area.left) {
this.contact.surface.left = b;
this.contact.surface.area.left = area;
}
this.contact.area.left = this.contact.area.left + area;
}
}
// in case we're stuck make sure push us out
var pushVel = {
x: 0,
y: 0
};
var acy = a.min.y + a.size.y / 2,
bcy = b.min.y + b.size.y / 2,
acx = a.min.x + a.size.x / 2,
bcx = b.min.x + b.size.x / 2;
if (Math.abs(acx - bcx) < (a.size.x + b.size.x) / 2 && Math.abs(acy - bcy) < (a.size.y + b.size.y) / 2) {
if (acy < bcy) {
pushVel.y = b.min.y - a.max.y;
} else {
pushVel.y = b.max.y - a.min.y;
}
if (acx < bcx) {
pushVel.x = b.min.x - a.max.x;
} else {
pushVel.x = b.max.x - a.min.x;
}
// handle non-blocking edges
if (pushVel.x < 0 && !b._col.right && v.x >= 0) {
pushVel.x = 0;
} else if (pushVel.x > 0 && !b._col.left && v.x <= 0) {
pushVel.x = 0;
}
if (pushVel.y < 0 && !b._col.down && v.y >= 0) {
pushVel.y = 0;
} else if (pushVel.y > 0 && !b._col.up && v.y <= 0) {
pushVel.y = 0;
}
// Correct possible floating point issues
if (pushVel.y > 0) {
pushVel.y = Math.max(1, pushVel.y);
} else if (pushVel.y < 0) {
pushVel.y = Math.min(-1, pushVel.y);
}
if (pushVel.x > 0) {
pushVel.x = Math.max(1, pushVel.x);
} else if (pushVel.x < 0) {
pushVel.x = Math.min(-1, pushVel.x);
}
// now choose the minimum / maximum of the push / out values
// to resolve the stuck state in the "best" possible way
if (pushVel.y !== 0 && Math.abs(pushVel.y) < Math.abs(pushVel.x)) {
outVel.y = outVel.y + pushVel.y;
} else if (pushVel.x !== 0 && Math.abs(pushVel.x) < Math.abs(pushVel.y)) {
outVel.x = outVel.x + pushVel.x;
} else {
outVel.y = outVel.y + pushVel.y;
outVel.x = outVel.x + pushVel.x;
}
}
}
// in case the block isn't blocking in specific directions+
// reset the out velocity
// up / down
if (outVel.y > 0 && !b._col.down && v.y >= 0) {
outVel.y = 0;
} else if (!b._col.down && v.y >= 0) {
outVel.y = 0;
} else if (outVel.y < 0 && !b._col.up && v.y <= 0) {
outVel.y = 0;
} else if (!b._col.up && v.y <= 0) {
outVel.y = 0;
}
// left / right
if (outVel.x > 0 && !b._col.right && v.x >= 0) {
outVel.x = 0;
} else if (!b._col.right && v.x >= 0) {
outVel.x = 0;
} else if (outVel.x < 0 && !b._col.left && v.x >= 0) {
outVel.x = 0;
} else if (!b._col.left && v.x <= 0) {
outVel.x = 0;
}
if (a.max.y > b.min.y && !b._col.down) {
outVel.y = 0;
}
// final correction for ghosting artifacts
if (outVel.y !== 0 && !(a.min.x < b.max.x && a.max.x > b.min.x)) {
// only return in case x is also 0, otherwise we'll clip into walls
// in case of diagonal movement
if (outVel.x === 0) {
return false;
} else {
hitNormal.y = 0;
outVel.y = 0;
}
}
if (outVel.x !== 0 && !(a.min.y < b.max.y && a.max.y > b.min.y)) {
// only return in case x is also 0, otherwise we'll clip into floors
// in case of diagonal movement
if (outVel.y === 0) {
return false;
} else {
hitNormal.x = 0;
outVel.x = 0;
}
}
// keep track of the biggest overlapping rectangle
area = a.overlapArea(b);
if (area > a.insideArea) {
a.inside.area = area;
a.inside.surface = b;
}
return {
vel: outVel,
normal: hitNormal
};
}
});
function xOverlap(v, b, a, outTime, overlapsTime, hitTime) {
// X axis overlap
if (v.x < 0) {
if (b.max.x < a.min.x) {
return false;
}
if (b.max.x > a.min.x) {
outTime = Math.min((a.min.x - b.max.x) / v.x, outTime);
}
if (a.max.x < b.min.x) {
overlapsTime.x = (a.max.x - b.min.x) / v.x;
hitTime = Math.max(overlapsTime.x, hitTime);
}
} else if (v.x > 0) {
if (b.min.x > a.max.x) {
return false;
}
if (a.max.x > b.min.x) {
outTime = Math.min((a.max.x - b.min.x) / v.x, outTime);
}
if (b.max.x < a.min.x) {
overlapsTime.x = (a.min.x - b.max.x) / v.x;
hitTime = Math.max(overlapsTime.x, hitTime);
}
}
return [hitTime, outTime];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment