DEV Community

Cover image for 🎮 Building a 3D Game in Limn Engine — Without a 3D Engine
Kehinde Owolabi
Kehinde Owolabi

Posted on

🎮 Building a 3D Game in Limn Engine — Without a 3D Engine

🎮 Building a 3D Game in Limn Engine — Without a 3D Engine

⚠️ THIS IS AN EXPERIMENTAL APPROACH — USE WITH CAUTION
Comment if error is found


📖 Introduction

Limn Engine is a 2D game engine — it was built for 2D games. But what if you want to create a 3D experience without switching to a 3D engine?

The answer: You fake it.

Many classic games created the illusion of 3D using 2D techniques. Games like Doom, Wolfenstein 3D, and Mode 7 racing games all used 2D rendering to simulate 3D worlds.

In this guide, we'll explore several techniques to create 3D-like experiences in Limn Engine — without a 3D engine.


🎯 The Challenge

Limitation What It Means
No Z-axis Everything is 2D — no depth
No 3D rendering No WebGL, no 3D models
No 3D physics No collisions in 3D space
No perspective No vanishing points by default

The solution: Use 2D tricks to simulate 3D depth, perspective, and movement.


🧩 Technique 1: Mode 7 (Fake 3D Racing)

The Concept

Mode 7 was used in games like F-Zero and Mario Kart on the SNES. It creates the illusion of a 3D ground plane by scaling and rotating a 2D image.

How it works: Instead of a 3D world, you have a 2D image that you transform in real-time. You scale it, rotate it, and move it to simulate movement.

What We're About to Do

We're going to create a simple Mode 7 effect with a road image that scales and moves.

The Code

// ── MODE 7 FAKE 3D RACING (EXPERIMENTAL) ──

const display = new Display();
display.perform();
display.start(800, 600);
display.backgroundColor("#1a1a2e");

// ── CREATE ROAD IMAGE ──
// In a real game, you'd load an image. For this demo, we draw it.

const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;
const ctx = canvas.getContext('2d');

// Draw a simple road with lanes
ctx.fillStyle = "#333";
ctx.fillRect(0, 0, 200, 200);

// Road
ctx.fillStyle = "#555";
ctx.fillRect(40, 0, 120, 200);

// Lane markings
ctx.strokeStyle = "white";
ctx.setLineDash([20, 30]);
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(100, 0);
ctx.lineTo(100, 200);
ctx.stroke();

// Road edges
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(40, 0);
ctx.lineTo(40, 200);
ctx.stroke();

ctx.beginPath();
ctx.moveTo(160, 0);
ctx.lineTo(160, 200);
ctx.stroke();

// ── CREATE ROAD COMPONENT ──
const road = new Component(200, 200, null, 400, 300, "image");
road.setImage(canvas.toDataURL());
display.add(road);

// ── GAME STATE ──
let distance = 0;
let speed = 0;
const MAX_SPEED = 200;
const ACCELERATION = 200;
const FRICTION = 100;

// ── GAME LOOP ──
function update(dt) {
    // ── ACCELERATION ──
    if (display.keys[38]) {  // Up arrow to accelerate
        speed = Math.min(speed + ACCELERATION * dt, MAX_SPEED);
    } else {
        speed = Math.max(0, speed - FRICTION * dt);
    }

    // ── UPDATE DISTANCE ──
    distance += speed * dt;

    // ── CALCULATE SCALE ──
    // As distance increases, the road gets smaller (simulating distance)
    const scale = Math.max(0.5, 1 + (distance / 1000));
    const newWidth = 200 / scale;
    const newHeight = 200 / scale;

    // ── APPLY TRANSFORM ──
    road.width = newWidth;
    road.height = newHeight;
    road.x = 400 - newWidth / 2;
    road.y = 300 - newHeight / 2;

    // ── ROTATE FOR TURNING ──
    let rotation = 0;
    if (display.keys[37]) rotation = -0.02;
    if (display.keys[39]) rotation = 0.02;
    road.angle += rotation * speed * 0.001;
    road.changeAngle = true;

    // ── UI ──
    // (Add speed display, etc.)
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 9-31: Creates a road image on a temporary canvas

  • The road is 200x200 pixels with lane markings
  • This is the image that will be transformed

Line 34: const road = new Component(200, 200, null, 400, 300, "image");

  • Creates a component that will display the road image

Line 35: road.setImage(canvas.toDataURL());

  • Converts the canvas to an image and sets it on the component

Line 39-41: Sets up game state variables

Line 47-50: Accelerates the car when Up arrow is pressed

Line 53: distance += speed * dt;

  • Increases the distance traveled

Line 56: const scale = Math.max(0.5, 1 + (distance / 1000));

  • Calculates the scale based on distance traveled

Line 57-58: const newWidth = 200 / scale; const newHeight = 200 / scale;

  • As scale increases, the image gets smaller (simulating distance)

Line 61-62: road.width = newWidth; road.height = newHeight;

  • Applies the new size to the road component

Line 68-70: Rotates the image for turning


🧩 Technique 2: Parallax Scrolling (Fake 3D Depth)

The Concept

Parallax scrolling creates the illusion of 3D depth by moving background layers at different speeds. This is how many classic 2D games created depth.

How it works: You have multiple layers of images. The foreground moves fast, the midground moves medium, and the background moves slow. This simulates the 3D perspective.

What We're About to Do

We're going to create a parallax scrolling effect with three layers.

The Code

// ── PARALLAX SCROLLING (FAKE 3D DEPTH) ──

const display = new Display();
display.perform();
display.start(800, 600);
display.backgroundColor("#1a1a2e");

// ── CREATE LAYERS ──
// Layer 1: Background (far mountains) - slowest
const bg = new Component(800, 200, "#2d5a7a", 0, 0, "rect");
display.add(bg);

// Layer 2: Midground (hills) - medium
const mid = new Component(800, 300, "#3a7a3a", 0, 200, "rect");
display.add(mid);

// Layer 3: Foreground (trees) - fastest
const fg = new Component(800, 200, "#5a8a3a", 0, 400, "rect");
display.add(fg);

// Add some trees to foreground (simplified)
for (let i = 0; i < 10; i++) {
    const tree = new Component(20, 40, "#2d5a2d", i * 80, 380, "rect");
    display.add(tree);
}

// ── GAME STATE ──
let offset = 0;
const SPEED = 100;

// ── GAME LOOP ──
function update(dt) {
    // Move offset based on speed
    offset += SPEED * dt;
    if (offset > 800) offset = 0;

    // ── MOVE LAYERS AT DIFFERENT SPEEDS ──
    // Background moves slow (20% speed)
    bg.x = -offset * 0.2;
    // Midground moves medium (60% speed)
    mid.x = -offset * 0.6;
    // Foreground moves fast (100% speed)
    fg.x = -offset * 1.0;

    // Keep layers in bounds by wrapping
    if (bg.x < -800) bg.x = 0;
    if (mid.x < -800) mid.x = 0;
    if (fg.x < -800) fg.x = 0;
}
Enter fullscreen mode Exit fullscreen mode

🧩 Technique 3: Z-Sorting (Fake 3D Depth Ordering)

The Concept

In 3D games, objects closer to the camera are drawn on top of objects further away. You can simulate this in 2D by sorting objects by their "depth" and drawing them in order.

How it works: Give each object a z value. The lower the z value, the further away it is. Sort objects by z and draw them from back to front.

What We're About to Do

We're going to create a scene with objects at different depths, drawn in the correct order.

The Code

// ── Z-SORTING (FAKE 3D DEPTH ORDERING) ──

const display = new Display();
display.perform();
display.start(800, 600);
display.backgroundColor("#1a1a2e");

// ── CREATE OBJECTS WITH DEPTH ──

// Far background (z = -10)
const bg = new Component(800, 600, "#2d5a7a", 0, 0, "rect");
bg.z = -10;
display.add(bg);

// Mountains (z = -5)
const mountain1 = new Component(200, 150, "#3a5a3a", 100, 400, "rect");
mountain1.z = -5;
display.add(mountain1);

// Trees (z = 0)
const tree1 = new Component(30, 60, "#2d5a2d", 200, 500, "rect");
tree1.z = 0;
display.add(tree1);

const tree2 = new Component(30, 60, "#2d5a2d", 400, 480, "rect");
tree2.z = 0;
display.add(tree2);

// Player (z = 5)
const player = new Component(40, 40, "blue", 380, 520, "rect");
player.z = 5;
display.add(player);

// Enemy (z = 10) - drawn on top of player if overlapping
const enemy = new Component(40, 40, "red", 360, 530, "rect");
enemy.z = 10;
display.add(enemy);

// ── SORT OBJECTS BY Z ──

function sortByZ() {
    // Get all components
    const components = comm.map(c => c.x);

    // Sort by z (lower z first = drawn first = further away)
    components.sort((a, b) => (a.z || 0) - (b.z || 0));

    // Rebuild comm array in sorted order
    comm.length = 0;
    for (let comp of components) {
        comm.push({ x: comp, scene: display.scene });
    }
}

// ── GAME LOOP ──
function update(dt) {
    // Sort objects by z every frame
    sortByZ();

    // Move enemy to demonstrate overlap
    enemy.x += 20 * dt;
    if (enemy.x > 420 || enemy.x < 360) {
        // Reverse direction
        enemy.speedX = -enemy.speedX;
    }
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 10-31: Creates objects with z values

  • Lower z = further away (drawn first)
  • Higher z = closer (drawn last)

Line 34-44: sortByZ() function

  • Gets all components from the comm array
  • Sorts them by their z value
  • Rebuilds the comm array in sorted order

Line 47-49: sortByZ(); called every frame

  • Ensures objects are always drawn in the correct order

🧩 Technique 4: 2.5D Isometric Projection

The Concept

Isometric projection is a way to create a 3D look using 2D graphics. Objects are drawn with a 45-degree angle, creating a "3D" view from above.

How it works: Instead of square tiles, you use diamond-shaped tiles. Objects are drawn with width and height, but positioned using isometric coordinates.

What We're About to Do

We're going to create a simple isometric grid with objects.

The Code

// ── 2.5D ISOMETRIC PROJECTION (EXPERIMENTAL) ──

const display = new Display();
display.perform();
display.start(800, 600);
display.backgroundColor("#1a1a2e");

// ── ISOMETRIC HELPERS ──

function isoToScreen(isoX, isoY, tileWidth, tileHeight) {
    const screenX = (isoX - isoY) * tileWidth / 2 + 400;
    const screenY = (isoX + isoY) * tileHeight / 2 + 100;
    return { x: screenX, y: screenY };
}

function screenToIso(screenX, screenY, tileWidth, tileHeight) {
    const isoX = (screenX - 400) / (tileWidth / 2) + (screenY - 100) / (tileHeight / 2);
    const isoY = (screenY - 100) / (tileHeight / 2) - (screenX - 400) / (tileWidth / 2);
    return { x: isoX, y: isoY };
}

// ── CREATE ISOMETRIC TILES ──

const tileWidth = 80;
const tileHeight = 40;

// Create a grid of isometric tiles
for (let row = 0; row < 5; row++) {
    for (let col = 0; col < 5; col++) {
        const pos = isoToScreen(col, row, tileWidth, tileHeight);

        // Alternate colors for checkerboard
        const color = (row + col) % 2 === 0 ? "#3a7a3a" : "#4a8a4a";

        // Create diamond-shaped tile
        // In a real implementation, you'd use a custom shape or image
        const tile = new Component(tileWidth, tileHeight, color, pos.x, pos.y, "rect");
        tile.changeAngle = true;
        tile.angle = Math.PI / 4; // 45 degrees
        display.add(tile);
    }
}

// ── CREATE OBJECT ON ISOMETRIC GRID ──

// Place a player at isometric position (2, 2)
const playerPos = isoToScreen(2, 2, tileWidth, tileHeight);
const player = new Component(30, 50, "blue", playerPos.x, playerPos.y - 20, "rect");
display.add(player);

// Place an enemy at isometric position (3, 1)
const enemyPos = isoToScreen(3, 1, tileWidth, tileHeight);
const enemy = new Component(30, 50, "red", enemyPos.x, enemyPos.y - 20, "rect");
display.add(enemy);
Enter fullscreen mode Exit fullscreen mode

📊 Comparison of Techniques

Technique Best For Complexity Performance
Mode 7 Racing games, top-down games Medium High
Parallax Scrolling Platformers, side-scrollers Low High
Z-Sorting Any game with depth Low High
Isometric Strategy games, RPGs Medium Medium

💡 Pro Tips

1. Combine Techniques

// ── USE MULTIPLE TECHNIQUES TOGETHER ──
// Use parallax + z-sorting for a 3D feel
// Use isometric + z-sorting for strategy games
Enter fullscreen mode Exit fullscreen mode

2. Use Images for Better Visuals

// ── USE IMAGES INSTEAD OF RECTANGLES ──
// Create a cube image for isometric tiles
// Use sprite sheets for different angles
Enter fullscreen mode Exit fullscreen mode

3. Keep It Simple

// ── DON'T OVER-COMPLICATE ──
// Start with one technique and master it
// Add more techniques as needed
Enter fullscreen mode Exit fullscreen mode

🎯 The One-Line Summary

"You can create 3D-like experiences in Limn Engine using 2D tricks — Mode 7, parallax scrolling, z-sorting, and isometric projection."


Draw your game into existence — even in 3D. 🎮🚀

Top comments (0)