DEV Community

Cover image for 🏃 The Ultimate Guide to Movement in Limn Engine
Kehinde Owolabi
Kehinde Owolabi

Posted on

🏃 The Ultimate Guide to Movement in Limn Engine

🏃 The Ultimate Guide to Movement in Limn Engine

Everything You Need to Know About Moving Objects in Limn Engine


📖 Introduction

Movement is the heartbeat of every game. Whether you're dodging bullets in a top-down shooter, leaping across platforms, or guiding a character through a vast open world, how things move defines the player's experience. Get it right, and your game feels smooth, responsive, and alive. Get it wrong, and it feels clunky and frustrating.

Limn Engine offers a rich toolkit for movement — from simple speed-based motion to complex physics and smooth gliding. In this guide, we'll explore every movement method available, when to use each one, and how they work under the hood.


🎯 The Two Core Movement Methods

Before we dive into the specific functions, it's important to understand the two fundamental ways Limn Engine handles movement:

Method How It Works When to Use
move() Moves an object using speedX and speedY Standard movement — independent of angle
moveAngle() Moves an object along its current angle Directional movement — speed applied relative to angle

These two methods are called automatically by the engine based on the angularMovement property of your component.


🧩 Method 1: Basic Movement with move()

The Concept

move() is the default movement method in Limn Engine. It works by taking the values stored in speedX and speedY and adding them to the component's x and y position every frame. This is the simplest way to move objects.

How it works internally:

When the engine calls move() on a component, it does this:

  1. If physics is enabled, it adds gravity to the vertical speed
  2. It adds speedX to x (horizontal movement)
  3. It adds speedY to y (vertical movement)

This means you control movement by setting speedX and speedY each frame. If you set speedX = 5, the object moves 5 pixels right every frame. If you set speedY = -3, it moves 3 pixels up every frame.

Why you need dt:

Without dt, movement speed depends on frame rate — faster computers move faster. By multiplying speed by dt (delta time), you convert "pixels per frame" to "pixels per second," making movement consistent on all computers.

The key principle: In move(), you control speed directly. The engine applies it automatically. You don't calculate positions manually — you set speeds.

What We're About to Do

We're going to create a player that moves with arrow keys. This is the most common type of movement in games — simple, responsive, and easy to understand. We'll use move() to handle the actual movement, and we'll use dt to make it frame-rate independent.

The Code

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

const player = new Component(40, 40, "#5b8cff", 400, 300, "rect");
display.add(player);

function update(dt) {
    // Reset speed each frame
    player.speedX = 0;
    player.speedY = 0;

    // Arrow keys
    if (display.keys[37]) player.speedX = -200 * dt; // Left
    if (display.keys[39]) player.speedX =  200 * dt; // Right
    if (display.keys[38]) player.speedY = -200 * dt; // Up
    if (display.keys[40]) player.speedY =  200 * dt; // Down

    // Keep player on screen
    move.bound(player);
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 1-4: Creates the game window, activates 60fps mode, sets the canvas size to 800x600, and sets the background to dark blue

Line 6-7: Creates a 40x40 blue square at the center of the screen and adds it to the game world

Line 9: function update(dt) {

  • The game loop function, called 60 times per second
  • dt (delta time) is the time since the last frame, usually about 0.016 seconds
  • Using dt makes movement consistent on all computers

Line 10-11: player.speedX = 0; player.speedY = 0;

  • Resets speed to 0 at the start of each frame
  • Without this, the player would keep moving forever in the last direction

Line 13-16: if (display.keys[37]) player.speedX = -200 * dt;

  • display.keys[37] checks if the left arrow key is pressed
  • 37 is the keycode for the left arrow
  • player.speedX = -200 * dt sets horizontal speed to -200 pixels per second (moving left)
  • 39 is right arrow, 200 * dt means 200 pixels per second to the right
  • 38 is up arrow, -200 * dt means moving up
  • 40 is down arrow, 200 * dt means moving down

Line 18: move.bound(player);

  • Keeps the player on screen by clamping their position to the canvas edges
  • If the player tries to go past an edge, it stops them

🧭 Method 2: Angular Movement with moveAngle()

The Concept

moveAngle() is the second movement method in Limn Engine. Instead of using speedX and speedY directly, it applies speed relative to the component's angle property. This is useful when you want objects to move in the direction they're facing.

How it works internally:

When the engine calls moveAngle() on a component, it does this:

  1. If physics is enabled, it adds gravity to the vertical speed
  2. It calculates the horizontal movement: speedX * cos(angle)
  3. It calculates the vertical movement: speedY * sin(angle)

This means if you set angle = 45 degrees and speedX = 5, the object moves diagonally. As the angle changes, the direction of movement changes.

The key principle: In moveAngle(), the speed values are applied relative to the object's facing direction. This makes it perfect for rotating objects, orbiting, and directional movement.

What We're About to Do

We're going to create an object that moves in the direction it's facing. This is useful for orbiting objects, enemies that chase the player, and driving games.

The Code

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

const orbiter = new Component(20, 20, "cyan", 400, 300, "rect");
orbiter.angularMovement = true;  // Uses moveAngle() instead of move()
orbiter.speedX = 2;
orbiter.speedY = 2;
display.add(orbiter);

function update(dt) {
    // Rotate continuously
    orbiter.angle += 0.02;
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 6: const orbiter = new Component(20, 20, "cyan", 400, 300, "rect");

  • Creates a 20x20 cyan square at the center of the screen

Line 7: orbiter.angularMovement = true;

  • This is the key line. Tells the engine to use moveAngle() instead of move()
  • With this set to true, the component's speed is applied relative to its angle

Line 8-9: orbiter.speedX = 2; orbiter.speedY = 2;

  • Sets the speed values that will be applied in the direction the object is facing

Line 10: display.add(orbiter);

  • Adds the orbiter to the game world

Line 13-14: function update(dt) { orbiter.angle += 0.02; }

  • Increases the angle by 0.02 radians each frame
  • As the angle changes, the direction of movement changes — creating an orbit

🚗 Method 3: Directional Movement (Car Example)

The Concept

Directional movement combines rotation with movement. The object can turn and then move forward in its new direction. This is how cars, tanks, and many game characters work.

How it works:

Limn Engine provides helper functions for directional movement:

  • move.forward() — Moves the object in the direction it's facing
  • move.backward() — Moves the object opposite to the direction it's facing
  • move.turnLeft() and move.turnRight() — Rotate the object

These functions use the object's angle property internally. move.forward() calculates the direction using cos(angle) and sin(angle), then moves the object that way.

The key principle: Directional movement separates turning from moving. You turn first, then you move forward. This creates realistic movement like a car.

What We're About to Do

We're going to build a car that drives forward, backward, and turns. This demonstrates directional movement, rotation, and how to combine movement functions.

The Code

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

const car = new Component(40, 20, "#ff6b6b", 400, 300, "rect");
car.changeAngle = true;  // Enable rotation
display.add(car);

function update(dt) {
    // Car controls
    if (display.keys[38]) move.forward(car, 3);      // Up: drive forward
    if (display.keys[40]) move.backward(car, 2);     // Down: reverse
    if (display.keys[37]) move.turnLeft(car, 0.05);  // Left: turn left
    if (display.keys[39]) move.turnRight(car, 0.05); // Right: turn right

    // Keep car on screen
    move.bound(car);
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 6-8: const car = new Component(40, 20, "#ff6b6b", 400, 300, "rect"); car.changeAngle = true; display.add(car);

  • Creates a 40x20 red rectangle at the center (car shape)
  • changeAngle = true — Enables rotation on the car so it can turn
  • Adds the car to the game world

Line 11: if (display.keys[38]) move.forward(car, 3);

  • display.keys[38] checks if the up arrow is pressed
  • move.forward(car, 3) — Moves the car forward 3 pixels in the direction it's facing

Line 12: if (display.keys[40]) move.backward(car, 2);

  • display.keys[40] checks if the down arrow is pressed
  • move.backward(car, 2) — Moves the car backward 2 pixels

Line 13: if (display.keys[37]) move.turnLeft(car, 0.05);

  • display.keys[37] checks if the left arrow is pressed
  • move.turnLeft(car, 0.05) — Rotates the car left by 0.05 radians (about 3 degrees)

Line 14: if (display.keys[39]) move.turnRight(car, 0.05);

  • display.keys[39] checks if the right arrow is pressed
  • move.turnRight(car, 0.05) — Rotates the car right by 0.05 radians

Line 16: move.bound(car);

  • Keeps the car on screen

✨ Method 4: Smooth Movement with glideTo()

The Concept

glideTo() creates smooth, easing-based movement to a target position. Instead of teleporting or moving at constant speed, the object starts fast and slows down as it approaches its destination. This is called "ease-out" and creates natural-looking motion.

How it works internally:

When you call move.glideTo(coin, 1000, 500, 300):

  1. Records the starting position and the current time
  2. Each frame, calculates how much time has passed (progress from 0 to 1)
  3. Applies cubic easing formula: eased = 1 - (1 - progress)³
  4. Calculates the new position based on the eased value
  5. Updates the object's position

The key principle: glideTo() handles all the timing and easing for you. You just specify the target and how long it should take.

What We're About to Do

We're going to move a coin smoothly to random positions with easing. This creates professional-looking movement without manual lerp functions.

The Code

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

const coin = new Component(30, 30, "gold", 400, 300, "rect");
display.add(coin);

const infoText = new Tctxt("18px", "Arial", "white", 20, 50);
infoText.setText("Press Space to move coin");
display.add(infoText);
infoText.fixed();

function update(dt) {
    // Move coin to random position when Space is pressed
    if (display.keys[32]) {
        move.glideTo(coin, 1000, Math.random() * 700, Math.random() * 500);
        display.keys[32] = false; // Prevent holding
    }
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 6-7: Creates a 30x30 gold square at the center and adds it to the game world

Line 9-12: Creates a text label at the top-left and locks it to the screen position so it doesn't move with the camera

Line 15-19: Checks if Space is pressed, then glides the coin to a random position over 1 second


⚡ Method 5: Physics-Based Movement

The Concept

Physics-based movement uses acceleration and deceleration to create realistic, weighty motion. Instead of moving at constant speed, objects smoothly speed up and slow down.

How it works:

  • move.accelerate() — Adds acceleration to speed, clamped to a maximum
  • move.decelerate() — Reduces speed toward zero without overshooting

The key principle: You control acceleration, not speed directly. This creates realistic physics like a car.

What We're About to Do

We're going to create a player with smooth acceleration and deceleration, like a car. This creates realistic, weighty movement.

The Code

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

const player = new Component(40, 40, "#5b8cff", 400, 300, "rect");
display.add(player);

function update(dt) {
    // Accelerate or decelerate
    if (display.keys[68]) {  // D key
        move.accelerate(player, 0.6, 0, 8, 0);  // Accelerate right
    } else if (display.keys[65]) {  // A key
        move.accelerate(player, -0.6, 0, 8, 0); // Accelerate left
    } else {
        move.decelerate(player, 0.4, 0);        // Slow down
    }

    // Keep player on screen
    move.bound(player);
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Line 9: if (display.keys[68]) { move.accelerate(player, 0.6, 0, 8, 0); }

  • Checks if the D key is pressed
  • move.accelerate() — Adds horizontal acceleration at 0.6 per frame, max speed 8

Line 11: else if (display.keys[65]) { move.accelerate(player, -0.6, 0, 8, 0); }

  • Checks if the A key is pressed
  • Negative acceleration moves the player left

Line 13: else { move.decelerate(player, 0.4, 0); }

  • move.decelerate() — Reduces horizontal speed toward zero at 0.4 per frame

Line 16: move.bound(player);

  • Keeps the player on screen

🎯 Method 6: Complete Movement Demo

The Concept

In a real game, you'll often need to combine multiple movement methods. This demo shows how different movement techniques can work together in one game.

What We're About to Do

We're going to combine multiple movement methods into a single complete game.

The Code

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

// ── PLAYER ──
const player = new Component(40, 40, "#5b8cff", 400, 300, "rect");
display.add(player);

// ── ORBITER ──
const orbiter = new Component(20, 20, "cyan", 400, 300, "rect");
orbiter.angularMovement = true;
orbiter.speedX = 2;
orbiter.speedY = 2;
display.add(orbiter);

// ── UI ──
const infoText = new Tctxt("18px", "Arial", "white", 20, 50);
infoText.setText("Arrow keys to move, Space to glide, A/D to accelerate");
display.add(infoText);
infoText.fixed();

function update(dt) {
    // ── PLAYER MOVEMENT (Arrow Keys) ──
    player.speedX = 0;
    player.speedY = 0;
    if (display.keys[37]) player.speedX = -200 * dt;
    if (display.keys[39]) player.speedX =  200 * dt;
    if (display.keys[38]) player.speedY = -200 * dt;
    if (display.keys[40]) player.speedY =  200 * dt;
    move.bound(player);

    // ── ORBITER (Angular Movement) ──
    orbiter.angle += 0.02;

    // ── GLIDE (Space Key) ──
    if (display.keys[32]) {
        move.glideTo(player, 1000, Math.random() * 700, Math.random() * 500);
        display.keys[32] = false;
    }
}
Enter fullscreen mode Exit fullscreen mode

What Each Line Does

Lines 6-7: Creates the player with basic arrow key movement

Lines 10-14: Creates an orbiter with angular movement that orbits continuously

Lines 17-20: Creates a UI label that stays on screen

Lines 23-29: Player movement with arrow keys and boundary keeping

Lines 31-32: Orbiter rotation — increases angle each frame

Lines 34-38: Glide effect — when Space is pressed, the player glides to a random position


💡 Pro Tips

1. Always Use dt for Consistent Speed

// ✅ CORRECT: Frame-rate independent
player.speedX = 200 * dt;

// ❌ WRONG: Frame-rate dependent
player.speedX = 4;
Enter fullscreen mode Exit fullscreen mode

Why this matters: On a fast computer, speedX = 4 moves at 240 pixels per second. On a slow computer, it moves at 120 pixels per second. Using dt ensures everyone plays at the same speed.


2. Normalize Diagonal Movement

let mx = 0, my = 0;
if (display.keys[37]) mx = -1;
if (display.keys[39]) mx = 1;
if (display.keys[38]) my = -1;
if (display.keys[40]) my = 1;

if (mx !== 0 && my !== 0) {
    mx *= 0.707;  // Prevent faster diagonal movement
    my *= 0.707;
}

player.x += mx * 200 * dt;
player.y += my * 200 * dt;
Enter fullscreen mode Exit fullscreen mode

Why this matters: Without normalization, moving diagonally is 1.4 times faster than moving in a single direction. The 0.707 factor (which is 1/√2) keeps the speed consistent.


3. Combine Movement Methods

// Use accelerate for smooth movement + bound to stay on screen
move.accelerate(player, 0.6, 0, 8, 0);
move.bound(player);

// Use pointTo to aim + forward to move
move.pointTo(enemy, player.x, player.y);
move.forward(enemy, 2);
Enter fullscreen mode Exit fullscreen mode

Why this works: Each function handles one aspect of movement. Combining them gives you complex behavior with simple code.


📊 Movement Method Reference

Method What It Does When to Use
move.bound() Keeps object on screen Every game
move.boundTo() Custom boundaries Arena games
move.forward() Move in facing direction Driving games
move.backward() Move opposite facing direction Driving games
move.turnLeft() Rotate left Driving games
move.turnRight() Rotate right Driving games
move.pointTo() Face a target Aiming, tracking
move.circle() Orbital motion Orbiting enemies
move.glideTo() Smooth movement Animations, cutscenes
move.accelerate() Smooth acceleration Physics-based movement
move.decelerate() Smooth deceleration Physics-based movement
move.project() Projectile physics Arrows, fireballs
move.hitObject() Platform collision Platformers
move.teleport() Instant position Respawns, checkpoints

🎯 The One-Line Summary

"Limn Engine offers 15+ movement methods — from simple speed-based movement to complex physics, smooth gliding, and angular motion — so you can build any game mechanic."


Draw your game into existence — one movement at a time. 🏃🚀

Top comments (0)