const System = require('engine/system'); const Vector = require('engine/Vector'); const { removeItems } = require('engine/utils/array'); const { clamp } = require('engine/utils/math'); /** * Physics system. * * @class SystemPhysics */ class SystemPhysics extends System { /** * @constructor * @param {Object} [settings] Settings to be merged in. */ constructor(settings) { super(); this.name = 'Physics'; /** * Gravity of physics world. * @property {Vector} gravity */ this.gravity = Vector.create(); /** * Spatial hash shift factor (larger number = less division) * @type {Number} */ this.spatialShift = 5; /** * Collision solver instance. * @type {SATSolver|AABBSolver} */ this.solver = null; /** * List of colliders in world. * @type {array} */ this.colliders = []; /** * Collision map to trace colliders against. * @type {CollisionMap} */ this.collisionMap = null; /** * Save whether collision of a pair of objects are checked * @type {object} * @private */ this.checks = {}; /** * How many pair of colliders have been checked in this frame * @type {number} * @protected */ this.collisionChecks = 0; /** * Collision map trace result * @type {Object} * @private */ this.res = { x: 0, y: 0, hitX: false, hitY: false, }; this.setup(settings); } /** * Setup this system with setting object. * @memberof SystemPhysics# * @method setup * @param {Object} settings Setting object. */ setup(settings) { for (let k in settings) { switch (k) { // Value case 'name': case 'spatialShift': case 'solver': case 'collisionMap': this[k] = settings[k]; break; // Vector case 'gravity': this.gravity.x = settings.gravity.x || 0; this.gravity.y = settings.gravity.y || 0; break; } } } /** * Add collider to world. * @memberof SystemPhysics# * @method addCollider * @param {Coll} coll Collider to add */ addCollider(coll) { coll.world = this; coll.isRemoved = false; if (this.colliders.indexOf(coll) === -1) { this.colliders.push(coll); } } /** * Remove collider from world. * @memberof SystemPhysics# * @method removeCollider * @param {Coll} coll Collider to remove */ removeCollider(coll) { if (!coll.world) {return;} coll.world = null; coll.isRemoved = true; } /** * Update colliders and check collisions. * @memberof SystemPhysics# * @method fixedUpdate * @param {Number} dt Delta time in millisecond * @param {Number} delta Delta time in second */ fixedUpdate(dt, delta) { this.collisionChecks = 0; this.checks = {}; let i, j, coll, coll2, group, hash = {}; let halfWidth, halfHeight, sx, sy, ex, ey, x, y, a2b, b2a, key; // Process colliders for (i = 0; i < this.colliders.length; i++) { coll = this.colliders[i]; // Save position of last frame coll.last.copy(coll.position); // Collider is already removed, just remove it if (coll.isRemoved) { removeItems(this.colliders, i--, 1); continue; } if (!coll.isStatic) { // Update velocity if (coll.mass !== 0) { coll.velocity.add( this.gravity.x * coll.mass * delta, this.gravity.y * coll.mass * delta ); } coll.velocity.add(coll.force.x * delta, coll.force.y * delta); if (coll.damping > 0 && coll.damping < 1) { coll.velocity.multiply(Math.pow(1 - coll.damping, delta)); } // Clamp the velocity if (coll.velocityLimit.x > 0) { coll.velocity.x = clamp(coll.velocity.x, -coll.velocityLimit.x, coll.velocityLimit.x); } if (coll.velocityLimit.y > 0) { coll.velocity.y = clamp(coll.velocity.y, -coll.velocityLimit.y, coll.velocityLimit.y); } // Calculate delta movement during this frame this.res.x = coll.velocity.x * delta; this.res.y = coll.velocity.y * delta; // Trace against the map there is one // TODO: add a flag to pass this step if (this.collisionMap) { this.collisionMap.trace(coll, this.res.x, this.res.y, this.res); // Manually handle trace result coll.handleMovementTrace(this.res); } // Apply trace result coll.position.x += this.res.x; coll.position.y += this.res.y; } // Update bounds if (coll.shape) { halfWidth = coll.shape.width * 0.5; halfHeight = coll.shape.height * 0.5; coll.lastLeft = Math.floor(coll.last.x - halfWidth); coll.lastRight = Math.floor(coll.last.x + halfWidth); coll.lastTop = Math.floor(coll.last.y - halfHeight); coll.lastBottom = Math.floor(coll.last.y + halfHeight); coll.left = Math.floor(coll.position.x - halfWidth); coll.right = Math.floor(coll.position.x + halfWidth); coll.top = Math.floor(coll.position.y - halfHeight); coll.bottom = Math.floor(coll.position.y + halfHeight); } // Insert the hash and test collisions sx = coll.left >> this.spatialShift; sy = coll.top >> this.spatialShift; ex = coll.right >> this.spatialShift; ey = coll.bottom >> this.spatialShift; // Non-static colliders will be notified before collision if (!coll.isStatic) { coll.beforeCollide(); } for (y = sy; y <= ey; y++) { for (x = sx; x <= ex; x++) { // Find or create the list if (!hash[x]) { hash[x] = {}; } if (!hash[x][y]) { hash[x][y] = []; } group = hash[x][y]; // Insert collider into the group group.push(coll); // Pass: only one collider if (group.length === 1) { continue; } // Test colliders in the same group for (j = 0; j < group.length; j++) { coll2 = group[j]; // Pass: same collider or someone is already removed if (coll2 === coll || coll.isRemoved || coll2.isRemoved) { continue; } a2b = !!(coll.collideAgainst & coll2.collisionGroup) && !(coll.isStatic); b2a = !!(coll2.collideAgainst & coll.collisionGroup) && !(coll2.isStatic); // Pass: never collide with each other if (!a2b && !b2a) { continue; } key = `${coll.id < coll2.id ? coll.id : coll2.id}:${coll.id > coll2.id ? coll.id : coll2.id}`; // Pass: already checked if (this.checks[key]) { continue; } // Mark this pair is already checked this.checks[key] = true; this.collisionChecks++; // Test overlap if (this.solver.hitTest(coll, coll2)) { // Apply response this.solver.hitResponse(coll, coll2, a2b, b2a); } } } } } } /** * Remove all colliders and collision groups. * @memberof SystemPhysics# * @method cleanup */ cleanup() { this.colliders.length = 0; } /** * Callback that will be invoked on each entity spawn. * @memberof SystemPhysics# * @method onEntitySpawn * @param {Entity} ent Entity instance */ onEntitySpawn(ent) { if (ent.coll) { ent.coll.entity = ent; this.addCollider(ent.coll); // Override coll's position with the entity's ent.coll.position = ent.position; } } /** * Callback that will be invoked on each entity remove. * @memberof SystemPhysics# * @method onEntityRemove * @param {Entity} ent Entity instance */ onEntityRemove(ent) { if (ent.coll) { ent.coll.remove(); ent.coll.entity = null; } } } module.exports = SystemPhysics; /** * Get a collision group by index * @memberof module:engine/physics * @param {Number} idx Index of the group. * @return {Number} Group mask */ module.exports.getGroupMask = function(idx) { if (idx < 31) { return 1 << idx; } else { console.log('Warning: only 0-30 indexed collision group is supported!'); return 0; } };