diff --git a/src/gameobjects/components/PathFollower.js b/src/gameobjects/components/PathFollower.js new file mode 100644 index 000000000..e356e05ab --- /dev/null +++ b/src/gameobjects/components/PathFollower.js @@ -0,0 +1,392 @@ +/** + * @author Richard Davey + * @copyright 2019 Photon Storm Ltd. + * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} + */ + +var DegToRad = require('../../math/DegToRad'); +var GetBoolean = require('../../tweens/builders/GetBoolean'); +var GetValue = require('../../utils/object/GetValue'); +var TWEEN_CONST = require('../../tweens/tween/const'); +var Vector2 = require('../../math/Vector2'); + +/** + * Provides methods used for managing a Game Object following a Path. + * Should be applied as a mixin and not used directly. + * + * @namespace Phaser.GameObjects.Components.PathFollower + * @since 3.17.0 + */ + +var PathFollower = { + + /** + * The Path this PathFollower is following. It can only follow one Path at a time. + * + * @name Phaser.GameObjects.Components.PathFollower#path + * @type {Phaser.Curves.Path} + * @since 3.0.0 + */ + path: null, + + /** + * Should the PathFollower automatically rotate to point in the direction of the Path? + * + * @name Phaser.GameObjects.Components.PathFollower#rotateToPath + * @type {boolean} + * @default false + * @since 3.0.0 + */ + rotateToPath: false, + + /** + * If the PathFollower is rotating to match the Path (@see Phaser.GameObjects.PathFollower#rotateToPath) + * this value is added to the rotation value. This allows you to rotate objects to a path but control + * the angle of the rotation as well. + * + * @name Phaser.GameObjects.PathFollower#pathRotationOffset + * @type {number} + * @default 0 + * @since 3.0.0 + */ + pathRotationOffset: 0, + + /** + * An additional vector to add to the PathFollowers position, allowing you to offset it from the + * Path coordinates. + * + * @name Phaser.GameObjects.PathFollower#pathOffset + * @type {Phaser.Math.Vector2} + * @since 3.0.0 + */ + pathOffset: null, + + /** + * A Vector2 that stores the current point of the path the follower is on. + * + * @name Phaser.GameObjects.PathFollower#pathVector + * @type {Phaser.Math.Vector2} + * @since 3.0.0 + */ + pathVector: null, + + /** + * The Tween used for following the Path. + * + * @name Phaser.GameObjects.PathFollower#pathTween + * @type {Phaser.Tweens.Tween} + * @since 3.0.0 + */ + pathTween: null, + + /** + * Settings for the PathFollower. + * + * @name Phaser.GameObjects.PathFollower#pathConfig + * @type {?Phaser.GameObjects.PathFollower.Types.PathConfig} + * @default null + * @since 3.0.0 + */ + pathConfig: null, + + /** + * Records the direction of the follower so it can change direction. + * + * @name Phaser.GameObjects.PathFollower#_prevDirection + * @type {integer} + * @private + * @since 3.0.0 + */ + _prevDirection: TWEEN_CONST.PLAYING_FORWARD, + + /** + * Set the Path that this PathFollower should follow. + * + * Optionally accepts {@link Phaser.GameObjects.PathFollower.Types.PathConfig} settings. + * + * @method Phaser.GameObjects.Components.PathFollower#setPath + * @since 3.0.0 + * + * @param {Phaser.Curves.Path} path - The Path this PathFollower is following. It can only follow one Path at a time. + * @param {Phaser.GameObjects.PathFollower.Types.PathConfig} [config] - Settings for the PathFollower. + * + * @return {Phaser.GameObjects.PathFollower} This Game Object. + */ + setPath: function (path, config) + { + if (config === undefined) { config = this.pathConfig; } + + var tween = this.pathTween; + + if (tween && tween.isPlaying()) + { + tween.stop(); + } + + this.path = path; + + if (config) + { + this.startFollow(config); + } + + return this; + }, + + /** + * Set whether the PathFollower should automatically rotate to point in the direction of the Path. + * + * @method Phaser.GameObjects.Components.PathFollower#setRotateToPath + * @since 3.0.0 + * + * @param {boolean} value - Whether the PathFollower should automatically rotate to point in the direction of the Path. + * @param {number} [offset=0] - Rotation offset in degrees. + * + * @return {Phaser.GameObjects.PathFollower} This Game Object. + */ + setRotateToPath: function (value, offset) + { + if (offset === undefined) { offset = 0; } + + this.rotateToPath = value; + + this.pathRotationOffset = offset; + + return this; + }, + + /** + * Is this PathFollower actively following a Path or not? + * + * To be considered as `isFollowing` it must be currently moving on a Path, and not paused. + * + * @method Phaser.GameObjects.Components.PathFollower#isFollowing + * @since 3.0.0 + * + * @return {boolean} `true` is this PathFollower is actively following a Path, otherwise `false`. + */ + isFollowing: function () + { + var tween = this.pathTween; + + return (tween && tween.isPlaying()); + }, + + /** + * Starts this PathFollower following its given Path. + * + * @method Phaser.GameObjects.Components.PathFollower#startFollow + * @since 3.3.0 + * + * @param {(number|Phaser.GameObjects.PathFollower.Types.PathConfig)} [config={}] - The duration of the follow, or a PathFollower config object. + * @param {number} [startAt=0] - Optional start position of the follow, between 0 and 1. + * + * @return {Phaser.GameObjects.PathFollower} This Game Object. + */ + startFollow: function (config, startAt) + { + if (config === undefined) { config = {}; } + if (startAt === undefined) { startAt = 0; } + + var tween = this.pathTween; + + if (tween && tween.isPlaying()) + { + tween.stop(); + } + + if (typeof config === 'number') + { + config = { duration: config }; + } + + // Override in case they've been specified in the config + config.from = GetValue(config, 'from', 0); + config.to = GetValue(config, 'to', 1); + + var positionOnPath = GetBoolean(config, 'positionOnPath', false); + + this.rotateToPath = GetBoolean(config, 'rotateToPath', false); + this.pathRotationOffset = GetValue(config, 'rotationOffset', 0); + + // This works, but it's not an ideal way of doing it as the follower jumps position + var seek = GetValue(config, 'startAt', startAt); + + if (seek) + { + config.onStart = function (tween) + { + var tweenData = tween.data[0]; + tweenData.progress = seek; + tweenData.elapsed = tweenData.duration * seek; + var v = tweenData.ease(tweenData.progress); + tweenData.current = tweenData.start + ((tweenData.end - tweenData.start) * v); + tweenData.target[tweenData.key] = tweenData.current; + }; + } + + if (!this.pathOffset) + { + this.pathOffset = new Vector2(this.x, this.y); + } + + if (!this.pathVector) + { + this.pathVector = new Vector2(); + } + + this.pathTween = this.scene.sys.tweens.addCounter(config); + + // The starting point of the path, relative to this follower + this.path.getStartPoint(this.pathOffset); + + if (positionOnPath) + { + this.x = this.pathOffset.x; + this.y = this.pathOffset.y; + } + + this.pathOffset.x = this.x - this.pathOffset.x; + this.pathOffset.y = this.y - this.pathOffset.y; + + this._prevDirection = TWEEN_CONST.PLAYING_FORWARD; + + if (this.rotateToPath) + { + // Set the rotation now (in case the tween has a delay on it, etc) + var nextPoint = this.path.getPoint(0.1); + + this.rotation = Math.atan2(nextPoint.y - this.y, nextPoint.x - this.x) + DegToRad(this.pathRotationOffset); + } + + this.pathConfig = config; + + return this; + }, + + /** + * Pauses this PathFollower. It will still continue to render, but it will remain motionless at the + * point on the Path at which you paused it. + * + * @method Phaser.GameObjects.Components.PathFollower#pauseFollow + * @since 3.3.0 + * + * @return {Phaser.GameObjects.PathFollower} This Game Object. + */ + pauseFollow: function () + { + var tween = this.pathTween; + + if (tween && tween.isPlaying()) + { + tween.pause(); + } + + return this; + }, + + /** + * Resumes a previously paused PathFollower. + * + * If the PathFollower was not paused this has no effect. + * + * @method Phaser.GameObjects.Components.PathFollower#resumeFollow + * @since 3.3.0 + * + * @return {Phaser.GameObjects.PathFollower} This Game Object. + */ + resumeFollow: function () + { + var tween = this.pathTween; + + if (tween && tween.isPaused()) + { + tween.resume(); + } + + return this; + }, + + /** + * Stops this PathFollower from following the path any longer. + * + * This will invoke any 'stop' conditions that may exist on the Path, or for the follower. + * + * @method Phaser.GameObjects.Components.PathFollower#stopFollow + * @since 3.3.0 + * + * @return {Phaser.GameObjects.PathFollower} This Game Object. + */ + stopFollow: function () + { + var tween = this.pathTween; + + if (tween && tween.isPlaying()) + { + tween.stop(); + } + + return this; + }, + + /** + * Internal update handler that advances this PathFollower along the path. + * + * Called automatically by the Scene step, should not typically be called directly. + * + * @method Phaser.GameObjects.Components.PathFollower#pathUpdate + * @protected + * @since 3.17.0 + */ + pathUpdate: function () + { + var tween = this.pathTween; + + if (tween) + { + var tweenData = tween.data[0]; + + if (tweenData.state !== TWEEN_CONST.PLAYING_FORWARD && tweenData.state !== TWEEN_CONST.PLAYING_BACKWARD) + { + // If delayed, etc then bail out + return; + } + + var pathVector = this.pathVector; + + this.path.getPoint(tween.getValue(), pathVector); + + pathVector.add(this.pathOffset); + + var oldX = this.x; + var oldY = this.y; + + this.setPosition(pathVector.x, pathVector.y); + + var speedX = this.x - oldX; + var speedY = this.y - oldY; + + if (speedX === 0 && speedY === 0) + { + // Bail out early + return; + } + + if (tweenData.state !== this._prevDirection) + { + // We've changed direction, so don't do a rotate this frame + this._prevDirection = tweenData.state; + + return; + } + + if (this.rotateToPath) + { + this.rotation = Math.atan2(speedY, speedX) + DegToRad(this.pathRotationOffset); + } + } + } + +}; + +module.exports = PathFollower; diff --git a/src/gameobjects/components/index.js b/src/gameobjects/components/index.js index c7cbf2158..3415d0f8e 100644 --- a/src/gameobjects/components/index.js +++ b/src/gameobjects/components/index.js @@ -20,6 +20,7 @@ module.exports = { GetBounds: require('./GetBounds'), Mask: require('./Mask'), Origin: require('./Origin'), + PathFollower: require('./PathFollower'), Pipeline: require('./Pipeline'), ScrollFactor: require('./ScrollFactor'), Size: require('./Size'), diff --git a/src/gameobjects/pathfollower/PathFollower.js b/src/gameobjects/pathfollower/PathFollower.js index 0ce952a77..af62ad653 100644 --- a/src/gameobjects/pathfollower/PathFollower.js +++ b/src/gameobjects/pathfollower/PathFollower.js @@ -5,12 +5,8 @@ */ var Class = require('../../utils/Class'); -var DegToRad = require('../../math/DegToRad'); -var GetBoolean = require('../../tweens/builders/GetBoolean'); -var GetValue = require('../../utils/object/GetValue'); +var Components = require('../components'); var Sprite = require('../sprite/Sprite'); -var TWEEN_CONST = require('../../tweens/tween/const'); -var Vector2 = require('../../math/Vector2'); /** * @classdesc @@ -30,6 +26,8 @@ var Vector2 = require('../../math/Vector2'); * @memberof Phaser.GameObjects * @constructor * @since 3.0.0 + * + * @extends Phaser.GameObjects.Components.PathFollower * * @param {Phaser.Scene} scene - The Scene to which this PathFollower belongs. * @param {Phaser.Curves.Path} path - The Path this PathFollower is following. It can only follow one Path at a time. @@ -42,310 +40,17 @@ var PathFollower = new Class({ Extends: Sprite, + Mixins: [ + Components.PathFollower + ], + initialize: function PathFollower (scene, path, x, y, texture, frame) { Sprite.call(this, scene, x, y, texture, frame); - /** - * The Path this PathFollower is following. It can only follow one Path at a time. - * - * @name Phaser.GameObjects.PathFollower#path - * @type {Phaser.Curves.Path} - * @since 3.0.0 - */ this.path = path; - - /** - * Should the PathFollower automatically rotate to point in the direction of the Path? - * - * @name Phaser.GameObjects.PathFollower#rotateToPath - * @type {boolean} - * @default false - * @since 3.0.0 - */ - this.rotateToPath = false; - - /** - * If the PathFollower is rotating to match the Path (@see Phaser.GameObjects.PathFollower#rotateToPath) - * this value is added to the rotation value. This allows you to rotate objects to a path but control - * the angle of the rotation as well. - * - * @name Phaser.GameObjects.PathFollower#pathRotationOffset - * @type {number} - * @default 0 - * @since 3.0.0 - */ - this.pathRotationOffset = 0; - - /** - * An additional vector to add to the PathFollowers position, allowing you to offset it from the - * Path coordinates. - * - * @name Phaser.GameObjects.PathFollower#pathOffset - * @type {Phaser.Math.Vector2} - * @since 3.0.0 - */ - this.pathOffset = new Vector2(x, y); - - /** - * A Vector2 that stores the current point of the path the follower is on. - * - * @name Phaser.GameObjects.PathFollower#pathVector - * @type {Phaser.Math.Vector2} - * @since 3.0.0 - */ - this.pathVector = new Vector2(); - - /** - * The Tween used for following the Path. - * - * @name Phaser.GameObjects.PathFollower#pathTween - * @type {Phaser.Tweens.Tween} - * @since 3.0.0 - */ - this.pathTween; - - /** - * Settings for the PathFollower. - * - * @name Phaser.GameObjects.PathFollower#pathConfig - * @type {?Phaser.GameObjects.PathFollower.Types.PathConfig} - * @default null - * @since 3.0.0 - */ - this.pathConfig = null; - - /** - * Records the direction of the follower so it can change direction. - * - * @name Phaser.GameObjects.PathFollower#_prevDirection - * @type {integer} - * @private - * @since 3.0.0 - */ - this._prevDirection = TWEEN_CONST.PLAYING_FORWARD; - }, - - /** - * Set the Path that this PathFollower should follow. - * - * Optionally accepts {@link Phaser.GameObjects.PathFollower.Types.PathConfig} settings. - * - * @method Phaser.GameObjects.PathFollower#setPath - * @since 3.0.0 - * - * @param {Phaser.Curves.Path} path - The Path this PathFollower is following. It can only follow one Path at a time. - * @param {Phaser.GameObjects.PathFollower.Types.PathConfig} [config] - Settings for the PathFollower. - * - * @return {Phaser.GameObjects.PathFollower} This Game Object. - */ - setPath: function (path, config) - { - if (config === undefined) { config = this.pathConfig; } - - var tween = this.pathTween; - - if (tween && tween.isPlaying()) - { - tween.stop(); - } - - this.path = path; - - if (config) - { - this.startFollow(config); - } - - return this; - }, - - /** - * Set whether the PathFollower should automatically rotate to point in the direction of the Path. - * - * @method Phaser.GameObjects.PathFollower#setRotateToPath - * @since 3.0.0 - * - * @param {boolean} value - Whether the PathFollower should automatically rotate to point in the direction of the Path. - * @param {number} [offset=0] - Rotation offset in degrees. - * - * @return {Phaser.GameObjects.PathFollower} This Game Object. - */ - setRotateToPath: function (value, offset) - { - if (offset === undefined) { offset = 0; } - - this.rotateToPath = value; - - this.pathRotationOffset = offset; - - return this; - }, - - /** - * Is this PathFollower actively following a Path or not? - * - * To be considered as `isFollowing` it must be currently moving on a Path, and not paused. - * - * @method Phaser.GameObjects.PathFollower#isFollowing - * @since 3.0.0 - * - * @return {boolean} `true` is this PathFollower is actively following a Path, otherwise `false`. - */ - isFollowing: function () - { - var tween = this.pathTween; - - return (tween && tween.isPlaying()); - }, - - /** - * Starts this PathFollower following its given Path. - * - * @method Phaser.GameObjects.PathFollower#startFollow - * @since 3.3.0 - * - * @param {(number|Phaser.GameObjects.PathFollower.Types.PathConfig)} [config={}] - The duration of the follow, or a PathFollower config object. - * @param {number} [startAt=0] - Optional start position of the follow, between 0 and 1. - * - * @return {Phaser.GameObjects.PathFollower} This Game Object. - */ - startFollow: function (config, startAt) - { - if (config === undefined) { config = {}; } - if (startAt === undefined) { startAt = 0; } - - var tween = this.pathTween; - - if (tween && tween.isPlaying()) - { - tween.stop(); - } - - if (typeof config === 'number') - { - config = { duration: config }; - } - - // Override in case they've been specified in the config - config.from = GetValue(config, 'from', 0); - config.to = GetValue(config, 'to', 1); - - var positionOnPath = GetBoolean(config, 'positionOnPath', false); - - this.rotateToPath = GetBoolean(config, 'rotateToPath', false); - this.pathRotationOffset = GetValue(config, 'rotationOffset', 0); - - // This works, but it's not an ideal way of doing it as the follower jumps position - var seek = GetValue(config, 'startAt', startAt); - - if (seek) - { - config.onStart = function (tween) - { - var tweenData = tween.data[0]; - tweenData.progress = seek; - tweenData.elapsed = tweenData.duration * seek; - var v = tweenData.ease(tweenData.progress); - tweenData.current = tweenData.start + ((tweenData.end - tweenData.start) * v); - tweenData.target[tweenData.key] = tweenData.current; - }; - } - - this.pathTween = this.scene.sys.tweens.addCounter(config); - - // The starting point of the path, relative to this follower - this.path.getStartPoint(this.pathOffset); - - if (positionOnPath) - { - this.x = this.pathOffset.x; - this.y = this.pathOffset.y; - } - - this.pathOffset.x = this.x - this.pathOffset.x; - this.pathOffset.y = this.y - this.pathOffset.y; - - this._prevDirection = TWEEN_CONST.PLAYING_FORWARD; - - if (this.rotateToPath) - { - // Set the rotation now (in case the tween has a delay on it, etc) - var nextPoint = this.path.getPoint(0.1); - - this.rotation = Math.atan2(nextPoint.y - this.y, nextPoint.x - this.x) + DegToRad(this.pathRotationOffset); - } - - this.pathConfig = config; - - return this; - }, - - /** - * Pauses this PathFollower. It will still continue to render, but it will remain motionless at the - * point on the Path at which you paused it. - * - * @method Phaser.GameObjects.PathFollower#pauseFollow - * @since 3.3.0 - * - * @return {Phaser.GameObjects.PathFollower} This Game Object. - */ - pauseFollow: function () - { - var tween = this.pathTween; - - if (tween && tween.isPlaying()) - { - tween.pause(); - } - - return this; - }, - - /** - * Resumes a previously paused PathFollower. - * - * If the PathFollower was not paused this has no effect. - * - * @method Phaser.GameObjects.PathFollower#resumeFollow - * @since 3.3.0 - * - * @return {Phaser.GameObjects.PathFollower} This Game Object. - */ - resumeFollow: function () - { - var tween = this.pathTween; - - if (tween && tween.isPaused()) - { - tween.resume(); - } - - return this; - }, - - /** - * Stops this PathFollower from following the path any longer. - * - * This will invoke any 'stop' conditions that may exist on the Path, or for the follower. - * - * @method Phaser.GameObjects.PathFollower#stopFollow - * @since 3.3.0 - * - * @return {Phaser.GameObjects.PathFollower} This Game Object. - */ - stopFollow: function () - { - var tween = this.pathTween; - - if (tween && tween.isPlaying()) - { - tween.stop(); - } - - return this; }, /** @@ -363,52 +68,7 @@ var PathFollower = new Class({ preUpdate: function (time, delta) { this.anims.update(time, delta); - - var tween = this.pathTween; - - if (tween) - { - var tweenData = tween.data[0]; - - if (tweenData.state !== TWEEN_CONST.PLAYING_FORWARD && tweenData.state !== TWEEN_CONST.PLAYING_BACKWARD) - { - // If delayed, etc then bail out - return; - } - - var pathVector = this.pathVector; - - this.path.getPoint(tween.getValue(), pathVector); - - pathVector.add(this.pathOffset); - - var oldX = this.x; - var oldY = this.y; - - this.setPosition(pathVector.x, pathVector.y); - - var speedX = this.x - oldX; - var speedY = this.y - oldY; - - if (speedX === 0 && speedY === 0) - { - // Bail out early - return; - } - - if (tweenData.state !== this._prevDirection) - { - // We've changed direction, so don't do a rotate this frame - this._prevDirection = tweenData.state; - - return; - } - - if (this.rotateToPath) - { - this.rotation = Math.atan2(speedY, speedX) + DegToRad(this.pathRotationOffset); - } - } + this.pathUpdate(time); } });