diff --git a/build/config.php b/build/config.php index 2d79165b6..808903c4f 100644 --- a/build/config.php +++ b/build/config.php @@ -134,6 +134,7 @@ EOL; + @@ -296,10 +297,10 @@ EOL; } echo << - - - + + + + diff --git a/src/core/Game.js b/src/core/Game.js index e6870e379..476f6708d 100644 --- a/src/core/Game.js +++ b/src/core/Game.js @@ -144,11 +144,17 @@ Phaser.Game = function (width, height, renderer, parent, state, transparent, ant */ this.isRunning = false; + /** + * @property {Phaser.MainLoop} mainloop - Automatically handles the core game loop via requestAnimationFrame or setTimeout. + * @protected + */ + this.mainloop = null; + /** * @property {Phaser.RequestAnimationFrame} raf - Automatically handles the core game loop via requestAnimationFrame or setTimeout * @protected */ - this.raf = null; + // this.raf = null; /** * @property {Phaser.GameObjectFactory} add - Reference to the Phaser.GameObjectFactory. @@ -583,6 +589,7 @@ Phaser.Game.prototype = { this.isRunning = true; + /* if (this.config && this.config['forceSetTimeOut']) { this.raf = new Phaser.RequestAnimationFrame(this, this.config['forceSetTimeOut']); @@ -593,6 +600,7 @@ Phaser.Game.prototype = { } this._kickstart = true; + */ if (window['focus']) { @@ -602,7 +610,11 @@ Phaser.Game.prototype = { } } - this.raf.start(); + this.mainloop = new Phaser.MainLoop(this); + + this.mainloop.start(); + + // this.raf.start(); }, diff --git a/src/core/MainLoop.js b/src/core/MainLoop.js new file mode 100644 index 000000000..2a82a7839 --- /dev/null +++ b/src/core/MainLoop.js @@ -0,0 +1,342 @@ +Phaser.MainLoop = function (game, framerate, forceSetTimeOut) { + + if (framerate === undefined) { framerate = 60; } + if (forceSetTimeOut === undefined) { forceSetTimeOut = false; } + + this.game = game; + + // Move to external file once tested + /* + this.getTime = function () { return Date.now; }; + + if (window.performance) + { + if (window.performance.now) + { + this.getTime = window.performance.now; + } + else if (window.performance.webkitNow) + { + this.getTime = window.performance.webkitNow; + } + } + */ + + this.timestep = 1000 / framerate; + this.physicsStep = 1 / framerate; + + // The cumulative amount of in-app time that hasn't been simulated yet. + this.frameDelta = 0; + + // The timestamp in milliseconds of the last time the main loop was run. + // Used to compute the time elapsed between frames. + this.lastFrameTimeMs = 0; + + // An exponential moving average of the frames per second. + this.framerate = framerate; + this.fps = framerate; + + // The timestamp (in milliseconds) of the last time the `fps` moving + // average was updated. + this.lastFpsUpdate = 0; + + // The number of frames delivered in the current second. + this.framesThisSecond = 0; + + // The number of times update() is called in a given frame. This is only + // relevant inside of animate(), but a reference is held externally so that + // this variable is not marked for garbage collection every time the main + // loop runs. + this.numUpdateSteps = 0; + + // The minimum amount of time in milliseconds that must pass since the last + // frame was executed before another frame can be executed. The + // multiplicative inverse caps the FPS (the default of zero means there is + // no cap). + this.minFrameDelay = 0; + + // Whether the main loop is running. + this.running = false; + + // `true` if `MainLoop.start()` has been called and the most recent time it + // was called has not been followed by a call to `MainLoop.stop()`. This is + // different than `running` because there is a delay of a few milliseconds + // after `MainLoop.start()` is called before the application is considered + // "running." This delay is due to waiting for the next frame. + this.started = false; + + // Whether the simulation has fallen too far behind real time. + // Specifically, `panic` will be set to `true` if too many updates occur in + // one frame. This is only relevant inside of animate(), but a reference is + // held externally so that this variable is not marked for garbage + // collection every time the main loop runs. + this.panic = false; + + /** + * @property {boolean} _isSetTimeOut - true if the browser is using setTimeout instead of raf. + * @private + */ + this._isSetTimeOut = false; + + /** + * @property {number} _handleID - The callback ID used when calling cancel. + * @private + */ + this._handleID = null; + +}; + +Phaser.MainLoop.prototype = { + + start: function () { + + if (this.started) + { + return this; + } + + this.started = true; + this.running = true; + + // draw once? + + this.lastFrameTimeMs = window.performance.now(); + this.lastFpsUpdate = window.performance.now(); + this.framesThisSecond = 0; + + if (!window.requestAnimationFrame || this.forceSetTimeOut) + { + // var _this = this; + + // The function that runs the main loop. The unprefixed version of + // `window.requestAnimationFrame()` is available in all modern browsers + // now, but node.js doesn't have it, so fall back to timers. The polyfill + // is adapted from the MIT-licensed + // https://github.com/underscorediscovery/realtime-multiplayer-in-html5 + + + /* + this.raf = window.requestAnimationFrame || (function() { + var lastTimestamp = Date.now(), + now, + timeout; + return function(callback) { + now = Date.now(); + // The next frame should run no sooner than the simulation allows, + // but as soon as possible if the current frame has already taken + // more time to run than is simulated in one timestep. + timeout = Math.max(0, _this.timestep - (now - lastTimestamp)); + lastTimestamp = now + timeout; + return setTimeout(function() { + callback(now + timeout); + }, timeout); + }; + })(); + */ + this._isSetTimeOut = true; + + this._handleID = window.setTimeout(this.step.bind(this), 0); + } + else + { + this._isSetTimeOut = false; + + this._handleID = window.requestAnimationFrame(this.step.bind(this)); + } + + }, + + step: function (timestamp) { + + // console.log(timestamp); + // debugger; + + // Throttle the frame rate (if minFrameDelay is set to a non-zero value by + // `MainLoop.setMaxAllowedFPS()`). + // if (timestamp < this.lastFrameTimeMs + this.minFrameDelay) + // { + // Run the loop again the next time the browser is ready to render. + // this._handleID = window.requestAnimationFrame(this.step.bind(this)); + // return; + // } + + // frameDelta is the cumulative amount of in-app time that hasn't been + // simulated yet. Add the time since the last frame. We need to track total + // not-yet-simulated time (as opposed to just the time elapsed since the + // last frame) because not all actually elapsed time is guaranteed to be + // simulated each frame. See the comments below for details. + this.frameDelta += timestamp - this.lastFrameTimeMs; + this.lastFrameTimeMs = timestamp; + + // Run any updates that are not dependent on time in the simulation. See + // `MainLoop.setBegin()` for additional details on how to use this. + + // BEGIN --------------------------------------------------------------- + + this.begin(timestamp); + + // UPDATE --------------------------------------------------------------- + + // Update the estimate of the frame rate, `fps`. Every second, the number + // of frames that occurred in that second are included in an exponential + // moving average of all frames per second, with an alpha of 0.25. This + // means that more recent seconds affect the estimated frame rate more than + // older seconds. + if (timestamp > this.lastFpsUpdate + 1000) + { + // Compute the new exponential moving average with an alpha of 0.25. + // Using constants inline is okay here. + this.fps = 0.25 * this.framesThisSecond + 0.75 * this.fps; + + this.lastFpsUpdate = timestamp; + this.framesThisSecond = 0; + } + + this.framesThisSecond++; + + this.numUpdateSteps = 0; + + while (this.frameDelta >= this.timestep) + { + this.update(this.timestep); + + this.frameDelta -= this.timestep; + + if (++this.numUpdateSteps >= 240) + { + this.panic = true; + break; + } + } + + // RENDER --------------------------------------------------------------- + + this.render(this.frameDelta / this.timestep); + + // END --------------------------------------------------------------- + + // Run any updates that are not dependent on time in the simulation. + // this.end(this.fps, this.panic); + + this.panic = false; + + this._handleID = window.requestAnimationFrame(this.step.bind(this)); + + }, + + begin: function (timestamp) { + + this.game.time.update(timestamp); + + this.game.scale.preUpdate(timestamp, this.frameDelta); + this.game.debug.preUpdate(timestamp, this.frameDelta); + this.game.camera.preUpdate(timestamp, this.frameDelta); + this.game.physics.preUpdate(timestamp, this.frameDelta); + this.game.state.preUpdate(timestamp, this.frameDelta); + this.game.plugins.preUpdate(timestamp, this.frameDelta); + this.game.stage.preUpdate(timestamp, this.frameDelta); + + }, + + update: function (timestep) { + + this.game.state.update(timestep); + this.game.stage.update(timestep); + this.game.tweens.update(timestep); + this.game.sound.update(timestep); + this.game.input.update(timestep); + this.game.physics.update(timestep); + this.game.particles.update(timestep); + this.game.plugins.update(timestep); + + this.game.stage.postUpdate(timestep); + this.game.plugins.postUpdate(timestep); + + this.game.stage.updateTransform(); + // this.game.time.update(timestamp); + + }, + + render: function (dt) { + + this.game.renderer.renderSession.interpolation = dt; + + // this.game.stage.updateTransform(); + + this.game.state.preRender(dt); + + if (this.game.renderType !== Phaser.HEADLESS) + { + this.game.renderer.render(this.game.stage); + + this.game.plugins.render(dt); + + this.game.state.render(dt); + } + + this.game.plugins.postRender(dt); + + }, + + stop: function () { + + this.running = false; + this.started = false; + + if (this._isSetTimeOut) + { + clearTimeout(this._handleID); + } + else + { + window.cancelAnimationFrame(this._handleID); + } + + return this; + + }, + + resetFrameDelta: function () { + + var oldFrameDelta = this.frameDelta; + + this.frameDelta = 0; + + return oldFrameDelta; + + } + +}; + +/** +* @name Phaser.MainLoop#maxFPS +* @property {number} maxFPS - The maximum frame rate. +*/ +Object.defineProperty(Phaser.MainLoop.prototype, 'maxFPS', { + + get: function() { + + return 1000 / this.minFrameDelay; + + }, + + set: function (value) { + + if (fps === 0) + { + this.stop(); + } + else + { + this.minFrameDelay = 1000 / value; + } + + } + +}); + +Phaser.MainLoop.prototype.constructor = Phaser.MainLoop; + +Phaser.NOOP = function () { + // No-operation +}; diff --git a/src/gameobjects/components/Core.js b/src/gameobjects/components/Core.js index 2cb45d06a..248ff1503 100644 --- a/src/gameobjects/components/Core.js +++ b/src/gameobjects/components/Core.js @@ -101,6 +101,9 @@ Phaser.Component.Core.preUpdate = function () { this.previousPosition.set(this.world.x, this.world.y); this.previousRotation = this.rotation; + // TEST + this.prevPosition.set(this.worldPosition.x, this.worldPosition.y); + if (!this.exists || !this.parent.exists) { this.renderOrderID = -1; diff --git a/src/pixi/display/DisplayObject.js b/src/pixi/display/DisplayObject.js index b1f327ef2..0e198fe74 100644 --- a/src/pixi/display/DisplayObject.js +++ b/src/pixi/display/DisplayObject.js @@ -28,6 +28,8 @@ PIXI.DisplayObject = function() { */ this.position = new PIXI.Point(0, 0); + this.interpolate = true; + /** * The scale of this DisplayObject. A scale of 1:1 represents the DisplayObject * at its default size. A value of 0.5 would scale this DisplayObject by half, and so on. @@ -171,6 +173,8 @@ PIXI.DisplayObject = function() { */ this.worldPosition = new PIXI.Point(0, 0); + this.prevPosition = new PIXI.Point(0, 0); + /** * The global scale of this DisplayObject. * diff --git a/src/pixi/display/Sprite.js b/src/pixi/display/Sprite.js index 2494e3acc..1d895fe33 100644 --- a/src/pixi/display/Sprite.js +++ b/src/pixi/display/Sprite.js @@ -337,6 +337,13 @@ PIXI.Sprite.prototype._renderWebGL = function(renderSession, matrix) wt = matrix; } + // if interpolation + if (this.interpolate) + { + wt.tx = this.prevPosition.x + (this.worldPosition.x - this.prevPosition.x) * renderSession.interpolation; + wt.ty = this.prevPosition.y + (this.worldPosition.y - this.prevPosition.y) * renderSession.interpolation; + } + // A quick check to see if this element has a mask or a filter. if (this._mask || this._filters) { @@ -442,6 +449,13 @@ PIXI.Sprite.prototype._renderCanvas = function(renderSession, matrix) var tx = (wt.tx * renderSession.resolution) + renderSession.shakeX; var ty = (wt.ty * renderSession.resolution) + renderSession.shakeY; + // if interpolation + if (this.interpolate) + { + tx = this.prevPosition.x + (this.worldPosition.x - this.prevPosition.x) * renderSession.interpolation; + ty = this.prevPosition.y + (this.worldPosition.y - this.prevPosition.y) * renderSession.interpolation; + } + // Allow for pixel rounding if (renderSession.roundPixels) { diff --git a/src/polyfills.js b/src/polyfills.js index b22eb1d1d..7b0fb150f 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -151,3 +151,50 @@ if (!window.console) window.console.log = window.console.assert = function(){}; window.console.warn = window.console.assert = function(){}; } + +/* + * window.performance polyfill for IE9 and other lesser browsers. + * Based on code by Paul Irish (MIT) + */ + +(function(){ + + if ("performance" in window === false) + { + window.performance = {}; + } + + if ("now" in window.performance === false) + { + var nowOffset = Date.now(); + + if (performance.timing && performance.timing.navigationStart) + { + nowOffset = performance.timing.navigationStart; + } + + window.performance.now = function now() { + return Date.now() - nowOffset; + } + + } + +})(); + +/* + * window.requestAnimationFrame polyfill. + */ + +(function(){ + + if (!window.requestAnimationFrame) + { + ['ms', 'moz', 'webkit', 'o'].forEach(function(prefix) { + + window.requestAnimationFrame = window[prefix + 'RequestAnimationFrame']; + window.cancelAnimationFrame = window[prefix + 'CancelAnimationFrame']; + + }); + } + +})(); diff --git a/src/time/Time.js b/src/time/Time.js index 1df9be424..c8e296c6b 100644 --- a/src/time/Time.js +++ b/src/time/Time.js @@ -403,6 +403,7 @@ Phaser.Time.prototype = { // elapsed time between previous call and now - this could be a high resolution value this.elapsed = this.now - this.prevTime; + /* if (this.game.raf._isSetTimeOut) { // console.log('Time isSet', this._desiredFps, 'te', this.timeExpected, 'time', time); @@ -415,6 +416,7 @@ Phaser.Time.prototype = { // console.log('Time expect', this.timeExpected); } + */ if (this.advancedTiming) {