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); }