๐ก Level 2 Tutorial โ Limn Engine Intermediate Guide
Physics, Tilemaps, Camera Follow & More
Welcome to Level 2: Intermediate of Limn Engine! You've mastered the basics โ moving objects, collision, text, and simple games. Now it's time to add physics, tilemaps, camera work, and polish that makes your games feel professional.
A special thank you to **GyaanSetu Javascript* for featuring our work and helping us share Limn Engine with the JavaScript community. Your support means the world to us. ๐*
What You'll Learn
| Topic | What You'll Build |
|---|---|
| 1. Physics & Gravity | Realistic falling and jumping |
| 2. hitBottom() & move.hitObject() | Floor and platform collision |
| 3. move.glideX / glideY / glideTo | Smooth point-to-point movement |
| 4. move.project | Projectile motion (arcs, bouncing) |
| 5. Camera follow | Track the player |
| 6. Camera zoom | Zoom in/out |
| 7. Sprite & AnimatedSprite | Spritesheet animation |
| 8. TileMap layers with addMap() | Multi-layer levels |
| 9. Tctxt styled text UI | Professional HUD styling |
| 10. Accelerate & decelerate | Vehicle-style momentum |
| 11. Platform game tutorial | Complete platformer |
| 12. Quick reference | All the essentials |
Prerequisites
Before starting Level 2, you should be comfortable with:
- โ Creating a Display and starting the game
- โ Adding and moving Components
- โ
Keyboard input (
display.keys) - โ
Basic collision (
crashWith()) - โ
move.bound()for boundaries
If you're not confident with these, complete Level 1: Beginner first.
1. Physics & Gravity
Enabling physics on a Component tells Limn Engine to apply gravity automatically every frame. The engine accumulates downward speed into a gravitySpeed value that compounds over time, creating realistic freefall motion.
How It Works
When player.physics = true, the engine's internal move() method:
- Adds
this.gravitytothis.gravitySpeedevery frame - Adds
this.gravitySpeedto the Component's Y position - After 10 frames of gravity 0.4, the object falls at 4px per frame
const player = new Component(40, 56, "blue", 100, 50, "rect");
display.add(player);
player.physics = true; // Enable gravity accumulation
player.gravity = 0.4; // Added to gravitySpeed each frame
player.bounce = 0.2; // 20% energy kept on floor impact
function update(dt) {
player.hitBottom(); // Clamp to canvas floor and bounce
// Left/right movement still works normally alongside physics
if (display.keys[39]) player.speedX = 4;
if (display.keys[37]) player.speedX = -4;
else if (!display.keys[39]) player.speedX = 0;
}
Properties Explained
| Property | Type | Purpose |
|---|---|---|
physics |
boolean | Enable gravity accumulation in move() |
gravity |
number | Amount added to gravitySpeed per frame |
gravitySpeed |
number | Accumulated downward speed โ set negative to jump |
bounce |
0โ1 | Fraction of speed kept on floor impact |
Visual Example
Frame 1: gravitySpeed = 0.4 (falling slow)
Frame 2: gravitySpeed = 0.8 (falling faster)
Frame 3: gravitySpeed = 1.2 (falling faster)
Frame 10: gravitySpeed = 4.0 (full speed)
โ ๏ธ Important: You must call
player.hitBottom()insideupdate()โ the engine applies gravity but does NOT automatically stop the player at any surface.
2. hitBottom() and move.hitObject()
hitBottom() clamps a physics Component to the canvas floor. move.hitObject() does the same but uses another Component as the landing surface.
hitBottom()
player.hitBottom(); // Uses canvas height
player.hitBottom(400); // Custom ground at y=400
move.hitObject()
const player = new Component(36, 48, "cyan", 100, 0, "rect");
const platform = new Component(200, 20, "#555", 200, 300, "rect");
display.add(player);
display.add(platform);
player.physics = true;
player.gravity = 0.5;
player.bounce = 0.05;
function update(dt) {
// Land on the platform
move.hitObject(player, platform);
// Also stop at the canvas floor (safety net)
player.hitBottom();
}
Visual Explanation
Without hitBottom:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Player falls through floor โ
โ โ โ
โ โ โ
โ โ โ
โ ๐ Player disappears โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
With hitBottom:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Player falls โ
โ โ โ
โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ Floor
โ โ
Player lands & bounces โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Quick Reference
| Method | Parameters | Purpose |
|---|---|---|
hitBottom() |
optional groundY | Clamp to canvas floor and bounce |
move.hitObject(id, floor) |
Component, floor Component | Treat another Component's top edge as landing surface |
3. move.glideX / glideY / glideTo
The glide functions ease a Component from its current position to a target coordinate over a specified duration using a cubic ease-out curve.
How It Works
The movement starts fast and decelerates smoothly as it approaches the target โ no manual lerp code required.
const box = new Component(50, 50, "#7fffb2", 0, 200, "rect");
display.add(box);
// On any keypress, glide the box to a new position
window.addEventListener("keydown", () => {
// Glide horizontally
move.glideX(box, 2000, 700); // to x=700 over 2 seconds
// Glide vertically
move.glideY(box, 1500, 400); // to y=400 over 1.5 seconds
// Or both axes together in perfect sync
move.glideTo(box, 2000, 700, 400); // id, ms, x, y
});
Visual Timeline
Time: 0ms 500ms 1000ms 1500ms 2000ms
X: 0 200 400 550 700
โ โ โ โ โ
Start Fast Medium Slow Arrive
(Easing out โ decelerates naturally)
Method Reference
| Method | Parameters | Easing |
|---|---|---|
move.glideX(id, ms, x) |
Component, duration ms, target x | Cubic ease-out |
move.glideY(id, ms, y) |
Component, duration ms, target y | Cubic ease-out |
move.glideTo(id, ms, x, y) |
Component, duration ms, x, y | Both axes in sync |
๐ก Tip: Glide runs independently of your game loop. You call it once and the Component arrives at the target position precisely at the end of the duration regardless of frame rate.
4. move.project โ Projectile Motion
move.project() launches a Component as a physics projectile โ you specify velocity, launch angle, and gravity, and the engine handles the arc and bouncing.
const ball = new Component(20, 20, "orange", 100, 450, "rect");
display.add(ball);
ball.bounce = 0.5;
window.addEventListener("dblclick", () => {
// Launch at 45ยฐ with velocity 12 โ arcs and bounces on canvas floor
move.project(ball, 12, 45, 0.3);
// Custom ground height โ bounce on a platform at y=400
move.project(ball, 12, 45, 0.3, 400);
});
Visual Arc
45ยฐ Launch
โ
/ \
/ \
/ \
/ \
/ \
โโโโโโโโโโโโโโโโโโโโ Ground
--- Bounce --- Bounce --- Stop
Parameter Reference
| Parameter | Type | Purpose |
|---|---|---|
| velocity | number | Launch speed in pixels per frame |
| angle | degrees | 0ยฐ = right, 90ยฐ = up, 45ยฐ = classic arc |
| gravity | number | Added to speedY every frame โ controls arc steepness |
| ground | number (optional) | Custom floor Y โ defaults to display.canvas.height |
๐ก Tip: Angle 0ยฐ launches horizontally to the right, 90ยฐ launches straight up. Angles above 90ยฐ launch backward and upward.
5. Camera Follow
display.camera.follow(target) translates the canvas context so the view tracks a Component โ keeping the player centred on screen as they move through a world larger than the canvas.
display.camera.worldWidth = 3000; // Level is 3000px wide
display.camera.worldHeight = 1000; // Level is 1000px tall
function update(dt) {
if (display.keys[68]) player.speedX = 4;
if (display.keys[65]) player.speedX = -4;
else if (!display.keys[68]) player.speedX = 0;
// Smooth follow โ lerps 10% of the gap each frame (recommended)
display.camera.follow(player, true);
// Hard follow โ snaps to player exactly
// display.camera.follow(player);
}
Smooth vs Instant Follow
| Mode | Visual Effect | Use Case |
|---|---|---|
smooth = true |
Gentle trailing camera | Platformers, exploration games |
smooth = false |
Instant centering | Arcade games, precise control |
How It Works
Smooth Follow:
Player โ โ โ โ โ โ โ
Camera โ โ โ โ โ โ (10% lag behind)
โ
Camera follows but never catches up completely
Instant Follow:
Player โ โ โ โ โ โ โ
Camera โ โ โ โ โ โ (perfect sync)
Properties
| Property | Purpose |
|---|---|
camera.worldWidth |
World size โ camera won't show outside this |
camera.worldHeight |
World size โ camera won't show outside this |
camera.x / camera.y |
Current camera offset (read or set manually) |
6. Camera Zoom
display.camera.setZoom(amount) scales the entire canvas context. Call it once and the zoom stays until you change it.
// Zoom in once โ stays zoomed in
display.camera.setZoom(1.5);
// Zoom out โ stays zoomed out
display.camera.setZoom(0.5);
// Back to normal
display.camera.setZoom(1);
How It Actually Works
setZoom() calls display.context.scale(amount, amount) on the canvas context. The scale transform persists until the context is restored or the canvas is cleared.
// Internal implementation:
setZoom(amount) {
display.context.scale(amount, amount);
}
Interactive Zoom Example
let zoomLevel = 1.0;
// Adjust zoom with keys โ only when key is pressed
window.addEventListener("keydown", (e) => {
if (e.key === "+" || e.key === "=") {
zoomLevel = Math.min(zoomLevel + 0.1, 2.5);
display.camera.setZoom(zoomLevel);
}
if (e.key === "-") {
zoomLevel = Math.max(zoomLevel - 0.1, 0.5);
display.camera.setZoom(zoomLevel);
}
});
Visual Effect
Zoom 0.5 (Zoom Out):
โโโโโโโโโโโโโโโโโโโโโโโ
โ See more of world โ
โ โโโโโ โ
โ โ P โ (smaller) โ
โ โโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโ
Zoom 1.0 (Normal):
โโโโโโโโโโโโโโโโโโโโโโโ
โ โโโโโโโโโโโโ โ
โ โ Player โ โ
โ โโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโ
Zoom 2.0 (Zoom In):
โโโโโโโโโโโโโโโโโโโโโโโ
โ โโโโโโโโโโโโโโโโโโ โ
โ โ Player โ โ
โ โ (close-up) โ โ
โ โโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโ
When to Call setZoom()
| Scenario | When to Call |
|---|---|
| Static zoom | Once at startup |
| Zoom on key press | Only when key is pressed |
| Zoom on game event | Only when the event happens |
| Continuous zoom effect | Every frame (if you want gradual zoom) |
๐ก Tip: The zoom persists automatically. You only need to call
setZoom()again if you want to change the zoom level.
7. Sprite & AnimatedSprite
Sprite animates a horizontal spritesheet by cycling through evenly spaced frames. AnimatedSprite extends it with named animation clips (idle, run, jump).
Sprite (Basic)
// Sprite(src, frameW, frameH, frameCount, frameSpeed, x, y)
const explosion = new Sprite("explode.png", 64, 64, 8, 4, 300, 200);
display.add(explosion);
AnimatedSprite (Named Clips)
const hero = new AnimatedSprite("hero_sheet.png", 64, 64, 200, 300);
// Define animation clips
hero.addAnimation("idle", 0, 3, 10, true); // loop
hero.addAnimation("run", 4, 11, 5, true); // loop
hero.addAnimation("jump", 12, 15, 4, false); // one-shot
hero.addAnimation("attack",16, 19, 3, false); // one-shot
display.add(hero);
let attacking = false;
function update(dt) {
// Switch animations based on state
if (display.keys[68]) hero.playAnimation("run");
else hero.playAnimation("idle");
// One-shot attack
if (display.keys[90] && !attacking) { // Z key
hero.playAnimation("attack");
attacking = true;
setTimeout(() => attacking = false, 300);
}
// Must call every frame to advance frames
hero.updateAnimation();
// hero.paused is true when a one-shot animation finishes
}
Spritesheet Format
One image file: hero_sheet.png
โโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโ
โFrame0โFrame1โFrame2โFrame3โFrame4โFrame5โFrame6โFrame7โ
โIdle0 โIdle1 โRun0 โRun1 โRun2 โJump0 โJump1 โAttackโ
โโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโ
Each frame = 64x64 pixels
Total width = 8 ร 64 = 512 pixels
AnimatedSprite Methods
| Method | Parameters | Purpose |
|---|---|---|
addAnimation(name, s, e, spd, loop) |
string, int, int, int, boolean | Define a named clip |
playAnimation(name) |
string | Switch to that clip โ only resets if name changed |
updateAnimation() |
None | Advance the frame counter โ call every frame |
paused |
boolean | True when a one-shot animation finishes |
faceLeft() / faceRight() |
None | Flip sprite horizontally |
๐ก Tip: One-shot animations set
paused = truewhen they reach their last frame, which you can check to know when the animation finished.
8. TileMap Layers with addMap()
The TileMap in Limn Engine is a multi-layer system where each layer is an independent 2D number array. Layers are SWITCHABLE, not stackable โ you show one layer at a time.
How Layers Actually Work
// โ ๏ธ Important: Layers are SWITCHABLE, not STACKABLE
// Only ONE layer is visible at a time!
display.tileFace.show(0); // Layer 0 visible (layers 1, 2 hidden)
display.tileFace.show(1); // Layer 1 visible (layers 0, 2 hidden)
display.tileFace.show(2); // Layer 2 visible (layers 0, 1 hidden)
Setup
// All layers share these tile templates
display.tile = [
new Component(64, 64, "#2d6a2d", 0, 0), // tile 1 = grass
new Component(64, 64, "#8B4513", 0, 0), // tile 2 = dirt
new Component(64, 64, "#228B22", 0, 0), // tile 3 = tree
new Component(64, 64, "#FFD700", 0, 0), // tile 4 = coin
];
// Layer 0 โ Overworld
display.map = [
[1, 1, 2, 1, 1],
[2, 1, 1, 2, 1],
[2, 2, 2, 2, 2],
];
display.tileMap();
display.tileFace.show(0); // Show overworld
// Layer 1 โ Dungeon (different map)
display.tileFace.addMap([
[1, 0, 1, 0, 1],
[0, 2, 0, 2, 0],
[1, 0, 1, 0, 1],
]);
display.tileFace.show(1); // Switch to dungeon (overworld hidden)
// Layer 2 โ Boss Room
display.tileFace.addMap([
[1, 1, 1, 1, 1],
[1, 4, 4, 4, 1],
[1, 1, 1, 1, 1],
]);
display.tileFace.show(2); // Switch to boss room
Visual Layering (Switchable)
Layer 0 (Overworld):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ฉ๐ซ๐ซ๐ฉ๐ฉ โ
โ ๐ซ๐ฉ๐ฉ๐ซ๐ฉ โ
โ ๐ซ๐ซ๐ซ๐ซ๐ซ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Layer 1 (Dungeon):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ฉโฌ๐ฉโฌ๐ฉ โ
โ โฌ๐ซโฌ๐ซโฌ โ
โ ๐ฉโฌ๐ฉโฌ๐ฉ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Layer 2 (Boss Room):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ โ
โ ๐ฉ๐ช๐ช๐ช๐ฉ โ
โ ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Only ONE layer is visible at a time!
Runtime Switching
// Switch layers based on game state
function goToDungeon() {
display.tileFace.show(1); // Dungeon visible
// Overworld tiles are hidden
}
function goToBossRoom() {
display.tileFace.show(2); // Boss room visible
// Dungeon tiles are hidden
}
function goToOverworld() {
display.tileFace.show(0); // Overworld visible
// Dungeon and boss room hidden
}
Adding Foreground Objects
// Tiles are for ground/walls. Use fake.add() for objects on top:
const tree = new Component(64, 64, null, 400, 300, "image");
tree.setImage("tree.png");
fake.add(tree); // Renders on top of tilemap
const coin = new Component(30, 30, null, 500, 200, "image");
coin.setImage("coin.png");
fake.add(coin); // Renders on top of tilemap
โ ๏ธ Important: Layers do NOT stack!
show(1)replacesshow(0)โ only one layer is visible at a time. This is a design choice for level switching, not a limitation.
Runtime Editing
// Edit a layer at runtime โ fake.refresh() called automatically
display.tileFace.remove(1, 0, 2); // remove coin at grid(1,0) on layer 2
display.tileFace.add(3, 2, 1, 1); // add tree at grid(2,1) on layer 1
// Collision still works across all layers
if (display.tileFace.crashWith(player, 2)) {
// player touched a dirt tile (type 2)
}
// Get all tiles of a type
const allCoins = display.tileFace.tiles(4);
9. Tctxt โ Styled Text UI
Tctxt is the correct class for any on-screen text โ it gives you font size, font family, colour, alignment, optional background fill with padding, stroke, and baseline control.
const scoreText = new Tctxt(
"22px", // font size
"Arial", // font family
"white", // text colour
20, 40, // x, y screen position
"left", // "left" / "center" / "right"
false, // false=fill text, true=stroke/outline
"alphabetic", // text baseline
"rgba(0,0,0,0.6)", // background colour โ null to disable
14, 6 // paddingX, paddingY
);
scoreText.setText("Score: 0");
display.add(scoreText);
function update(dt) {
scoreText.setText("Score: " + score);
scoreText.fixed(); // lock to screen when camera moves
}
Parameter Reference
| Parameter | Example | Purpose |
|---|---|---|
| size | "22px" | Font size as CSS string |
| font | "Arial" | Font family name |
| align | "left" | "left", "center", or "right" |
| stroke | false | false = filled text, true = outlined |
| background | "rgba(0,0,0,0.6)" | Background fill (null to disable) |
| padX / padY | 14, 6 | Pixel padding inside background |
๐ก Tip: The base
Component.setText()method can draw text but offers no control over font, alignment, or background.Tctxtreplaces it for any UI work.
10. Accelerate & Decelerate
move.accelerate() and move.decelerate() give you vehicle-style movement where speed builds up gradually to a maximum and bleeds off smoothly to zero.
function update(dt) {
if (display.keys[68]) {
// Accelerate right, max speed 8, no vertical change
move.accelerate(player, 0.6, 0, 8, 0);
} else if (display.keys[65]) {
// Accelerate left
move.accelerate(player, -0.6, 0, 8, 0);
} else {
// Friction โ reduce speed by 0.4 per frame
move.decelerate(player, 0.4, 0);
}
move.bound(player);
}
Visual Comparison
Instant Movement:
Speed: โโโโ (jumps to max instantly)
โ
Press key โ Speed = 8 immediately
Accelerate/Decelerate:
Speed: โโโโโ (gradually speeds up, smoothly stops)
โ โ
Press key Release key
Method Reference
| Method | Parameters | Purpose |
|---|---|---|
move.accelerate(id, ax, ay, mx, my) |
Component, accelX, accelY, maxX, maxY | Add acceleration per frame up to max speed |
move.decelerate(id, dx, dy) |
Component, decelX, decelY | Reduce speed to zero without reversing direction |
11. Platform Game Tutorial
This tutorial puts together physics, jumping, accelerated movement, a floor platform, smooth camera follow, and a Tctxt score display into a complete playable platformer.
<!DOCTYPE html>
<html>
<head>
<title>Platform Game</title>
<script src="epic.js"></script>
</head>
<body>
<script>
const display = new Display();
display.start(800, 400);
display.backgroundColor("#1a1a2e");
display.camera.worldWidth = 3000;
const player = new Component(36, 48, "cyan", 100, 100, "rect");
player.physics = true;
player.gravity = 0.5;
player.bounce = 0.05;
display.add(player);
const floor = new Component(3000, 20, "#444", 0, 380, "rect");
display.add(floor);
// A few platforms at different heights
const p1 = new Component(200, 16, "#666", 400, 300, "rect");
const p2 = new Component(150, 16, "#666", 800, 240, "rect");
display.add(p1);
display.add(p2);
const scoreUI = new Tctxt(
"18px","Arial","white",14,28,
"left",false,"alphabetic",
"rgba(0,0,0,0.5)",10,4
);
scoreUI.setText("Distance: 0");
display.add(scoreUI);
function update(dt) {
// Horizontal movement with acceleration
if (display.keys[68] || display.keys[39]) {
move.accelerate(player, 0.7, 0, 7, 0);
} else if (display.keys[65] || display.keys[37]) {
move.accelerate(player, -0.7, 0, 7, 0);
} else {
move.decelerate(player, 0.5, 0);
}
// Jump โ only when grounded (gravitySpeed >= 0)
if ((display.keys[87] || display.keys[38]) && player.gravitySpeed >= 0) {
player.gravitySpeed = -11;
}
// Floor and platform collisions
move.hitObject(player, floor);
move.hitObject(player, p1);
move.hitObject(player, p2);
player.hitBottom();
display.camera.follow(player, true);
scoreUI.setText("Distance: " + Math.floor(player.x));
scoreUI.fixed();
}
</script>
</body>
</html>
โ What You Get:
- WASD or arrow keys to move
- W/Up to jump
- Multi-platform levels
- Smooth camera follow
- Live distance counter
- Physics-based movement
12. Quick Reference
| Feature | Code |
|---|---|
| Enable physics | player.physics=true; player.gravity=0.4; |
| Floor collision | player.hitBottom(); |
| Platform collision | move.hitObject(player, floor); |
| Jump | if(player.gravitySpeed >= 0) player.gravitySpeed = -10; |
| Glide to position | move.glideTo(obj, 2000, x, y); |
| Projectile launch | move.project(ball, 12, 45, 0.3); |
| Camera follow | display.camera.follow(player, true); |
| Zoom (call once) | display.camera.setZoom(1.5); |
| AnimatedSprite | hero.addAnimation("run",4,11,5,true); hero.playAnimation("run"); hero.updateAnimation(); |
| TileMap layer (switchable) | tileFace.addMap(arr); tileFace.show(1); |
| Accelerate | move.accelerate(obj,0.6,0,8,0); |
| Decelerate | move.decelerate(obj,0.4,0); |
What's Next?
You've completed Level 2: Intermediate! ๐
| Level | Description |
|---|---|
| ๐ข Level 1 โ Beginner | โ Basics complete |
| ๐ก Level 2 โ Intermediate | โ You are here |
| ๐ Level 3 โ Advanced | Particles, circle collision, dynamic tilemaps, screen shake |
| ๐ด Level 4 โ 10x | Dualโrenderer, performance optimization, engine extension |
Special Thanks
"A heartfelt thank you to **GyaanSetu Javascript* for featuring Limn Engine and helping us share this tutorial with the JavaScript community. Your support means the world to us."*
Useful Links
| Resource | Link |
|---|---|
| GitHub Repository | github.com/terracodes004/limn-engine-doc |
| Direct Download (epic.js) | github.com/terracodes004/limn-engine-doc/blob/main/asset/epic.js |
| Complete API Reference | limn-engine.vercel.app/reference.html |
| Discord Community | discord.gg/ZqnUtTQb8 |
Draw your game into existence โ one physics simulation at a time. ๐ฎ
Top comments (1)
Quick one on the tilemap layers. You flag twice that they're switchable rather than stackable, and I'm curious about the reasoning, since the usual reason to have layers at all is drawing a background, a collision layer, and a foreground on top of each other at once. Switching between whole maps reads more like scenes than layers to me. Was dropping the stacking a performance call, or just not the use case you were building for?