🎮ArcadeLab

3D Runner Mobile

by GlitchBolt30
440 lines9.8 KB🛠️ Three.js (3D graphics)
▶ Play

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

<style>
* { box-sizing: border-box; }
body {
  margin: 0;
  overflow: hidden;
  font-family: Arial, sans-serif;
  background: #87ceeb;
  touch-action: none;
}
canvas {
  display: block;
  width: 100%;
  height: 100%;
}
#hud {
  position: fixed;
  top: 15px;
  left: 15px;
  color: white;
  font-size: 20px;
  font-weight: bold;
  text-shadow: 2px 2px 4px black;
  z-index: 2;
}
#controls {
  position: fixed;
  bottom: 22px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-around;
  align-items: center;
  z-index: 3;
}
button {
  border: 2px solid #ffffff99;
  border-radius: 18px;
  background: #162b49cc;
  color: white;
  font-size: 25px;
  font-weight: bold;
  width: 76px;
  height: 65px;
  touch-action: none;
  user-select: none;
}
button:active {
  background: #00aaff;
  transform: scale(.94);
}
#overlay {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: #07182acb;
  color: white;
  text-align: center;
  z-index: 5;
  padding: 20px;
}
#overlay h1 { font-size: 34px; }
#overlay p { line-height: 1.6; }
#start {
  width: auto;
  padding: 0 28px;
  background: #00a86b;
  border: none;
}
</style>
</head>

<body>

<div id="hud">
  🪙 Skor: <span id="score">0</span><br>
  🏆 Terbaik: <span id="best">0</span>
</div>

<div id="controls">
  <button id="left">◀</button>
  <button id="jump">⬆</button>
  <button id="right">▶</button>
</div>

<div id="overlay">
  <h1 id="title">🏃 3D RUNNER</h1>
  <p id="message">
    Hindari rintangan dan kumpulkan koin!<br>
    Gunakan tombol kiri, kanan, dan lompat.
  </p>
  <button id="start">▶ MULAI</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>

<script>
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 25, 90);

const camera = new THREE.PerspectiveCamera(
  65, window.innerWidth / window.innerHeight, 0.1, 150
);
camera.position.set(0, 5, 9);
camera.lookAt(0, 1, -5);

const renderer = new THREE.WebGLRenderer({
  antialias: true,
  powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.shadowMap.enabled = false;
document.body.appendChild(renderer.domElement);

// Pencahayaan
scene.add(new THREE.HemisphereLight(0xffffff, 0x456044, 2));

const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(5, 10, 5);
scene.add(sun);

// Tanah
const ground = new THREE.Mesh(
  new THREE.PlaneGeometry(200, 200),
  new THREE.MeshStandardMaterial({ color: 0x278b48 })
);
ground.rotation.x = -Math.PI / 2;
ground.position.set(0, -0.12, -35);
scene.add(ground);

// Jalan
const road = new THREE.Mesh(
  new THREE.PlaneGeometry(8, 150),
  new THREE.MeshStandardMaterial({ color: 0x333b4b })
);
road.rotation.x = -Math.PI / 2;
road.position.set(0, -0.08, -35);
scene.add(road);

// Garis jalan
for (const x of [-1.33, 1.33]) {
  for (let i = 0; i < 20; i++) {
    const line = new THREE.Mesh(
      new THREE.BoxGeometry(0.07, 0.025, 2),
      new THREE.MeshStandardMaterial({ color: 0xffffff })
    );
    line.position.set(x, -0.04, -i * 5);
    scene.add(line);
  }
}

// Membuat pemain
const player = new THREE.Group();
scene.add(player);

const body = new THREE.Mesh(
  new THREE.BoxGeometry(0.8, 1, 0.55),
  new THREE.MeshStandardMaterial({ color: 0x00aaff })
);
body.position.y = 1;
player.add(body);

const head = new THREE.Mesh(
  new THREE.SphereGeometry(0.34, 16, 12),
  new THREE.MeshStandardMaterial({ color: 0xffcc99 })
);
head.position.y = 1.72;
player.add(head);

// Kaki
const legs = [];
for (const x of [-0.23, 0.23]) {
  const leg = new THREE.Mesh(
    new THREE.BoxGeometry(0.23, 0.55, 0.25),
    new THREE.MeshStandardMaterial({ color: 0x182b80 })
  );
  leg.position.set(x, 0.28, 0);
  player.add(leg);
  legs.push(leg);
}

const lanes = [-2, 0, 2];
let lane = 1;
let jumping = false;
let velocityY = 0;
let score = 0;
let speed = 0.24;
let running = false;
let spawnTimer = 0;
let obstacles = [];
let coins = [];
let clock = new THREE.Clock();

const scoreEl = document.getElementById("score");
const bestEl = document.getElementById("best");
const overlay = document.getElementById("overlay");
const title = document.getElementById("title");
const message = document.getElementById("message");

let best = 0;
try {
  best = Number(localStorage.getItem("runnerBest")) || 0;
} catch(e) {}
bestEl.textContent = best;

function moveLeft() {
  if (running) lane = Math.max(0, lane - 1);
}
function moveRight() {
  if (running) lane = Math.min(2, lane + 1);
}
function jump() {
  if (running && !jumping) {
    jumping = true;
    velocityY = 0.22;
  }
}

document.getElementById("left").addEventListener("pointerdown",
  e => { e.preventDefault(); moveLeft(); });
document.getElementById("right").addEventListener("pointerdown",
  e => { e.preventDefault(); moveRight(); });
document.getElementById("jump").addEventListener("pointerdown",
  e => { e.preventDefault(); jump(); });

window.addEventListener("keydown", e => {
  if (e.key === "ArrowLeft" || e.key === "a") moveLeft();
  if (e.key === "ArrowRight" || e.key === "d") moveRight();
  if (e.key === "ArrowUp" || e.key === " ") jump();
});

// Kontrol geser layar
let touchX = 0;
let touchY = 0;
renderer.domElement.addEventListener("touchstart", e => {
  touchX = e.changedTouches[0].clientX;
  touchY = e.changedTouches[0].clientY;
}, { passive: true });

renderer.domElement.addEventListener("touchend", e => {
  const dx = e.changedTouches[0].clientX - touchX;
  const dy = e.changedTouches[0].clientY - touchY;
  if (Math.abs(dx) > 35) {
    dx < 0 ? moveLeft() : moveRight();
  } else if (dy < -35) {
    jump();
  }
}, { passive: true });

// Membuat rintangan
function makeObstacle() {
  const obj = new THREE.Mesh(
    new THREE.BoxGeometry(1.15, 1.2, 0.9),
    new THREE.MeshStandardMaterial({ color: 0xf04444 })
  );
  const ln = Math.floor(Math.random() * 3);
  obj.position.set(lanes[ln], 0.6, -48);
  scene.add(obj);
  obstacles.push(obj);
}

// Membuat koin
function makeCoin() {
  const obj = new THREE.Mesh(
    new THREE.TorusGeometry(0.32, 0.1, 8, 16),
    new THREE.MeshStandardMaterial({
      color: 0xffd700,
      metalness: 0.5,
      roughness: 0.3
    })
  );
  const ln = Math.floor(Math.random() * 3);
  obj.position.set(lanes[ln], 1.1, -48);
  scene.add(obj);
  coins.push(obj);
}

function clearObjects(list) {
  for (const obj of list) scene.remove(obj);
  list.length = 0;
}

function startGame() {
  clearObjects(obstacles);
  clearObjects(coins);

  lane = 1;
  player.position.set(0, 0, 0);
  player.rotation.set(0, 0, 0);
  jumping = false;
  velocityY = 0;
  score = 0;
  speed = 0.24;
  spawnTimer = 0;
  scoreEl.textContent = score;
  running = true;
  clock.getDelta();

  overlay.style.display = "none";
}

document.getElementById("start").addEventListener("click", startGame);

function gameOver() {
  running = false;

  if (score > best) {
    best = score;
    bestEl.textContent = best;
    try {
      localStorage.setItem("runnerBest", best);
    } catch(e) {}
  }

  title.textContent = "💥 GAME OVER";
  message.innerHTML =
    "Skor kamu: <b>" + score + "</b><br>" +
    "Coba lagi dan kumpulkan lebih banyak koin!";
  document.getElementById("start").textContent = "🔄 MAIN LAGI";
  overlay.style.display = "flex";
}

// Loop game
function animate() {
  requestAnimationFrame(animate);

  const dt = Math.min(clock.getDelta(), 0.05);

  if (running) {
    const step = dt * 60;
    spawnTimer += dt;

    // Gerakan kiri-kanan yang halus
    player.position.x +=
      (lanes[lane] - player.position.x) * Math.min(1, dt * 12);

    // Animasi lari
    if (!jumping) {
      const t = performance.now() * 0.012;
      legs[0].rotation.x = Math.sin(t) * 0.7;
      legs[1].rotation.x = -Math.sin(t) * 0.7;
    }

    // Melompat
    if (jumping) {
      player.position.y += velocityY * step;
      velocityY -= 0.012 * step;

      if (player.position.y <= 0) {
        player.position.y = 0;
        velocityY = 0;
        jumping = false;
      }
    }

    // Tambahkan rintangan dan koin
    if (spawnTimer > 0.85) {
      spawnTimer = 0;
      makeObstacle();
      if (Math.random() > 0.25) makeCoin();
    }

    speed = Math.min(0.48, speed + dt * 0.003);

    // Gerakkan rintangan
    for (let i = obstacles.length - 1; i >= 0; i--) {
      const o = obstacles[i];
      o.position.z += speed * step;

      if (
        Math.abs(o.position.z) < 0.8 &&
        Math.abs(o.position.x - player.position.x) < 0.8 &&
        player.position.y < 0.75
      ) {
        gameOver();
        break;
      }

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

    // Gerakkan dan ambil koin
    for (let i = coins.length - 1; i >= 0; i--) {
      const c = coins[i];
      c.position.z += speed * step;
      c.rotation.y += 0.08 * step;

      if (
        Math.abs(c.position.z) < 0.9 &&
        Math.abs(c.position.x - player.position.x) < 0.9 &&
        Math.abs(c.position.y - (player.position.y + 1)) < 1
      ) {
        score += 10;
        scoreEl.textContent = score;
        scene.remove(c);
        coins.splice(i, 1);
        continue;
      }

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

    // Skor bertambah seiring waktu
    score += dt * 2;
    scoreEl.textContent = Math.floor(score);
  }

  renderer.render(scene, camera);
}

window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

animate();
</script>
</body>
</html>

Game Source: 3D Runner Mobile

Creator: GlitchBolt30

Libraries: three

Complexity: complex (440 lines, 9.8 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: 3d-runner-mobile-glitchbolt30" to link back to the original. Then publish at arcadelab.ai/publish.