UNPKG

@knopkem/little-game-engine-ts

Version:

LittleGameEngineTS - Tiny and Fast HTML5 Game Engine typescript port of LittleJS

283 lines (226 loc) 11.7 kB
import { fixedWidth, fixedHeight, tileBleedShrinkFix, pixelated, fixedFitToWindow, maxHeight, maxWidth } from "./index"; import { ASSERT, debugInit, debugUpdate, debugRender, debug, showWatermark } from "./index"; import { min, lerp, vec2, isOverlapping } from "./index"; import { tileImage, mainCanvas, setMainCanvas, mainContext, setMainContext, overlayCanvas, setOverlayCanvas, overlayContext, setOverlayContext, setMainCanvasSize } from "./index"; import { medalsRender } from "./index"; import { glInit, glPreRender } from "./index"; import { glCanvas } from "./index"; import { keyIsDown, inputUpdatePost, inputUpdate, glCopyToContext } from "./index"; /* 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 - Call engineInit() to start it up! */ /** Name of engine */ export const engineName = 'LittleJS'; /** Version of engine */ export const engineVersion = '1.1.2'; /** Frames per second to update objects * @default */ export const FPS = 60; /** How many seconds each frame lasts, engine uses a fixed time step * @default 1/60 */ export const timeDelta = 1 / FPS; /** Array containing all engine objects */ export let engineObjects: any = []; /** Array containing only objects that are set to collide with other objects (for optimization) */ export let engineCollideObjects: any = []; /** Current update frame, used to calculate time */ export let frame = 0; /** Current engine time since start in seconds, derived from frame */ export let time = 0; /** Actual clock time since start in seconds (not affected by pause or frame rate clamping) */ export let timeReal = 0; /** Is the game paused? Causes time and objects to not be updated. */ export let paused = 0; // Engine internal variables not exposed to documentation export let frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0, shrinkTilesX: number, shrinkTilesY: number, drawCount: any, tileImageSize: any, tileImageSizeInverse: any; export function setDrawCount(count: number): number { return drawCount = count; } /////////////////////////////////////////////////////////////////////////////// /** 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} tileImageSource - Tile image to use, everything starts when the image is finished loading */ export function engineInit(gameInit: any, gameUpdate: any, gameUpdatePost: any, gameRender: any, gameRenderPost: any, tileImageSource: any) { // init engine when tiles load tileImage.onload = () => { // save tile image info tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height)); debug && (tileImage.onload = () => ASSERT(1)); // tile sheet can not reloaded shrinkTilesX = tileBleedShrinkFix / tileImageSize.x; shrinkTilesY = tileBleedShrinkFix / tileImageSize.y; // setup html document.body.appendChild(setMainCanvas(document.createElement('canvas'))); document.body.setAttribute('style', 'margin:0;overflow:hidden;background:#000'); mainCanvas.setAttribute('style', 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)' + (pixelated ? ';image-rendering:crisp-edges;image-rendering:pixelated' : '')); // pixelated rendering setMainContext(mainCanvas.getContext('2d')); // init stuff and start engine debugInit(); glInit(); // create overlay canvas for hud to appear above gl canvas document.body.appendChild(setOverlayCanvas(document.createElement('canvas'))); overlayCanvas.setAttribute('style', 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)'); setOverlayContext(overlayCanvas.getContext('2d')); gameInit(); engineUpdate(); }; // main update loop const engineUpdate = (frameTimeMS = 0) => { requestAnimationFrame(engineUpdate); // update time keeping let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS; frameTimeLastMS = frameTimeMS; if (debug || showWatermark) debugFPS = lerp(.05, 1e3 / (frameTimeDeltaMS || 1), debugFPS); if (debug) frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time timeReal += frameTimeDeltaMS / 1e3; // @ts-expect-error ts-migrate(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message frameTimeBufferMS = min(frameTimeBufferMS + !paused * frameTimeDeltaMS, 50); // clamp incase of slow framerate 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 / FPS) { // update game and objects inputUpdate(); gameUpdate(); engineObjectsUpdate(); // do post update debugUpdate(); gameUpdatePost(); inputUpdatePost(); } // add the time smoothing back in frameTimeBufferMS += deltaSmooth; } if (fixedWidth) { // clear set fixed size mainCanvas.width = fixedWidth; mainCanvas.height = fixedHeight; if (fixedFitToWindow) { // fit to window by adding space on top or bottom if necessary const aspect = innerWidth / innerHeight; const fixedAspect = fixedWidth / fixedHeight; mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : ''; mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%'; if (glCanvas) { glCanvas.style.width = mainCanvas.style.width; glCanvas.style.height = mainCanvas.style.height; } } } else { // clear and set size to same as window mainCanvas.width = min(innerWidth, maxWidth); mainCanvas.height = min(innerHeight, maxHeight); } // save canvas size and clear overlay canvas setMainCanvasSize(vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height)); mainContext.imageSmoothingEnabled = !pixelated; // disable smoothing for pixel art // render sort then render while removing destroyed objects glPreRender(mainCanvas.width, mainCanvas.height); gameRender(); engineObjects.sort((a: any, b: any) => a.renderOrder - b.renderOrder); for (const o of engineObjects) o.destroyed || o.render(); gameRenderPost(); medalsRender(); debugRender(); 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 + ' / ' + debugFPS.toFixed(1); overlayContext.fillText(text, mainCanvas.width - 3, 3); overlayContext.fillStyle = '#fff'; overlayContext.fillText(text, mainCanvas.width - 2, 2); drawCount = 0; } } // set tile image source to load the image and start the engine tileImageSource ? tileImage.src = tileImageSource : tileImage.onload; } /////////////////////////////////////////////////////////////////////////////// /** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */ export function engineObjectsUpdate() { // recursive object update const updateObject = (o: any) => { 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: any) => !o.destroyed); engineCollideObjects = engineCollideObjects.filter((o: any) => !o.destroyed); // increment frame and update time time = ++frame / FPS; } /** Detroy and remove all objects that are not persistent or descendants of a persistent object */ export function engineObjectsDestroy() { for (const o of engineObjects) o.persistent || o.parent || o.destroy(); engineObjects = engineObjects.filter((o: any) => !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 */ export function engineObjectsCallback(pos: any, size: any, callbackFunction: any, objects = engineObjects) { if (!pos) { // all objects for (const o of objects) callbackFunction(o); } else if (size.x != undefined) { // aabb 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); } }