shaku
Version:
A simple and effective JavaScript game development framework that knows its place!
407 lines (350 loc) • 11.5 kB
JavaScript
/**
* Shaku Main.
*
* |-- copyright and license --|
* @module Shaku
* @file shaku\src\shaku.js
* @author Ronen Ness (ronenness@gmail.com | http://ronenness.com)
* @copyright (c) 2021 Ronen Ness
* @license MIT
* |-- end copyright and license --|
*
*/
;
const isBrowser = typeof window !== 'undefined';
const IManager = require("./manager");
const logger = require('./logger');
const sfx = isBrowser ? require('./sfx') : null;
const gfx = isBrowser ? require('./gfx') : null;
const input = isBrowser ? require('./input') : null;
const assets = require('./assets');
const collision = require('./collision');
const utils = require('./utils');
const GameTime = require("./utils/game_time");
const _logger = logger.getLogger('shaku');
// is shaku in silent mode
var _isSilent = false;
// manager state and gametime
let _usedManagers = null;
let _prevUpdateTime = null;
// current game time
let _gameTime = null;
// to measure fps
let _currFpsCounter = 0;
let _countSecond = 0;
let _currFps = 0;
// to measure time it takes for frames to finish
let _startFrameTime = 0;
let _frameTimeMeasuresCount = 0;
let _totalFrameTimes = 0;
// are managers currently in 'started' mode?
let _managersStarted = false;
// were we previously paused?
let _wasPaused = false;
// current version
const version = "2.2.4";
/**
* Shaku's main object.
* This object wraps the entire lib namespace, and this is what you use to access all managers and manage your main loop.
*/
class Shaku
{
/**
* Create the Shaku main object.
*/
constructor()
{
/**
* Different utilities and framework objects, like vectors, rectangles, colors, etc.
* @name Shaku#utils
* @type {Utils}
*/
this.utils = utils;
/**
* Sound effects and music manager.
* @name Shaku#sfx
* @type {Sfx}
*/
this.sfx = sfx;
/**
* Graphics manager.
* @name Shaku#gfx
* @type {Gfx}
*/
this.gfx = gfx;
/**
* Input manager.
* @name Shaku#input
* @type {Input}
*/
this.input = input;
/**
* Assets manager.
* @name Shaku#assets
* @type {Assets}
*/
this.assets = assets;
/**
* Collision detection manager.
* @name Shaku#collision
* @type {Collision}
*/
this.collision = collision;
/**
* If true, will pause the updates and drawing calls when window is not focused.
* Will also not update elapsed time.
* @name Shaku#pauseWhenNotFocused
* @type {Boolean}
*/
this.pauseWhenNotFocused = false;
/**
* Set to true to completely pause Shaku (will skip updates, drawing, and time counting).
* @name Shaku#pause
* @type {Boolean}
*/
this.pause = false;
/**
* Set to true to pause just the game time.
* This will not pause real-life time. If you need real-life time stop please use the Python package.
* @name Shaku#pauseGameTime
* @type {Boolean}
*/
this.pauseGameTime = false;
}
/**
* Method to select managers to use + initialize them.
* @param {Array<IManager>=} managers Array with list of managers to use or null to use all.
* @returns {Promise} promise to resolve when finish initialization.
*/
async init(managers)
{
return new Promise(async (resolve, reject) => {
// welcome message
if (!_isSilent) {
console.log(`%c\u{1F9D9} Shaku ${version}\n%c Game dev lib by Ronen Ness`, "color:orange; font-size: 18pt", "color:grey; font-size: 8pt");
}
// sanity & log
if (_usedManagers) { throw new Error("Already initialized!"); }
_logger.info(`Initialize Shaku v${version}.`);
// reset game start time
GameTime.reset();
// setup used managers
_usedManagers = managers || (isBrowser ? [assets, sfx, gfx, input, collision] : [assets, collision]);
// init all managers
for (let i = 0; i < _usedManagers.length; ++i) {
await _usedManagers[i].setup();
}
// set starting time
_prevUpdateTime = new GameTime();
// done!
_logger.info(`Shaku ${version} was initialized successfully!`);
_logger.info(`------------------------------------------------`);
resolve();
});
}
/**
* Destroy all managers
*/
destroy()
{
// sanity
if (!_usedManagers) {
throw new Error("Not initialized!");
}
// destroy all managers
for (let i = 0; i < _usedManagers.length; ++i) {
_usedManagers[i].destroy();
}
}
/**
* Get if the Shaku is currently paused, either because the 'paused' property is set, or because the document is not focused.
* @returns {Boolean} True if currently paused for any reason.
*/
isCurrentlyPaused()
{
return this.pause || (this.pauseWhenNotFocused && !document.hasFocus());
}
/**
* Start frame (update all managers).
*/
startFrame()
{
// if paused, skip
if (this.isCurrentlyPaused())
{
if (this.input) {
this.input.startFrame();
}
GameTime.updateRawData();
_gameTime = new GameTime();
_wasPaused = true;
return;
}
// returning from pause
if (_wasPaused) {
_wasPaused = false;
GameTime.resetDelta();
}
// reset delta if paused
if (this.pauseGameTime) {
GameTime.resetDelta();
}
// update game time
else {
GameTime.update();
}
// get frame start time
_startFrameTime = GameTime.rawTimestamp();
// create new gameTime object to freeze values
_gameTime = new GameTime();
// update animators
utils.Animator.updatePlayingAnimations(_gameTime.delta);
// update managers
for (let i = 0; i < _usedManagers.length; ++i) {
_usedManagers[i].startFrame();
}
_managersStarted = true;
}
/**
* End frame (update all managers).
*/
endFrame()
{
// update managers
if (_managersStarted) {
for (let i = 0; i < _usedManagers.length; ++i) {
_usedManagers[i].endFrame();
}
_managersStarted = false;
}
// if paused, skip
if (this.isCurrentlyPaused()) {
if (this.input) {
this.input.endFrame();
}
return;
}
// store previous gameTime object
_prevUpdateTime = _gameTime;
// count fps and time stats
if (_gameTime) {
this.#_updateFpsAndTimeStats();
}
}
/**
* Measure FPS and averege update times.
* @private
*/
#_updateFpsAndTimeStats()
{
// update fps count and second counter
_currFpsCounter++;
_countSecond += _gameTime.delta;
// a second passed:
if (_countSecond >= 1) {
// reset second count and set current fps value
_countSecond = 0;
_currFps = _currFpsCounter;
_currFpsCounter = 0;
// trim the frames avg time, so we won't drag peaks for too long
_totalFrameTimes = this.getAverageFrameTime();
_frameTimeMeasuresCount = 1;
}
// get frame end time and update average frames time
let _endFrameTime = GameTime.rawTimestamp();
_frameTimeMeasuresCount++;
_totalFrameTimes += (_endFrameTime - _startFrameTime);
}
/**
* Make Shaku run in silent mode, without logs.
* You can call this before init.
*/
silent()
{
_isSilent = true;
logger.silent();
}
/**
* Set logger to throw an error every time a log message with severity higher than warning is written.
* You can call this before init.
* @param {Boolean} enable Set to true to throw error on warnings.
*/
throwErrorOnWarnings(enable)
{
if (enable === undefined) { throw Error("Must provide a value!"); }
logger.throwErrorOnWarnings(enable);
}
/**
* Get current frame game time.
* Only valid between startFrame() and endFrame().
* @returns {GameTime} Current frame's gametime.
*/
get gameTime()
{
return _gameTime;
}
/**
* Get Shaku's version.
* @returns {String} Shaku's version.
*/
get version() { return version; }
/**
* Return current FPS count.
* Note: will return 0 until at least one second have passed.
* @returns {Number} FPS count.
*/
getFpsCount()
{
return _currFps;
}
/**
* Get how long on average it takes to complete a game frame.
* @returns {Number} Average time, in milliseconds, it takes to complete a game frame.
*/
getAverageFrameTime()
{
if (_frameTimeMeasuresCount === 0) { return 0; }
return _totalFrameTimes / _frameTimeMeasuresCount;
}
/**
* Request animation frame with fallbacks.
* @param {Function} callback Method to invoke in next animation frame.
* @returns {Number} Handle for cancellation.
*/
requestAnimationFrame(callback)
{
if (window.requestAnimationFrame) return window.requestAnimationFrame(callback);
else if (window.mozRequestAnimationFrame) return window.mozRequestAnimationFrame(callback);
else if (window.webkitRequestAnimationFrame) return window.webkitRequestAnimationFrame(callback);
else if (window.msRequestAnimationFrame) return window.msRequestAnimationFrame(callback);
else return setTimeout(callback, 1000/60);
}
/**
* Cancel animation frame with fallbacks.
* @param {Number} id Request handle.
*/
cancelAnimationFrame(id)
{
if (window.cancelAnimationFrame) return window.cancelAnimationFrame(id);
else if (window.mozCancelAnimationFrame) return window.mozCancelAnimationFrame(id);
else clearTimeout(id);
}
/**
* Set the logger writer class (will replace the default console output).
* @param {*} loggerHandler New logger handler (must implement trace, debug, info, warn, error methods).
*/
setLogger(loggerHandler)
{
logger.setDrivers(loggerHandler);
}
/**
* Get / create a custom logger.
* @returns {Logger} Logger instance.
*/
getLogger(name)
{
return logger.getLogger(name);
}
};
// return the main Shaku object.
module.exports = new Shaku();