/** * @author Richard Davey * @copyright 2013-2023 Photon Storm Ltd. * @license {@link https://opensource.org/licenses/MIT|MIT License} */ var Class = require('../utils/Class'); var EventEmitter = require('eventemitter3'); var GameObjectFactory = require('../gameobjects/GameObjectFactory'); var GetFastValue = require('../utils/object/GetFastValue'); var SceneEvents = require('../scene/events'); /** * @classdesc * A Timeline is a way to schedule events to happen at specific times in the future. * * You can think of it as an event sequencer for your game, allowing you to schedule the * running of callbacks, events and other actions at specific times in the future. * * A Timeline is a Scene level system, meaning you can have as many Timelines as you like, each * belonging to a different Scene. You can also have multiple Timelines running at the same time. * * If the Scene is paused, the Timeline will also pause. If the Scene is destroyed, the Timeline * will be automatically destroyed. However, you can control the Timeline directly, pausing, * resuming and stopping it at any time. * * Create an instance of a Timeline via the Game Object Factory: * * ```js * const timeline = this.add.timeline(); * ``` * * The Timeline always starts paused. You must call `play` on it to start it running. * * You can also pass in a configuration object on creation, or an array of them: * * ```js * const timeline = this.add.timeline({ * at: 1000, * run: () => { * this.add.sprite(400, 300, 'logo'); * } * }); * * timeline.play(); * ``` * * In this example we sequence a few different events: * * ```js * const timeline = this.add.timeline([ * { * at: 1000, * run: () => { this.logo = this.add.sprite(400, 300, 'logo'); }, * sound: 'TitleMusic' * }, * { * at: 2500, * tween: { * targets: this.logo, * y: 600, * yoyo: true * }, * sound: 'Explode' * }, * { * at: 8000, * event: 'HURRY_PLAYER', * target: this.background, * set: { * tint: 0xff0000 * } * } * ]); * * timeline.play(); * ``` * * There are lots of options available to you via the configuration object. See the * {@link Phaser.Types.Time.TimelineEventConfig} typedef for more details. * * @class Timeline * @memberof Phaser.Time * @constructor * @since 3.60.0 * * @param {Phaser.Scene} scene - The Scene which owns this Timeline. * @param {Phaser.Types.Time.TimelineEventConfig|Phaser.Types.Time.TimelineEventConfig[]} config - The configuration object for this Timeline Event, or an array of them. */ var Timeline = new Class({ Extends: EventEmitter, initialize: function Timeline (scene, config) { EventEmitter.call(this); /** * The Scene to which this Timeline belongs. * * @name Phaser.Time.Timeline#scene * @type {Phaser.Scene} * @since 3.60.0 */ this.scene = scene; /** * A reference to the Scene Systems. * * @name Phaser.Time.Timeline#systems * @type {Phaser.Scenes.Systems} * @since 3.60.0 */ this.systems = scene.sys; /** * The elapsed time counter. * * Treat this as read-only. * * @name Phaser.Time.Timeline#elapsed * @type {number} * @since 3.60.0 */ this.elapsed = 0; /** * Whether the Timeline is running (`true`) or active (`false`). * * When paused, the Timeline will not run any of its actions. * * By default a Timeline is always paused and should be started by * calling the `Timeline.play` method. * * You can use the `Timeline.pause` and `Timeline.resume` methods to control * this value in a chainable way. * * @name Phaser.Time.Timeline#paused * @type {boolean} * @default true * @since 3.60.0 */ this.paused = true; /** * An array of all the Timeline Events. * * @name Phaser.Time.Timeline#events * @type {Phaser.Types.Time.TimelineEvent[]} * @since 3.60.0 */ this.events = []; var eventEmitter = this.systems.events; eventEmitter.on(SceneEvents.PRE_UPDATE, this.preUpdate, this); eventEmitter.on(SceneEvents.UPDATE, this.update, this); eventEmitter.once(SceneEvents.SHUTDOWN, this.destroy, this); if (config) { this.add(config); } }, /** * Updates the elapsed time counter, if this Timeline is not paused. * * @method Phaser.Time.Timeline#preUpdate * @since 3.60.0 * * @param {number} time - The current time. Either a High Resolution Timer value if it comes from Request Animation Frame, or Date.now if using SetTimeout. * @param {number} delta - The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ preUpdate: function (time, delta) { if (this.paused) { return; } this.elapsed += delta; }, /** * Called automatically by the Scene update step. * * Iterates through all of the Timeline Events and checks to see if they should be run. * * If they should be run, then the `TimelineEvent.action` callback is invoked. * * If the `TimelineEvent.once` property is `true` then the event is removed from the Timeline. * * If the `TimelineEvent.event` property is set then the Timeline emits that event. * * If the `TimelineEvent.run` property is set then the Timeline invokes that method. * * If the `TimelineEvent.target` property is set then the Timeline invokes the `run` method on that target. * * @method Phaser.Time.Timeline#update * @since 3.60.0 * * @param {number} time - The current time. Either a High Resolution Timer value if it comes from Request Animation Frame, or Date.now if using SetTimeout. * @param {number} delta - The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ update: function () { if (this.paused) { return; } var i; var events = this.events; var removeSweep = false; var sys = this.systems; var target; for (i = 0; i < events.length; i++) { var event = events[i]; if (!event.complete && event.time <= this.elapsed) { event.complete = true; if (event.once) { removeSweep = true; } if (event.set && event.target) { // set is an object of key value pairs, apply them to target for (var key in event.set) { event.target[key] = event.set[key]; } } if (event.tween) { sys.tweens.add(event.tween); } if (event.sound) { if (typeof event.sound === 'string') { sys.sound.play(event.sound); } else { sys.sound.play(event.sound.key, event.sound.config); } } target = (event.target) ? event.target : this; if (event.event) { this.emit(event.event, target); } if (event.run) { event.run.call(target); } } } if (removeSweep) { for (i = 0; i < events.length; i++) { if (events[i].complete && events[i].once) { events.splice(i, 1); i--; } } } }, /** * Starts this Timeline running. * * If the Timeline is already running and the `fromStart` parameter is `true`, * then calling this method will reset it the Timeline to the start. * * If you wish to resume a paused Timeline, then use the `Timeline.resume` method instead. * * @method Phaser.Time.Timeline#play * @since 3.60.0 * * @param {boolean} [fromStart=true] - Reset this Timeline back to the start before playing. * * @return {this} This Timeline instance. */ play: function (fromStart) { if (fromStart === undefined) { fromStart = true; } this.paused = false; if (fromStart) { this.reset(); } return this; }, /** * Pauses this Timeline. * * To resume it again, call the `Timeline.resume` method or set the `Timeline.paused` property to `false`. * * If the Timeline is paused while processing the current game step, then it * will carry on with all events that are due to run during that step and pause * from the next game step. * * @method Phaser.Time.Timeline#pause * @since 3.60.0 * * @return {this} This Timeline instance. */ pause: function () { this.paused = true; return this; }, /** * Resumes this Timeline from a paused state. * * The Timeline will carry on from where it left off. * * If you need to reset the Timeline to the start, then call the `Timeline.reset` method. * * @method Phaser.Time.Timeline#resume * @since 3.60.0 * * @return {this} This Timeline instance. */ resume: function () { this.paused = false; return this; }, /** * Resets this Timeline back to the start. * * This will set the elapsed time to zero and set all events to be incomplete. * * If the Timeline had any events that were set to `once` that have already * been removed, they will **not** be present again after calling this method. * * @method Phaser.Time.Timeline#reset * @since 3.60.0 * * @return {this} This Timeline instance. */ reset: function () { this.elapsed = 0; for (var i = 0; i < this.events.length; i++) { this.events[i].complete = false; } return this; }, /** * Adds one or more events to this Timeline. * * * @method Phaser.Time.Timeline#add * @since 3.60.0 * * @param {Phaser.Types.Time.TimelineEventConfig|Phaser.Types.Time.TimelineEventConfig[]} config - The configuration object for this Timeline Event, or an array of them. * * @return {this} This Timeline instance. */ add: function (config) { if (!Array.isArray(config)) { config = [ config ]; } var events = this.events; var prevTime = 0; if (events.length > 0) { prevTime = events[events.length - 1].time; } for (var i = 0; i < config.length; i++) { var entry = config[i]; // Start at the exact time given, based on elapsed time (i.e. x ms from the start of the Timeline) var startTime = GetFastValue(entry, 'at', 0); // Start in x ms from whatever the current elapsed time is (i.e. x ms from now) var offsetTime = GetFastValue(entry, 'in', null); if (offsetTime !== null) { startTime = this.elapsed + offsetTime; } // Start in x ms from whatever the previous event's start time was (i.e. x ms after the previous event) var fromTime = GetFastValue(entry, 'from', null); if (fromTime !== null) { startTime = prevTime + fromTime; } events.push({ complete: false, time: startTime, run: GetFastValue(entry, 'run', null), event: GetFastValue(entry, 'event', null), target: GetFastValue(entry, 'target', null), set: GetFastValue(entry, 'set', null), tween: GetFastValue(entry, 'tween', null), sound: GetFastValue(entry, 'sound', null), once: GetFastValue(entry, 'once', false) }); prevTime = startTime; } return this; }, /** * Removes all events from this Timeline, resets the elapsed time to zero * and pauses the Timeline. * * @method Phaser.Time.Timeline#clear * @since 3.60.0 * * @return {this} This Timeline instance. */ clear: function () { this.events = []; this.elapsed = 0; this.paused = true; return this; }, /** * Destroys this Timeline. * * This will remove all events from the Timeline and stop it from processing. * * This method is called automatically when the Scene shuts down, but you may * also call it directly should you need to destroy the Timeline earlier. * * @method Phaser.Time.Timeline#destroy * @since 3.60.0 */ destroy: function () { var eventEmitter = this.systems.events; eventEmitter.off(SceneEvents.PRE_UPDATE, this.preUpdate, this); eventEmitter.off(SceneEvents.UPDATE, this.update, this); eventEmitter.off(SceneEvents.SHUTDOWN, this.destroy, this); this.scene = null; this.systems = null; this.events = []; } }); /** * A Timeline is a way to schedule events to happen at specific times in the future. * * You can think of it as an event sequencer for your game, allowing you to schedule the * running of callbacks, events and other actions at specific times in the future. * * A Timeline is a Scene level system, meaning you can have as many Timelines as you like, each * belonging to a different Scene. You can also have multiple Timelines running at the same time. * * If the Scene is paused, the Timeline will also pause. If the Scene is destroyed, the Timeline * will be automatically destroyed. However, you can control the Timeline directly, pausing, * resuming and stopping it at any time. * * Create an instance of a Timeline via the Game Object Factory: * * ```js * const timeline = this.add.timeline(); * ``` * * The Timeline always starts paused. You must call `play` on it to start it running. * * You can also pass in a configuration object on creation, or an array of them: * * ```js * const timeline = this.add.timeline({ * at: 1000, * run: () => { * this.add.sprite(400, 300, 'logo'); * } * }); * * timeline.play(); * ``` * * In this example we sequence a few different events: * * ```js * const timeline = this.add.timeline([ * { * at: 1000, * run: () => { this.logo = this.add.sprite(400, 300, 'logo'); }, * sound: 'TitleMusic' * }, * { * at: 2500, * tween: { * targets: this.logo, * y: 600, * yoyo: true * }, * sound: 'Explode' * }, * { * at: 8000, * event: 'HURRY_PLAYER', * target: this.background, * set: { * tint: 0xff0000 * } * } * ]); * * timeline.play(); * ``` * * There are lots of options available to you via the configuration object. See the * {@link Phaser.Types.Time.TimelineEventConfig} typedef for more details. * * @method Phaser.GameObjects.GameObjectFactory#timeline * @since 3.60.0 * * @param {Phaser.Types.Time.TimelineEventConfig|Phaser.Types.Time.TimelineEventConfig[]} config - The configuration object for this Timeline Event, or an array of them. * * @return {Phaser.Time.Timeline} The Timeline that was created. */ GameObjectFactory.register('timeline', function (config) { return new Timeline(this.scene, config); }); module.exports = Timeline;