diff --git a/CHANGELOG.md b/CHANGELOG.md
index a905c8478e..32a2a80658 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
### Changed
- General: further code revamping to make melonJS more modular and allow instantiation of different app/games
+- Physic: new `Detector` class instantiated by each physic world instance to detect and solve collisions
+
+### Fixed
+- Doc: fix `fps` type in the World class
## [14.2.0] (melonJS 2) - _2022-12-26_
diff --git a/src/physics/collision.js b/src/physics/collision.js
index 000e0dd857..4885c62784 100644
--- a/src/physics/collision.js
+++ b/src/physics/collision.js
@@ -1,4 +1,4 @@
-import { rayCast } from "./detector.js";
+import { game } from "../index.js";
/**
* Collision detection (and projection-based collision response) of 2D shapes.
@@ -117,7 +117,7 @@ var collision = {
* // ...
* }
*/
- rayCast(line, result) { return rayCast(line, result); }
+ rayCast(line, result) { return game.world.rayCast(line, result); }
};
export default collision;
diff --git a/src/physics/detector.js b/src/physics/detector.js
index 4f71b5882f..a848c02e21 100644
--- a/src/physics/detector.js
+++ b/src/physics/detector.js
@@ -1,10 +1,8 @@
import * as SAT from "./sat.js";
import ResponseObject from "./response.js";
import Vector2d from "./../math/vector2.js";
-import { game } from "../index.js";
import Bounds from "./bounds.js";
-
// a dummy object when using Line for raycasting
let dummyObj = {
pos : new Vector2d(0, 0),
@@ -16,167 +14,196 @@ let dummyObj = {
}
};
+// some cache bounds object used for collision detection
let boundsA = new Bounds();
let boundsB = new Bounds();
-// the global response object used for collisions
-let globalResponse = new ResponseObject();
-
/**
- * a function used to determine if two objects should collide (based on both respective objects collision mask and type).
- * you can redefine this function if you need any specific rules over what should collide with what.
- * @ignore
- * @param {Renderable} a - a reference to the object A.
- * @param {Renderable} b - a reference to the object B.
- * @returns {boolean} true if they should collide, false otherwise
+ * the Detector class contains methods for detecting collisions between bodies using a broadphase algorithm.
*/
-function shouldCollide(a, b) {
- var bodyA = a.body,
- bodyB = b.body;
- return (
- a !== b &&
- a.isKinematic !== true && b.isKinematic !== true &&
- typeof bodyA === "object" && typeof bodyB === "object" &&
- bodyA.shapes.length > 0 && bodyB.shapes.length > 0 &&
- !(bodyA.isStatic === true && bodyB.isStatic === true) &&
- (bodyA.collisionMask & bodyB.collisionType) !== 0 &&
- (bodyA.collisionType & bodyB.collisionMask) !== 0
- );
-}
-
-
-
-/**
- * find all the collisions for the specified object
- * @ignore
- * @param {Renderable} objA - object to be tested for collision
- * @param {ResponseObject} [response] - a user defined response object that will be populated if they intersect.
- * @returns {boolean} in case of collision, false otherwise
- */
-export function collisionCheck(objA, response = globalResponse) {
- var collisionCounter = 0;
- // retreive a list of potential colliding objects from the game world
- var candidates = game.world.broadphase.retrieve(objA);
+export default class Detector {
+ /**
+ * @param {Container} world - the physic world this detector is bind to
+ */
+ constructor(world) {
+ // @ignore
+ this.world = world;
+
+ /**
+ * the default response object used for collisions
+ * (will be automatically populated by the collides functions)
+ * @type {ResponseObject}
+ */
+ this.response = new ResponseObject();
+ }
- boundsA.addBounds(objA.getBounds(), true);
- boundsA.addBounds(objA.body.getBounds());
+ /**
+ * determine if two objects should collide (based on both respective objects body collision mask and type).
+ * you can redefine this function if you need any specific rules over what should collide with what.
+ * @param {Renderable} a - a reference to the object A.
+ * @param {Renderable} b - a reference to the object B.
+ * @returns {boolean} true if they should collide, false otherwise
+ */
+ shouldCollide(a, b) {
+ var bodyA = a.body,
+ bodyB = b.body;
+ return (
+ (typeof bodyA === "object" && typeof bodyB === "object") &&
+ a !== b &&
+ a.isKinematic !== true && b.isKinematic !== true &&
+ bodyA.shapes.length > 0 && bodyB.shapes.length > 0 &&
+ !(bodyA.isStatic === true && bodyB.isStatic === true) &&
+ (bodyA.collisionMask & bodyB.collisionType) !== 0 &&
+ (bodyA.collisionType & bodyB.collisionMask) !== 0
+ );
+ }
- candidates.forEach((objB) => {
- // check if both objects "should" collide
- if (shouldCollide(objA, objB)) {
+ /**
+ * detect collision between two bodies.
+ * @param {Body} bodyA - a reference to body A.
+ * @param {Body} bodyB - a reference to body B.
+ * @returns {Boolean} true if colliding
+ */
+ collides(bodyA, bodyB, response = this.response) {
+ // for each shape in body A
+ for (var indexA = bodyA.shapes.length, shapeA; indexA--, (shapeA = bodyA.shapes[indexA]);) {
+ // for each shape in body B
+ for (var indexB = bodyB.shapes.length, shapeB; indexB--, (shapeB = bodyB.shapes[indexB]);) {
+ // full SAT collision check
+ if (SAT["test" + shapeA.shapeType + shapeB.shapeType].call(
+ this,
+ bodyA.ancestor, // a reference to the object A
+ shapeA,
+ bodyB.ancestor, // a reference to the object B
+ shapeB,
+ // clear response object before reusing
+ response.clear()) === true
+ ) {
- boundsB.addBounds(objB.getBounds(), true);
- boundsB.addBounds(objB.body.getBounds());
+ // set the shape index
+ response.indexShapeA = indexA;
+ response.indexShapeB = indexB;
- // fast AABB check if both bounding boxes are overlaping
- if (boundsA.overlaps(boundsB)) {
- // for each shape in body A
- objA.body.shapes.forEach((shapeA, indexA) => {
- // for each shape in body B
- objB.body.shapes.forEach((shapeB, indexB) => {
- // full SAT collision check
- if (SAT["test" + shapeA.shapeType + shapeB.shapeType].call(
- this,
- objA, // a reference to the object A
- shapeA,
- objB, // a reference to the object B
- shapeB,
- // clear response object before reusing
- response.clear()) === true
- ) {
- // we touched something !
- collisionCounter++;
-
- // set the shape index
- response.indexShapeA = indexA;
- response.indexShapeB = indexB;
-
- // execute the onCollision callback
- if (objA.onCollision && objA.onCollision(response, objB) !== false && objA.body.isStatic === false) {
- objA.body.respondToCollision.call(objA.body, response);
- }
- if (objB.onCollision && objB.onCollision(response, objA) !== false && objB.body.isStatic === false) {
- objB.body.respondToCollision.call(objB.body, response);
- }
- }
- });
- });
+ return true;
+ }
}
}
- });
- // we could return the amount of objects we collided with ?
- return collisionCounter > 0;
-}
-
-/**
- * Checks for object colliding with the given line
- * @ignore
- * @param {Line} line - line to be tested for collision
- * @param {Array.} [result] - a user defined array that will be populated with intersecting physic objects.
- * @returns {Array.} an array of intersecting physic objects
- * @example
- * // define a line accross the viewport
- * var ray = new me.Line(
- * // absolute position of the line
- * 0, 0, [
- * // starting point relative to the initial position
- * new me.Vector2d(0, 0),
- * // ending point
- * new me.Vector2d(me.game.viewport.width, me.game.viewport.height)
- * ]);
- *
- * // check for collition
- * result = me.collision.rayCast(ray);
- *
- * if (result.length > 0) {
- * // ...
- * }
- */
-export function rayCast(line, result = []) {
- var collisionCounter = 0;
-
- // retrieve a list of potential colliding objects from the game world
- var candidates = game.world.broadphase.retrieve(line);
-
- for (var i = candidates.length, objB; i--, (objB = candidates[i]);) {
-
- // fast AABB check if both bounding boxes are overlaping
- if (objB.body && line.getBounds().overlaps(objB.getBounds())) {
+ return false;
+ }
- // go trough all defined shapes in B (if any)
- var bLen = objB.body.shapes.length;
- if ( objB.body.shapes.length === 0) {
- continue;
+ /**
+ * find all the collisions for the specified object using a broadphase algorithm
+ * @ignore
+ * @param {Renderable} objA - object to be tested for collision
+ * @returns {boolean} in case of collision, false otherwise
+ */
+ collisions(objA) {
+ var collisionCounter = 0;
+ // retreive a list of potential colliding objects from the game world
+ var candidates = this.world.broadphase.retrieve(objA);
+
+ boundsA.addBounds(objA.getBounds(), true);
+ boundsA.addBounds(objA.body.getBounds());
+
+ candidates.forEach((objB) => {
+ // check if both objects "should" collide
+ if (this.shouldCollide(objA, objB)) {
+
+ boundsB.addBounds(objB.getBounds(), true);
+ boundsB.addBounds(objB.body.getBounds());
+
+ // fast AABB check if both bounding boxes are overlaping
+ if (boundsA.overlaps(boundsB)) {
+
+ if (this.collides(objA.body, objB.body)) {
+ // we touched something !
+ collisionCounter++;
+
+ // execute the onCollision callback
+ if (objA.onCollision && objA.onCollision(this.response, objB) !== false && objA.body.isStatic === false) {
+ objA.body.respondToCollision.call(objA.body, this.response);
+ }
+ if (objB.onCollision && objB.onCollision(this.response, objA) !== false && objB.body.isStatic === false) {
+ objB.body.respondToCollision.call(objB.body, this.response);
+ }
+ }
+ }
}
+ });
+ // we could return the amount of objects we collided with ?
+ return collisionCounter > 0;
+ }
- var shapeA = line;
+ /**
+ * Checks for object colliding with the given line
+ * @ignore
+ * @param {Line} line - line to be tested for collision
+ * @param {Array.} [result] - a user defined array that will be populated with intersecting physic objects.
+ * @returns {Array.} an array of intersecting physic objects
+ * @example
+ * // define a line accross the viewport
+ * var ray = new me.Line(
+ * // absolute position of the line
+ * 0, 0, [
+ * // starting point relative to the initial position
+ * new me.Vector2d(0, 0),
+ * // ending point
+ * new me.Vector2d(me.game.viewport.width, me.game.viewport.height)
+ * ]);
+ *
+ * // check for collition
+ * result = me.collision.rayCast(ray);
+ *
+ * if (result.length > 0) {
+ * // ...
+ * }
+ */
+ rayCast(line, result = []) {
+ var collisionCounter = 0;
+
+ // retrieve a list of potential colliding objects from the game world
+ var candidates = this.world.broadphase.retrieve(line);
+
+ for (var i = candidates.length, objB; i--, (objB = candidates[i]);) {
- // go through all defined shapes in B
- var indexB = 0;
- do {
- var shapeB = objB.body.getShape(indexB);
+ // fast AABB check if both bounding boxes are overlaping
+ if (objB.body && line.getBounds().overlaps(objB.getBounds())) {
- // full SAT collision check
- if (SAT["test" + shapeA.shapeType + shapeB.shapeType]
- .call(
- this,
- dummyObj, // a reference to the object A
- shapeA,
- objB, // a reference to the object B
- shapeB
- )) {
- // we touched something !
- result[collisionCounter] = objB;
- collisionCounter++;
+ // go trough all defined shapes in B (if any)
+ var bLen = objB.body.shapes.length;
+ if ( objB.body.shapes.length === 0) {
+ continue;
}
- indexB++;
- } while (indexB < bLen);
+
+ var shapeA = line;
+
+ // go through all defined shapes in B
+ var indexB = 0;
+ do {
+ var shapeB = objB.body.getShape(indexB);
+
+ // full SAT collision check
+ if (SAT["test" + shapeA.shapeType + shapeB.shapeType]
+ .call(
+ this,
+ dummyObj, // a reference to the object A
+ shapeA,
+ objB, // a reference to the object B
+ shapeB
+ )) {
+ // we touched something !
+ result[collisionCounter] = objB;
+ collisionCounter++;
+ }
+ indexB++;
+ } while (indexB < bLen);
+ }
}
- }
- // cap result in case it was not empty
- result.length = collisionCounter;
+ // cap result in case it was not empty
+ result.length = collisionCounter;
- // return the list of colliding objects
- return result;
+ // return the list of colliding objects
+ return result;
+ }
}
diff --git a/src/physics/world.js b/src/physics/world.js
index 79de900473..342a17abd4 100644
--- a/src/physics/world.js
+++ b/src/physics/world.js
@@ -3,7 +3,7 @@ import * as event from "./../system/event.js";
import QuadTree from "./quadtree.js";
import Container from "../renderable/container.js";
import collision from "./collision.js";
-import { collisionCheck } from "./detector.js";
+import Detector from "./detector.js";
import state from "./../state/state.js";
/**
@@ -38,22 +38,15 @@ import state from "./../state/state.js";
/**
* the rate at which the game world is updated,
* may be greater than or lower than the display fps
- * @public
- * @type {Vector2d}
* @default 60
- * @name fps
- * @memberof World
* @see timer.maxfps
*/
this.fps = 60;
/**
* world gravity
- * @public
* @type {Vector2d}
* @default <0,0.98>
- * @name gravity
- * @memberof World
*/
this.gravity = new Vector2d(0, 0.98);
@@ -67,28 +60,27 @@ import state from "./../state/state.js";
* property to your layer (in Tiled).
* @type {boolean}
* @default false
- * @memberof World
*/
this.preRender = false;
/**
* the active physic bodies in this simulation
- * @name bodies
- * @memberof World
- * @public
* @type {Set}
*/
this.bodies = new Set();
/**
* the instance of the game world quadtree used for broadphase
- * @name broadphase
- * @memberof World
- * @public
* @type {QuadTree}
*/
this.broadphase = new QuadTree(this, this.getBounds().clone(), collision.maxChildren, collision.maxDepth);
+ /**
+ * the collision detector instance used by this world instance
+ * @type {Detector}
+ */
+ this.detector = new Detector(this);
+
// reset the world container on the game reset signal
event.on(event.GAME_RESET, this.reset, this);
@@ -101,8 +93,6 @@ import state from "./../state/state.js";
/**
* reset the game world
- * @name reset
- * @memberof World
*/
reset() {
// clear the quadtree
@@ -121,8 +111,6 @@ import state from "./../state/state.js";
/**
* Add a physic body to the game world
- * @name addBody
- * @memberof World
* @see Container.addChild
* @param {Body} body
* @returns {World} this game world
@@ -135,8 +123,6 @@ import state from "./../state/state.js";
/**
* Remove a physic body from the game world
- * @name removeBody
- * @memberof World
* @see Container.removeChild
* @param {Body} body
* @returns {World} this game world
@@ -149,8 +135,6 @@ import state from "./../state/state.js";
/**
* Apply gravity to the given body
- * @name bodyApplyVelocity
- * @memberof World
* @private
* @param {Body} body
*/
@@ -167,8 +151,6 @@ import state from "./../state/state.js";
/**
* update the game world
- * @name reset
- * @memberof World
* @param {number} dt - the time passed since the last frame update
* @returns {boolean} true if the word is dirty
*/
@@ -196,7 +178,7 @@ import state from "./../state/state.js";
ancestor.isDirty = true;
}
// handle collisions against other objects
- collisionCheck(ancestor);
+ this.detector.collisions(ancestor);
// clear body force
body.force.set(0, 0);
}