UNPKG

arcade-physics

Version:
1,180 lines 83.4 kB
"use strict"; /** * @author Richard Davey <rich@photonstorm.com> * @copyright 2020 Photon Storm Ltd. * @license {@link https://opensource.org/licenses/MIT|MIT License} */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.World = void 0; const BetweenPoints_1 = __importDefault(require("../../math/angle/BetweenPoints")); const Clamp_1 = __importDefault(require("../../math/Clamp")); const DistanceBetween_1 = __importDefault(require("../../math/distance/DistanceBetween")); const eventemitter3_1 = __importDefault(require("eventemitter3")); const events_1 = __importDefault(require("./events")); const Equal_1 = __importDefault(require("../../math/fuzzy/Equal")); const GreaterThan_1 = __importDefault(require("../../math/fuzzy/GreaterThan")); const LessThan_1 = __importDefault(require("../../math/fuzzy/LessThan")); const GetOverlapX_1 = require("./GetOverlapX"); const GetOverlapY_1 = require("./GetOverlapY"); const GetValue_1 = __importDefault(require("../../utils/object/GetValue")); const RTree_1 = __importDefault(require("../../structs/RTree")); const Rectangle_1 = require("../../geom/rectangle/Rectangle"); const SeparateX_1 = require("./SeparateX"); const SeparateY_1 = require("./SeparateY"); const Wrap_1 = __importDefault(require("../../math/Wrap")); const const_1 = __importDefault(require("./const")); const const_2 = __importDefault(require("../../math/const")); const Collider_1 = require("./Collider"); const ProcessQueue_1 = require("../../structs/ProcessQueue"); const Vector2_1 = require("../../math/Vector2"); class World extends eventemitter3_1.default { /** * The Arcade Physics World. * * The World is responsible for creating, managing, colliding and updating all of the bodies within it. * * An instance of the World belongs to a Phaser.Scene and is accessed via the property `physics.world`. * * @param scene The Scene this simulation belongs to. * @param config An Arcade Physics Configuration object. */ constructor(scene, config) { super(); this.scene = scene; this.config = config; /** Dynamic Bodies in this simulation. */ this.bodies = new Set(); /** Static Bodies in this simulation. */ this.staticBodies = new Set(); /** Static Bodies marked for deletion. */ this.pendingDestroy = new Set(); /** The amount of elapsed ms since the last frame. */ this._elapsed = 0; /** This simulation's collision processors. */ this.colliders = new ProcessQueue_1.ProcessQueue(); /** Acceleration of Bodies due to gravity, in pixels per second. */ this.gravity = new Vector2_1.Vector2((0, GetValue_1.default)(config, 'gravity.x', 0), (0, GetValue_1.default)(config, 'gravity.y', 0)); this.bounds = new Rectangle_1.Rectangle((0, GetValue_1.default)(config, 'x', 0), (0, GetValue_1.default)(config, 'y', 0), (0, GetValue_1.default)(config, 'width', scene.sys.scale.width), (0, GetValue_1.default)(config, 'height', scene.sys.scale.height)); /** The boundary edges that Bodies can collide with. */ this.checkCollision = { up: (0, GetValue_1.default)(config, 'checkCollision.up', true), down: (0, GetValue_1.default)(config, 'checkCollision.down', true), left: (0, GetValue_1.default)(config, 'checkCollision.left', true), right: (0, GetValue_1.default)(config, 'checkCollision.right', true) }; /** * The number of physics steps to be taken per second. * * This property is read-only. Use the `setFPS` method to modify it at run-time. */ this.fps = (0, GetValue_1.default)(config, 'fps', 60); /** * Should Physics use a fixed update time-step (true) or sync to the render fps (false)?. * False value of this property disables fps and timeScale properties. */ this.fixedStep = (0, GetValue_1.default)(config, 'fixedStep', true); /** Internal frame time value. */ this._frameTime = 1 / this.fps; /** Internal frame time ms value. */ this._frameTimeMS = 1000 * this._frameTime; /** The number of steps that took place in the last frame. */ this.stepsLastFrame = 0; /** * Scaling factor applied to the frame rate. * * - 1.0 = normal speed * - 2.0 = half speed * - 0.5 = double speed */ this.timeScale = (0, GetValue_1.default)(config, 'timeScale', 1); this.OVERLAP_BIAS = (0, GetValue_1.default)(config, 'overlapBias', 4); /** * The maximum absolute value of a Body's overlap with a tile that will result in separation on *each axis*. * Larger values favor separation. * Smaller values favor no separation. * The optimum value may be similar to the tile size. * @default 16 */ this.TILE_BIAS = (0, GetValue_1.default)(config, 'tileBias', 16); /** * Always separate overlapping Bodies horizontally before vertically. * False (the default) means Bodies are first separated on the axis of greater gravity, or the vertical axis if neither is greater. * @default false */ this.forceX = (0, GetValue_1.default)(config, 'forceX', false); /** * Whether the simulation advances with the game loop. * @default false */ this.isPaused = (0, GetValue_1.default)(config, 'isPaused', false); /** * Temporary total of colliding Bodies. * @default 0 */ this._total = 0; /** * Enables the debug display. * @default false */ this.drawDebug = (0, GetValue_1.default)(config, 'debug', false); /** * The graphics object drawing the debug display. * * @name Phaser.Physics.Arcade.World#debugGraphic * @type {Phaser.GameObjects.Graphics} * @since 3.0.0 */ this.debugGraphic; this.defaults = { debugShowBody: (0, GetValue_1.default)(config, 'debugShowBody', true), debugShowStaticBody: (0, GetValue_1.default)(config, 'debugShowStaticBody', true), debugShowVelocity: (0, GetValue_1.default)(config, 'debugShowVelocity', true), bodyDebugColor: (0, GetValue_1.default)(config, 'debugBodyColor', 0xff00ff), staticBodyDebugColor: (0, GetValue_1.default)(config, 'debugStaticBodyColor', 0x0000ff), velocityDebugColor: (0, GetValue_1.default)(config, 'debugVelocityColor', 0x00ff00) }; /** * The maximum number of items per node on the RTree. * * This is ignored if `useTree` is `false`. If you have a large number of bodies in * your world then you may find search performance improves by increasing this value, * to allow more items per node and less node division. * * @name Phaser.Physics.Arcade.World#maxEntries * @type {number} * @default 16 * @since 3.0.0 */ this.maxEntries = (0, GetValue_1.default)(config, 'maxEntries', 16); /** * Should this Arcade Physics World use an RTree for Dynamic bodies? * * An RTree is a fast way of spatially sorting of all the bodies in the world. * However, at certain limits, the cost of clearing and inserting the bodies into the * tree every frame becomes more expensive than the search speed gains it provides. * * If you have a large number of dynamic bodies in your world then it may be best to * disable the use of the RTree by setting this property to `false` in the physics config. * * The number it can cope with depends on browser and device, but a conservative estimate * of around 5,000 bodies should be considered the max before disabling it. * * This only applies to dynamic bodies. Static bodies are always kept in an RTree, * because they don't have to be cleared every frame, so you benefit from the * massive search speeds all the time. * * @name Phaser.Physics.Arcade.World#useTree * @type {boolean} * @default true * @since 3.10.0 */ this.useTree = (0, GetValue_1.default)(config, 'useTree', true); /** * The spatial index of Dynamic Bodies. * * @name Phaser.Physics.Arcade.World#tree * @type {Phaser.Structs.RTree} * @since 3.0.0 */ this.tree = new RTree_1.default(this.maxEntries); /** * The spatial index of Static Bodies. * * @name Phaser.Physics.Arcade.World#staticTree * @type {Phaser.Structs.RTree} * @since 3.0.0 */ this.staticTree = new RTree_1.default(this.maxEntries); this.treeMinMax = { minX: 0, minY: 0, maxX: 0, maxY: 0 }; if (this.drawDebug) { this.createDebugGraphic(); } } // /** // * Adds an Arcade Physics Body to a Game Object, an array of Game Objects, or the children of a Group. // * // * The difference between this and the `enableBody` method is that you can pass arrays or Groups // * to this method. // * // * You can specify if the bodies are to be Dynamic or Static. A dynamic body can move via velocity and // * acceleration. A static body remains fixed in place and as such is able to use an optimized search // * tree, making it ideal for static elements such as level objects. You can still collide and overlap // * with static bodies. // * // * Normally, rather than calling this method directly, you'd use the helper methods available in the // * Arcade Physics Factory, such as: // * // * ```javascript // * this.physics.add.image(x, y, textureKey); // * this.physics.add.sprite(x, y, textureKey); // * ``` // * // * Calling factory methods encapsulates the creation of a Game Object and the creation of its // * body at the same time. If you are creating custom classes then you can pass them to this // * method to have their bodies created. // * // * @method Phaser.Physics.Arcade.World#enable // * @since 3.0.0 // * // * @param {(Phaser.GameObjects.GameObject|Phaser.GameObjects.GameObject[]|Phaser.GameObjects.Group|Phaser.GameObjects.Group[])} object - The object, or objects, on which to create the bodies. // * @param {number} [bodyType] - The type of Body to create. Either `DYNAMIC_BODY` or `STATIC_BODY`. // */ // enable(object, bodyType) { // if (bodyType === undefined) { // bodyType = CONST.DYNAMIC_BODY // } // if (!Array.isArray(object)) { // object = [object] // } // for (let i = 0; i < object.length; i++) { // let entry = object[i] // if (entry.isParent) { // let children = entry.getChildren() // for (let c = 0; c < children.length; c++) { // let child = children[c] // if (child.isParent) { // // Handle Groups nested inside of Groups // this.enable(child, bodyType) // } else { // this.enableBody(child, bodyType) // } // } // } else { // this.enableBody(entry, bodyType) // } // } // } // /** // * Creates an Arcade Physics Body on a single Game Object. // * // * If the Game Object already has a body, this method will simply add it back into the simulation. // * // * You can specify if the body is Dynamic or Static. A dynamic body can move via velocity and // * acceleration. A static body remains fixed in place and as such is able to use an optimized search // * tree, making it ideal for static elements such as level objects. You can still collide and overlap // * with static bodies. // * // * Normally, rather than calling this method directly, you'd use the helper methods available in the // * Arcade Physics Factory, such as: // * // * ```javascript // * this.physics.add.image(x, y, textureKey); // * this.physics.add.sprite(x, y, textureKey); // * ``` // * // * Calling factory methods encapsulates the creation of a Game Object and the creation of its // * body at the same time. If you are creating custom classes then you can pass them to this // * method to have their bodies created. // * // * @method Phaser.Physics.Arcade.World#enableBody // * @since 3.0.0 // * // * @param {Phaser.GameObjects.GameObject} object - The Game Object on which to create the body. // * @param {number} [bodyType] - The type of Body to create. Either `DYNAMIC_BODY` or `STATIC_BODY`. // * // * @return {Phaser.GameObjects.GameObject} The Game Object on which the body was created. // */ // enableBody(object, bodyType) { // if (bodyType === undefined) { // bodyType = CONST.DYNAMIC_BODY // } // if (!object.body) { // if (bodyType === CONST.DYNAMIC_BODY) { // object.body = new Body(this, object) // } else if (bodyType === CONST.STATIC_BODY) { // object.body = new StaticBody(this, object) // } // } // this.add(object.body) // return object // } /** * Adds an existing Arcade Physics Body or StaticBody to the simulation. * * The body is enabled and added to the local search trees. * * @method Phaser.Physics.Arcade.World#add * @since 3.10.0 * * @param {(Phaser.Physics.Arcade.Body|Phaser.Physics.Arcade.StaticBody)} body - The Body to be added to the simulation. * * @return {(Phaser.Physics.Arcade.Body|Phaser.Physics.Arcade.StaticBody)} The Body that was added to the simulation. */ add(body) { if (body.physicsType === const_1.default.PHYSICS_TYPE.DYNAMIC_BODY) { this.bodies.add(body); this.tree.insert(body); } else if (body.physicsType === const_1.default.PHYSICS_TYPE.STATIC_BODY) { this.staticBodies.add(body); this.staticTree.insert(body); } body.enable = true; return body; } /** * Disables the Arcade Physics Body of a Game Object, an array of Game Objects, or the children of a Group. * * The difference between this and the `disableBody` method is that you can pass arrays or Groups * to this method. * * The body itself is not deleted, it just has its `enable` property set to false, which * means you can re-enable it again at any point by passing it to enable `World.enable` or `World.add`. * * @method Phaser.Physics.Arcade.World#disable * @since 3.0.0 * * @param {(Phaser.GameObjects.GameObject|Phaser.GameObjects.GameObject[]|Phaser.GameObjects.Group|Phaser.GameObjects.Group[])} object - The object, or objects, on which to disable the bodies. */ disable(object) { if (!Array.isArray(object)) { object = [object]; } for (let i = 0; i < object.length; i++) { const entry = object[i]; if (entry.isParent) { const children = entry.getChildren(); for (let c = 0; c < children.length; c++) { const child = children[c]; if (child.isParent) { // Handle Groups nested inside of Groups this.disable(child); } else { this.disableBody(child.body); } } } else { this.disableBody(entry.body); } } } /** * Disables an existing Arcade Physics Body or StaticBody and removes it from the simulation. * * The body is disabled and removed from the local search trees. * * The body itself is not deleted, it just has its `enable` property set to false, which * means you can re-enable it again at any point by passing it to enable `World.enable` or `World.add`. * * @method Phaser.Physics.Arcade.World#disableBody * @since 3.0.0 * * @param {(Phaser.Physics.Arcade.Body|Phaser.Physics.Arcade.StaticBody)} body - The Body to be disabled. */ disableBody(body) { this.remove(body); body.enable = false; } /** * Removes an existing Arcade Physics Body or StaticBody from the simulation. * * The body is disabled and removed from the local search trees. * * The body itself is not deleted, it just has its `enabled` property set to false, which * means you can re-enable it again at any point by passing it to enable `enable` or `add`. * * @method Phaser.Physics.Arcade.World#remove * @since 3.0.0 * * @param {(Phaser.Physics.Arcade.Body|Phaser.Physics.Arcade.StaticBody)} body - The body to be removed from the simulation. */ remove(body) { if (body.physicsType === const_1.default.PHYSICS_TYPE.DYNAMIC_BODY) { this.bodies.delete(body); this.tree.remove(body); } else if (body.physicsType === const_1.default.PHYSICS_TYPE.STATIC_BODY) { this.staticBodies.delete(body); this.staticTree.remove(body); } } /** * Creates a Graphics Game Object that the world will use to render the debug display to. * * This is called automatically when the World is instantiated if the `debug` config property * was set to `true`. However, you can call it at any point should you need to display the * debug Graphic from a fixed point. * * You can control which objects are drawn to the Graphics object, and the colors they use, * by setting the debug properties in the physics config. * * You should not typically use this in a production game. Use it to aid during debugging. * * @method Phaser.Physics.Arcade.World#createDebugGraphic * @since 3.0.0 * * @return {Phaser.GameObjects.Graphics} The Graphics object that was created for use by the World. */ createDebugGraphic() { // let graphic = this.scene.sys.add.graphics({ x: 0, y: 0 }) // graphic.setDepth(Number.MAX_VALUE) // this.debugGraphic = graphic // this.drawDebug = true // return graphic } /** * Sets the position, size and properties of the World boundary. * * The World boundary is an invisible rectangle that defines the edges of the World. * If a Body is set to collide with the world bounds then it will automatically stop * when it reaches any of the edges. You can optionally set which edges of the boundary * should be checked against. * * @method Phaser.Physics.Arcade.World#setBounds * @since 3.0.0 * * @param {number} x - The top-left x coordinate of the boundary. * @param {number} y - The top-left y coordinate of the boundary. * @param {number} width - The width of the boundary. * @param {number} height - The height of the boundary. * @param {boolean} [checkLeft] - Should bodies check against the left edge of the boundary? * @param {boolean} [checkRight] - Should bodies check against the right edge of the boundary? * @param {boolean} [checkUp] - Should bodies check against the top edge of the boundary? * @param {boolean} [checkDown] - Should bodies check against the bottom edge of the boundary? * * @return {Phaser.Physics.Arcade.World} This World object. */ setBounds(x, y, width, height, checkLeft, checkRight, checkUp, checkDown) { this.bounds.setTo(x, y, width, height); if (checkLeft !== undefined) { this.setBoundsCollision(checkLeft, checkRight, checkUp, checkDown); } return this; } /** * Enables or disables collisions on each edge of the World boundary. * * @method Phaser.Physics.Arcade.World#setBoundsCollision * @since 3.0.0 * * @param {boolean} [left=true] - Should bodies check against the left edge of the boundary? * @param {boolean} [right=true] - Should bodies check against the right edge of the boundary? * @param {boolean} [up=true] - Should bodies check against the top edge of the boundary? * @param {boolean} [down=true] - Should bodies check against the bottom edge of the boundary? * * @return {Phaser.Physics.Arcade.World} This World object. */ setBoundsCollision(left, right, up, down) { if (left === undefined) { left = true; } if (right === undefined) { right = true; } if (up === undefined) { up = true; } if (down === undefined) { down = true; } this.checkCollision.left = left; this.checkCollision.right = right; this.checkCollision.up = up; this.checkCollision.down = down; return this; } /** * Pauses the simulation. * * A paused simulation does not update any existing bodies, or run any Colliders. * * However, you can still enable and disable bodies within it, or manually run collide or overlap * checks. * * @method Phaser.Physics.Arcade.World#pause * @fires Phaser.Physics.Arcade.Events#PAUSE * @since 3.0.0 * * @return {Phaser.Physics.Arcade.World} This World object. */ pause() { this.isPaused = true; this.emit(events_1.default.PAUSE); return this; } /** * Resumes the simulation, if paused. * * @method Phaser.Physics.Arcade.World#resume * @fires Phaser.Physics.Arcade.Events#RESUME * @since 3.0.0 * * @return {Phaser.Physics.Arcade.World} This World object. */ resume() { this.isPaused = false; this.emit(events_1.default.RESUME); return this; } /** * Creates a new Collider object and adds it to the simulation. * * A Collider is a way to automatically perform collision checks between two objects, * calling the collide and process callbacks if they occur. * * Colliders are run as part of the World update, after all of the Bodies have updated. * * By creating a Collider you don't need then call `World.collide` in your `update` loop, * as it will be handled for you automatically. * * @method Phaser.Physics.Arcade.World#addCollider * @since 3.0.0 * @see Phaser.Physics.Arcade.World#collide * * @param {Phaser.Types.Physics.Arcade.ArcadeColliderType} body1 - The first object to check for collision. * @param {Phaser.Types.Physics.Arcade.ArcadeColliderType} body2 - The second object to check for collision. * @param {ArcadePhysicsCallback} [collideCallback] - The callback to invoke when the two objects collide. * @param {ArcadePhysicsCallback} [processCallback] - The callback to invoke when the two objects collide. Must return a boolean. * @param {*} [callbackContext] - The scope in which to call the callbacks. * * @return {Phaser.Physics.Arcade.Collider} The Collider that was created. */ addCollider(body1, body2, collideCallback, processCallback, callbackContext) { if (collideCallback === undefined) { collideCallback = null; } if (processCallback === undefined) { processCallback = null; } if (callbackContext === undefined) { callbackContext = collideCallback; } const collider = new Collider_1.Collider(this, false, body1, body2, collideCallback, processCallback, callbackContext); this.colliders.add(collider); return collider; } /** * Creates a new Overlap Collider object and adds it to the simulation. * * A Collider is a way to automatically perform overlap checks between two objects, * calling the collide and process callbacks if they occur. * * Colliders are run as part of the World update, after all of the Bodies have updated. * * By creating a Collider you don't need then call `World.overlap` in your `update` loop, * as it will be handled for you automatically. * * @method Phaser.Physics.Arcade.World#addOverlap * @since 3.0.0 * * @param {Phaser.Types.Physics.Arcade.ArcadeColliderType} body1 - The first object to check for overlap. * @param {Phaser.Types.Physics.Arcade.ArcadeColliderType} body2 - The second object to check for overlap. * @param {ArcadePhysicsCallback} [collideCallback] - The callback to invoke when the two objects overlap. * @param {ArcadePhysicsCallback} [processCallback] - The callback to invoke when the two objects overlap. Must return a boolean. * @param {*} [callbackContext] - The scope in which to call the callbacks. * * @return {Phaser.Physics.Arcade.Collider} The Collider that was created. */ addOverlap(body1, body2, collideCallback, processCallback, callbackContext) { if (collideCallback === undefined) { collideCallback = null; } if (processCallback === undefined) { processCallback = null; } if (callbackContext === undefined) { callbackContext = collideCallback; } const collider = new Collider_1.Collider(this, true, body1, body2, collideCallback, processCallback, callbackContext); this.colliders.add(collider); return collider; } /** * Removes a Collider from the simulation so it is no longer processed. * * This method does not destroy the Collider. If you wish to add it back at a later stage you can call * `World.colliders.add(Collider)`. * * If you no longer need the Collider you can call the `Collider.destroy` method instead, which will * automatically clear all of its references and then remove it from the World. If you call destroy on * a Collider you _don't_ need to pass it to this method too. * * @method Phaser.Physics.Arcade.World#removeCollider * @since 3.0.0 * * @param {Phaser.Physics.Arcade.Collider} collider - The Collider to remove from the simulation. * * @return {Phaser.Physics.Arcade.World} This World object. */ removeCollider(collider) { this.colliders.remove(collider); return this; } /** * Sets the frame rate to run the simulation at. * * The frame rate value is used to simulate a fixed update time step. This fixed * time step allows for a straightforward implementation of a deterministic game state. * * This frame rate is independent of the frequency at which the game is rendering. The * higher you set the fps, the more physics simulation steps will occur per game step. * Conversely, the lower you set it, the less will take place. * * You can optionally advance the simulation directly yourself by calling the `step` method. * * @method Phaser.Physics.Arcade.World#setFPS * @since 3.10.0 * * @param {number} framerate - The frame rate to advance the simulation at. * * @return {this} This World object. */ setFPS(framerate) { this.fps = framerate; this._frameTime = 1 / this.fps; this._frameTimeMS = 1000 * this._frameTime; return this; } /** * Advances the simulation based on the elapsed time and fps rate. * * This is called automatically by your Scene and does not need to be invoked directly. * * @method Phaser.Physics.Arcade.World#update * @fires Phaser.Physics.Arcade.Events#WORLD_STEP * @since 3.0.0 * * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout. * @param {number} delta - The delta time, in ms, elapsed since the last frame. */ update(time, delta) { if (this.isPaused || this.bodies.size === 0) { return; } let i; let fixedDelta = this._frameTime; const msPerFrame = this._frameTimeMS * this.timeScale; this._elapsed += delta; // Update all active bodies let body; const bodies = this.bodies; // Will a step happen this frame? let willStep = this._elapsed >= msPerFrame; if (!this.fixedStep) { fixedDelta = delta * 0.001; willStep = true; this._elapsed = 0; } for (const body of bodies) { if (body.enable) { body.preUpdate(willStep, fixedDelta); } } // We know that a step will happen this frame, so let's bundle it all together to save branching and iteration costs if (willStep) { this._elapsed -= msPerFrame; this.stepsLastFrame = 1; // Optionally populate our dynamic collision tree if (this.useTree) { this.tree.clear(); this.tree.load(Array.from(bodies)); } // Process any colliders const colliders = this.colliders.update(); for (i = 0; i < colliders.length; i++) { const collider = colliders[i]; if (collider.active) { collider.update(); } } this.emit(events_1.default.WORLD_STEP, fixedDelta); } // Process any additional steps this frame while (this._elapsed >= msPerFrame) { this._elapsed -= msPerFrame; this.step(fixedDelta); } } /** * Advances the simulation by a time increment. * * @method Phaser.Physics.Arcade.World#step * @fires Phaser.Physics.Arcade.Events#WORLD_STEP * @since 3.10.0 * * @param {number} delta - The delta time amount, in seconds, by which to advance the simulation. */ step(delta) { // Update all active bodies let i; let body; const bodies = this.bodies; const len = bodies.size; for (const body of bodies) { if (body.enable) { body.update(delta); } } // Optionally populate our dynamic collision tree if (this.useTree) { this.tree.clear(); this.tree.load(Array.from(bodies)); } // Process any colliders const colliders = this.colliders.update(); for (i = 0; i < colliders.length; i++) { const collider = colliders[i]; if (collider.active) { collider.update(); } } this.emit(events_1.default.WORLD_STEP, delta); this.stepsLastFrame++; } /** * Updates bodies, draws the debug display, and handles pending queue operations. * * @method Phaser.Physics.Arcade.World#postUpdate * @since 3.0.0 */ postUpdate() { let bodies = this.bodies; let len; const dynamic = this.bodies; const staticBodies = this.staticBodies; // We don't need to postUpdate if there wasn't a step this frame if (this.stepsLastFrame) { this.stepsLastFrame = 0; for (const body of bodies) { if (body.enable) { body.postUpdate(); } } } if (this.drawDebug) { const graphics = this.debugGraphic; graphics.clear(); for (const body of bodies) { if (body.willDrawDebug()) { body.drawDebug(graphics); } } bodies = staticBodies; len = bodies.size; for (const body of bodies) { if (body.willDrawDebug()) { body.drawDebug(graphics); } } } const pending = this.pendingDestroy; if (pending.size > 0) { const dynamicTree = this.tree; const staticTree = this.staticTree; bodies = pending; len = bodies.size; for (const body of bodies) { if (body.physicsType === const_1.default.PHYSICS_TYPE.DYNAMIC_BODY) { dynamicTree.remove(body); dynamic.delete(body); } else if (body.physicsType === const_1.default.PHYSICS_TYPE.STATIC_BODY) { staticTree.remove(body); staticBodies.delete(body); } // @ts-ignore body.world = undefined; // @ts-ignore body.gameObject = undefined; } pending.clear(); } } /** * Calculates a Body's velocity and updates its position. * * @method Phaser.Physics.Arcade.World#updateMotion * @since 3.0.0 * * @param {Phaser.Physics.Arcade.Body} body - The Body to be updated. * @param {number} delta - The delta value to be used in the motion calculations, in seconds. */ updateMotion(body, delta) { if (body.allowRotation) { this.computeAngularVelocity(body, delta); } this.computeVelocity(body, delta); } /** * Calculates a Body's angular velocity. * * @method Phaser.Physics.Arcade.World#computeAngularVelocity * @since 3.10.0 * * @param {Phaser.Physics.Arcade.Body} body - The Body to compute the velocity for. * @param {number} delta - The delta value to be used in the calculation, in seconds. */ computeAngularVelocity(body, delta) { let velocity = body.angularVelocity; const acceleration = body.angularAcceleration; let drag = body.angularDrag; const max = body.maxAngular; if (acceleration) { velocity += acceleration * delta; } else if (body.allowDrag && drag) { drag *= delta; if ((0, GreaterThan_1.default)(velocity - drag, 0, 0.1)) { velocity -= drag; } else if ((0, LessThan_1.default)(velocity + drag, 0, 0.1)) { velocity += drag; } else { velocity = 0; } } velocity = (0, Clamp_1.default)(velocity, -max, max); const velocityDelta = velocity - body.angularVelocity; body.angularVelocity += velocityDelta; body.rotation += body.angularVelocity * delta; } /** * Calculates a Body's per-axis velocity. * * @method Phaser.Physics.Arcade.World#computeVelocity * @since 3.0.0 * * @param {Phaser.Physics.Arcade.Body} body - The Body to compute the velocity for. * @param {number} delta - The delta value to be used in the calculation, in seconds. */ computeVelocity(body, delta) { let velocityX = body.velocity.x; const accelerationX = body.acceleration.x; let dragX = body.drag.x; const maxX = body.maxVelocity.x; let velocityY = body.velocity.y; const accelerationY = body.acceleration.y; let dragY = body.drag.y; const maxY = body.maxVelocity.y; let speed = body.speed; const maxSpeed = body.maxSpeed; const allowDrag = body.allowDrag; const useDamping = body.useDamping; if (body.allowGravity) { velocityX += (this.gravity.x + body.gravity.x) * delta; velocityY += (this.gravity.y + body.gravity.y) * delta; } if (accelerationX) { velocityX += accelerationX * delta; } else if (allowDrag && dragX) { if (useDamping) { // Damping based deceleration dragX = Math.pow(dragX, delta); velocityX *= dragX; speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); if ((0, Equal_1.default)(speed, 0, 0.001)) { velocityX = 0; } } else { // Linear deceleration dragX *= delta; if ((0, GreaterThan_1.default)(velocityX - dragX, 0, 0.01)) { velocityX -= dragX; } else if ((0, LessThan_1.default)(velocityX + dragX, 0, 0.01)) { velocityX += dragX; } else { velocityX = 0; } } } if (accelerationY) { velocityY += accelerationY * delta; } else if (allowDrag && dragY) { if (useDamping) { // Damping based deceleration dragY = Math.pow(dragY, delta); velocityY *= dragY; speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); if ((0, Equal_1.default)(speed, 0, 0.001)) { velocityY = 0; } } else { // Linear deceleration dragY *= delta; if ((0, GreaterThan_1.default)(velocityY - dragY, 0, 0.01)) { velocityY -= dragY; } else if ((0, LessThan_1.default)(velocityY + dragY, 0, 0.01)) { velocityY += dragY; } else { velocityY = 0; } } } velocityX = (0, Clamp_1.default)(velocityX, -maxX, maxX); velocityY = (0, Clamp_1.default)(velocityY, -maxY, maxY); body.velocity.set(velocityX, velocityY); if (maxSpeed > -1 && speed > maxSpeed) { body.velocity.normalize().scale(maxSpeed); speed = maxSpeed; } body.speed = speed; } /** * Separates two Bodies. * * @method Phaser.Physics.Arcade.World#separate * @fires Phaser.Physics.Arcade.Events#COLLIDE * @fires Phaser.Physics.Arcade.Events#OVERLAP * @since 3.0.0 * * @param {Phaser.Physics.Arcade.Body} body1 - The first Body to be separated. * @param {Phaser.Physics.Arcade.Body} body2 - The second Body to be separated. * @param {ArcadePhysicsCallback} [processCallback] - The process callback. * @param {*} [callbackContext] - The context in which to invoke the callback. * @param {boolean} [overlapOnly] - If this a collide or overlap check? * @param {boolean} [intersects] - Assert that the bodies intersect and should not be tested before separation. * * @return {boolean} True if separation occurred, otherwise false. */ separate(body1, body2, processCallback, callbackContext, overlapOnly, intersects) { if ((!intersects && !body1.enable) || !body2.enable || body1.checkCollision.none || body2.checkCollision.none || !this.intersects(body1, body2)) { return false; } // They overlap. Is there a custom process callback? If it returns true then we can carry on, otherwise we should abort. if (processCallback && processCallback.call(callbackContext, body1, body2) === false) { return false; } // Circle vs. Circle quick bail out if (body1.isCircle && body2.isCircle) { return this.separateCircle(body1, body2, overlapOnly); } // We define the behavior of bodies in a collision circle and rectangle // If a collision occurs in the corner points of the rectangle, the body behave like circles // Either body1 or body2 is a circle if (body1.isCircle !== body2.isCircle) { const bodyRect = body1.isCircle ? body2 : body1; const bodyCircle = body1.isCircle ? body1 : body2; const rect = { x: bodyRect.x, y: bodyRect.y, right: bodyRect.right, bottom: bodyRect.bottom }; const circle = bodyCircle.center; if (circle.y < rect.y || circle.y > rect.bottom) { if (circle.x < rect.x || circle.x > rect.right) { return this.separateCircle(body1, body2, overlapOnly); } } } let resultX = false; let resultY = false; // Do we separate on x first or y first or both? if (overlapOnly) { // No separation but we need to calculate overlapX, overlapY, etc. resultX = (0, SeparateX_1.SeparateX)(body1, body2, overlapOnly, this.OVERLAP_BIAS); resultY = (0, SeparateY_1.SeparateY)(body1, body2, overlapOnly, this.OVERLAP_BIAS); } else if (this.forceX || Math.abs(this.gravity.y + body1.gravity.y) < Math.abs(this.gravity.x + body1.gravity.x)) { resultX = (0, SeparateX_1.SeparateX)(body1, body2, overlapOnly, this.OVERLAP_BIAS); // Are they still intersecting? Let's do the other axis then if (this.intersects(body1, body2)) { resultY = (0, SeparateY_1.SeparateY)(body1, body2, overlapOnly, this.OVERLAP_BIAS); } } else { resultY = (0, SeparateY_1.SeparateY)(body1, body2, overlapOnly, this.OVERLAP_BIAS); // Are they still intersecting? Let's do the other axis then if (this.intersects(body1, body2)) { resultX = (0, SeparateX_1.SeparateX)(body1, body2, overlapOnly, this.OVERLAP_BIAS); } } const result = resultX || resultY; if (result) { if (overlapOnly) { if (body1.onOverlap || body2.onOverlap) { this.emit(events_1.default.OVERLAP, body1, body2); } } else if (body1.onCollide || body2.onCollide) { this.emit(events_1.default.COLLIDE, body1, body2); } } return result; } /** * Separates two Bodies, when both are circular. * * @method Phaser.Physics.Arcade.World#separateCircle * @fires Phaser.Physics.Arcade.Events#COLLIDE * @fires Phaser.Physics.Arcade.Events#OVERLAP * @since 3.0.0 * * @param {Phaser.Physics.Arcade.Body} body1 - The first Body to be separated. * @param {Phaser.Physics.Arcade.Body} body2 - The second Body to be separated. * @param {boolean} [overlapOnly] - If this a collide or overlap check? * @param {number} [bias] - A small value added to the calculations. * * @return {boolean} True if separation occurred, otherwise false. */ separateCircle(body1, body2, overlapOnly, bias) { // Set the bounding box overlap values into the bodies themselves (hence we don't use the return values here) (0, GetOverlapX_1.GetOverlapX)(body1, body2, false, bias); (0, GetOverlapY_1.GetOverlapY)(body1, body2, false, bias); let overlap = 0; if (body1.isCircle !== body2.isCircle) { const rect = { x: body2.isCircle ? body1.position.x : body2.position.x, y: body2.isCircle ? body1.position.y : body2.position.y, right: body2.isCircle ? body1.right : body2.right, bottom: body2.isCircle ? body1.bottom : body2.bottom }; const circle = { x: body1.isCircle ? body1.center.x : body2.center.x, y: body1.isCircle ? body1.center.y : body2.center.y, radius: body1.isCircle ? body1.halfWidth : body2.halfWidth }; if (circle.y < rect.y) { if (circle.x < rect.x) { overlap = (0, DistanceBetween_1.default)(circle.x, circle.y, rect.x, rect.y) - circle.radius; } else if (circle.x > rect.right) { overlap = (0, DistanceBetween_1.default)(circle.x, circle.y, rect.right, rect.y) - circle.radius; } } else if (circle.y > rect.bottom) { if (circle.x < rect.x) { overlap = (0, DistanceBetween_1.default)(circle.x, circle.y, rect.x, rect.bottom) - circle.radius; } else if (circle.x > rect.right) { overlap = (0, DistanceBetween_1.default)(circle.x, circle.y, rect.right, rect.bottom) - circle.radius; } } overlap *= -1; } else { overlap = body1.halfWidth + body2.halfWidth - (0, DistanceBetween_1.default)(body1.center.x, body1.center.y, body2.center.x, body2.center.y); } body1.overlapR = overlap; body2.overlapR = overlap; // Can't separate two immovable bodies, or a body with its own custom separation logic if (overlapOnly || overlap === 0 || (body1.immovable && body2.immovable) || body1.customSeparateX || body2.customSeparateX) { if (overlap !== 0 && (body1.onOverlap || body2.onOverlap)) { this.emit(events_1.default.OVERLAP, body1, body2); } // return true if there was some overlap, otherwise false return overlap !== 0; } const dx = body1.center.x - body2.center.x; const dy = body1.center.y - body2.center.y; const d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); const nx = (body2.center.x - body1.center.x) / d || 0; const ny = (body2.center.y - body1.center.y) / d || 0; let p = (2 * (body1.velocity.x * nx + body1.velocity.y * ny - body2.velocity.x * nx - body2.velocity.y * ny)) / (body1.mass + body2.mass); if (body1.immovable || body2.immovable) { p *= 2; } if (!body1.immovable) { body1.velocity.x = body1.velocity.x - (p / body1.mass) * nx; body1.velocity.y = body1.velocity.y - (p / body1.mass) * ny; } if (!body2.immovable) { body2.velocity.x = body2.velocity.x + (p / body2.mass) * nx; body2.velocity.y = body2.velocity.y + (p / body2.mass) * ny; } if (!body1.immovable && !body2.immovable) { overlap /= 2; } // Note: This is inadequate for circle-rectangle separation const angle = (0, BetweenPoints_1.default)(body1.center, body2.center); const overlapX = (overlap + const_2.default.EPSILON) * Math.cos(angle); const overlapY = (overlap + const_2.default.EPSILON) * Math.sin(angle); if (!body1.immovable) { body1.x -= overlapX; body1.y -= overlapY; body1.updateCenter(); } if (!body2.immovable) { body2.x += overlapX; body2.y += overlapY; body2.updateCenter(); } body1.velocity.x *= body1.bounce.x; body1.velocity.y *= body1.bounce.y; body2.velocity.x *= body2.bounce.x; body2.velocity.y *= body2.bounce.y; if (body1.onCollide || body2.onCollide) { this.emit(events_1.default.COLLIDE, body1, body2); } return true; } /** * Checks to see if two Bodies intersect at all. * * @method Phaser.Physics.Arcade.World#intersects * @since 3.0.0 * * @param {Phaser.Physics.Arcade.Body} body1 - The first body to check. * @param {Phaser.Physics.Arcade.Body} body2 - The second body to check. * * @return {boolean} True if the two bodies intersect, otherwise false. */ intersects(body1, body2) { if (body1 === body2) return false; if (!body1.isCircle && !body2.isCircle) { // Rect vs. Rect return !(body1.right <= body2.position.x || body1.bottom <= body2.position.y || body1.position.x >= body2.right || body1.position.y >= body2.bottom); } else if (body1.isCircle) { if (body2.isCircle) { // Circle vs. Circle return ((0, DistanceBetween_1.default)(body1.center.x, body1.center.y, body2.center.x, body2.center.y) <= body1.halfWidth + body2.halfWidth); } else { // Circle vs. Rect return this.circleBodyIntersects(body1, body2); } } else { // Rect vs. Circle return this.circleBodyIntersects(body2, body1); } } /** * Tests if a circular Body intersects with another Body. * * @method Phaser.Physics.Arcade.World#circleBodyIntersects * @since 3.0.0 * * @param {Phaser.Physics.Arcade.Body} circle - The circular body to test. * @param {Phaser.Physics.Arcade.Body} body - The rectangular body to test. * * @return {boolean} True if the two bodies intersect, otherwise false. */ circleBodyIntersects(circle, body) { const x = (0, Clamp_1.default)(circle.center.x, body.left, body.right); const y = (0, Clamp_1.default)(circle.center.y, body.top, body.bottom); const dx = (circle.center.x - x) * (circle.center.x - x); const dy = (circle.center.y - y) * (circle.center.y - y); return dx + dy <= circle.h