Skip to content

Instantly share code, notes, and snippets.

@tyage
Created March 31, 2011 06:40
Show Gist options
  • Save tyage/895920 to your computer and use it in GitHub Desktop.
Save tyage/895920 to your computer and use it in GitHub Desktop.
window.Boku2D = (function() {
// RDC
var RDC = (function() {
var Entity = function (type, pos, obj) {
this.type = type;
this.pos = pos;
this.obj = obj;
};
var RDC = function (collide) {
this.collide = collide;
};
RDC.SUBDIVISION_THRESHOLD = 4;
RDC.CONTACT_THRESHOLD = 0.001;
RDC.prototype.bruteForce = function (group) {
for (var i=0,l=group.length; i<l; i++) {
for (var j=i+1; j<l; j++) {
this.collide(group[i], group[j]);
}
}
};
RDC.prototype.recursiveClustering = function (group, axis1, axis2) {
if (axis1 === -1 || group.length < RDC.SUBDIVISION_THRESHOLD) {
this.bruteForce(group);
} else {
var boundaries = this.getOpenCloseBounds(group, axis1);
boundaries.sort(function (a, b) {
return a.pos - b.pos;
});
var newAxis1 = axis2,
newAxis2 = -1,
groupSubdivided = false,
subgroup = [],
count = 0,
l = boundaries.length;
for (var i=0; i<l; i++)
{
var b = boundaries[i];
if (b.type === "open") {
count++;
subgroup.push(b.obj);
} else {
count--;
if (count === 0) {
if (i !== (l - 1)) {
groupSubdivided = true;
}
if (groupSubdivided) {
if (axis1 === 0) {
newAxis1 = 1;
} else if (axis1 === 1) {
newAxis1 = 0;
}
}
this.recursiveClustering(subgroup, newAxis1, newAxis2);
subgroup = [];
}
}
}
}
};
RDC.prototype.getOpenCloseBounds = function (group, axis) {
var l = group.length,
boundaries = [];
switch(axis)
{
case 0:
for (i=0; i<l; i++) {
o = group[i];
boundaries.push(new Entity("open",
o.center.x - o.size.x + RDC.CONTACT_THRESHOLD, o));
boundaries.push(new Entity("close",
o.center.x + o.size.x - RDC.CONTACT_THRESHOLD, o));
}
break;
case 1:
for (i=0; i<l; i++) {
o = group[i];
boundaries.push(new Entity("open",
o.center.y - o.size.y + RDC.CONTACT_THRESHOLD, o));
boundaries.push(new Entity("close",
o.center.y + o.size.y - RDC.CONTACT_THRESHOLD, o));
}
}
return boundaries;
};
return RDC;
})();
var IsNumber = function(num) {
return typeof num === "number";
};
var Extend = function(option) {
if (!option) {
return;
}
var key;
for (key in option) {
if (option.hasOwnProperty(key)) {
this[key] = option[key];
}
}
};
var Vec = function(x, y) {
this.x = (IsNumber(x) ? x : parseInt(x, 10)) || 0;
this.y = (IsNumber(y) ? y : parseInt(y, 10)) || 0;
};
Vec.prototype = {
copy: function() {
return new Vec(this.x, this.y);
},
add: function(v, y) {
if (y !== undefined) {
v = new Vec(v, y);
}
var x = this.x + v.x,
y = this.y + v.y;
return new Vec(x, y);
},
subtract: function(v, y) {
if (y !== undefined) {
v = new Vec(v, y);
}
var x = this.x - v.x,
y = this.y - v.y;
return new Vec(x, y);
},
multiply: function(i) {
var x = this.x * i,
y = this.y * i;
return new Vec(x, y);
},
divide: function(i) {
var x = this.x / i,
y = this.y / i;
return new Vec(x, y);
},
dot: function(v) {
return this.x * v.x + this.y * v.y;
},
length: function() {
return Math.sqrt(this.x*this.x + this.y*this.y);
},
normalize: function() {
var length = this.length();
return this.divide(length);
}
};
var Manifold = function(object, opponent) {
this.object = object;
this.opponent = opponent;
this.normal = new Vec();
};
Manifold.prototype = {
solve: function() {
var move = this.object,
fixed = this.opponent;
if (move.fixed) {
return false;
}
var friction = Math.sqrt(move.friction * fixed.friction);
var spring = Math.sqrt(move.elastic * fixed.elastic);
var damp = Math.sqrt(move.viscosity * fixed.viscosity);
// var relSpeed = move.speed.subtract(fixed.speed);
var relSpeed = {
x: move.speed.x - fixed.speed.x,
y: move.speed.y - fixed.speed.y
};
// 衝突ソルバー
// this.normal = new Vec();
this.normal.x = 0;
this.normal.y = 0;
if (this.direction.x < 0) {
var diff = move.maxPos().x - fixed.minPos().x;
var adj = spring * diff + damp * relSpeed.x;
// this.normal = this.normal.add(-adj, 0);
this.normal.x += -adj;
} else if (this.direction.x > 0) {
var diff = fixed.maxPos().x - move.minPos().x;
var adj = spring * diff - damp * relSpeed.x;
// this.normal = this.normal.add(adj, 0);
this.normal.x += adj;
}
if (this.direction.y < 0) {
var diff = move.maxPos().y - fixed.minPos().y;
var adj = spring * diff + damp * relSpeed.y;
// this.normal = this.normal.add(0, -adj);
this.normal.y += -adj;
} else if (this.direction.y > 0) {
var diff = fixed.maxPos().y - move.minPos().y;
var adj = spring * diff - damp * relSpeed.y;
// this.normal = this.normal.add(0, adj);
this.normal.y += adj;
}
move.applyForce(this.normal);
// 摩擦
var n = this.normal.length(),
force = new Vec(n, n).multiply(friction);
if (force.x > Math.abs(move.force.x)) {
force.x = Math.abs(move.force.x);
}
if (force.y > Math.abs(move.force.y)) {
force.y = Math.abs(move.force.y);
}
if (this.direction.y !== 0) {
if (relSpeed.x > 0) {
force.x = -force.x;
}
} else {
force.x = 0;
}
if (this.direction.x !== 0) {
if (relSpeed.y > 0) {
force.y = -force.y;
}
} else {
force.y = 0;
}
move.applyForce(force);
// !fixed.fixed && fixed.applyForce(force.multiply(-1));
!fixed.fixed && fixed.applyForce({
x: force.x * -1,
y: force.y * -1
});
}
};
var Contact = function(object1, object2) {
var direction1 = new Vec(),
direction2 = new Vec();
// 前回の位置から衝突方向を調べる
if (object1.maxTmpPos().x <= object2.minTmpPos().x &&
object1.maxPos().x >= object2.minPos().x) {
// direction1 = direction1.add(-1, 0);
direction1.x += -1;
} else if (object1.minTmpPos().x >= object2.maxTmpPos().x &&
object1.minPos().x <= object2.maxPos().x) {
// direction1 = direction1.add(1, 0);
direction1.x += 1;
}
if (object1.maxTmpPos().y <= object2.minTmpPos().y &&
object1.maxPos().y >= object2.minPos().y) {
// direction1 = direction1.add(0, -1);
direction1.y += -1;
} else if (object1.minTmpPos().y >= object2.maxTmpPos().y &&
object1.minPos().y <= object2.maxPos().y) {
// direction1 = direction1.add(0, 1);
direction1.y += 1;
}
if (direction1.x === 0 && direction1.y === 0) {
// 最小分離距離を計る
var normals = [];
if (object2.minPos().x <= object1.minPos().x &&
object1.minPos().x <= object2.maxPos().x) {
// 左衝突
var d = (object2.maxPos().x - object1.minPos().x)/2;
// normal = new Vec(d, 0);
// normal.direction = new Vec(1, 0);
var normal = {
length: d,
direction: {
x: 1,
y: 0
}
};
normals.push(normal);
} else if (object2.minPos().x <= object1.maxPos().x &&
object1.maxPos().x <= object2.maxPos().x) {
// 右衝突
var d = (object2.minPos().x - object1.maxPos().x)/2;
// normal = new Vec(d, 0);
// normal.direction = new Vec(-1, 0);
var normal = {
length: d,
direction: {
x: -1,
y: 0
}
};
normals.push(normal);
}
if (object2.minPos().y <= object1.minPos().y &&
object1.minPos().y <= object2.maxPos().y) {
// 上衝突
var d = (object2.maxPos().y - object1.minPos().y)/2;
// normal = new Vec(0, d);
// normal.direction = new Vec(0, 1);
var normal = {
length: d,
direction: {
x: 0,
y: 1
}
};
normals.push(normal);
} else if (object2.minPos().y <= object1.maxPos().y &&
object1.maxPos().y <= object2.maxPos().y) {
// 下衝突
var d = (object2.minPos().y - object1.maxPos().y)/2;
// normal = new Vec(0, d);
// normal.direction = new Vec(0, -1);
var normal = {
length: d,
direction: {
x: 0,
y: -1
}
};
normals.push(normal);
}
var normal = normals[0];
for (var i=0,l=normals.length;i<l;i++) {
// normal = normal.length() > normals[i].length() ?
normal = normal.length > normals[i].length ?
normals[i] :
normal;
}
// direction1 = normal.direction;
direction1.x = normal.direction.x;
direction1.y = normal.direction.y;
}
// direction2 = direction1.multiply(-1);
direction2.x = direction1.x * -1;
direction2.y = direction1.y * -1;
var manifold1 = new Manifold(object1, object2),
manifold2 = new Manifold(object2, object1);
manifold1.direction = direction1;
manifold2.direction = direction2;
this.manifolds = [manifold1, manifold2];
this.timeStep = 0;
this.world = object1.world;
this.object1 = object1;
this.object2 = object2;
this.update();
var manifolds = this.manifolds;
for (var i=0,l=manifolds.length;i<l;i++) {
var manifold = manifolds[i];
manifold.object.createContact(this, manifold);
}
};
Contact.prototype = {
update: function() {
this.timeStep = this.world.timeStep;
var manifolds = this.manifolds;
for (var i=0,l=manifolds.length;i<l;i++) {
var manifold = manifolds[i];
manifold.object.updateContact(this, manifold);
}
},
solve: function() {
var manifolds = this.manifolds;
for (var i=0,l=manifolds.length;i<l;i++) {
var manifold = manifolds[i];
manifold.object.beforeSolve(this, manifold);
manifold.solve();
manifold.object.afterSolve(this, manifold);
}
},
destroy: function() {
this._removeFromList(this.world.contacts);
this._removeFromList(this.object1.contacts);
this._removeFromList(this.object2.contacts);
var manifolds = this.manifolds;
for (var i=0,l=manifolds.length;i<l;i++) {
var manifold = manifolds[i];
manifold.object.destroyContact(this, manifold);
}
},
_removeFromList: function(contacts) {
for (var i=0,l=contacts.length;i<l;i++) {
if (contacts[i] == this) {
delete contacts[i];
contacts.splice(i, 1);
}
}
}
};
var Collide = function(object1, object2) {
if (!CheckContact(object1, object2)) {
return;
}
var list = object1.contacts,
contact = null;
for (var i=0,l=list.length;i<l;i++) {
var c = list[i];
if (c.manifolds[0].opponent === object2 ||
c.manifolds[1].opponent === object2) {
contact = c;
break;
}
}
if (contact) {
contact.update();
} else {
contact = new Contact(object1, object2);
object1.contacts.push(contact);
object2.contacts.push(contact);
object1.world.contacts.push(contact);
}
};
var CheckContact = function(object1, object2) {
return object1.maxPos().x >= object2.minPos().x &&
object1.minPos().x <= object2.maxPos().x &&
object1.maxPos().y >= object2.minPos().y &&
object1.minPos().y <= object2.maxPos().y;
};
var Model = function() {
var newObject = function(option) {
this._init(option);
};
/*
object <- object.prototype
<- object.prototype.prototype( = Model.prototype)
*/
var newModel = function() {};
newModel.prototype = Model.defaults;
newObject.prototype = new newModel();
return newObject;
};
Model.defaults = {
speed: new Vec(),
force: new Vec(),
accel: new Vec(),
center: new Vec(),
centerTmp: new Vec(),
size: new Vec(0, 0),
gravity: new Vec(0, 10),
weight: 1, // 質量
elastic: 7, // 反発係数 → バネ定数 (侵入量が減る)
viscosity: 3, // 粘性係数 → ダンパ係数 (速度が落ちる)
friction: 0.1, // 摩擦係数
fixed: false,
contacts: [],
init: function() {},
beforeStep: function(time) {},
afterStep: function(time) {},
createContact: function(contact, manifold) {},
destroyContact: function(contact, manifold) {},
updateContact: function(contact, manifold) {},
beforeSolve: function(contact, manifold) {},
afterSolve: function(contact, manifold) {},
_init: function(option) {
this.speed = this.speed.copy();
this.accel = this.accel.copy();
this.center = this.center.copy();
this.centerTmp = this.center.copy();
this.size = this.size.copy();
this.gravity = this.gravity.copy();
this.contacts = [];
Extend.call(this, option);
this.init(option);
},
_move: function(time) {
//this.centerTmp = this.center.copy();
this.centerTmp.x = this.center.x;
this.centerTmp.y = this.center.y;
//this.center = this.center.add(this.speed.multiply(time));
this.center.x += this.speed.x * time;
this.center.y += this.speed.y * time;
},
_step: function(time) {
this.beforeStep(time);
// this.applyForce(this.gravity.multiply(this.weight));
this.applyForce({
x: this.gravity.x * this.weight,
y: this.gravity.y * this.weight
});
// this.speed = this.speed.add(this.accel.multiply(time));
this.speed.x += this.accel.x * time;
this.speed.y += this.accel.y * time;
this._move(time);
this.afterStep(time);
this.resetForce();
},
maxTmpPos: function() {
// return this.centerTmp.add(this.size.divide(2));
return {
x: this.centerTmp.x + this.size.x/2,
y: this.centerTmp.y + this.size.y/2
};
},
minTmpPos: function() {
// return this.centerTmp.subtract(this.size.divide(2));
return {
x: this.centerTmp.x - this.size.x/2,
y: this.centerTmp.y - this.size.y/2
};
},
maxPos: function() {
// return this.center.add(this.size.divide(2));
return {
x: this.center.x + this.size.x/2,
y: this.center.y + this.size.y/2
};
},
minPos: function() {
// return this.center.subtract(this.size.divide(2));
return {
x: this.center.x - this.size.x/2,
y: this.center.y - this.size.y/2
};
},
applyForce: function(force) {
// this.force = this.force.add(force);
this.force.x += force.x;
this.force.y += force.y;
// this.accel = this.accel.add(force.divide(this.weight));
this.accel.x += force.x/this.weight;
this.accel.y += force.y/this.weight;
},
resetForce: function() {
// this.accel = new Vec();
this.accel.x = 0;
this.accel.y = 0;
}
};
var Rdc = new RDC(Collide);
var World = function(option) {
this.objects = [];
this.size = new Vec(0, 0);
this.timeStep = 0;
this.contacts = [];
Extend.call(this, option);
this.init(option);
};
World.prototype = {
step: function(time) {
this.beforeStep(time);
this.timeStep += time;
// 衝突情報取得
Rdc.recursiveClustering(this.objects, 0, 1);
// 衝突処理
var contacts = this.contacts;
for (var i=0;i<contacts.length;i++) {
var contact = contacts[i];
if (contact.timeStep !== this.timeStep) {
contact.destroy();
i--;
} else {
contact.solve();
}
}
// 各アイテムの進行
for (var i=0,l=this.objects.length;i<l;i++) {
var object = this.objects[i];
object._step(time);
}
this.afterStep(time);
return this;
},
createObject: function(object) {
object.world = this;
this.objects.push(object);
},
destroyObject: function(object) {
for (var i=0,l=object.contacts.length;i<l;i++) {
var contact = object.contacts[i];
if (contact) {
contact.destroy();
}
}
for (i=0,l=this.objects.length;i<l;i++) {
if (this.objects[i] === object) {
delete this.objects[i];
this.objects.splice(i, 1);
}
}
},
init: function(option) {},
beforeStep: function(time) {},
afterStep: function(time) {}
};
return {
Vec: Vec,
World: World,
Model: Model
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment