UNPKG

@theatrejs/theatrejs

Version:

🎮 A JavaScript 2D Game Engine focused on creating pixel art games.

543 lines (426 loc) • 12.4 kB
import {AABB, Actor, Loop, MEDIA_TYPES, Stage, SystemActor, SystemAudio, SystemCollision, SystemInput, SystemRender, SystemVibration, UTILS, Vector2, Vector3} from '../index.js'; /** * Creates game engines. * * @example * * // minimal * const engine = new Engine(); * engine.initiate(60); * * await engine.preloadStage(SceneExample); * * engine.createStage(SceneExample); * * @example * * // full * const engine = new Engine({$color, $container, $framing}); * engine.initiate(60); * * await engine.preloadStage(SceneExample); * * engine.createStage(SceneExample); */ class Engine { /** * Stores the rendering background color. * @type {Vector3} * @private */ $color; /** * Stores the container. * @type {HTMLElement} * @private */ $container; /** * Stores the framing (rendering resolution). * @type {Vector2} * @private */ $framing; /** * Stores the loop. * @type {Loop} * @private */ $loop; /** * Stores the next stage. * @type {typeof Stage} * @private */ $next; /** * Stores the preloaded status of the assets. * @type {Set<string>} * @private */ $preloaded; /** * Stores the current stage. * @type {Stage} * @private */ $stage; /** * Stores the current actor system. * @type {SystemActor} * @private */ $systemActor; /** * Stores the current audio system. * @type {SystemAudio} * @private */ $systemAudio; /** * Stores the current collision system. * @type {SystemCollision} * @private */ $systemCollision; /** * Stores the current input system. * @type {SystemInput} * @private */ $systemInput; /** * Stores the current render system. * @type {SystemRender} * @private */ $systemRender; /** * Stores the current vibration system. * @type {SystemVibration} * @private */ $systemVibration; /** * Stores the uuid. * @type {string} * @private */ $uuid; /** * Gets the container. * @type {HTMLElement} * @public */ get container() { return this.$container; } /** * Gets the current stage. * @type {Stage} * @public */ get stage() { return this.$stage; } /** * Gets the uuid. * @type {string} * @public */ get uuid() { return this.$uuid; } /** * Creates a new game engine. * @param {object} [$parameters] The given parameters. * @param {Vector3} [$parameters.$color] The rendering background color to use. * @param {HTMLElement} [$parameters.$container] The container for the game engine to create. * @param {Vector2} [$parameters.$framing] The framing (rendering resolution) to use. */ constructor({$color = new Vector3(0, 0, 0), $container = document.body, $framing = new Vector2(320, 240)} = {}) { this.$color = $color.clone(); this.$container = $container; this.$framing = $framing.clone(); this.$uuid = UTILS.uuid(); this.$loop = new Loop(this.tick.bind(this)); this.$preloaded = new Set(); this.$systemActor = new SystemActor(); this.$systemAudio = new SystemAudio(); this.$systemCollision = new SystemCollision(); this.$systemInput = new SystemInput({$container: this.$container}); this.$systemRender = new SystemRender({$color: this.$color, $container: this.$container, $framing: this.$framing}); this.$systemVibration = new SystemVibration(); } /** * Creates the given stage. * @param {typeof Stage} $stage The stage to create. * @private */ $createStage($stage) { this.$stage = new $stage(this); this.$stage.onCreate(); } /** * Checks the resized status. * @returns {boolean} * @public */ checkResized() { return this.$systemRender.checkResized() === true; } /** * Creates the given stage on the next tick update. * @param {typeof Stage} $stage The stage to create on the next tick update. * @public */ createStage($stage) { this.$next = $stage; } /** * Gets the boundaries in the current stage from the framing (rendering resolution). * @returns {AABB} * @public */ getBoundariesFromFraming() { return this.$systemRender.getBoundariesFromFraming(this.$stage); } /** * Gets the boundaries in the current stage from the screen. * @returns {AABB} * @public */ getBoundariesFromScreen() { return this.$systemRender.getBoundariesFromScreen(this.$stage); } /** * Gets the persisted status of the given input. * @param {string} $input The event code of the input. * @returns {boolean} * @public */ getInput($input) { return this.$systemInput.getInput($input); } /** * Gets the analog value of the given input. * @param {string} $input The event code of the input. * @returns {number} * @public */ getInputAnalog($input) { return this.$systemInput.getInputAnalog($input); } /** * Gets the initiated status of the given input. * @param {string} $input The event code of the input. * @returns {boolean} * @public */ getInputDown($input) { return this.$systemInput.getInputDown($input); } /** * Gets the terminated status of the given input. * @param {string} $input The event code of the input. * @returns {boolean} * @public */ getInputUp($input) { return this.$systemInput.getInputUp($input); } /** * Gets the position in the current stage from the given clipped position in the screen. * @param {Vector2} $vector The position in the screen (with values in [-1, 1] ranges). * @returns {Vector2} * @public */ getTranslationFromScreen($vector) { return this.$systemRender.getTranslationFromScreen(this.$stage, $vector.clone()); } /** * Gets the position in the current stage from the given pointer event in the viewport. * @param {PointerEvent} $event The pointer event. * @returns {Vector2} * @public */ getTranslationFromViewportPointer($event) { return this.$systemRender.getTranslationFromScreen(this.$stage, new Vector2( 2 * ($event.clientX / this.container.clientWidth) - 1, 2 * ($event.clientY / this.container.clientHeight) - 1 )); } /** * Checks if the engine has loaded the given asset. * @param {string} $asset The asset source. * @returns {boolean} * @public */ hasAssetLoaded($asset) { return this.$preloaded.has($asset) === true; } /** * Initiates the engine. * @param {number} [$tickrateMinimum] The minimum acceptable number of ticks per virtual second (in ticks/s). * @public */ initiate($tickrateMinimum = 60) { this.$systemInput.initiate(); this.$systemActor.initiate(); this.$systemCollision.initiate(); this.$systemRender.initiate(); this.$systemAudio.initiate(); this.$systemVibration.initiate(); this.$loop.initiate($tickrateMinimum); } /** * Preloads the assets of the given stage. * @param {typeof Stage} $stage The stage to preload the assets from. * @returns {Promise<Array<undefined | AudioBuffer | WebGLTexture>>} * @public */ preloadStage($stage) { /** * @type {Array<Promise<undefined | AudioBuffer | WebGLTexture>>} */ const promises = []; UTILS.deduplicate($stage.preloadables).forEach(($asset) => { if (this.hasAssetLoaded($asset) === true) { return; } this.$preloaded.add($asset); if (this.$systemAudio.hasAssetLoaded($asset) === true) { return; } if (this.$systemRender.hasAssetLoaded($asset) === true) { return; } /** * @type {Promise<undefined | AudioBuffer | WebGLTexture>} */ const promise = new Promise(($resolve) => { fetch($asset) .then(($content) => { const contentType = $content.headers.get('Content-Type'); const [mediaType] = contentType.toLowerCase().split('/'); switch (mediaType) { case MEDIA_TYPES.AUDIO: { this.$systemAudio.loadAudio($content) .then(($bufferAudio) => { $resolve($bufferAudio); }); break; } case MEDIA_TYPES.IMAGE: { this.$systemRender.loadTexture($content) .then(($texture) => { $resolve($texture); }); break; } default: { $resolve(undefined); } } }); }); promises.push(promise); }); return Promise.all(promises); } /** * Gets all the visible actors at the given position (from top to bottom). * @param {Vector2} $position The position to use. * @returns {Array<Actor>} * @public */ raycast($position) { return this.$systemRender.raycast({ $position: $position, $stage: this.$stage }); } /** * Removes the native pointer display. * @public */ removePointerNative() { this.$systemRender.removePointerNative(); } /** * Sets the rendering background color. * @param {Vector3} $color The rendering background color to set. * @public */ setColor($color) { this.$systemRender.setColor($color.clone()); } /** * Sets the framing (rendering resolution). * @param {Vector2} $framing The framing (rendering resolution) to set. * @public */ setFraming($framing) { this.$systemRender.setFraming($framing.clone()); } /** * Terminates the engine (immediately) (must be used outside the lifecycle of this engine). * @public */ terminate() { this.$loop.terminate(); this.$createStage(Stage); this.tick(0); this.$systemInput.terminate(); this.$systemActor.terminate(); this.$systemCollision.terminate(); this.$systemRender.terminate(); this.$systemAudio.terminate(); this.$systemVibration.terminate(); this.$preloaded = new Set(); this.$resized = false; } /** * Updates each system once. * @param {number} $timetick The tick duration (in ms). * @public */ tick($timetick) { if (typeof this.$next === typeof Stage) { const stage = this.$next; this.$next = undefined; this.$createStage(stage); } if (typeof this.$stage === 'undefined') { return; } this.$systemInput.tick({ $stage: this.$stage, $timetick: $timetick }); this.$systemActor.tick({ $stage: this.$stage, $timetick: $timetick }); this.$systemCollision.tick({ $stage: this.$stage, $timetick: $timetick }); this.$systemRender.tick({ $stage: this.$stage, $timetick: $timetick }); this.$systemAudio.tick({ $stage: this.$stage, $timetick: $timetick }); this.$systemVibration.tick({ $stage: this.$stage, $timetick: $timetick }); } } export { Engine }; export default Engine;