phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
881 lines (782 loc) • 25.5 kB
JavaScript
/**
* @author Richard Davey <rich@phaser.io>
* @copyright 2013-2025 Phaser Studio Inc.
* @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');
var Events = require('./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();
* ```
*
* The Timeline can also be looped with the repeat method:
* ```js
* timeline.repeat().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
* @extends Phaser.Events.EventEmitter
* @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;
/**
* The Timeline's delta time scale.
*
* Values higher than 1 increase the speed of time, while values smaller than 1 decrease it.
* A value of 0 freezes time and is effectively equivalent to pausing the Timeline.
*
* This doesn't affect the delta time scale of any Tweens created by the Timeline.
* You will have to set the `timeScale` of each Tween or the Tween Manager if you want them to match.
*
* @name Phaser.Time.Timeline#timeScale
* @type {number}
* @default
* @since 3.85.0
*/
this.timeScale = 1;
/**
* 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;
/**
* Whether the Timeline is complete (`true`) or not (`false`).
*
* A Timeline is considered complete when all of its events have been run.
*
* If you wish to reset a Timeline after it has completed, you can do so
* by calling the `Timeline.reset` method.
*
* You can also use the `Timeline.stop` method to stop a running Timeline,
* at any point, without resetting it.
*
* @name Phaser.Time.Timeline#complete
* @type {boolean}
* @default false
* @since 3.60.0
*/
this.complete = false;
/**
* The total number of events that have been run.
*
* This value is reset to zero if the Timeline is restarted.
*
* Treat this as read-only.
*
* @name Phaser.Time.Timeline#totalComplete
* @type {number}
* @since 3.60.0
*/
this.totalComplete = 0;
/**
* The number of times this timeline should loop.
*
* If this value is -1 or any negative number this Timeline will not stop.
*
* @name Phaser.Time.Timeline#loop
* @type {number}
* @since 3.80.0
*/
this.loop = 0;
/**
* The number of times this Timeline has looped.
*
* This value is incremented each loop if looping is enabled.
*
* @name Phaser.Time.Timeline#iteration
* @type {number}
* @since 3.80.0
*/
this.iteration = 0;
/**
* 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 * this.timeScale;
},
/**
* 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.loop` property is set then the Timeline invokes that method when repeated.
*
* If the `TimelineEvent.target` property is set then the Timeline invokes the `run` method on that target.
*
* @method Phaser.Time.Timeline#update
* @fires Phaser.Time.Events#COMPLETE
* @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 || this.complete)
{
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;
this.totalComplete++;
target = (event.target) ? event.target : this;
if (event.if)
{
if (!event.if.call(target, event))
{
continue;
}
}
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 (this.iteration)
{
event.repeat++;
}
if (event.loop && event.repeat)
{
event.loop.call(target);
}
if (event.tween)
{
event.tweenInstance = 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);
}
}
if (event.event)
{
this.emit(event.event, target);
}
if (event.run)
{
event.run.call(target);
}
if (event.stop)
{
this.stop();
}
}
}
if (removeSweep)
{
for (i = 0; i < events.length; i++)
{
if (events[i].complete && events[i].once)
{
events.splice(i, 1);
i--;
}
}
}
// It may be greater than the length if events have been removed
if (this.totalComplete >= events.length)
{
if (this.loop !== 0 && (this.loop === -1 || this.loop > this.iteration))
{
this.iteration++;
this.reset(true);
}
else
{
this.complete = true;
}
}
if (this.complete)
{
this.emit(Events.COMPLETE, this);
}
},
/**
* Starts this Timeline running.
*
* If the Timeline is already running and the `fromStart` parameter is `true`,
* then calling this method will reset the Timeline events as incomplete.
*
* 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;
this.complete = false;
this.totalComplete = 0;
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.
*
* Note that if any Tweens have been started prior to calling this method, they will **not** be paused as well.
*
* @method Phaser.Time.Timeline#pause
* @since 3.60.0
*
* @return {this} This Timeline instance.
*/
pause: function ()
{
this.paused = true;
var events = this.events;
for (var i = 0; i < events.length; i++)
{
var event = events[i];
if (event.tweenInstance)
{
event.tweenInstance.paused = true;
}
}
return this;
},
/**
* Repeats this Timeline.
*
* If the value for `amount` is positive, the Timeline will repeat that many additional times.
* For example a value of 1 will actually run this Timeline twice.
*
* Depending on the value given, `false` is 0 and `true`, undefined and negative numbers are infinite.
*
* If this Timeline had any events set to `once` that have already been removed,
* they will **not** be repeated each loop.
*
* @method Phaser.Time.Timeline#repeat
* @since 3.80.0
*
* @param {number|boolean} [amount=-1] - Amount of times to repeat, if `true` or negative it will be infinite.
*
* @return {this} This Timeline instance.
*/
repeat: function (amount)
{
if (amount === undefined || amount === true) { amount = -1; }
if (amount === false) { amount = 0; }
this.loop = amount;
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;
var events = this.events;
for (var i = 0; i < events.length; i++)
{
var event = events[i];
if (event.tweenInstance)
{
event.tweenInstance.paused = false;
}
}
return this;
},
/**
* Stops this Timeline.
*
* This will set the `paused` and `complete` properties to `true`.
*
* If you wish to reset the Timeline to the start, then call the `Timeline.reset` method.
*
* @method Phaser.Time.Timeline#stop
* @since 3.60.0
*
* @return {this} This Timeline instance.
*/
stop: function ()
{
this.paused = true;
this.complete = true;
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.
*
* If the Timeline isn't currently running (i.e. it's paused or complete) then
* calling this method resets those states, the same as calling `Timeline.play(true)`.
*
* Any Tweens that were currently running by this Timeline will be stopped.
*
* @method Phaser.Time.Timeline#reset
* @since 3.60.0
*
* @param {boolean} [loop=false] - Set to true if you do not want to reset the loop counters.
*
* @return {this} This Timeline instance.
*/
reset: function (loop)
{
if (loop === undefined) { loop = false; }
this.elapsed = 0;
if (!loop)
{
this.iteration = 0;
}
var events = this.events;
for (var i = 0; i < events.length; i++)
{
var event = events[i];
event.complete = false;
if (!loop)
{
event.repeat = 0;
}
if (event.tweenInstance)
{
event.tweenInstance.stop();
}
}
return this.play(false);
},
/**
* Adds one or more events to this Timeline.
*
* You can pass in a single configuration object, or an array of them:
*
* ```js
* const timeline = this.add.timeline({
* at: 1000,
* run: () => {
* this.add.sprite(400, 300, 'logo');
* }
* });
* ```
*
* @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,
repeat: 0,
if: GetFastValue(entry, 'if', null),
run: GetFastValue(entry, 'run', null),
loop: GetFastValue(entry, 'loop', 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),
stop: GetFastValue(entry, 'stop', false)
});
prevTime = startTime;
}
this.complete = false;
return this;
},
/**
* Removes all events from this Timeline, resets the elapsed time to zero
* and pauses the Timeline.
*
* Any Tweens that were currently running as a result of this Timeline will be stopped.
*
* @method Phaser.Time.Timeline#clear
* @since 3.60.0
*
* @return {this} This Timeline instance.
*/
clear: function ()
{
var events = this.events;
for (var i = 0; i < events.length; i++)
{
var event = events[i];
if (event.tweenInstance)
{
event.tweenInstance.stop();
}
}
events = [];
this.elapsed = 0;
this.paused = true;
return this;
},
/**
* Returns `true` if this Timeline is currently playing.
*
* A Timeline is playing if it is not paused or not complete.
*
* @method Phaser.Time.Timeline#isPlaying
* @since 3.60.0
*
* @return {boolean} `true` if this Timeline is playing, otherwise `false`.
*/
isPlaying: function ()
{
return (!this.paused && !this.complete);
},
/**
* Returns a number between 0 and 1 representing the progress of this Timeline.
*
* A value of 0 means the Timeline has just started, 0.5 means it's half way through,
* and 1 means it's complete.
*
* If the Timeline has no events, or all events have been removed, this will return 1.
*
* If the Timeline is paused, this will return the progress value at the time it was paused.
*
* Note that the value returned is based on the number of events that have been completed,
* not the 'duration' of the events (as this is unknown to the Timeline).
*
* @method Phaser.Time.Timeline#getProgress
* @since 3.60.0
*
* @return {number} A number between 0 and 1 representing the progress of this Timeline.
*/
getProgress: function ()
{
var total = Math.min(this.totalComplete, this.events.length);
return total / this.events.length;
},
/**
* Destroys this Timeline.
*
* This will remove all events from the Timeline and stop it from processing.
*
* Any Tweens that were currently running as a result of this Timeline will be stopped.
*
* 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.clear();
this.scene = null;
this.systems = null;
}
});
/**
* 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();
* ```
*
* The Timeline can also be looped with the repeat method:
* ```js
* timeline.repeat().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;