DEV Community

Cover image for TCJSgame Movement Utility: The Complete Guide (Correcting Previous Tutorial Mistakes)
Kehinde Owolabi
Kehinde Owolabi

Posted on

TCJSgame Movement Utility: The Complete Guide (Correcting Previous Tutorial Mistakes)

TCJSgame Movement Utility: The Complete Guide (Correcting Previous Tutorial Mistakes)

If you've been following my TCJSgame tutorials, I need to make an important correction. In previous articles, I incorrectly explained how certain movement functions work. Specifically, glideX, glideY, and glideTo MUST be called in the update() function (the game loop) to work properly. This article corrects those mistakes and provides the definitive guide to TCJSgame's movement system.

🚨 Important Correction to Previous Tutorials

What I Got Wrong: I previously stated that glide functions were "one-time actions" that could be called outside the game loop.

The Truth: glideX, glideY, and glideTo need continuous updates in the update() function because they manage movement over multiple frames. Calling them once outside the loop will start the movement but won't properly manage its completion.

Let's fix this misunderstanding once and for all.

🎯 Understanding TCJSgame's Game Loop

TCJSgame runs at approximately 50 FPS using this core loop:

// Inside Display class:
this.interval = setInterval(() => this.updat(), 20); // ~50 FPS

// The updat() method calls YOUR update() function:
updat() {
    this.clear();
    this.frameNo += 1;
    // ... camera setup ...
    try {
        update(); // ← YOUR GAME LOGIC GOES HERE
    } catch (e) {}
    // ... render components ...
}
Enter fullscreen mode Exit fullscreen mode

Key Insight: Your update() function runs ~50 times per second. Any movement that needs frame-by-frame adjustment must be managed within this function.

🔧 Movement Functions: Loop vs. One-Time

MUST BE IN update() LOOP:

function update() {
    // These need continuous frame-by-frame updates:

    // 1. GLIDE FUNCTIONS (Corrected!)
    move.glideX(object, duration, targetX);
    move.glideY(object, duration, targetY);
    move.glideTo(object, duration, targetX, targetY);

    // 2. BOUNDARY FUNCTIONS
    move.bound(object);
    move.boundTo(object, left, right, top, bottom);

    // 3. PHYSICS FUNCTIONS
    move.hitObject(object, otherObject);
    move.accelerate(object, accelX, accelY, maxX, maxY);
    move.decelerate(object, decelX, decelY);

    // 4. CONTINUOUS ROTATION
    move.circle(object, angleIncrement); // If you want continuous rotation

    // 5. CONTINUOUS DIRECTION TRACKING
    move.pointTo(object, targetX, targetY); // If target moves
}
Enter fullscreen mode Exit fullscreen mode

CAN BE OUTSIDE LOOP (One-time actions):

// These work as instant actions:
move.teleport(object, x, y);      // Instant position change
move.setX(object, x);             // Set X position
move.setY(object, y);             // Set Y position
move.position(object, "center");  // Pre-defined positioning
move.circle(object, 45);          // Set static rotation angle
move.project(object, 10, 45, 0.1); // Projectile with built-in animation
Enter fullscreen mode Exit fullscreen mode

🛠️ The Glide Functions: Correct Implementation

Here's the correct way to use glide functions, fixing my previous tutorials:

The WRONG Way (From Previous Tutorials):

// ❌ INCORRECT - This doesn't work properly!
button.onclick = () => {
    move.glideTo(player, 2, 400, 300); // Called once
};
Enter fullscreen mode Exit fullscreen mode

The RIGHT Way:

// ✅ CORRECT - Manages glide state in update()
let isGliding = false;
let glideTargetX = 0;
let glideTargetY = 0;
let glideDuration = 0;
let glideStartTime = 0;

// Trigger glide start (can be outside loop)
button.onclick = () => {
    isGliding = true;
    glideTargetX = 400;
    glideTargetY = 300;
    glideDuration = 2; // seconds
    glideStartTime = Date.now();
};

// Manage glide in update() loop
function update() {
    if (isGliding) {
        // Calculate progress
        const elapsed = (Date.now() - glideStartTime) / 1000;
        const progress = Math.min(elapsed / glideDuration, 1);

        // Apply glide (this sets speedX/speedY)
        move.glideTo(player, glideDuration, glideTargetX, glideTargetY);

        // Check if glide is complete
        if (progress >= 1) {
            isGliding = false;
            player.speedX = 0;
            player.speedY = 0;
            player.x = glideTargetX; // Ensure exact position
            player.y = glideTargetY;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

📝 Complete Movement Reference (Corrected)

1. Basic Positioning (Outside Loop)

// Instant positioning - works anywhere
move.teleport(player, 100, 200);
move.setX(enemy, 300);
move.setY(collectible, 150);

// Pre-defined positions
move.position(scoreText, "top", 10);     // Top center, 10px margin
move.position(healthBar, "right", 20);   // Right center
move.position(title, "center");          // Absolute center
Enter fullscreen mode Exit fullscreen mode

2. Continuous Movement (Inside Loop)

function update() {
    // Keyboard-controlled movement
    if (display.keys[39]) { // Right arrow
        move.accelerate(player, 0.5, 0, 5); // Max speed: 5
    } else if (display.keys[37]) { // Left arrow
        move.accelerate(player, -0.5, 0, 5);
    } else {
        move.decelerate(player, 0.3, 0); // Friction
    }

    // Jump with physics
    if (display.keys[38] && player.y >= groundLevel) {
        player.speedY = -10;
    }

    // Apply gravity
    player.gravity = 0.5;
}
Enter fullscreen mode Exit fullscreen mode

3. Glide Functions with State Management

class GlideController {
    constructor() {
        this.activeGlides = new Map();
    }

    startGlide(object, targetX, targetY, duration) {
        this.activeGlides.set(object, {
            targetX, targetY, duration,
            startX: object.x, startY: object.y,
            startTime: Date.now()
        });
    }

    update() {
        const now = Date.now();

        for (const [object, glide] of this.activeGlides.entries()) {
            const elapsed = (now - glide.startTime) / 1000;
            const progress = Math.min(elapsed / glide.duration, 1);

            if (progress >= 1) {
                // Glide complete
                object.x = glide.targetX;
                object.y = glide.targetY;
                object.speedX = 0;
                object.speedY = 0;
                this.activeGlides.delete(object);
            } else {
                // Update glide in the loop
                move.glideTo(object, glide.duration, glide.targetX, glide.targetY);
            }
        }
    }
}

// Usage
const glideController = new GlideController();

button.onclick = () => {
    glideController.startGlide(player, 500, 200, 3); // 3-second glide
};

function update() {
    glideController.update();
    // ... other update logic
}
Enter fullscreen mode Exit fullscreen mode

4. Boundary Management (Inside Loop)

function update() {
    // Keep object within canvas
    move.bound(player);

    // Or within specific bounds
    move.boundTo(
        enemy,
        100,   // left
        700,   // right
        50,    // top
        500    // bottom
    );

    // Screen wrapping
    if (player.x > 800) player.x = 0;
    if (player.x < 0) player.x = 800;
    if (player.y > 600) player.y = 0;
    if (player.y < 0) player.y = 600;
}
Enter fullscreen mode Exit fullscreen mode

🎮 Practical Examples (Corrected)

Example 1: Platformer Character (Fixed)

const player = new Component(32, 32, "blue", 100, 300);

function update() {
    // Horizontal movement with acceleration
    if (display.keys[39]) { // Right
        move.accelerate(player, 0.5, 0, 8);
    } else if (display.keys[37]) { // Left
        move.accelerate(player, -0.5, 0, 8);
    } else {
        move.decelerate(player, 0.8, 0);
    }

    // Jump
    if (display.keys[38] && isOnGround(player)) {
        player.speedY = -12;
    }

    // Apply gravity
    player.gravity = 0.6;

    // Keep on screen
    move.boundTo(player, 0, 768, 0, 432);

    // Check ground collision
    if (player.y >= 400) {
        player.y = 400;
        player.speedY = 0;
        player.gravitySpeed = 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Example 2: Smooth Menu Navigation (Fixed)

let currentSelection = 0;
let menuItems = [];
let isNavigating = false;

// Setup menu
for (let i = 0; i < 5; i++) {
    const item = new Component(200, 50, "gray", 100, 100 + i * 70);
    item.text = `Option ${i + 1}`;
    menuItems.push(item);
    display.add(item);
}

function update() {
    // Navigation with smooth gliding
    if (!isNavigating) {
        if (display.keys[40]) { // Down arrow
            currentSelection = (currentSelection + 1) % menuItems.length;
            isNavigating = true;

            // Glide all items (called in loop via state management)
            menuItems.forEach((item, index) => {
                const targetY = 100 + ((index - currentSelection) * 70);
                // Store glide state and manage in loop
                startGlide(item, item.x, targetY, 0.3);
            });

            // Reset flag after glide duration
            setTimeout(() => { isNavigating = false; }, 300);
        }
    }

    // Update active glides
    updateGlides();
}
Enter fullscreen mode Exit fullscreen mode

🚨 Common Mistakes and Solutions

Mistake 1: Glide Outside Loop

// ❌ WRONG
move.glideTo(object, 2, 400, 300); // Single call

// ✅ CORRECT
let glide = { active: true, targetX: 400, targetY: 300, duration: 2 };
function update() {
    if (glide.active) {
        move.glideTo(object, glide.duration, glide.targetX, glide.targetY);
        // Check completion and set glide.active = false when done
    }
}
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Not Resetting Speeds

// ❌ WRONG - Object keeps moving after glide
move.glideTo(object, 2, 400, 300);

// ✅ CORRECT - Stop movement after glide
let glideComplete = false;
function update() {
    if (!glideComplete) {
        move.glideTo(object, 2, 400, 300);
        if (Math.abs(object.x - 400) < 1 && Math.abs(object.y - 300) < 1) {
            object.speedX = 0;
            object.speedY = 0;
            glideComplete = true;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Multiple Conflicting Glides

// ❌ WRONG - Overwrites previous glide
button1.onclick = () => move.glideTo(object, 2, 100, 100);
button2.onclick = () => move.glideTo(object, 2, 200, 200); // Cancels first

// ✅ CORRECT - Queue or replace glides
let currentGlide = null;
function startNewGlide(targetX, targetY) {
    if (currentGlide) {
        // Cancel previous glide
        object.speedX = 0;
        object.speedY = 0;
    }
    currentGlide = { targetX, targetY, startTime: Date.now() };
}
Enter fullscreen mode Exit fullscreen mode

🔧 Debugging Movement Issues

Add this debug utility to your games:

class MovementDebug {
    static logMovement(object, name) {
        console.log(`${name}:`, {
            position: `(${object.x.toFixed(1)}, ${object.y.toFixed(1)})`,
            speed: `(${object.speedX.toFixed(2)}, ${object.speedY.toFixed(2)})`,
            gravity: object.gravity,
            physics: object.physics,
            angle: (object.angle * 180 / Math.PI).toFixed(1) + '°'
        });
    }

    static drawTrajectory(object, ctx) {
        // Draw predicted path
        ctx.beginPath();
        ctx.moveTo(object.x, object.y);

        // Simulate next 60 frames
        let simX = object.x;
        let simY = object.y;
        let simSpeedX = object.speedX;
        let simSpeedY = object.speedY;

        for (let i = 0; i < 60; i++) {
            simSpeedY += object.gravity;
            simX += simSpeedX;
            simY += simSpeedY;
            ctx.lineTo(simX, simY);
        }

        ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';
        ctx.stroke();
    }
}

// Usage in update()
function update() {
    if (debugMode) {
        MovementDebug.logMovement(player, "Player");
        MovementDebug.drawTrajectory(player, display.context);
    }
}
Enter fullscreen mode Exit fullscreen mode

📚 Conclusion and Apology

To everyone who followed my previous tutorials: I apologize for the incorrect information about glide functions. Game development is a learning process, and even tutorial writers make mistakes. The key takeaway is:

TCJSgame's glideX, glideY, and glideTo functions need to be managed in the update() loop because they control movement over multiple frames.

Remember:

  1. Continuous movement = needs to be in update()
  2. Instant actions = can be outside update()
  3. Always test boundary cases and completion states
  4. Use state management for complex movement sequences

I've updated all my TCJSgame tutorials with corrections, and I'm committed to providing accurate information moving forward. If you find any other issues, please reach out on GitHub or Dev.to!


Try the corrected examples in the TCJSgame Playground and share your creations with #TCJSgameCorrected.

Learning in public means admitting mistakes. Thank you for your understanding as we improve these resources together.

Top comments (0)