Keys to Move

Keyboard input in the browser works via two events: keydown fires when a key is pressed, keyup fires when it’s released. Store the current state of each key in an object, then read that state each frame in the game loop.

Loading editor…

Character setup

The hero object from the previous tutorial, with direction and movement state added:

const hero = {
    tileX: 3,
    tileY: 2,
    x: 0,
    y: 0,
    speed: 2,
    direction: 'down',
    isMoving: false,
    sprite: null
};

direction and isMoving aren’t used by the movement logic itself — they exist so that animation code (which runs separately) can read the hero’s state without needing to know how movement works.

Keyboard input

Track which keys are currently held with an event listener pair:

const keys = {
    ArrowUp: false,
    ArrowDown: false,
    ArrowLeft: false,
    ArrowRight: false,
    Space: false   // add any keys you need
};

window.addEventListener('keydown', (event) => {
    if (keys.hasOwnProperty(event.code)) {
        keys[event.code] = true;
        event.preventDefault();  // Prevent browser scroll on arrow keys
    }
});

window.addEventListener('keyup', (event) => {
    if (keys.hasOwnProperty(event.code)) {
        keys[event.code] = false;
        event.preventDefault();
    }
});

hasOwnProperty means the handler only processes keys that are explicitly listed — any unrelated key press is ignored. event.preventDefault() stops the browser from scrolling the page when arrow keys are pressed.

Movement logic

Read the key state once per frame and update tile position accordingly:

function updateMovement() {
    let moved = false;
    let newDirection = hero.direction;

    if (keys.ArrowUp)    { hero.tileY--; newDirection = 'up';    moved = true; }
    else if (keys.ArrowDown)  { hero.tileY++; newDirection = 'down';  moved = true; }
    else if (keys.ArrowLeft)  { hero.tileX--; newDirection = 'left';  moved = true; }
    else if (keys.ArrowRight) { hero.tileX++; newDirection = 'right'; moved = true; }

    if (moved) {
        hero.direction = newDirection;
        hero.isMoving = true;
        updateHeroPosition();
    } else {
        hero.isMoving = false;
    }
}

function updateHeroPosition() {
    hero.x = (hero.tileX * TILE_SIZE) + (TILE_SIZE / 2);
    hero.y = (hero.tileY * TILE_SIZE) + (TILE_SIZE / 2);
    hero.sprite.x = hero.x;
    hero.sprite.y = hero.y;
}

app.ticker.add(updateMovement);

This version has no boundary or collision checks — the hero can walk off the edge of the map. The next tutorial adds collision detection to fix that.

Visual direction

For a simple directional indicator, flip or rotate the sprite based on hero.direction:

function updateHeroAppearance() {
    const sprite = hero.sprite;

    sprite.scale.x = 1;
    sprite.rotation = 0;

    switch (hero.direction) {
        case 'left':  sprite.scale.x = -1;             break;
        case 'up':    sprite.rotation = -Math.PI / 2;  break;
        case 'down':  sprite.rotation =  Math.PI / 2;  break;
    }
}

Smooth movement

Tile-by-tile movement jumps instantly from one grid position to the next. For smooth sliding, interpolate toward the target pixel position each frame instead:

function updateSmoothMovement() {
    const targetX = (hero.tileX * TILE_SIZE) + (TILE_SIZE / 2);
    const targetY = (hero.tileY * TILE_SIZE) + (TILE_SIZE / 2);

    const lerpSpeed = 0.2;
    hero.x += (targetX - hero.x) * lerpSpeed;
    hero.y += (targetY - hero.y) * lerpSpeed;

    hero.sprite.x = hero.x;
    hero.sprite.y = hero.y;
}

The tile coordinates are still updated immediately on keypress — the smooth movement is only visual. Collision checks still run against tileX/tileY.

What you built:

  • A key-state object updated by keydown/keyup event listeners
  • A movement function that reads that state each frame and updates tile coordinates
  • A updateHeroPosition function that translates tile coordinates to pixel position

Next: Hit the Wall