jsgametemplate - commit b58caae
Initial commitb58caae35824320e564be7972e3dc8dc4e7df9e4LukeJ<152485046+LukePJ25@users.noreply.github.com>2025-08-16T19:11:15+01:00diff --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