🔧 Creating Extensions for Limn Engine — Make Game Development Better and Faster
A Complete Guide to Extending Limn Engine with Custom Features
📖 Introduction
Limn Engine is already powerful out of the box. But what if you need a feature that doesn't exist yet? What if you want to add a health system, a level editor, or a custom physics behavior?
The beauty of Limn Engine is that it's plain JavaScript. You can extend it without touching the core engine code. In this guide, we'll show you how to create extensions that make game development faster and more powerful.
🎯 Why Create Extensions?
| Reason | What It Means |
|---|---|
| Reusability | Write once, use in multiple projects |
| Sharing | Share extensions with the community |
| Productivity | Build common features faster |
| Learning | Understand the engine internals |
| Customization | Add exactly what you need |
🧩 Extension Pattern 1: Prototype Extension
The Concept
JavaScript allows you to add methods to existing classes using prototype. This is the simplest way to extend Limn Engine.
How it works: Every Limn Engine class (like Component, Display, Camera) is a JavaScript class. You can add new methods to these classes that will be available on all instances.
What We're About to Do
We're going to add a health system to every component. This is useful for players, enemies, and anything else that needs health.
The Code
// ── EXTEND COMPONENT WITH HEALTH SYSTEM ──
// Add health system to every component
Component.prototype.setHealth = function(max) {
this.hp = max; // Current health
this.maxHp = max; // Maximum health
this.invincible = 0; // Invincibility frames counter
console.log(`💚 Health set to ${max}`);
};
// Damage method
Component.prototype.damage = function(amount) {
// Check if invincible
if (this.invincible > 0) {
console.log(`🛡️ Invincible! No damage taken.`);
return false;
}
// Apply damage
this.hp = Math.max(0, this.hp - amount);
this.invincible = 30; // 30 frames of invincibility
// Check if dead
if (this.hp === 0) {
console.log(`💀 ${this.constructor.name} died!`);
return true; // Returns true if dead
}
console.log(`❤️ ${this.hp}/${this.maxHp} health remaining`);
return false; // Returns false if still alive
};
// Tick invincibility frames
Component.prototype.tickInvincible = function() {
if (this.invincible > 0) {
this.invincible--;
}
};
// Draw health bar above component
Component.prototype.drawHealthBar = function(ctx) {
// Don't draw if at max health
if (this.hp === this.maxHp) return;
const pct = this.hp / this.maxHp; // Health percentage (0 to 1)
const barW = this.width; // Bar width matches component width
const barH = 5; // Bar height
// Background bar (dark gray)
ctx.fillStyle = "#333";
ctx.fillRect(this.x, this.y - 10, barW, barH);
// Health fill (color based on health)
let color;
if (pct > 0.5) color = "#00cc44"; // Green — healthy
else if (pct > 0.25) color = "#ffaa00"; // Yellow — wounded
else color = "#cc0000"; // Red — critical
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y - 10, barW * pct, barH);
};
// ── USAGE ──
// Create player
const player = new Component(40, 40, "blue", 400, 300, "rect");
display.add(player);
// Set health
player.setHealth(100);
// In the game loop
function update(dt) {
// Tick invincibility frames
player.tickInvincible();
// When enemy hits player
if (player.crashWith(enemy)) {
const died = player.damage(15);
if (died) {
console.log("Game Over!");
display.stop();
}
}
// Draw health bar
player.drawHealthBar(display.context);
}
What Each Line Does
Line 4: Component.prototype.setHealth = function(max) {
- Adds a new method called
setHealthto every component - The
maxparameter is the maximum health
Line 5-6: this.hp = max; this.maxHp = max;
- Sets the current health and maximum health to the same value
Line 7: this.invincible = 0;
- Stores invincibility frames counter (0 = not invincible)
Line 12: Component.prototype.damage = function(amount) {
- Adds a
damagemethod to every component
Line 14-16: if (this.invincible > 0) { return false; }
- If the component is invincible, no damage is taken
Line 19: this.hp = Math.max(0, this.hp - amount);
- Reduces health, but not below 0
Line 20: this.invincible = 30;
- Makes the component invincible for 30 frames
Line 23-24: if (this.hp === 0) { return true; }
- Returns
trueif the component is dead
Line 36: Component.prototype.tickInvincible = function() {
- Adds a method to count down invincibility frames
Line 37-39: if (this.invincible > 0) { this.invincible--; }
- Reduces invincibility counter by 1 each frame
Line 42: Component.prototype.drawHealthBar = function(ctx) {
- Adds a method to draw a health bar above the component
Line 45: if (this.hp === this.maxHp) return;
- Skips drawing if health is full
Line 47: const pct = this.hp / this.maxHp;
- Calculates health percentage
Line 48-49: const barW = this.width; const barH = 5;
- Sets the health bar size
Line 52-58: Draws the background and health fill with colors based on health percentage
🧩 Extension Pattern 2: Utility Extension
The Concept
Instead of adding methods to existing classes, you can create your own utility objects. This is cleaner for features that don't naturally belong to a specific class.
How it works: You create a new object that contains helper functions. These functions take components as parameters and work on them.
What We're About to Do
We're going to create a level utility that helps with level design.
The Code
// ── LEVEL UTILITY ──
const level = {
// Create a simple level from a 2D array
create: function(map, tileSize, tileColors) {
const tiles = [];
for (let row = 0; row < map.length; row++) {
for (let col = 0; col < map[row].length; col++) {
const tileId = map[row][col];
if (tileId === 0) continue; // Skip empty
const color = tileColors[tileId] || "#555";
const tile = new Component(
tileSize,
tileSize,
color,
col * tileSize,
row * tileSize,
"rect"
);
display.add(tile);
tiles.push(tile);
}
}
return tiles;
},
// Add walls around the edge of a level
addWalls: function(map, wallId = 1) {
const rows = map.length;
const cols = map[0].length;
// Top and bottom walls
for (let c = 0; c < cols; c++) {
map[0][c] = wallId;
map[rows - 1][c] = wallId;
}
// Left and right walls
for (let r = 0; r < rows; r++) {
map[r][0] = wallId;
map[r][cols - 1] = wallId;
}
return map;
},
// Find empty spaces in a level
findEmpty: function(map) {
const empties = [];
for (let row = 0; row < map.length; row++) {
for (let col = 0; col < map[row].length; col++) {
if (map[row][col] === 0) {
empties.push({ row, col });
}
}
}
return empties;
},
// Get a random empty position
randomEmpty: function(map) {
const empties = this.findEmpty(map);
if (empties.length === 0) return null;
return empties[Math.floor(Math.random() * empties.length)];
}
};
// ── USAGE ──
// Define a level map
let myMap = [
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0]
];
// Add walls
myMap = level.addWalls(myMap);
// Define tile colors
const tileColors = {
1: "#654321", // Wall
2: "gold", // Coin
3: "red" // Enemy spawn
};
// Create the level
const tiles = level.create(myMap, 64, tileColors);
// Find a random empty position
const empty = level.randomEmpty(myMap);
if (empty) {
console.log(`Empty space at row ${empty.row}, col ${empty.col}`);
// Place a coin at that position
myMap[empty.row][empty.col] = 2;
}
// ── SAVE AND LOAD LEVELS ──
level.save = function(map, name = "myLevel") {
localStorage.setItem(name, JSON.stringify(map));
console.log(`💾 Level "${name}" saved!`);
};
level.load = function(name = "myLevel") {
const data = localStorage.getItem(name);
if (!data) {
console.log(`❌ Level "${name}" not found!`);
return null;
}
return JSON.parse(data);
};
// Save a level
level.save(myMap, "level1");
// Load a level later
const loadedMap = level.load("level1");
if (loadedMap) {
console.log("✅ Level loaded successfully!");
// Recreate the level with the loaded map
const newTiles = level.create(loadedMap, 64, tileColors);
}
What Each Line Does
Line 4: const level = {
- Creates a new utility object called
level
Line 6: create: function(map, tileSize, tileColors) {
- Takes a 2D array map, tile size, and color mapping
- Creates components for each non-zero tile
Line 9-14: for (let row = 0; row < map.length; row++) { for (let col = 0; col < map[row].length; col++) { const tileId = map[row][col]; if (tileId === 0) continue;
- Loops through every cell in the map and skips empty cells (0)
Line 16-22: Creates a component for each tile with the right position and color
Line 33: addWalls: function(map, wallId = 1) {
- Adds walls around the edge of a map
Line 35-37: const rows = map.length; const cols = map[0].length;
- Gets the dimensions of the map
Line 39-42: Adds walls to the top and bottom rows
Line 44-47: Adds walls to the left and right columns
Line 54: findEmpty: function(map) {
- Finds all empty cells (value = 0) in a map
Line 68: randomEmpty: function(map) {
- Returns a random empty position from the map
Line 79-82: save: function(map, name = "myLevel") { localStorage.setItem(name, JSON.stringify(map)); }
- Saves a level to localStorage as JSON
Line 86-90: load: function(name = "myLevel") { const data = localStorage.getItem(name); if (!data) { return null; } return JSON.parse(data); }
- Loads a level from localStorage
🧩 Extension Pattern 3: Custom Class Extension
The Concept
Sometimes you need a completely new class that extends Limn Engine's classes. This is useful for creating complex objects with specialized behavior.
How it works: You create a new class that inherits from a Limn Engine class (like Component) and adds new properties and methods.
What We're About to Do
We're going to create a Player class that extends Component. This class will have built-in health, movement, and shooting.
The Code
// ── CUSTOM PLAYER CLASS ──
class Player extends Component {
constructor(x, y) {
super(40, 40, "#5b8cff", x, y, "rect");
// Player-specific properties
this.speed = 200;
this.hp = 100;
this.maxHp = 100;
this.invincible = 0;
this.score = 0;
this.bullets = [];
this.lastShot = 0;
this.shootCooldown = 0.3; // seconds
// Enable physics
this.physics = false;
}
// Update player (called every frame)
updatePlayer(dt) {
// Movement
this.speedX = 0;
this.speedY = 0;
if (display.keys[37]) this.speedX = -this.speed * dt;
if (display.keys[39]) this.speedX = this.speed * dt;
if (display.keys[38]) this.speedY = -this.speed * dt;
if (display.keys[40]) this.speedY = this.speed * dt;
// Shoot
if (display.keys[32]) {
this.shoot();
}
// Invincibility timer
if (this.invincible > 0) {
this.invincible--;
this.alpha = 0.5; // Blink effect
} else {
this.alpha = 1;
}
}
// Shoot a bullet
shoot() {
const now = performance.now() / 1000;
if (now - this.lastShot < this.shootCooldown) return;
this.lastShot = now;
const bullet = new Component(6, 12, "yellow",
this.x + this.width/2 - 3,
this.y - 5,
"rect"
);
bullet.speedY = -300;
display.add(bullet);
this.bullets.push(bullet);
// Limit bullets
if (this.bullets.length > 50) {
const old = this.bullets.shift();
old.destroy();
}
}
// Take damage
takeDamage(amount) {
if (this.invincible > 0) return false;
this.hp = Math.max(0, this.hp - amount);
this.invincible = 60; // 1 second at 60fps
display.camera.shake(8, 8);
if (this.hp === 0) {
console.log("💀 Player died!");
return true; // Dead
}
return false; // Still alive
}
// Draw health bar
drawHealth(ctx) {
const pct = this.hp / this.maxHp;
const barW = this.width;
const barH = 5;
ctx.fillStyle = "#333";
ctx.fillRect(this.x, this.y - 10, barW, barH);
const color = pct > 0.5 ? "#00cc44" : pct > 0.25 ? "#ffaa00" : "#cc0000";
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y - 10, barW * pct, barH);
}
// Update bullets
updateBullets() {
for (let i = this.bullets.length - 1; i >= 0; i--) {
const b = this.bullets[i];
if (b.y < -50) {
b.destroy();
this.bullets.splice(i, 1);
}
}
}
}
// ── USAGE ──
// Create player
const player = new Player(400, 300);
display.add(player);
// In the game loop
function update(dt) {
// Update player
player.updatePlayer(dt);
player.updateBullets();
player.drawHealth(display.context);
// Check player collision with enemy
if (player.crashWith(enemy)) {
const died = player.takeDamage(15);
if (died) {
display.stop();
}
}
}
What Each Line Does
Line 4: class Player extends Component {
- Creates a new class called
Playerthat inherits fromComponent - This means it has all Component properties and methods
Line 6: constructor(x, y) {
- The constructor function runs when you create a new Player
Line 7: super(40, 40, "#5b8cff", x, y, "rect");
- Calls the parent
Componentconstructor with size, color, and position
Line 10-16: this.speed = 200; this.hp = 100; ...
- Adds player-specific properties
Line 24: updatePlayer(dt) {
- Custom update method for player logic
Line 26-31: this.speedX = 0; ...
- Handles movement with arrow keys
Line 34-36: if (display.keys[32]) { this.shoot(); }
- Checks for Space key to shoot
Line 39-44: if (this.invincible > 0) { this.invincible--; this.alpha = 0.5; } else { this.alpha = 1; }
- Manages invincibility frames and blink effect
Line 49: shoot() {
- Creates and fires a bullet
Line 50-51: const now = performance.now() / 1000; if (now - this.lastShot < this.shootCooldown) return;
- Enforces a cooldown between shots
Line 55-59: Creates a bullet at the player's position and adds it to the game
Line 62-65: if (this.bullets.length > 50) { const old = this.bullets.shift(); old.destroy(); }
- Limits the number of bullets to prevent memory issues
Line 70: takeDamage(amount) {
- Handles damage to the player
Line 72: if (this.invincible > 0) return false;
- Ignores damage if invincible
Line 74: this.hp = Math.max(0, this.hp - amount);
- Reduces health
Line 75: this.invincible = 60;
- Sets invincibility for 1 second
Line 76: display.camera.shake(8, 8);
- Shakes the camera for impact
Line 78-80: if (this.hp === 0) { return true; }
- Returns true if dead
Line 85-96: drawHealth(ctx) {
- Draws a health bar above the player
🧩 Extension Pattern 4: Scene Management Extension
The Concept
A scene manager helps you organize different game states like menu, gameplay, and game over screens.
What We're About to Do
We're going to create a scene manager that handles switching between scenes.
The Code
// ── SCENE MANAGER ──
class SceneManager {
constructor() {
this.scenes = {};
this.currentScene = null;
this.objects = {};
}
// Add a scene
addScene(name, config) {
this.scenes[name] = {
enter: config.enter || function() {},
update: config.update || function() {},
exit: config.exit || function() {},
objects: config.objects || []
};
console.log(`📋 Scene "${name}" added`);
}
// Switch to a scene
switchTo(name) {
// Exit current scene
if (this.currentScene && this.scenes[this.currentScene]) {
this.scenes[this.currentScene].exit();
// Remove objects from previous scene
if (this.objects[this.currentScene]) {
for (let obj of this.objects[this.currentScene]) {
obj.destroy();
}
this.objects[this.currentScene] = [];
}
}
// Enter new scene
this.currentScene = name;
const scene = this.scenes[name];
if (!scene) {
console.error(`❌ Scene "${name}" not found!`);
return;
}
// Add objects for new scene
this.objects[name] = [];
for (let obj of scene.objects) {
display.add(obj);
this.objects[name].push(obj);
}
scene.enter();
console.log(`▶️ Scene switched to "${name}"`);
}
// Update current scene
update(dt) {
if (this.currentScene && this.scenes[this.currentScene]) {
this.scenes[this.currentScene].update(dt);
}
}
// Get current scene name
getCurrentScene() {
return this.currentScene;
}
// Add object to current scene
addObject(obj) {
if (!this.currentScene) return;
if (!this.objects[this.currentScene]) {
this.objects[this.currentScene] = [];
}
this.objects[this.currentScene].push(obj);
display.add(obj);
}
}
// ── USAGE ──
// Create scene manager
const scenes = new SceneManager();
// ── MENU SCENE ──
const titleText = new Tctxt("48px", "Arial", "white", 400, 200);
titleText.align = "center";
titleText.setText("🎮 MY GAME");
titleText.hide();
const startText = new Tctxt("24px", "Arial", "white", 400, 300);
startText.align = "center";
startText.setText("Press SPACE to start");
startText.hide();
scenes.addScene("menu", {
objects: [titleText, startText],
enter: function() {
titleText.show();
startText.show();
console.log("🎮 Menu scene entered");
},
exit: function() {
titleText.hide();
startText.hide();
console.log("🚪 Menu scene exited");
},
update: function(dt) {
if (display.keys[32]) {
display.keys[32] = false;
scenes.switchTo("game");
}
}
});
// ── GAME SCENE ──
const player = new Player(400, 300);
player.hide();
const scoreText = new Tctxt("24px", "Arial", "white", 20, 50);
scoreText.setText("Score: 0");
scoreText.hide();
scenes.addScene("game", {
objects: [player, scoreText],
enter: function() {
player.show();
scoreText.show();
console.log("🎯 Game scene entered");
},
exit: function() {
player.hide();
scoreText.hide();
console.log("🚪 Game scene exited");
},
update: function(dt) {
player.updatePlayer(dt);
player.updateBullets();
player.drawHealth(display.context);
scoreText.setText("Score: " + player.score);
}
});
// ── START THE GAME ──
scenes.switchTo("menu");
// ── IN THE GAME LOOP ──
function update(dt) {
scenes.update(dt);
}
What Each Line Does
Line 4: class SceneManager {
- Creates a class to manage scenes
Line 6-9: this.scenes = {}; this.currentScene = null; this.objects = {};
- Stores all scenes, current scene name, and objects per scene
Line 12: addScene(name, config) {
- Adds a new scene with a name and configuration
Line 13-18: this.scenes[name] = { enter: ..., update: ..., exit: ..., objects: ... }
- Each scene has enter, update, and exit functions, plus a list of objects
Line 23: switchTo(name) {
- Switches to a different scene
Line 25-35: Exits the current scene and removes its objects
Line 37-40: Enters the new scene
Line 42-48: Adds objects for the new scene
Line 61: addObject(obj) {
- Adds an object to the current scene
📊 Extension Patterns Comparison
| Pattern | Best For | Complexity |
|---|---|---|
| Prototype Extension | Adding simple methods to existing classes | Low |
| Utility Extension | Standalone helper functions | Low-Medium |
| Custom Class Extension | Complex objects with specialized behavior | Medium |
| Scene Manager | Organizing game states | Medium |
💡 Pro Tips
1. Keep Extensions Modular
// ── GOOD: Each extension in its own file ──
// health-extension.js
// level-extension.js
// player-class.js
// scene-manager.js
2. Use Namespaces
// ── PREVENT NAMING CONFLICTS ──
const LimnExtensions = {
Health: { ... },
Level: { ... },
Scene: { ... }
};
3. Check for Existing Methods
// ── DON'T OVERRIDE EXISTING METHODS ──
if (!Component.prototype.setHealth) {
Component.prototype.setHealth = function(max) { ... };
}
🎯 The One-Line Summary
"Extensions make Limn Engine infinitely extensible — add health systems, level editors, scene managers, and custom classes without touching the core engine."
Draw your game into existence — and extend it. 🔧🚀
Top comments (0)