Advertisement
SReject

GameLoop.js

Nov 28th, 2019
643
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ES6 module transpilable via babel
  2.  
  3. // References to requestAnimationFrame and cancelAnimationFrame
  4. let raf, caf;
  5.  
  6. // Support: requestAnimationFrame and cancelAnimation
  7. if (typeof window === 'object' && window != null && window.requestAnimationFrame != null) {
  8.     raf = window.requestAnimationFrame;
  9.     caf = window.cancelAnimationFrame;
  10.  
  11. // Polyfill requestAnimationFrame and cancelAnimationFrame
  12. } else {
  13.     raf = callback => setTimeout(() => callback(Date.now()), 0);
  14.     caf = clearInterval;
  15. }
  16.  
  17. // List of game loop event names
  18. const keys = ['start', 'update', 'draw', 'end'];
  19.  
  20. /** Calls registered event handlers
  21.  * @private
  22.  * @param {GameLoop} self  The game loop instance that the event is to be emitted on
  23.  * @param {String}   event The event name to emit
  24.  * @param {Array}    args  List of arguments to pass to the handler
  25.  */
  26. function emit(self, event, ...args) {
  27.  
  28.     // retrieve a list of callbacks for the event
  29.     let handlers = self.state.handlers[event],
  30.         keep = [];
  31.  
  32.     // loop over the list of registered event handlers
  33.     for (
  34.         let index = 0, length = handlers.length;
  35.         index < length;
  36.         index++
  37.     ) {
  38.  
  39.         // call the callback
  40.         handlers[index].callback.apply(self, args);
  41.  
  42.         // remove non one-time emitters from handler list
  43.         if (handlers[index].once !== true) {
  44.             keep[keep.length] = handlers[index];
  45.         }
  46.     }
  47.  
  48.     self.state.handlers[event] = keep;
  49. }
  50.  
  51. /** @param {Number} timestamp The current timestamp*/
  52. function tick(timestamp) {
  53.  
  54.     // quick references
  55.     let self    = this,
  56.         state   = self.state;
  57.  
  58.     // request a new animation frame
  59.     state.id = raf(state.fnc);
  60.  
  61.     // get the number of milliseconds that elapsed since last call that have not been included as part of a simulation step
  62.     // that is: the number of ms to simulate
  63.     state.delta += timestamp - state.ts;
  64.  
  65.     // emit tick-start event
  66.     emit(self, 'start', state.delta);
  67.  
  68.     let updates = 0, panic = false;
  69.  
  70.     // loop while there's a full 'time-step' to simulate
  71.     while (state.delta >= state.step) {
  72.  
  73.         // emit update event
  74.         emit(self, 'update');
  75.  
  76.         // decrease the number of seconds to simulate by the 'time-step'
  77.         state.delta -= state.step;
  78.  
  79.         // increase the update counter
  80.         // if it goes above 240 we've entered a death spiral where updates are taking longer than they can render
  81.         if (++updates >= 240) {
  82.             panic = true;
  83.             break;
  84.         }
  85.     }
  86.  
  87.     // emit the draw event
  88.     emit(self, 'draw', state.delta / state.step, !!updates, panic);
  89.  
  90.     // done processing, emit the end event
  91.     emit(self, 'end', panic);
  92.  
  93.     // update timestamp
  94.     state.ts = timestamp;
  95. }
  96.  
  97. export default class GameLoop {
  98.  
  99.     /** @param {Number} [tickrate] The target number of ticks per second*/
  100.     constructor(tickrate) {
  101.         let self = this;
  102.  
  103.         self.state = {
  104.             step:     1000 / (tickrate || 60), // number of milliseconds that must elapse for an update to be triggered
  105.             delta:    0,
  106.             ts:       0,
  107.             running:  false,
  108.             id:       null,
  109.             handlers: {start: [], update: [], draw: [], end:[]},
  110.             fnc:      tick.bind(self)
  111.         };
  112.     }
  113.  
  114.     /** Registers an event handler; handlers are called in the order inwhich they were added
  115.      * @param {String} event The event name inwhich to hook
  116.      * @param {Function} callback The handling function to call when the event is emitted
  117.      * @returns {GameLoop} The game loop instance the event handler was applied to
  118.     */
  119.     on(event, callback, once) {
  120.         if (keys.indexOf(event = event.toLowerCase()) === -1) {
  121.             throw new Error(`Invalid onGameLoop event`);
  122.         }
  123.         if (typeof callback !== 'function') {
  124.             throw new Error(`Invalid onGameLoop event handler`);
  125.         }
  126.  
  127.         // add the event handler
  128.         this.state.handlers[event].push({callback, once: !!once});
  129.         return this;
  130.     }
  131.  
  132.     /** Removes the first matching registered event handler
  133.      * @param {String} event The name of the event from which to remove the handler
  134.      * @param {Function} callback the handling function to remove
  135.      * @returns {GameLoop} the game loop instance the event handler was removed from.
  136.      */
  137.     off(event, callback, once) {
  138.         once = !!once;
  139.  
  140.         // if the event is applicable and the callback is a function
  141.         if (keys.indexOf(event = event.toLowerCase()) > -1 && typeof callback === 'function') {
  142.  
  143.             // Loop over all registered handlers until a matching handler is found
  144.             for (
  145.                 let handlers = this.state.handlers[event],
  146.                     index = 0,
  147.                     length = handlers.length,
  148.                     handler;
  149.                 index < length;
  150.                 index++
  151.             ) {
  152.                 handler = handlers[index];
  153.  
  154.                 // matching handler found, remove it from the handlers array and exit the loop
  155.                 if (handler.callback === callback && handler.once === once) {
  156.                     handlers.splice(index, 1);
  157.                     break;
  158.                 }
  159.             }
  160.         }
  161.         return this;
  162.     }
  163.  
  164.     // Starts the game loop if it is not running
  165.     start() {
  166.         let self  = this,
  167.             state = self.state;
  168.  
  169.         if (!state.running) {
  170.             state.running = true;
  171.             state.delta   = 0;
  172.             state.ts      = Date.now();
  173.             state.id      = raf(state.fnc);
  174.         }
  175.  
  176.         return self;
  177.     }
  178.  
  179.     // Stop the game loop
  180.     stop() {
  181.         let self  = this,
  182.             state = self.state;
  183.  
  184.         caf(state.id);
  185.         state.running = false;
  186.         state.id      = null;
  187.  
  188.         return self;
  189.     }
  190. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement