🎮ArcadeLab

Dhoni 7 - 3D Runner

by LaserHawk84
539 lines14.2 KB🛠️ Three.js (3D graphics)
▶ Play

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Dhoni 7 - 3D Runner</title>

<style>
* { box-sizing: border-box; }
body {
    margin: 0;
    overflow: hidden;
    background: #87ceeb;
    font-family: Arial, sans-serif;
    touch-action: none;
}
canvas { display: block; }
#hud {
    position: fixed;
    top: 12px;
    left: 12px;
    color: white;
    font-weight: bold;
    font-size: 18px;
    text-shadow: 2px 2px 4px black;
    z-index: 5;
}
#controls {
    position: fixed;
    bottom: 20px;
    left: 0;
    right: 0;
    display: flex;
    justify-content: space-between;
    padding: 0 18px;
    z-index: 6;
    pointer-events: none;
}
.control-group { display: flex; gap: 10px; }
button {
    pointer-events: auto;
    border: 2px solid white;
    background: #101b32dd;
    color: white;
    border-radius: 16px;
    width: 65px;
    height: 65px;
    font-size: 25px;
    font-weight: bold;
    touch-action: none;
}
#jumpBtn {
    width: 82px;
    height: 82px;
    border-radius: 50%;
    background: #ff8c00ee;
    font-size: 17px;
}
#overlay {
    position: fixed;
    inset: 0;
    z-index: 10;
    background: #0009;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: white;
    text-align: center;
    padding: 20px;
}
#overlay h1 { font-size: 36px; margin: 0 0 12px; }
#overlay p { line-height: 1.5; }
#startBtn {
    width: auto;
    height: auto;
    padding: 15px 28px;
    background: #0c9c55;
    font-size: 19px;
}
</style>
</head>

<body>
<div id="hud">
    SCORE: <span id="score">0</span><br>
    COINS: <span id="coins">0</span>
</div>

<div id="controls">
    <div class="control-group">
        <button id="leftBtn" aria-label="Move left">◀</button>
        <button id="rightBtn" aria-label="Move right">▶</button>
    </div>
    <button id="jumpBtn">JUMP ⬆</button>
</div>

<div id="overlay">
    <h1>DHONI RUNNER 7</h1>
    <p>3 lanes • Collect coins • Dodge obstacles<br>Use arrows or buttons to move<br>Press JUMP to jump over obstacles</p>
    <button id="startBtn">START GAME</button>
</div>

<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 35, 115);

const camera = new THREE.PerspectiveCamera(
    65, window.innerWidth / window.innerHeight, 0.1, 250
);
camera.position.set(0, 6, 10);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

scene.add(new THREE.HemisphereLight(0xffffff, 0x567044, 2));
const sun = new THREE.DirectionalLight(0xffffff, 2.2);
sun.position.set(-5, 12, 8);
sun.castShadow = true;
scene.add(sun);

const mat = (color, roughness = 0.8) =>
    new THREE.MeshStandardMaterial({ color, roughness });

const green = mat(0x339344);
const roadMat = mat(0x333640);
const white = mat(0xffffff);
const blue = mat(0x0878e8);
const darkBlue = mat(0x063f91);
const skin = mat(0xb8794d);
const black = mat(0x202027);
const gold = mat(0xffc928);
const red = mat(0xf04438);

const boxGeo = new THREE.BoxGeometry(1, 1, 1);

function cube(parent, material, x, y, z, sx, sy, sz) {
    const mesh = new THREE.Mesh(boxGeo, material);
    mesh.position.set(x, y, z);
    mesh.scale.set(sx, sy, sz);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    parent.add(mesh);
    return mesh;
}

const ground = new THREE.Mesh(
    new THREE.PlaneGeometry(200, 240),
    green
);
ground.rotation.x = -Math.PI / 2;
ground.position.set(0, -0.13, -65);
ground.receiveShadow = true;
scene.add(ground);

const road = new THREE.Mesh(
    new THREE.PlaneGeometry(10, 220),
    roadMat
);
road.rotation.x = -Math.PI / 2;
road.position.set(0, -0.08, -55);
scene.add(road);

// Road lane markings
for (const x of [-1.67, 1.67]) {
    for (let i = 0; i < 22; i++) {
        const dash = new THREE.Mesh(
            new THREE.BoxGeometry(0.07, 0.025, 3),
            white
        );
        dash.position.set(x, -0.045, 5 - i * 10);
        scene.add(dash);
    }
}

// Trees along both sides
const scenery = [];
function makeTree(x, z) {
    const group = new THREE.Group();
    group.position.set(x, 0, z);
    cube(group, mat(0x79502d), 0, 1, 0, 0.45, 2, 0.45);
    const leaves = new THREE.Mesh(
        new THREE.ConeGeometry(1.25, 3.2, 7),
        mat(0x16753c)
    );
    leaves.position.y = 3;
    leaves.castShadow = true;
    group.add(leaves);
    scene.add(group);
    scenery.push(group);
}
for (let i = 0; i < 24; i++) {
    makeTree(-7 - Math.random() * 3, -i * 8 - 5);
    makeTree(7 + Math.random() * 3, -i * 8 - 5);
}

// Player model, facing forward along -Z
const player = new THREE.Group();
scene.add(player);
player.position.set(0, 0, 2);

const leftLeg = new THREE.Group();
leftLeg.position.set(-0.23, 0.85, 0);
player.add(leftLeg);
cube(leftLeg, darkBlue, 0, -0.3, 0, 0.28, 0.8, 0.34);
cube(leftLeg, black, 0, -0.72, -0.08, 0.32, 0.16, 0.5);

const rightLeg = new THREE.Group();
rightLeg.position.set(0.23, 0.85, 0);
player.add(rightLeg);
cube(rightLeg, darkBlue, 0, -0.3, 0, 0.28, 0.8, 0.34);
cube(rightLeg, black, 0, -0.72, -0.08, 0.32, 0.16, 0.5);

// Body and blue jersey
cube(player, blue, 0, 1.65, 0, 0.9, 1.05, 0.48);

// Jersey text on the front, facing the camera
function makeJerseyTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 512;
    canvas.height = 512;
    const ctx = canvas.getContext('2d');

    ctx.fillStyle = '#0878e8';
    ctx.fillRect(0, 0, 512, 512);

    ctx.fillStyle = '#ffffff';
    ctx.textAlign = 'center';
    ctx.font = 'bold 66px Arial';
    ctx.fillText('DHONI', 256, 220);
    ctx.font = 'bold 150px Arial';
    ctx.fillText('7', 256, 380);

    const texture = new THREE.CanvasTexture(canvas);
    texture.colorSpace = THREE.SRGBColorSpace;
    return texture;
}
const jerseyFront = new THREE.Mesh(
    new THREE.PlaneGeometry(0.82, 0.95),
    new THREE.MeshBasicMaterial({
        map: makeJerseyTexture(),
        side: THREE.DoubleSide
    })
);
jerseyFront.position.set(0, 1.65, -0.247);
player.add(jerseyFront);

// Arms
const leftArm = new THREE.Group();
leftArm.position.set(-0.58, 1.95, 0);
player.add(leftArm);
cube(leftArm, blue, 0, -0.32, 0, 0.23, 0.75, 0.25);
cube(leftArm, skin, 0, -0.76, -0.01, 0.2, 0.22, 0.22);

const rightArm = new THREE.Group();
rightArm.position.set(0.58, 1.95, 0);
player.add(rightArm);
cube(rightArm, blue, 0, -0.32, 0, 0.23, 0.75, 0.25);
cube(rightArm, skin, 0, -0.76, -0.01, 0.2, 0.22, 0.22);

// Head, hair, face facing -Z
cube(player, skin, 0, 2.55, -0.02, 0.52, 0.55, 0.48);
cube(player, black, 0, 2.83, 0.015, 0.55, 0.18, 0.5);

// Eyes on the front
cube(player, white, -0.12, 2.59, -0.267, 0.09, 0.07, 0.025);
cube(player, white, 0.12, 2.59, -0.267, 0.09, 0.07, 0.025);
cube(player, black, -0.12, 2.59, -0.286, 0.035, 0.05, 0.015);
cube(player, black, 0.12, 2.59, -0.286, 0.035, 0.05, 0.015);

// Gameplay state
let running = false;
let score = 0;
let coinCount = 0;
let lane = 1;
const laneX = [-3, 0, 3];
let jumpY = 0;
let jumpVelocity = 0;
let isJumping = false;
let speed = 0.3;
let spawnTimer = 0;
let coinTimer = 0;
let lastTime = 0;

const obstacles = [];
const coins = [];

function makeObstacle() {
    const laneIndex = Math.floor(Math.random() * 3);
    const obstacle = new THREE.Group();
    obstacle.position.set(laneX[laneIndex], 0, -75);

    const color = Math.random() > 0.5 ? red : gold;
    cube(obstacle, color, 0, 0.65, 0, 1.15, 1.3, 0.8);

    scene.add(obstacle);
    obstacles.push(obstacle);
}

function makeCoin() {
    const laneIndex = Math.floor(Math.random() * 3);
    const coin = new THREE.Mesh(
        new THREE.TorusGeometry(0.38, 0.12, 10, 24),
        new THREE.MeshStandardMaterial({
            color: 0xffd700,
            metalness: 0.65,
            roughness: 0.25
        })
    );
    coin.position.set(laneX[laneIndex], 1.25, -75);
    coin.rotation.y = Math.PI / 2;
    scene.add(coin);
    coins.push(coin);
}

function updateHUD() {
    document.getElementById('score').textContent = Math.floor(score);
    document.getElementById('coins').textContent = coinCount;
}

function moveLane(direction) {
    if (!running) return;
    lane = Math.max(0, Math.min(2, lane + direction));
}

function jump() {
    // This is the real jump action, not just a button animation.
    if (!running || isJumping) return;
    isJumping = true;
    jumpVelocity = 0.24;
}

function startGame() {
    score = 0;
    coinCount = 0;
    lane = 1;
    jumpY = 0;
    jumpVelocity = 0;
    isJumping = false;
    speed = 0.3;
    spawnTimer = 0;
    coinTimer = 0;

    for (const item of obstacles) scene.remove(item);
    for (const item of coins) scene.remove(item);
    obstacles.length = 0;
    coins.length = 0;

    player.position.set(0, 0, 2);
    running = true;
    lastTime = performance.now();
    document.getElementById('overlay').style.display = 'none';
}

function gameOver() {
    running = false;
    document.getElementById('overlay').style.display = 'flex';
    document.querySelector('#overlay h1').textContent = 'GAME OVER!';
    document.querySelector('#overlay p').innerHTML =
        'Score: ' + Math.floor(score) +
        '<br>Coins: ' + coinCount +
        '<br>Press restart to play again';
    document.getElementById('startBtn').textContent = 'PLAY AGAIN';
}

function animate(time) {
    requestAnimationFrame(animate);

    const dt = Math.min((time - lastTime) / 16.67 || 1, 2);
    lastTime = time;

    if (running) {
        score += 0.15 * dt;
        speed = Math.min(0.65, speed + 0.00005 * dt);

        // Smoothly move between all three lanes
        player.position.x += (laneX[lane] - player.position.x) * 0.22 * dt;

        // WORKING JUMP PHYSICS: rises, slows, falls, then lands
        if (isJumping) {
            jumpY += jumpVelocity * dt;
            jumpVelocity -= 0.012 * dt;

            if (jumpY <= 0) {
                jumpY = 0;
                jumpVelocity = 0;
                isJumping = false;
            }
        }
        player.position.y = jumpY;

        // Running animation
        const t = time * 0.012;
        if (!isJumping) {
            leftLeg.rotation.x = Math.sin(t) * 0.7;
            rightLeg.rotation.x = -Math.sin(t) * 0.7;
            leftArm.rotation.x = -Math.sin(t) * 0.55;
            rightArm.rotation.x = Math.sin(t) * 0.55;
        } else {
            leftLeg.rotation.x = 0.35;
            rightLeg.rotation.x = -0.35;
            leftArm.rotation.x = -0.5;
            rightArm.rotation.x = -0.5;
        }

        // Recycle trees
        for (const tree of scenery) {
            tree.position.z += speed * dt;
            if (tree.position.z > 12) tree.position.z -= 192;
        }

        spawnTimer += dt;
        coinTimer += dt;

        if (spawnTimer > 42) {
            makeObstacle();
            spawnTimer = 0;
        }
        if (coinTimer > 24) {
            makeCoin();
            coinTimer = 0;
        }

        // Move obstacles towards the player and detect collisions
        for (let i = obstacles.length - 1; i >= 0; i--) {
            const obstacle = obstacles[i];
            obstacle.position.z += speed * dt;

            if (
                Math.abs(obstacle.position.z - player.position.z) < 0.85 &&
                Math.abs(obstacle.position.x - player.position.x) < 1.05 &&
                jumpY < 1.35
            ) {
                gameOver();
            }

            if (obstacle.position.z > 12) {
                scene.remove(obstacle);
                obstacles.splice(i, 1);
            }
        }

        // Collect coins
        for (let i = coins.length - 1; i >= 0; i--) {
            const coin = coins[i];
            coin.position.z += speed * dt;
            coin.rotation.y += 0.06 * dt;

            if (
                Math.abs(coin.position.z - player.position.z) < 1.0 &&
                Math.abs(coin.position.x - player.position.x) < 1.0
            ) {
                coinCount++;
                scene.remove(coin);
                coins.splice(i, 1);
                continue;
            }

            if (coin.position.z > 12) {
                scene.remove(coin);
                coins.splice(i, 1);
            }
        }

        updateHUD();
    }

    // Camera follows behind the character
    camera.position.x += (player.position.x * 0.45 - camera.position.x) * 0.06;
    camera.position.y = 5.8 + jumpY * 0.18;
    camera.lookAt(player.position.x * 0.3, 1.2 + jumpY * 0.2, -5);

    renderer.render(scene, camera);
}
requestAnimationFrame(animate);

// Connect all buttons to real actions
document.getElementById('startBtn').addEventListener('click', startGame);

document.getElementById('leftBtn').addEventListener('pointerdown', e => {
    e.preventDefault();
    moveLane(-1);
});

document.getElementById('rightBtn').addEventListener('pointerdown', e => {
    e.preventDefault();
    moveLane(1);
});

document.getElementById('jumpBtn').addEventListener('pointerdown', e => {
    e.preventDefault();
    jump();
});

// Keyboard support
window.addEventListener('keydown', e => {
    if (e.code === 'ArrowLeft' || e.code === 'KeyA') moveLane(-1);
    if (e.code === 'ArrowRight' || e.code === 'KeyD') moveLane(1);
    if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
        e.preventDefault();
        jump();
    }
});

// Swipe controls for mobile
let touchStartX = 0;
let touchStartY = 0;

renderer.domElement.addEventListener('touchstart', e => {
    touchStartX = e.changedTouches[0].clientX;
    touchStartY = e.changedTouches[0].clientY;
}, { passive: true });

renderer.domElement.addEventListener('touchend', e => {
    const dx = e.changedTouches[0].clientX - touchStartX;
    const dy = e.changedTouches[0].clientY - touchStartY;

    if (Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy)) {
        moveLane(dx > 0 ? 1 : -1);
    } else if (dy < -35) {
        jump();
    }
}, { passive: true });

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>

Game Source: Dhoni 7 - 3D Runner

Creator: LaserHawk84

Libraries: three

Complexity: complex (539 lines, 14.2 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: dhoni-7-3d-runner-laserhawk84" to link back to the original. Then publish at arcadelab.ai/publish.