Hit the Wall

Collision detection prevents the hero from entering solid tiles. Before applying a movement, check whether the destination tile is walkable — if it isn’t, cancel the move.

Loading editor…

The hero turns light red when a move is blocked and white when it moves freely.

Tile-based collision

The canMoveTo function checks whether a tile coordinate is both within bounds and walkable:

function canMoveTo(tileX, tileY, gameMap) {
    // Out of bounds — treat as solid
    if (tileX < 0 || tileX >= gameMap[0].length ||
        tileY < 0 || tileY >= gameMap.length) {
        return false;
    }

    // 0 = floor (walkable), 1 = wall (solid)
    return gameMap[tileY][tileX] === 0;
}

The movement function calls this before applying any change to tileX/tileY:

function tryMoveTo(hero, newTileX, newTileY, gameMap) {
    if (canMoveTo(newTileX, newTileY, gameMap)) {
        hero.tileX = newTileX;
        hero.tileY = newTileY;
        updateHeroPosition(hero);
        return true;
    }
    return false;  // Blocked — position unchanged
}

One array lookup per attempted move. For tile-based movement this is all the collision detection you need.

Bounding box collision

For pixel-based movement — where the hero moves a few pixels per frame rather than one tile at a time — tile-based collision needs to account for the hero’s size. A 12px hero can partially overlap a tile before its centre crosses the boundary.

Check all four corners of the hero’s bounding box:

function checkBoundingBoxCollision(x, y, width, height, gameMap, tileSize) {
    const leftTile   = Math.floor(x / tileSize);
    const rightTile  = Math.floor((x + width - 1) / tileSize);
    const topTile    = Math.floor(y / tileSize);
    const bottomTile = Math.floor((y + height - 1) / tileSize);

    for (let tileY = topTile; tileY <= bottomTile; tileY++) {
        for (let tileX = leftTile; tileX <= rightTile; tileX++) {
            if (tileX < 0 || tileX >= gameMap[0].length ||
                tileY < 0 || tileY >= gameMap.length) {
                return true;  // Map boundary
            }
            if (gameMap[tileY][tileX] === 1) return true;
        }
    }

    return false;
}

Apply X and Y movement separately so the hero can slide along walls when moving diagonally:

function updateSmoothMovement(hero, keys, gameMap, tileSize) {
    let newX = hero.x;
    let newY = hero.y;

    if (keys.ArrowLeft)  newX -= hero.speed;
    if (keys.ArrowRight) newX += hero.speed;
    if (keys.ArrowUp)    newY -= hero.speed;
    if (keys.ArrowDown)  newY += hero.speed;

    // Check X and Y independently — allows wall sliding
    if (!checkBoundingBoxCollision(newX, hero.y, hero.width, hero.height, gameMap, tileSize)) {
        hero.x = newX;
    }
    if (!checkBoundingBoxCollision(hero.x, newY, hero.width, hero.height, gameMap, tileSize)) {
        hero.y = newY;
    }

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

Checking X and Y separately means a diagonal collision into a corner doesn’t stop both axes — the hero slides along the wall face it’s parallel to.

Enhancements

Different tile behaviours:

const TileTypes = {
    FLOOR:  0,
    WALL:   1,
    WATER:  2,  // Slows movement
    SPIKES: 3,  // Damages player
    ICE:    4   // Slippery movement
};

function getTileEffect(tileType) {
    switch (tileType) {
        case TileTypes.WATER:  return { walkable: true, speedMultiplier: 0.5 };
        case TileTypes.SPIKES: return { walkable: true, damage: 10 };
        case TileTypes.ICE:    return { walkable: true, friction: 0.1 };
        default:               return { walkable: tileType === TileTypes.FLOOR };
    }
}

Wall-hit feedback:

function updateCollisionFeedback(hero, hitWall) {
    hero.sprite.tint = hitWall ? 0xff8888 : 0xffffff;
}

What you built:

  • A canMoveTo function that checks bounds and tile walkability
  • Separate X and Y collision checks for smooth wall-sliding movement
  • The pattern for per-tile effects (damage, friction, speed change)

Next: Open the Door