Build a Tilt-Controlled Maze Game with HTML, CSS, and JavaScript

CodeNPlay
CodeNPlay
Published on Oct, 06 2025 5 min read 1 comments
image

Are you ready to dive into a fun and interactive web project? In this tutorial, we'll build a Tilting Maze Game where the player navigates a ball to a goal by tilting their device or using arrow keys. It's a perfect project to understand device orientation events, HTML5 Canvas, and game logic.

Let's break down how it works and how you can build it yourself.

Project Overview

The core idea is simple: a ball rolls around a maze based on the tilt of your smartphone or tablet (using the DeviceOrientation API). For desktop users, we'll also implement keyboard controls as a fallback. The game's physics, rendering, and collision detection are all handled using the HTML5 <canvas> element.

Key Features & Concepts

  • Device Orientation: Using deviceorientation events to capture tilt data (gamma and beta).
  • HTML5 Canvas: For drawing the game's visual elements (ball, goal, walls).
  • Collision Detection: Simple logic to prevent the ball from passing through maze walls.
  • Keyboard Fallback: Ensuring the game is playable on all devices using arrow keys.
  • Responsive Design: Making the game canvas fit nicely on different screen sizes.

Step-by-Step Implementation

Here’s the complete code for the Tilting Maze Game. Create an index.html file and paste the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tilting Maze Game</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            font-family: 'Arial', sans-serif;
            overflow: hidden;
        }
        #gameContainer {
            text-align: center;
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            border-radius: 20px;
            padding: 20px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
        }
        #mazeCanvas {
            border: 3px solid #fff;
            border-radius: 10px;
            background-color: #2c3e50;
        }
        h1 {
            color: white;
            margin-bottom: 10px;
        }
        p {
            color: #ecf0f1;
            margin-bottom: 20px;
        }
        #winMessage {
            color: #2ecc71;
            font-size: 1.5em;
            font-weight: bold;
            margin-top: 15px;
            display: none;
        }
    </style>
</head>
<body>
    <div id="gameContainer">
        <h1>Tilt the Maze!</h1>
        <p>Tilt your device or use arrow keys to guide the ball to the green goal.</p>
        <canvas id="mazeCanvas" width="400" height="400"></canvas>
        <div id="winMessage">Congratulations! You Win!</div>
    </div>

    <script>
        // Canvas and Context Setup
        const canvas = document.getElementById('mazeCanvas');
        const ctx = canvas.getContext('2d');
        const winMessage = document.getElementById('winMessage');

        // Game Variables
        const ball = {
            x: 50,
            y: 50,
            radius: 10,
            speed: 0.5,
            color: '#e74c3c'
        };

        const goal = {
            x: canvas.width - 50,
            y: canvas.height - 50,
            radius: 15,
            color: '#2ecc71'
        };

        // Define Maze Walls (x, y, width, height)
        const walls = [
            { x: 0, y: 0, width: canvas.width, height: 10 },           // Top
            { x: 0, y: 0, width: 10, height: canvas.height },         // Left
            { x: canvas.width - 10, y: 0, width: 10, height: canvas.height }, // Right
            { x: 0, y: canvas.height - 10, width: canvas.width, height: 10 }, // Bottom

            // Inner Obstacles
            { x: 100, y: 0, width: 10, height: 200 },
            { x: 200, y: 100, width: 10, height: 200 },
            { x: 300, y: 200, width: 10, height: 200 },
            { x: 0, y: 300, width: 200, height: 10 },
        ];

        let tiltX = 0;
        let tiltY = 0;

        // Device Orientation Event Listener
        if (window.DeviceOrientationEvent) {
            window.addEventListener('deviceorientation', (event) => {
                // Gamma is left-to-right tilt, Beta is front-to-back tilt.
                // We invert them for intuitive movement.
                tiltX = (event.gamma || 0) / 30; // Normalize values
                tiltY = (event.beta || 0) / 30;
            });
        } else {
            console.log("DeviceOrientation not supported, using keyboard.");
        }

        // Keyboard Event Listeners (Fallback)
        const keys = {};
        window.addEventListener('keydown', (e) => { keys[e.key] = true; });
        window.addEventListener('keyup', (e) => { keys[e.key] = false; });

        function updateBallPosition() {
            let newX = ball.x;
            let newY = ball.y;

            // Apply tilt or keyboard input
            if (keys['ArrowUp'] || keys['w']) tiltY = -1;
            else if (keys['ArrowDown'] || keys['s']) tiltY = 1;
            else if (keys['ArrowLeft'] || keys['a']) tiltX = -1;
            else if (keys['ArrowRight'] || keys['d']) tiltX = 1;
            else {
                // If no keys are pressed, use tilt values (which might be 0)
            }

            newX += tiltX * ball.speed;
            newY += tiltY * ball.speed;

            // Check for collisions with walls before updating position
            if (!isColliding(newX, newY)) {
                ball.x = newX;
                ball.y = newY;
            }

            // Reset tilt for keyboard to not accumulate
            if (Object.keys(keys).some(key => ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 'a', 's', 'd'].includes(key))) {
                tiltX = 0;
                tiltY = 0;
            }
        }

        function isColliding(x, y) {
            // Check collision with walls
            for (let wall of walls) {
                if (x + ball.radius > wall.x &&
                    x - ball.radius < wall.x + wall.width &&
                    y + ball.radius > wall.y &&
                    y - ball.radius < wall.y + wall.height) {
                    return true;
                }
            }
            // Keep ball within canvas bounds (redundant with border walls, but safe)
            if (x - ball.radius < 0 || x + ball.radius > canvas.width || y - ball.radius < 0 || y + ball.radius > canvas.height) {
                return true;
            }
            return false;
        }

        function checkWinCondition() {
            const dx = ball.x - goal.x;
            const dy = ball.y - goal.y;
            const distance = Math.sqrt(dx * dx + dy * dy);

            if (distance < ball.radius + goal.radius) {
                winMessage.style.display = 'block';
                return true;
            }
            return false;
        }

        function draw() {
            // Clear the canvas
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // Draw Goal
            ctx.beginPath();
            ctx.arc(goal.x, goal.y, goal.radius, 0, Math.PI * 2);
            ctx.fillStyle = goal.color;
            ctx.fill();

            // Draw Walls
            ctx.fillStyle = '#34495e';
            for (let wall of walls) {
                ctx.fillRect(wall.x, wall.y, wall.width, wall.height);
            }

            // Draw Ball
            ctx.beginPath();
            ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
            ctx.fillStyle = ball.color;
            ctx.fill();
        }

        function gameLoop() {
            updateBallPosition();
            if (!checkWinCondition()) {
                draw();
                requestAnimationFrame(gameLoop);
            } else {
                draw(); // Draw one last time to show the ball in the goal
            }
        }

        // Start the game
        gameLoop();
    </script>
</body>
</html>

How It Works: A Technical Deep Dive

  1. The Canvas: This is our game board. Everything is drawn here—the ball, the goal, and the walls.
  2. Device Orientation: The deviceorientation event listener is the star of the show. It provides gamma (left-right tilt) and beta (front-back tilt) values. We normalize these values and use them to calculate the ball's movement direction.
  3. Keyboard Fallback: If the DeviceOrientation API isn't available (like on most desktops), we listen for keydown and keyup events on the arrow keys and WASD to simulate the tilt.
  4. Game Physics & Collision: The updateBallPosition function moves the ball based on the input. Before applying the new position, isColliding checks if the ball would intersect with any wall. If it would, the movement is blocked.
  5. Win Condition: We calculate the distance between the ball and the goal. If the distance is less than the sum of their radii, they are touching, and the player wins!
  6. The Game Loop: The gameLoop function, powered by requestAnimationFrame, continuously updates the game state (position, collisions) and redraws the canvas at a smooth 60 FPS.

Enhancements & Next Steps

This is a solid foundation. To level up your project, consider:

  • Multiple Levels: Create an array of different wall layouts and progress through them.
  • A Timer: Add a countdown or stopwatch to introduce a challenge.
  • Sound Effects: Play sounds for rolling, collisions, and winning.
  • Improved Graphics: Use sprites or gradients for a more polished look.
  • Local Storage: Save the best completion time.

This tilting maze game is a fantastic demonstration of how web technologies can create immersive, interactive experiences. Have fun playing and coding!

 

 

1 Comments