DEV Community

Cover image for 🔧 Creating Extensions for Limn Engine — Make Game Development Better and Faster
Kehinde Owolabi
Kehinde Owolabi

Posted on

🔧 Creating Extensions for Limn Engine — Make Game Development Better and Faster

🔧 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);
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 4: Component.prototype.setHealth = function(max) {

  • Adds a new method called setHealth to every component
  • The max parameter 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 damage method 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 true if 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);
}
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 4: class Player extends Component {

  • Creates a new class called Player that inherits from Component
  • 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 Component constructor 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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

2. Use Namespaces

// ── PREVENT NAMING CONFLICTS ──
const LimnExtensions = {
    Health: { ... },
    Level: { ... },
    Scene: { ... }
};
Enter fullscreen mode Exit fullscreen mode

3. Check for Existing Methods

// ── DON'T OVERRIDE EXISTING METHODS ──
if (!Component.prototype.setHealth) {
    Component.prototype.setHealth = function(max) { ... };
}
Enter fullscreen mode Exit fullscreen mode

🎯 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)