diff --git a/src/gameobjects/components/Path.js b/src/gameobjects/components/Path.js new file mode 100644 index 000000000..08fa57fb1 --- /dev/null +++ b/src/gameobjects/components/Path.js @@ -0,0 +1,418 @@ +/** + * @author Richard Davey + * @copyright 2018 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'); + +/** + * Settings for a PathFollower. + * + * @typedef {object} PathConfig + * + * @property {number} duration - The duration of the path follow. + * @property {number} from - The start position of the path follow, between 0 and 1. + * @property {number} to - The end position of the path follow, between 0 and 1. + * @property {boolean} [positionOnPath=false] - Whether to position the PathFollower on the Path using its path offset. + * @property {boolean} [rotateToPath=false] - Should the PathFollower automatically rotate to point in the direction of the Path? + * @property {number} [rotationOffset=0] - If the PathFollower is rotating to match the Path, 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. + * @property {boolean} [verticalAdjust=false] - [description] + */ + +/** + * Provides properties and methods used to allow a Game Object to follow a Path automatically. + * + * @name Phaser.GameObjects.Components.Transform + * @since 3.12.0 + */ +var Path = { + + /** + * The Path this PathFollower is following. It can only follow one Path at a time. + * + * @name Phaser.GameObjects.Components.Path#path + * @type {Phaser.Curves.Path} + * @since 3.12.0 + */ + path: null, + + /** + * Should the PathFollower automatically rotate to point in the direction of the Path? + * + * @name Phaser.GameObjects.Components.Path#rotateToPath + * @type {boolean} + * @default false + * @since 3.12.0 + */ + rotateToPath: false, + + /** + * [description] + * + * @name Phaser.GameObjects.Components.Path#pathRotationVerticalAdjust + * @type {boolean} + * @default false + * @since 3.12.0 + */ + pathRotationVerticalAdjust: false, + + /** + * If the PathFollower is rotating to match the Path (@see Phaser.GameObjects.Components.Path#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.Components.Path#pathRotationOffset + * @type {number} + * @default 0 + * @since 3.12.0 + */ + pathRotationOffset: 0, + + /** + * An additional vector to add to the PathFollowers position, allowing you to offset it from the + * Path coordinates. + * + * @name Phaser.GameObjects.Components.Path#pathOffset + * @type {Phaser.Math.Vector2} + * @since 3.12.0 + */ + pathOffset: { + + factory: function () + { + return new Vector2(); + } + + }, + + /** + * [description] + * + * @name Phaser.GameObjects.Components.Path#pathVector + * @type {Phaser.Math.Vector2} + * @since 3.12.0 + */ + pathVector: { + + factory: function () + { + return new Vector2(); + } + + }, + + /** + * The Tween used for following the Path. + * + * @name Phaser.GameObjects.Components.Path#pathTween + * @type {Phaser.Tweens.Tween} + * @since 3.12.0 + */ + pathTween: null, + + /** + * Settings for the PathFollower. + * + * @name Phaser.GameObjects.Components.Path#pathConfig + * @type {?PathConfig} + * @default null + * @since 3.12.0 + */ + pathConfig: null, + + /** + * Records the direction of the follower so it can change direction. + * + * @name Phaser.GameObjects.Components.Path#_prevDirection + * @type {integer} + * @private + * @since 3.12.0 + */ + _prevDirection: TWEEN_CONST.PLAYING_FORWARD, + + /** + * Set the Path that this PathFollower should follow. + * + * Optionally accepts {@link PathConfig} settings. + * + * @method Phaser.GameObjects.Components.Path#setPath + * @since 3.12.0 + * + * @param {Phaser.Curves.Path} path - The Path this PathFollower is following. It can only follow one Path at a time. + * @param {PathConfig} [config] - Settings for the PathFollower. + * + * @return {this} 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.Path#setRotateToPath + * @since 3.12.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. + * @param {boolean} [verticalAdjust=false] - [description] + * + * @return {this} This Game Object. + */ + setRotateToPath: function (value, offset, verticalAdjust) + { + if (offset === undefined) { offset = 0; } + if (verticalAdjust === undefined) { verticalAdjust = false; } + + this.rotateToPath = value; + + this.pathRotationOffset = offset; + this.pathRotationVerticalAdjust = verticalAdjust; + + 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.Path#isFollowing + * @since 3.12.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.Path#startFollow + * @since 3.12.0 + * + * @param {(number|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 {this} 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 = 0; + config.to = 1; + + // Can also read extra values out of the config: + + var positionOnPath = GetBoolean(config, 'positionOnPath', false); + + this.rotateToPath = GetBoolean(config, 'rotateToPath', false); + this.pathRotationOffset = GetValue(config, 'rotationOffset', 0); + this.pathRotationVerticalAdjust = GetBoolean(config, 'verticalAdjust', false); + + 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.Path#pauseFollow + * @since 3.12.0 + * + * @return {this} 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.Path#resumeFollow + * @since 3.12.0 + * + * @return {this} 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.Path#stopFollow + * @since 3.12.0 + * + * @return {this} 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.Path#preUpdate + * @protected + * @since 3.12.0 + * + * @param {integer} 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. + */ + 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); + + if (this.pathRotationVerticalAdjust) + { + this.flipY = (this.rotation !== 0 && tweenData.state === TWEEN_CONST.PLAYING_BACKWARD); + } + } + } + } + +}; + +module.exports = Path; diff --git a/src/gameobjects/components/index.js b/src/gameobjects/components/index.js index b6b038642..daa96b6c2 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'), + Path: require('./Path'), Pipeline: require('./Pipeline'), ScaleMode: require('./ScaleMode'), ScrollFactor: require('./ScrollFactor'), diff --git a/src/gameobjects/pathfollower/PathFollowerFactory.js b/src/gameobjects/pathfollower/PathFollowerFactory.js index b2698205c..6812ded6f 100644 --- a/src/gameobjects/pathfollower/PathFollowerFactory.js +++ b/src/gameobjects/pathfollower/PathFollowerFactory.js @@ -5,7 +5,23 @@ */ var GameObjectFactory = require('../GameObjectFactory'); -var PathFollower = require('./PathFollower'); +var Path = require('../components/Path'); +var Sprite = require('../sprite/Sprite'); + +/** + * [description] + * + * @function hasGetterOrSetter + * @private + * + * @param {object} def - The object to check. + * + * @return {boolean} True if it has a getter or setter, otherwise false. + */ +function hasGetterOrSetter (def) +{ + return def && def.constructor === Object && ((!!def.get && typeof def.get === 'function') || (!!def.set && typeof def.set === 'function')); +} /** * Creates a new PathFollower Game Object and adds it to the Scene. @@ -16,21 +32,65 @@ var PathFollower = require('./PathFollower'); * @since 3.0.0 * * @param {Phaser.Curves.Path} path - The Path this PathFollower is connected to. - * @param {number} x - The horizontal position of this Game Object in the world. - * @param {number} y - The vertical position of this Game Object in the world. - * @param {string} texture - The key of the Texture this Game Object will use to render with, as stored in the Texture Manager. + * @param {Phaser.GameObjects.GameObject|number} x - The GameObject to augment, or the horizontal position of this Game Object in the world. + * @param {number} [y] - The vertical position of this Game Object in the world. + * @param {string} [texture] - The key of the Texture this Game Object will use to render with, as stored in the Texture Manager. * @param {(string|integer)} [frame] - An optional frame from the Texture this Game Object is rendering with. * * @return {Phaser.GameObjects.PathFollower} The Game Object that was created. */ GameObjectFactory.register('follower', function (path, x, y, key, frame) { - var sprite = new PathFollower(this.scene, path, x, y, key, frame); + var gameObject = x; - this.displayList.add(sprite); - this.updateList.add(sprite); + // Create a sprite if x is not an object + if (typeof x !== 'object') + { + gameObject = new Sprite(this.scene, x, y, key, frame); + } - return sprite; + // Mixin the Path component + var mixins = [ + Path + ]; + + for (var i = 0; i < mixins.length; i++) + { + var mixin = mixins[i]; + + for (var m in mixin) + { + if (typeof mixin[m] === 'function') + { + Object.defineProperty(gameObject, m, {value: mixin[m]}); + } + else if (mixin[m] && typeof mixin[m].factory === 'function') + { + gameObject[m] = mixin[m].factory(); + } + else if (hasGetterOrSetter(mixin[m])) + { + Object.defineProperty(gameObject, key, { + get: mixin[key].get, + set: mixin[key].set + }); + } + else + { + gameObject[m] = mixin[m]; + } + } + } + + // Set the given arguments as properties + gameObject.path = path; + gameObject.pathOffset.set(x, y); + + // Add to the display list and update list + this.displayList.add(gameObject); + this.updateList.add(gameObject); + + return gameObject; }); // When registering a factory function 'this' refers to the GameObjectFactory context.