Luke Jones

jsgametemplate - commit b58caae

Initial commitb58caae35824320e564be7972e3dc8dc4e7df9e4LukeJ<152485046+LukePJ25@users.noreply.github.com>2025-08-16T19:11:15+01:00
diff --git a/JSGameTemplate.html b/JSGameTemplate.html new file mode 100644 index 0000000..6dd5578 --- /dev/null +++ b/JSGameTemplate.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>JSGameTemplate</title> + <link rel="stylesheet"> +</head> +<body> +<img id="gfx" alt="The game's sprite sheet." style="display: none;" src="./assets/graphics.png"> +<main> + <div id="content"> + <!-- + If you want the game to start automatically, remove + "onClick="requestStart()"" from the canvas' attributes. + --> + <script src="./game/main.js" type="module"></script> + <canvas width="800" height="600" onClick="requestStart()" id="gamebox"></canvas> + </div> +</main> +</body> +</html> \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb78dbd --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# JSGameTemplate + +This project exists as a simple basis for basic canvas-based projects I aim to carry out in the future. It is based on +the code written for a project from my degree, [JSDiver](https://github.com/LukePJ25/JSDiver). + +It is essentially the same source code, but with a few functions cleaned-up/moved around, and game-specific logic +removed to create a simple bare-bones game-loop system. + +I will eventually write a small guide on how to use the included features, but here is a brief summary of what's +included: + +- Basic game-loop cycle with configurable framerate. +- Separation of drawing and game-logic functionality. +- Simple debug menu for printing variables to the canvas. +- Simple math functions for calculating things like overlapping shapes, clamping values between boundaries, etc. + +This will hopefully be expanded on as I use it more in the future. I don't exactly intend for this to be used outside +my own projects purely because it's not written for that purpose, but if you would like to use/amend some of this code +feel free to. \ No newline at end of file diff --git a/common.js b/common.js new file mode 100644 index 0000000..4aad820 --- /dev/null +++ b/common.js @@ -0,0 +1,83 @@ +/** + * General common functions for use in JSGameTemplate + * + * common.js + * Luke Jones - Aug. 2025 + */ + +/** + * Clamp given value between provided minimum and maximum boundaries + * @param v value to clamp + * @param mn minimum boundary + * @param mx maximum boundary + * @returns {number} value clamped between minimum and maximum boundaries + */ +function clamp(v, mn, mx) { + return Math.max(mn, Math.min(v, mx)); +} + +/** + * Check if value v lies between mn and mx + * @param v value to check + * @param mn minimum end of range + * @param mx maximum end of range + * @returns {boolean} whether the value lies between the min and max. + */ +function rangeCheck(v, mn, mx) { + return (v <= mx && v >= mn); +} + +/** + * Return lerped value between two values based on provided ratio + * + * @example + * const a = 0; + * const b = 20; + * let c = lerp(a, b, 0.5); + * console.log(c); + * + * // Output: 10 + * + * @param va Start value + * @param vb End Value + * @param ratio Lerp ratio + * @returns {*} Lerped value + */ +function lerp(va, vb, ratio) { + return va + ratio * (vb - va); +} + +/** + * Check whether the boundaries of the radii of two points overlap eachother. + * @param x1 X position of point 1 + * @param y1 Y position of point 1 + * @param r1 Radius of the area around point 1 + * @param x2 X position of point 2 + * @param y2 Y position of point 2 + * @param r2 Radius of the area around point 2 + * @returns {boolean} Whether the areas overlap eachother + */ +function checkRadialOverlap(x1, y1, r1, x2, y2, r2) { + return (Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow(y1 - y2, 2)) <= (r1 + r2)); +} + +/** + * Check whether two provided rectangular regions overlap eachother + * @param x1 left position of region 1 + * @param y1 top position of region 1 + * @param w1 width of region 1 + * @param h1 height of region 1 + * @param x2 left position of region 2 + * @param y2 top position of region 2 + * @param w2 width of region 2 + * @param h2 height of region 2 + * @returns {boolean} whether the two regions overlap + */ +function checkRegionOverlapfunction(x1, y1, w1, h1, x2, y2, w2, h2) { + return !( + x1 + w1 <= x2 || + x2 + w2 <= x1 || + y1 + h1 <= y2 || + y2 + h2 <= y1 + ); +} diff --git a/graphics.png b/graphics.png new file mode 100644 index 0000000..e69de29 diff --git a/main.js b/main.js new file mode 100644 index 0000000..ad84ab7 --- /dev/null +++ b/main.js @@ -0,0 +1,220 @@ +/** + * Simple canvas render/gameloop engine for use as a template in other projects. + * + * main.js + * Luke Jones - Aug. 2025 + */ + +import "./common.js"; + +/** + * Config - Feel free to change these. + */ +let debug = false; // Draws extra debugging information to canvas. User can press "U" to enable debugging. +const DEBUG_KEY = "U"; // Key the user can press to enable/disable debug mode. Set to "" to disable this control. +const FRAME_RATE_TARGET = 60; +const DEBUG_MENU_TEXT_SIZE = 12; +const DEBUGGER_WIDTH = 300; + +/** + * These variables are part of the engine/interface controller. Ideally these should not be changed. + */ +// Engine Values +let frameInterval, frameNow, frameThen, elapsed; +let mainRunning = false; +let gameInitialised = false; +let graphics = document.getElementById("gfx"); +// Controls/User Input +let left_down = false, right_down = false, down_down = false, up_down = false, click_down = false; +let mousepos = [0, 0]; +// Stores whether the keys are held down for 1 frame. +let left_tapped = false, right_tapped = false, space_tapped = false; +// GameStates. Use to store different states of the game, e.g. in Menu, Paused, Loading, Animations, etc.. +const GameStates = {PLAYING: 'PLAYING'}; +let currentState = GameStates.PLAYING; +// DOM/Canvas Initialisation +const g_box = document.getElementById("gamebox"); +const ctx = g_box.getContext("2d"); +ctx.imageSmoothingEnabled = false; +let CANVAS_WIDTH = g_box.width; +let CANVAS_HEIGHT = g_box.height; + +/** + * Draw debug menu with given details + * @param context canvas context to draw to + * @param items key-value pairs of items to display with the relevant variables. + * @example + * drawDebugMenu( + * ctx, + * { + * "FPS": Math.trunc(1000 / elapsed), + * "Mouse Position": mousepos + * }); + */ +function drawDebugMenu(context, items) { + const keys = Object.keys(items); // turn object keys into array + context.save(); + context.shadowBlur = 0; + context.fillStyle = 'rgba(0,0,0,0.5)'; + context.fillRect(0, 0, DEBUGGER_WIDTH, DEBUG_MENU_TEXT_SIZE * (1 + keys.length)); + context.fillStyle = '#fff'; + context.font = `${DEBUG_MENU_TEXT_SIZE}px Consolas`; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = items[key]; + context.fillText(`${key}: ${value}`, 0, DEBUG_MENU_TEXT_SIZE * (i + 1), 300); + } + context.restore(); +} + + +/** + * Draw elements to screen. + * + * This needs to be populated with drawing logic. + */ +function draw() { + // Clear canvas + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // If debugging enabled, draw debug menu with given items + if (debug) { + console.log("debugging..."); + drawDebugMenu(ctx, { + "FPS": Math.trunc(1000 / elapsed), + "Mouse Position": mousepos + }); + } +} + +/** + * Update game logic by one tick. Called per-frame in main(). + */ +function tick() { + // Game Tick +} + +/** + * Request to start the game runtime handler. + */ +export function requestStart() { + if (!mainRunning) { + frameInterval = 1000 / FRAME_RATE_TARGET; + frameThen = performance.now(); + frameNow = performance.now(); + elapsed = frameNow - frameThen; + gameRuntimeHandler(); + } + mainRunning = true; +} + +/** + * Game runtime handler, main function driving the gameloop. + */ +function gameRuntimeHandler() { + frameNow = performance.now(); + elapsed = frameNow - frameThen; + if (elapsed >= frameInterval) { + frameThen = frameNow - (elapsed % frameInterval); + main(); + } + requestAnimationFrame(gameRuntimeHandler); +} + +/** + * Main game logic loop. This is the function called by gameloop. Calls to drawing and game logic events should all + * be called from within this function. + */ +function main() { + if (currentState === GameStates.PLAYING) { // If the current state is "PLAYING". These can be added to ensure + tick(); + draw(); + } + + left_tapped = false; + right_tapped = false; + space_tapped = false; +} + +/** + * User keyboard input handler + * @param input_event + * @param press + */ +function handleKeys(input_event, press) { + let inp_key; + if (input_event != null) { + inp_key = input_event.key; + // In the event that the user is currently focused on a text field, prioritise that over game control. + if (document.activeElement.tagName.toLowerCase() === "input") { + return; + } + input_event.preventDefault(); + switch (inp_key) { + case "ArrowLeft": + case "A": + case "a": + left_down = press; + left_tapped = press; + break; + case "ArrowRight": + case "D": + case "d": + right_down = press; + right_tapped = press; + break; + case "ArrowUp": + case "W": + case "w": + up_down = press; + break; + case "ArrowDown": + case "S": + case "s": + down_down = press; + break; + case " ": + space_tapped = press; + break; + case DEBUG_KEY: + case DEBUG_KEY.toLowerCase(): + if (press) { + debug = !debug; + } + break; + default: + break; + } + } +} + +document.onkeydown = function (event) { + handleKeys(event, true); +}; +document.onkeyup = function (event) { + handleKeys(event, false); +}; + +/** + * Mouse event handling logic + */ +document.onmousemove = function (event) { + let canvasBounds = g_box.getBoundingClientRect(); + mousepos = [Math.trunc(event.clientX - canvasBounds.left), Math.trunc(event.clientY - canvasBounds.top)]; +}; +document.onmousedown = function () { + click_down = true; +}; +document.onmouseup = function () { + click_down = false; +}; + +/** + * Update window details on resize + */ +window.onresize = function () { + CANVAS_WIDTH = g_box.width; + CANVAS_HEIGHT = g_box.height; +} + +window.requestStart = requestStart; \ No newline at end of file