From 6ee499132c8a015df93c6b9e848c3eb60e16f94b Mon Sep 17 00:00:00 2001 From: photonstorm Date: Wed, 25 Jan 2017 17:10:19 +0000 Subject: [PATCH] Huge refactor to sort out the game loop flow. --- v3/dev-guide/README.md | 41 ++- v3/src/boot/Config.js | 1 + v3/src/boot/Game.js | 17 +- v3/src/boot/MainLoop.js | 232 +++++++++++++++++ v3/src/camera/Camera.js | 8 +- v3/src/checksum.js | 2 +- v3/src/gameobjects/FactoryContainer.js | 5 - v3/src/gameobjects/GameObject.js | 2 +- v3/src/gameobjects/blitter/Blitter.js | 2 +- v3/src/gameobjects/blitter/BlitterFactory.js | 26 -- v3/src/gameobjects/image/Image.js | 2 +- v3/src/gameobjects/index.js | 4 +- v3/src/gameobjects/sprite/Sprite.js | 2 +- v3/src/renderer/webgl/WebGLRenderer.js | 68 +++-- v3/src/state/Settings.js | 45 ++-- v3/src/state/State.js | 26 +- v3/src/state/StateManager.js | 113 +-------- v3/src/state/Systems.js | 100 ++++---- v3/src/state/systems/GameObjectCreator.js | 7 +- v3/src/state/systems/GameObjectFactory.js | 5 - v3/src/state/systems/MainLoop.js | 251 ------------------- v3/src/state/systems/UpdateManager.js | 2 - 22 files changed, 392 insertions(+), 569 deletions(-) create mode 100644 v3/src/boot/MainLoop.js delete mode 100644 v3/src/state/systems/MainLoop.js diff --git a/v3/dev-guide/README.md b/v3/dev-guide/README.md index 23911fdf9..46ee2895d 100644 --- a/v3/dev-guide/README.md +++ b/v3/dev-guide/README.md @@ -270,29 +270,23 @@ When the DOM Content Loaded event happens, the Game calls Game.boot. This sets-u ### A single tick -Every time RAF ticks it calls the following: +Every time RAF ticks it calls the following (in order) -1. --removed-- -2. Calls `StateManager.step`, which ... -3. Iterates through all _active_ States, and ... -4. Calls `State.sys.mainloop.step`, which ... -5. Checks if the frame rate is throttled, and returns if so, otherwise ... -6. It updates the frame delta values, then ... -7. Calls `State.sys.begin` (which is an empty function by default) -8. While the frame delta is within range, it ... -9. Calls `State.sys.update` (empty by default) -10. If then iterates through all `State.children`, and if they exist calls `update` on each of them -11. It then calls `State.update` -12. When the while loop exits (because the frameDelta is > the step size), it ... -13. Calls `State.sys.preRender` (empty) -14. Calls `State.sys.updates.start` - this process the Update Managers list -15. If the State is visible, it then calls `Game.renderer.render`, passing in the State ... -16. If the Renderer is running it performs all of the related set-up calls (setting the blend mode, clearing the canvas, etc), then ... -17. Starts the Batch Manager (`src/renderer/webgl/BatchManager.start`) -18. Calls `StateManager.renderChildren`, which ... -19. Iterates through all children of the current State, calling `child.render` on each one. -20. It then calls `State.sys.updates.stop` - which stops the Update Manager -21. Finally it calls `State.sys.end` which just resets the frame delta / panic flags. +1. `Game.step` is called, which calls ... +2. `Game.mainloop.step` which checks frame rate, updates delta values, etc +3. This calls `State.sys.begin` once for all _active_ States +4. While the frame delta is within range it calls `State.sys.update` +5. This iterates all `State.children`, and calls `update` if they exist +6. It then calls `State.update` (dev level callback) +7. When the loop exits (because the frameDelta is > the step size) it .. +8. `Renderer.preRender` which resets the canvas, cls, then ... +9. Calls `State.sys.render` on all _active_ States, which each calls ... +10. `State.sys.updates.start` which processes the Update Managers list +11. If the State is visible, it then calls `Game.renderer.render`, which ... +12. Iterates through all children calling `child.render` on each one. +13. It then calls `State.sys.updates.stop` - which stops the Update Manager +14. Then calls `State.render` (dev level callback) +15. Finally mainloop calls `Renderer.postRender` and resets the panic flags. In a tree form it maps to the following: @@ -321,6 +315,9 @@ In a tree form it maps to the following: +- State.sys.end (resets frame delta and panic flags) ``` + + + The above is subject to change heavily! There are currently lots of empty function calls in there (State.sys.update for example), so we may well optimize this path considerably. In essence though the concept is: Once per frame we update all the timing values, update the core systems, update the children, and repeat this until our step value is high enough, then we render everything. diff --git a/v3/src/boot/Config.js b/v3/src/boot/Config.js index 0793c35d2..08eaea968 100644 --- a/v3/src/boot/Config.js +++ b/v3/src/boot/Config.js @@ -51,6 +51,7 @@ var Config = function (config) this.bannerTextColor = GetObjectValue(config, 'banner.text', defaultBannerTextColor); this.bannerBackgroundColor = GetObjectValue(config, 'banner.background', defaultBannerColor); + this.fps = GetObjectValue(config, 'fps', 60); this.forceSetTimeOut = GetObjectValue(config, 'forceSetTimeOut', false); this.transparent = GetObjectValue(config, 'transparent', false); this.pixelArt = GetObjectValue(config, 'pixelArt', false); diff --git a/v3/src/boot/Game.js b/v3/src/boot/Game.js index 1351ccd1e..6ccc7863f 100644 --- a/v3/src/boot/Game.js +++ b/v3/src/boot/Game.js @@ -12,6 +12,7 @@ var AddToDOM = require('../dom/AddToDOM'); var RequestAnimationFrame = require('../dom/RequestAnimationFrame'); var DOMContentLoaded = require('../dom/DOMContentLoaded'); +var MainLoop = require('./MainLoop'); var CreateRenderer = require('./CreateRenderer'); var StateManager = require('../state/StateManager'); var TextureManager = require('../textures/TextureManager'); @@ -58,6 +59,12 @@ var Game = function (config) */ this.device = Device; + /** + * @property {Phaser.MainLoop} mainloop - Main Loop handler. + * @protected + */ + this.mainloop = new MainLoop(this.config.fps); + // Wait for the DOM Ready event, then call boot. DOMContentLoaded(this.boot.bind(this)); @@ -87,7 +94,15 @@ Game.prototype = { this.config.postBoot(); - this.raf.start(this.state.step.bind(this.state), this.config.forceSetTimeOut); + this.mainloop.start(); + + this.raf.start(this.step.bind(this), this.config.forceSetTimeOut); + }, + + step: function (timestamp) + { + // Pass in via game to 'start' instead of every update? + this.mainloop.step(timestamp, this.state.active, this.renderer); } }; diff --git a/v3/src/boot/MainLoop.js b/v3/src/boot/MainLoop.js new file mode 100644 index 000000000..de5d62a9d --- /dev/null +++ b/v3/src/boot/MainLoop.js @@ -0,0 +1,232 @@ +/** +* @author Richard Davey +* @copyright 2016 Photon Storm Ltd. +* @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} +*/ + +// My thanks to Isaac Sukin for creating MainLoop.js, on which lots of this is based. + +var MainLoop = function (framerate) +{ + /** + * @property {number} timestep - The amount of time (in milliseconds) to simulate each time update() runs. + */ + this.timestep = 1000 / framerate; + + /** + * @property {number} physicsStep - 1 / framerate. + */ + this.physicsStep = 1 / framerate; + + /** + * @property {number} frameDelta - 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. + * @property {number} lastFrameTimeMs + */ + this.lastFrameTimeMs = 0; + + /** + * @property {number} fps - An exponential moving average of the frames per second. + */ + this.fps = 60; + + /** + * @property {number} lastFpsUpdate - The timestamp (in milliseconds) of the last time the `fps` moving average was updated. + */ + this.lastFpsUpdate = 0; + + /** + * @property {number} framesThisSecond - The number of frames delivered in the current second. + */ + this.framesThisSecond = 0; + + /** + * @property {number} numUpdateSteps - The number of times update() is called in a given frame. + */ + 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) + * @property {number} minFrameDelay + */ + this.minFrameDelay = 0; + + /** + * @property {boolean} running - 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. + * @property {boolean} started + */ + 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. + * @property {boolean} panic - Whether the simulation has fallen too far behind real time. + */ + this.panic = false; +}; + +MainLoop.prototype.constructor = MainLoop; + +MainLoop.prototype = { + + setMaxFPS: function (fps) + { + if (fps === 0) + { + this.stop(); + } + else + { + this.minFrameDelay = 1000 / fps; + } + }, + + getMaxFPS: function () + { + return 1000 / this.minFrameDelay; + }, + + resetFrameDelta: function () + { + var oldFrameDelta = this.frameDelta; + + this.frameDelta = 0; + + return oldFrameDelta; + }, + + start: function () + { + if (this.started) + { + return this; + } + + this.started = true; + this.running = true; + + this.lastFrameTimeMs = window.performance.now(); + this.lastFpsUpdate = window.performance.now(); + this.framesThisSecond = 0; + }, + + // timestamp = DOMHighResTimeStamp + // active = array containing: ({ index: i, state: state }) + + step: function (timestamp, active, renderer) + { + // Throttle the frame rate (if minFrameDelay is set to a non-zero value by + // `MainLoop.setMaxAllowedFPS()`). + if (active.length === 0 || timestamp < this.lastFrameTimeMs + this.minFrameDelay) + { + 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. + // Here we'll need to run things like tween.update, input.update, etc. + + for (var i = 0; i < active.length; i++) + { + active[i].state.sys.begin(timestamp, this.frameDelta); + } + + // 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) + { + for (var i = 0; i < active.length; i++) + { + active[i].state.sys.update(this.timestep, this.physicsStep); + } + + this.frameDelta -= this.timestep; + + if (++this.numUpdateSteps >= 240) + { + this.panic = true; + break; + } + } + + // Render + + var interpolation = this.frameDelta / this.timestep; + + renderer.preRender(); + + for (i = 0; i < active.length; i++) + { + active[i].state.sys.render(interpolation, renderer); + } + + renderer.postRender(); + + if (this.panic) + { + // This pattern introduces non-deterministic behavior, but in this case + // it's better than the alternative (the application would look like it + // was running very quickly until the simulation caught up to real + // time). + var discardedTime = Math.round(this.resetFrameDelta()); + + console.warn('Main loop panicked, tab probably put in the background. Discarding ' + discardedTime + 'ms'); + } + + this.panic = false; + }, + + stop: function () + { + this.running = false; + this.started = false; + + return this; + } + +}; + +module.exports = MainLoop; diff --git a/v3/src/camera/Camera.js b/v3/src/camera/Camera.js index 754ff872f..d1a94d7d1 100644 --- a/v3/src/camera/Camera.js +++ b/v3/src/camera/Camera.js @@ -8,6 +8,9 @@ var Component = require('../components'); var MATH_CONST = require('../math/const'); var WrapAngle = require('../math/angle/Wrap'); +// Swap to extending a BaseTransform +// var BaseTransform = require(''); + /** * A Camera is your view into the game world. It has a position and size and renders only those objects within its field of view. * The game automatically creates a single Stage sized camera on boot. Move the camera around the world with Phaser.Camera.x/y @@ -30,11 +33,6 @@ var Camera = function (state, x, y, viewportWidth, viewportHeight) */ this.state = state; - /** - * @property {Phaser.Game} game - A reference to the currently running Game. - */ - this.game = state.game; - this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; diff --git a/v3/src/checksum.js b/v3/src/checksum.js index f5e5f1b6b..922210fc8 100644 --- a/v3/src/checksum.js +++ b/v3/src/checksum.js @@ -1,4 +1,4 @@ var CHECKSUM = { -build: '56117780-e25d-11e6-9df1-9b17ba01ef95' +build: '1f102830-e320-11e6-897c-f1a016edce8a' }; module.exports = CHECKSUM; \ No newline at end of file diff --git a/v3/src/gameobjects/FactoryContainer.js b/v3/src/gameobjects/FactoryContainer.js index 20745bfd7..f165338fd 100644 --- a/v3/src/gameobjects/FactoryContainer.js +++ b/v3/src/gameobjects/FactoryContainer.js @@ -1,8 +1,3 @@ -/** -* @author Richard Davey -* @copyright 2016 Photon Storm Ltd. -* @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} -*/ /** * The GameObject Factory is a global level container of Factory instances. diff --git a/v3/src/gameobjects/GameObject.js b/v3/src/gameobjects/GameObject.js index db6d51aae..040666b13 100644 --- a/v3/src/gameobjects/GameObject.js +++ b/v3/src/gameobjects/GameObject.js @@ -24,7 +24,7 @@ var GameObject = function (state, x, y, texture, frame, parent) { this.state = state; - this.game = state.game; + this.game = state.sys.game; this.name = ''; diff --git a/v3/src/gameobjects/blitter/Blitter.js b/v3/src/gameobjects/blitter/Blitter.js index bd1e10bfa..7d9c976e9 100644 --- a/v3/src/gameobjects/blitter/Blitter.js +++ b/v3/src/gameobjects/blitter/Blitter.js @@ -28,7 +28,7 @@ var Children = require('../../components/Children'); */ var Blitter = function (state, x, y, key, frame) { - var _texture = state.game.textures.get(key); + var _texture = state.sys.textures.get(key); var _frame = _texture.get(frame); GameObject.call(this, state, x, y, _texture, _frame); diff --git a/v3/src/gameobjects/blitter/BlitterFactory.js b/v3/src/gameobjects/blitter/BlitterFactory.js index c17bc06be..738987b1a 100644 --- a/v3/src/gameobjects/blitter/BlitterFactory.js +++ b/v3/src/gameobjects/blitter/BlitterFactory.js @@ -1,8 +1,3 @@ -/** -* @author Richard Davey -* @copyright 2016 Photon Storm Ltd. -* @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} -*/ var Blitter = require('./Blitter'); var FactoryContainer = require('../../gameobjects/FactoryContainer'); @@ -11,36 +6,15 @@ var BlitterFactory = { KEY: 'blitter', - /** - * Create a new `Blitter` object. - * - * An Image is a light-weight object you can use to display anything that doesn't need physics or animation. - * - * It can still rotate, scale, crop and receive input events. - * This makes it perfect for logos, backgrounds, simple buttons and other non-Sprite graphics. - * - * @method Phaser.GameObject.Factory#image - * @param {number} [x=0] - The x coordinate of the Image. The coordinate is relative to any parent container this Image may be in. - * @param {number} [y=0] - The y coordinate of the Image. The coordinate is relative to any parent container this Image may be in. - * @param {string|Phaser.RenderTexture|Phaser.BitmapData|Phaser.Video|PIXI.Texture} [key] - The image used as a texture by this display object during rendering. If a string Phaser will get for an entry in the Image Cache. Or it can be an instance of a RenderTexture, BitmapData, Video or PIXI.Texture. - * @param {string|number} [frame] - If a Texture Atlas or Sprite Sheet is used this allows you to specify the frame to be used. Use either an integer for a Frame ID or a string for a frame name. - * @param {Phaser.Group} [group] - Optional Group to add the object to. If not specified it will be added to the World group. - * @return {Phaser.Image} The newly created Image object. - */ add: function (x, y, key, frame, group) { if (group === undefined) { group = this.state; } - // console.log('ImageFactory.add', key, x, y, frame, group); - // console.log('into State', this.state); - return group.children.add(new Blitter(this.state, x, y, key, frame)); }, make: function (x, y, key, frame) { - // console.log('ImageFactory.make', key, x, y, frame); - return new Blitter(this.state, x, y, key, frame); } diff --git a/v3/src/gameobjects/image/Image.js b/v3/src/gameobjects/image/Image.js index 8f761baf8..710eb4d96 100644 --- a/v3/src/gameobjects/image/Image.js +++ b/v3/src/gameobjects/image/Image.js @@ -22,7 +22,7 @@ var GameObject = require('../GameObject'); */ var Image = function (state, x, y, key, frame) { - var _texture = state.game.textures.get(key); + var _texture = state.sys.textures.get(key); var _frame = _texture.get(frame); GameObject.call(this, state, x, y, _texture, _frame); diff --git a/v3/src/gameobjects/index.js b/v3/src/gameobjects/index.js index f2112a8bb..0fc5972b8 100644 --- a/v3/src/gameobjects/index.js +++ b/v3/src/gameobjects/index.js @@ -1,4 +1,6 @@ // Include all of the Game Object Factories -require('./image/ImageFactory'); +require('./blitter/BlitterFactory'); require('./container/ContainerFactory'); +require('./image/ImageFactory'); +require('./sprite/SpriteFactory'); diff --git a/v3/src/gameobjects/sprite/Sprite.js b/v3/src/gameobjects/sprite/Sprite.js index fc7c917e1..6c077f936 100644 --- a/v3/src/gameobjects/sprite/Sprite.js +++ b/v3/src/gameobjects/sprite/Sprite.js @@ -10,7 +10,7 @@ var Children = require('../../components/Children'); var Sprite = function (state, x, y, key, frame) { - var _texture = state.game.textures.get(key); + var _texture = state.sys.textures.get(key); var _frame = _texture.get(frame); GameObject.call(this, state, x, y, _texture, _frame); diff --git a/v3/src/renderer/webgl/WebGLRenderer.js b/v3/src/renderer/webgl/WebGLRenderer.js index c3bb3182b..1286890ff 100644 --- a/v3/src/renderer/webgl/WebGLRenderer.js +++ b/v3/src/renderer/webgl/WebGLRenderer.js @@ -226,16 +226,8 @@ WebGLRenderer.prototype = { // this.projection.y = -(this.height / 2) / res; }, - /** - * Renders the State. - * - * @method render - * @param {Phaser.State} state - The State to be rendered. - * @param {number} interpolationPercentage - The cumulative amount of time that hasn't been simulated yet, divided - * by the amount of time that will be simulated the next time update() - * runs. Useful for interpolating frames. - */ - render: function (state, interpolationPercentage) + // Call at the start of the render loop + preRender: function () { // console.log('%c render start ', 'color: #ffffff; background: #00ff00;'); @@ -249,50 +241,50 @@ WebGLRenderer.prototype = { var gl = this.gl; - - - // This is the old render loop - add what you need here to replace it, - // but please allow each State to render to its own Quad FBO - - //var fbo = state.sys.fbo; - - //fbo.activate(); - // clear is needed for the FBO, otherwise corruption ... gl.clear(gl.COLOR_BUFFER_BIT); this.setBlendMode(BlendModes.NORMAL); + }, + + /** + * Renders a single State. + * + * @method render + * @param {Phaser.State} state - The State to be rendered. + * @param {number} interpolationPercentage - The cumulative amount of time that hasn't been simulated yet, divided + * by the amount of time that will be simulated the next time update() + * runs. Useful for interpolating frames. + */ + render: function (state, interpolationPercentage) + { + var batch = this.batch; // Could move to the State Systems or MainLoop for (var c = 0; c < state.sys.children.list.length; c++) { var child = state.sys.children.list[c]; + child.renderWebGL(this, child, interpolationPercentage); - var batch = this.batch; + if (batch && batch.isFull()) + { batch.flush(); + } } - var batch = this.batch; - if (batch) - batch.flush(); + }, - //this.batch.stop(); - - // Call state.render here, so we can do some extra shizzle on the top - // Maybe pass in the FBO texture too? - - //fbo.render(null); - - // Unbind the fbo texture and replace it with an empty texture. - // If we forget this we corrupt the main context texture! - // or get `RENDER WARNING: there is no texture bound to the unit 0` spam in the console - //gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture); - - - - // console.log('%c render end ', 'color: #ffffff; background: #ff0000;'); + // Called at the end of the render loop (tidy things up, etc) + postRender: function () + { + if (this.batch) + { + this.batch.flush(); + } // Add Post-render hook + + // console.log('%c render end ', 'color: #ffffff; background: #ff0000;'); }, destroy: function () diff --git a/v3/src/state/Settings.js b/v3/src/state/Settings.js index 909a254a6..a744ce600 100644 --- a/v3/src/state/Settings.js +++ b/v3/src/state/Settings.js @@ -2,7 +2,7 @@ var CONST = require('./const'); var ScaleModes = require('../renderer/ScaleModes'); var GetObjectValue = require('../utils/GetObjectValue'); -var Settings = function (state, config) +var Settings = function (config) { if (typeof config === 'string') { @@ -14,37 +14,32 @@ var Settings = function (state, config) config = {}; } - this.state = state; // Do we actually need this reference? This could just be a property bucket + return { - this.status = CONST.PENDING; + status: CONST.PENDING, - // Which part of this State is currently being processed? - // preload, create, update, shutdown, etc - this.op = CONST.BOOT; + op: CONST.BOOT, - this.key = GetObjectValue(config, 'key', ''); - this.active = GetObjectValue(config, 'active', false); - this.visible = GetObjectValue(config, 'visible', true); - this.scaleMode = GetObjectValue(config, 'scaleMode', ScaleModes.DEFAULT); - this.fps = GetObjectValue(config, 'fps', 60); - this.x = GetObjectValue(config, 'x', 0); - this.y = GetObjectValue(config, 'y', 0); + key: GetObjectValue(config, 'key', ''), + active: GetObjectValue(config, 'active', false), + visible: GetObjectValue(config, 'visible', true), + scaleMode: GetObjectValue(config, 'scaleMode', ScaleModes.DEFAULT), + x: GetObjectValue(config, 'x', 0), + y: GetObjectValue(config, 'y', 0), - // -1 means the State Manager will set it to be the Game dimensions - this.width = GetObjectValue(config, 'width', -1); - this.height = GetObjectValue(config, 'height', -1); + // -1 means the State Manager will set it to be the Game dimensions + width: GetObjectValue(config, 'width', -1), + height: GetObjectValue(config, 'height', -1), - // Renderer Settings + // Renderer Settings - this.clearBeforeRender = GetObjectValue(config, 'clearBeforeRender', true); - this.transparent = GetObjectValue(config, 'transparent', false); - this.autoResize = GetObjectValue(config, 'autoResize', false); - this.roundPixels = GetObjectValue(config, 'roundPixels', false); - this.drawToPrimaryCanvas = GetObjectValue(config, 'drawToPrimaryCanvas', false); + clearBeforeRender: GetObjectValue(config, 'clearBeforeRender', true), + transparent: GetObjectValue(config, 'transparent', false), + autoResize: GetObjectValue(config, 'autoResize', false), + roundPixels: GetObjectValue(config, 'roundPixels', false), + drawToPrimaryCanvas: GetObjectValue(config, 'drawToPrimaryCanvas', false), + }; }; -// Unless we add some actual functions in here, we'll make this just return an Object instead of an instance -Settings.prototype.constructor = Settings; - module.exports = Settings; diff --git a/v3/src/state/State.js b/v3/src/state/State.js index a24a6ca31..bb02070c3 100644 --- a/v3/src/state/State.js +++ b/v3/src/state/State.js @@ -4,7 +4,6 @@ * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} */ -var Settings = require('./Settings'); var Systems = require('./Systems'); /** @@ -15,39 +14,22 @@ var Systems = require('./Systems'); */ var State = function (config) { - // The properties a State *must* have, that cannot be changed without breaking it: - - this.game = null; - - // Maybe just an object? Doesn't have to instantiate I don't think ... - this.settings = new Settings(this, config); - + // The State Systems. You must never overwrite this property, or all hell will break lose. this.sys = new Systems(this, config); - // Reference to sys.children, set during sys.init only - this.children; + this.settings = this.sys.settings; }; State.prototype.constructor = State; State.prototype = { - // Can be overridden by your own States - preUpdate: function () - { - }, - - // Can be overridden by your own States + // Should be overridden by your own States update: function () { }, - // Can be overridden by your own States - postUpdate: function () - { - }, - - // Can be overridden by your own States + // Should be overridden by your own States render: function () { } diff --git a/v3/src/state/StateManager.js b/v3/src/state/StateManager.js index 285795207..7f438ea91 100644 --- a/v3/src/state/StateManager.js +++ b/v3/src/state/StateManager.js @@ -10,7 +10,7 @@ var State = require('./State'); var Settings = require('./Settings'); var Systems = require('./Systems'); var GetObjectValue = require('../utils/GetObjectValue'); -// var LoaderEvent = require('../loader/events/'); +var EventDispatcher = require('../events/EventDispatcher'); /** * The State Manager is responsible for loading, setting up and switching game states. @@ -72,9 +72,6 @@ StateManager.prototype = { */ boot: function () { - // this.game.onPause.add(this.pause, this); - // this.game.onResume.add(this.resume, this); - for (var i = 0; i < this._pending.length; i++) { var entry = this._pending[i]; @@ -185,11 +182,9 @@ StateManager.prototype = { createStateFromInstance: function (key, newState) { - newState.game = this.game; - newState.settings.key = key; - newState.sys.init(); + newState.sys.init(this.game); if (this.game.config.renderType === CONST.WEBGL) { @@ -203,9 +198,7 @@ StateManager.prototype = { { var newState = new State(stateConfig); - newState.game = this.game; - - newState.sys.init(); + newState.sys.init(this.game); if (this.game.config.renderType === CONST.WEBGL) { @@ -230,7 +223,7 @@ StateManager.prototype = { newState.settings = new Settings(newState, key); newState.sys = new Systems(newState); - newState.sys.init(); + newState.sys.init(this.game); if (this.game.config.renderType === CONST.WEBGL) { @@ -253,9 +246,9 @@ StateManager.prototype = { newState.create = GetObjectValue(stateConfig, 'create', NOOP); newState.shutdown = GetObjectValue(stateConfig, 'shutdown', NOOP); - newState.preUpdate = GetObjectValue(stateConfig, 'preUpdate', NOOP); + // Game Loop level callbacks + newState.update = GetObjectValue(stateConfig, 'update', NOOP); - newState.postUpdate = GetObjectValue(stateConfig, 'postUpdate', NOOP); newState.render = GetObjectValue(stateConfig, 'render', NOOP); return newState; @@ -375,7 +368,6 @@ StateManager.prototype = { // No preload? Then there was nothing to load either this.startCreate(state); } - } }, @@ -411,8 +403,6 @@ StateManager.prototype = { this.active.sort(this.sortStates.bind(this)); state.sys.updates.running = true; - - state.sys.mainloop.start(); }, pause: function (key) @@ -446,96 +436,7 @@ StateManager.prototype = { { return 0; } - }, - - // See if we can reduce this down to just update and render - - // timestamp = DOMHighResTimeStamp - step: function (timestamp) - { - for (var i = 0; i < this.active.length; i++) - { - var state = this.active[i].state; - - if (state.sys.mainloop.running) - { - state.sys.mainloop.step(timestamp); - } - } - }, - - /* - preUpdate: function () - { - for (var i = 0; i < this.active.length; i++) - { - var state = this.active[i].state; - - for (var c = 0; c < state.sys.children.list.length; c++) - { - state.sys.children.list[c].preUpdate(); - } - - state.preUpdate(); - } - }, - - update: function () - { - for (var i = 0; i < this.active.length; i++) - { - var state = this.active[i].state; - - // Invoke State Main Loop here - updating all of its systems (tweens, physics, etc) - - // This shouldn't be called if the State is still loading - // Have a State.STATUS const in the Settings, dictating what is going on - - for (var c = 0; c < state.sys.children.list.length; c++) - { - var child = state.sys.children.list[c]; - - if (child.exists) - { - child.update(); - } - } - - state.update(); - } - }, - - postUpdate: function () - { - for (var i = 0; i < this.active.length; i++) - { - var state = this.active[i].state; - - for (var c = 0; c < state.sys.children.list.length; c++) - { - state.sys.children.list[c].postUpdate(); - } - - state.postUpdate(); - } - }, - - render: function () - { - for (var i = 0; i < this.active.length; i++) - { - var state = this.active[i].state; - - // Can put all kinds of other checks in here, like MainLoop, FPS, etc. - if (!state.settings.visible || state.sys.color.alpha === 0 || state.sys.children.list.length === 0) - { - continue; - } - - this.game.renderer.render(state); - } - }, - */ + } }; diff --git a/v3/src/state/Systems.js b/v3/src/state/Systems.js index d42031466..107db122a 100644 --- a/v3/src/state/Systems.js +++ b/v3/src/state/Systems.js @@ -6,34 +6,37 @@ var EventDispatcher = require('../events/EventDispatcher'); var GameObjectFactory = require('./systems/GameObjectFactory'); -// var GameObjectCreator = require('./systems/GameObjectCreator'); +var GameObjectCreator = require('./systems/GameObjectCreator'); var Loader = require('./systems/Loader'); -var MainLoop = require('./systems/MainLoop'); var UpdateManager = require('./systems/UpdateManager'); var Component = require('../components'); var Camera = require('../camera/Camera'); +var Settings = require('./Settings'); var Systems = function (state, config) { this.state = state; + this.game = null; + this.config = config; - this.events; + this.settings = Settings(config); - // Reference to the global Game level TextureManager. - this.textures; + // Reference to the global Game level systems + this.textureManager; + this.stateManager; - // State specific managers (Factory, Tweens, Loader, Physics, etc) + // CORE SYSTEMS / PROPERTIES + + // Reference to State specific managers (Factory, Tweens, Loader, Physics, etc) this.add; this.make; - this.input; this.load; - this.tweens; - this.mainloop; + this.events; this.updates; - // State specific properties (transform, data, children, etc) + // State properties this.camera; this.children; this.color; @@ -47,40 +50,35 @@ Systems.prototype.constructor = Systems; Systems.prototype = { - init: function () + init: function (game) { console.log('State.Systems.init'); - this.textures = this.state.game.textures; - - // All of the systems can use the State level EventDispatcher, or their own - this.events = new EventDispatcher(); + this.game = game; // State specific managers (Factory, Tweens, Loader, Physics, etc) - // All these to be set by a State Config package + this.textures = game.textures; + this.events = new EventDispatcher(); this.add = new GameObjectFactory(this.state); - // this.make = GameObjectCreator(this.state); - this.mainloop = new MainLoop(this.state, this.state.settings.fps); + this.make = GameObjectCreator(this.state); this.updates = new UpdateManager(this.state); this.load = new Loader(this.state); - // this.tweens = new Phaser.TweenManager(this.state); - // this.input = new Phaser.State.Input(this.state); - // this.physics = new Phaser.Physics.Arcade(this.state, 800, 600); - // State specific properties (transform, data, children, etc) + this.camera = new Camera(this.state, 0, 0, 800, 600); this.children = new Component.Children(this.state); this.color = new Component.Color(this.state); this.data = new Component.Data(this.state); this.transform = this.camera.transform; - // Boot + this.inject(); + }, - // this.input.init(); - - // Defaults + inject: function () + { + // Defaults properties injected into the State this.state.events = this.events; this.state.add = this.add; @@ -88,43 +86,47 @@ Systems.prototype = { this.state.children = this.children; this.state.color = this.color; this.state.data = this.data; + this.state.state = this.game.state; // StateManager + this.state.camera = this.camera; this.state.transform = this.camera.transform; this.state.textures = this.textures; - - - - // this.state.input = this.input; - // this.state.state = this.state.game.state; - - // Here we can check which Systems to install as properties into the State object - // (default systems always exist in here, regardless) }, + // Called just once per frame, regardless of speed begin: function (timestamp, frameDelta) { }, + // Potentially called multiple times per frame (on super-fast systems) update: function (timestep, physicsStep) { - }, - - preRender: function () - { - }, - - end: function (fps, panic) - { - if (panic) + for (var c = 0; c < this.children.list.length; c++) { - // This pattern introduces non-deterministic behavior, but in this case - // it's better than the alternative (the application would look like it - // was running very quickly until the simulation caught up to real - // time). - var discardedTime = Math.round(this.mainloop.resetFrameDelta()); + var child = this.children.list[c]; - console.warn('Main loop panicked, probably because the browser tab was put in the background. Discarding ' + discardedTime + 'ms'); + if (child.exists) + { + child.update(timestep); + } } + + this.state.update(timestep, physicsStep); + }, + + // Called just once per frame, regardless of speed + render: function (interpolationPercentage, renderer) + { + this.updates.start(); + + if (this.settings.visible && this.color.alpha !== 0) + { + renderer.render(this.state, interpolationPercentage); + } + + this.updates.stop(); + + this.state.render(interpolationPercentage); } }; diff --git a/v3/src/state/systems/GameObjectCreator.js b/v3/src/state/systems/GameObjectCreator.js index 4590cf9d5..63e7d8c10 100644 --- a/v3/src/state/systems/GameObjectCreator.js +++ b/v3/src/state/systems/GameObjectCreator.js @@ -26,15 +26,10 @@ var GameObjectCreator = { function init (state) { - console.log('Creating GameObjectCreator instance for State', state); - GameObjectCreator.state = state; // Load the factories into this Object - - FactoryContainer.load(GameObjectCreator, false); - - return GameObjectCreator; + return FactoryContainer.load(GameObjectCreator, false); } module.exports = init; diff --git a/v3/src/state/systems/GameObjectFactory.js b/v3/src/state/systems/GameObjectFactory.js index 443c17d91..2b99f2fca 100644 --- a/v3/src/state/systems/GameObjectFactory.js +++ b/v3/src/state/systems/GameObjectFactory.js @@ -26,15 +26,10 @@ var GameObjectFactory = { function init (state) { - console.log('Creating GameObjectFactory instance for State'); - GameObjectFactory.state = state; // Load the factories into this Object - return FactoryContainer.load(GameObjectFactory, true); - - // return GameObjectFactory; } module.exports = init; diff --git a/v3/src/state/systems/MainLoop.js b/v3/src/state/systems/MainLoop.js deleted file mode 100644 index c464a6394..000000000 --- a/v3/src/state/systems/MainLoop.js +++ /dev/null @@ -1,251 +0,0 @@ -/** -* @author Richard Davey -* @copyright 2016 Photon Storm Ltd. -* @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} -*/ - -// My thanks to Isaac Sukin for creating MainLoop.js, on which lots of this is based. - -var MainLoop = function (state, framerate) -{ - /** - * @property {Phaser.State} state - */ - this.state = state; - - /** - * @property {Phaser.Game} game - A reference to the currently running Game. - */ - this.game = state.game; - - // The amount of time (in milliseconds) to simulate each time update() runs. - this.timestep = 1000 / framerate; - - this.physicsStep = 1 / framerate; - - // The cumulative amount of in-app time that hasn't been simulated yet. - // See the comments inside animate() for details. - 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.fps = 60; - - // 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; -}; - -MainLoop.prototype.constructor = MainLoop; - -MainLoop.prototype = { - - setMaxFPS: function (fps) - { - if (fps === 0) - { - this.stop(); - } - else - { - this.minFrameDelay = 1000 / fps; - } - }, - - getMaxFPS: function () - { - return 1000 / this.minFrameDelay; - }, - - resetFrameDelta: function () - { - var oldFrameDelta = this.frameDelta; - - this.frameDelta = 0; - - return oldFrameDelta; - }, - - start: function () - { - if (this.started) - { - return this; - } - - this.started = true; - this.running = true; - - this.lastFrameTimeMs = window.performance.now(); - this.lastFpsUpdate = window.performance.now(); - this.framesThisSecond = 0; - }, - - // timestamp = DOMHighResTimeStamp - step: function (timestamp) - { - // Throttle the frame rate (if minFrameDelay is set to a non-zero value by - // `MainLoop.setMaxAllowedFPS()`). - if (timestamp < this.lastFrameTimeMs + this.minFrameDelay) - { - 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. - - // Here we'll need to run things like tween.update, input.update, etc. - this.state.sys.begin(timestamp, this.frameDelta); - - // 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; - - var step = this.timestep; - - while (this.frameDelta >= step) - { - // this.update(this.timestep); - - this.state.sys.update(step, this.physicsStep); - - for (var c = 0; c < this.state.sys.children.list.length; c++) - { - var child = this.state.sys.children.list[c]; - - if (child.exists) - { - child.update(step); - } - } - - // Dev level callback - this.state.update(step); - - this.frameDelta -= this.timestep; - - if (++this.numUpdateSteps >= 240) - { - this.panic = true; - break; - } - } - - this.state.sys.preRender(); - - this.state.sys.updates.start(); - - if (this.state.settings.visible && this.state.sys.color.alpha !== 0) - { - this.game.renderer.render(this.state, this.frameDelta / this.timestep); - } - - this.state.sys.updates.stop(); - - // Run any updates that are not dependent on time in the simulation. - this.state.sys.end(this.fps, this.panic); - - this.panic = false; - }, - - /* - update: function (timestep) - { - this.state.sys.update(timestep); - - var c; - var child; - - for (var c = 0; c < this.state.sys.children.list.length; c++) - { - var child = this.state.sys.children.list[c]; - - if (child.exists) - { - child.update(timestep); - } - } - - // Dev level callback - this.state.update(timestep); - - for (c = 0; c < this.state.sys.children.list.length; c++) - { - var child = this.state.sys.children.list[c]; - - if (child.exists) - { - child.update(timestep); - } - } - }, - */ - - stop: function () - { - this.running = false; - this.started = false; - - return this; - } - -}; - -module.exports = MainLoop; diff --git a/v3/src/state/systems/UpdateManager.js b/v3/src/state/systems/UpdateManager.js index 222e25abc..06be46e80 100644 --- a/v3/src/state/systems/UpdateManager.js +++ b/v3/src/state/systems/UpdateManager.js @@ -13,8 +13,6 @@ var UpdateManager = function (state) { this.state = state; - this.game = state.game; - this.list = []; // this.i = 1;