/**
* LittleJS - The Tiny JavaScript Game Engine That Can!
* MIT License - Copyright 2021 Frank Force
*
* Engine Features
* - Object oriented system with base class engine object
* - Base class object handles update, physics, collision, rendering, etc
* - Engine helper classes and functions like Vector2, Color, and Timer
* - Super fast rendering system for tile sheets
* - Sound effects audio with zzfx and music with zzfxm
* - Input processing system with gamepad and touchscreen support
* - Tile layer rendering and collision system
* - Particle effect system
* - Medal system tracks and displays achievements
* - Debug tools and debug rendering system
* - Post processing effects
* - Call engineInit() to start it up!
* @namespace Engine
*/
'use strict';
/** Name of engine
* @type {String}
* @default
* @memberof Engine */
const engineName = 'LittleJS';
/** Version of engine
* @type {String}
* @default
* @memberof Engine */
const engineVersion = '1.8.1';
/** Frames per second to update objects
* @type {Number}
* @default
* @memberof Engine */
const frameRate = 60;
/** How many seconds each frame lasts, engine uses a fixed time step
* @type {Number}
* @default 1/60
* @memberof Engine */
const timeDelta = 1/frameRate;
/** Array containing all engine objects
* @type {Array}
* @memberof Engine */
let engineObjects = [];
/** Array containing only objects that are set to collide with other objects this frame (for optimization)
* @type {Array}
* @memberof Engine */
let engineObjectsCollide = [];
/** Current update frame, used to calculate time
* @type {Number}
* @memberof Engine */
let frame = 0;
/** Current engine time since start in seconds, derived from frame
* @type {Number}
* @memberof Engine */
let time = 0;
/** Actual clock time since start in seconds (not affected by pause or frame rate clamping)
* @type {Number}
* @memberof Engine */
let timeReal = 0;
/** Is the game paused? Causes time and objects to not be updated
* @type {Boolean}
* @default 0
* @memberof Engine */
let paused = 0;
/** Set if game is paused
* @param {Boolean} paused
* @memberof Engine */
function setPaused(_paused) { paused = _paused; }
// Frame time tracking
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
///////////////////////////////////////////////////////////////////////////////
/** Start up LittleJS engine with your callback functions
* @param {Function} gameInit - Called once after the engine starts up, setup the game
* @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
* @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
* @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
* @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
* @param {String} [imageSources='tiles.png'] - Image to load
* @memberof Engine */
function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=['tiles.png'])
{
ASSERT(Array.isArray(imageSources)); // pass in images as array
// internal update loop for engine
function engineUpdate(frameTimeMS=0)
{
// update time keeping
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
frameTimeLastMS = frameTimeMS;
if (debug || showWatermark)
averageFPS = lerp(.05, averageFPS, 1e3/(frameTimeDeltaMS||1));
const debugSpeedUp = debug && keyIsDown(107); // +
const debugSpeedDown = debug && keyIsDown(109); // -
if (debug) // +/- to speed/slow time
frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
timeReal += frameTimeDeltaMS / 1e3;
frameTimeBufferMS += !paused * frameTimeDeltaMS;
if (!debugSpeedUp)
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
if (canvasFixedSize.x)
{
// clear canvas and set fixed size
mainCanvas.width = canvasFixedSize.x;
mainCanvas.height = canvasFixedSize.y;
// fit to window by adding space on top or bottom if necessary
const aspect = innerWidth / innerHeight;
const fixedAspect = mainCanvas.width / mainCanvas.height;
(glCanvas||mainCanvas).style.width = mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
(glCanvas||mainCanvas).style.height = mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
}
else
{
// clear canvas and set size to same as window
mainCanvas.width = min(innerWidth, canvasMaxSize.x);
mainCanvas.height = min(innerHeight, canvasMaxSize.y);
}
// clear overlay canvas and set size
overlayCanvas.width = mainCanvas.width;
overlayCanvas.height = mainCanvas.height;
// save canvas size
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
if (paused)
{
// do post update even when paused
inputUpdate();
debugUpdate();
gameUpdatePost();
inputUpdatePost();
}
else
{
// apply time delta smoothing, improves smoothness of framerate in some browsers
let deltaSmooth = 0;
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
{
// force an update each frame if time is close enough (not just a fast refresh rate)
deltaSmooth = frameTimeBufferMS;
frameTimeBufferMS = 0;
}
// update multiple frames if necessary in case of slow framerate
for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
{
// increment frame and update time
time = frame++ / frameRate;
// update game and objects
inputUpdate();
gameUpdate();
engineObjectsUpdate();
// do post update
debugUpdate();
gameUpdatePost();
inputUpdatePost();
}
// add the time smoothing back in
frameTimeBufferMS += deltaSmooth;
}
// render sort then render while removing destroyed objects
enginePreRender();
gameRender();
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
for (const o of engineObjects)
o.destroyed || o.render();
gameRenderPost();
glRenderPostProcess();
medalsRender();
touchGamepadRender();
debugRender();
glEnable && glCopyToContext(mainContext);
if (showWatermark)
{
// update fps
overlayContext.textAlign = 'right';
overlayContext.textBaseline = 'top';
overlayContext.font = '1em monospace';
overlayContext.fillStyle = '#000';
const text = engineName + ' ' + 'v' + engineVersion + ' / '
+ drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
+ (glEnable ? ' GL' : ' 2D') ;
overlayContext.fillText(text, mainCanvas.width-3, 3);
overlayContext.fillStyle = '#fff';
overlayContext.fillText(text, mainCanvas.width-2, 2);
drawCount = 0;
}
requestAnimationFrame(engineUpdate);
}
// setup html
const styleBody =
'margin:0;overflow:hidden;' + // fill the window
'background:#000;' + // set background color
'touch-action:none;' + // prevent mobile pinch to resize
'user-select:none;' + // prevent mobile hold to select
'-webkit-user-select:none;' + // compatibility for ios
'-webkit-touch-callout:none'; // compatibility for ios
document.body.style = styleBody;
document.body.appendChild(mainCanvas = document.createElement('canvas'));
mainContext = mainCanvas.getContext('2d');
// init stuff and start engine
debugInit();
glEnable && glInit();
// create overlay canvas for hud to appear above gl canvas
document.body.appendChild(overlayCanvas = document.createElement('canvas'));
overlayContext = overlayCanvas.getContext('2d');
// set canvas style
const styleCanvas =
'position:absolute;' + // position
'top:50%;left:50%;transform:translate(-50%,-50%);' + // center
(canvasPixelated?'image-rendering:pixelated':''); // pixelated rendering
(glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
// load all of the images
Promise.all(imageSources.map((src, textureIndex)=>
new Promise((resolve, reject)=>
{
const image = new Image;
image.onerror = image.onload = ()=>
{
textureInfos[textureIndex] = new TextureInfo(image);
resolve();
}
image.src = src;
})
)).then(()=>
{
// start the engine
gameInit();
engineUpdate();
});
}
// Called automatically by engine to setup render system
function enginePreRender()
{
// save canvas size
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
// disable smoothing for pixel art
mainContext.imageSmoothingEnabled = !canvasPixelated;
// setup gl rendering if enabled
glEnable && glPreRender();
}
/** Update each engine object, remove destroyed objects, and update time
* @memberof Engine */
function engineObjectsUpdate()
{
// get list of solid objects for physics optimzation
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
// recursive object update
function updateObject(o)
{
if (!o.destroyed)
{
o.update();
for (const child of o.children)
updateObject(child);
}
}
for (const o of engineObjects)
o.parent || updateObject(o);
// remove destroyed objects
engineObjects = engineObjects.filter(o=>!o.destroyed);
}
/** Destroy and remove all objects
* @memberof Engine */
function engineObjectsDestroy()
{
for (const o of engineObjects)
o.parent || o.destroy();
engineObjects = engineObjects.filter(o=>!o.destroyed);
}
/** Triggers a callback for each object within a given area
* @param {Vector2} [pos] - Center of test area
* @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
* @param {Function} [callbackFunction] - Calls this function on every object that passes the test
* @param {Array} [objects=engineObjects] - List of objects to check
* @memberof Engine */
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
{
if (!pos) // all objects
{
for (const o of objects)
callbackFunction(o);
}
else if (size.x != undefined) // bounding box test
{
for (const o of objects)
isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
}
else // circle test
{
const sizeSquared = size*size;
for (const o of objects)
pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
}
}