🎮ArcadeLab

Neon Dash 3D

by AtomicBolt46
655 lines15.6 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">
<title>Neon Dash 3D</title>
<style>
* { box-sizing: border-box; }
body {
  margin: 0;
  overflow: hidden;
  background: #111827;
  font-family: Arial, sans-serif;
  color: white;
}
canvas { display: block; }
#hud {
  position: absolute;
  top: 15px;
  left: 15px;
  z-index: 2;
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}
.stat {
  background: #101827dd;
  padding: 10px 15px;
  border: 2px solid #36d9ff;
  border-radius: 12px;
  font-weight: bold;
  font-size: 17px;
}
#message {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #07101dbb;
  z-index: 5;
  text-align: center;
}
.panel {
  background: #17243a;
  border: 3px solid #36d9ff;
  border-radius: 22px;
  padding: 28px;
  width: min(90%, 420px);
  box-shadow: 0 0 30px #00bfff66;
}
h1 {
  color: #36d9ff;
  font-size: 38px;
  margin: 0 0 12px;
}
button {
  background: #20c9e8;
  color: #061321;
  border: 0;
  padding: 13px 22px;
  font-size: 18px;
  font-weight: bold;
  border-radius: 12px;
  cursor: pointer;
  margin-top: 12px;
}
button:hover { background: #83edff; }
#controls {
  position: absolute;
  bottom: 20px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-between;
  padding: 0 20px;
  z-index: 3;
  pointer-events: none;
}
.control-group {
  display: flex;
  gap: 10px;
}
.control {
  pointer-events: auto;
  width: 58px;
  height: 58px;
  padding: 0;
  border-radius: 50%;
  background: #14253dcc;
  color: white;
  border: 2px solid #36d9ff;
  font-size: 24px;
  touch-action: none;
}
.small {
  font-size: 13px;
  color: #cbd5e1;
  line-height: 1.5;
}
</style>
</head>
<body>

<div id="hud">
  <div class="stat">⭐ Score: <span id="score">0</span></div>
  <div class="stat">🪙 Coins: <span id="coins">0</span></div>
  <div class="stat">⚡ Speed: <span id="speed">1</span></div>
</div>

<div id="message">
  <div class="panel">
    <h1 id="title">NEON DASH 3D</h1>
    <p id="description">
      Run through the city, dodge obstacles and collect coins!
    </p>
    <p class="small">
      A / D or ← / → = Move<br>
      SPACE or ↑ = Jump<br>
      S or ↓ = Slide
    </p>
    <button id="startButton">START RUNNING</button>
  </div>
</div>

<div id="controls">
  <div class="control-group">
    <button class="control" id="left">◀</button>
    <button class="control" id="right">▶</button>
  </div>
  <div class="control-group">
    <button class="control" id="slide">▼</button>
    <button class="control" id="jump">▲</button>
  </div>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
  }
}
</script>

<script type="module">
import * as THREE from "three";

// ---------- GAME SETUP ----------
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 35, 125);

const camera = new THREE.PerspectiveCamera(
  65, window.innerWidth / window.innerHeight, 0.1, 200
);
camera.position.set(0, 7, 13);
camera.lookAt(0, 1, -8);

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

scene.add(new THREE.HemisphereLight(0xffffff, 0x526078, 2));

const sunlight = new THREE.DirectionalLight(0xffffff, 2.3);
sunlight.position.set(-8, 15, 8);
sunlight.castShadow = true;
scene.add(sunlight);

const laneX = [-2.5, 0, 2.5];
const playerZ = 5;
let playerLane = 1;
let targetX = 0;
let jumping = false;
let sliding = false;
let jumpTime = 0;
let slideTime = 0;
let running = false;
let gameOver = false;
let score = 0;
let coins = 0;
let speed = 22;
let distance = 0;
let spawnDistance = 0;
let objects = [];
let scenery = [];
let lastTime = 0;

// ---------- MATERIALS ----------
const materials = {
  road: new THREE.MeshStandardMaterial({ color: 0x303b4d }),
  line: new THREE.MeshStandardMaterial({ color: 0xffffff }),
  rail: new THREE.MeshStandardMaterial({ color: 0x777f8c, metalness: 0.7 }),
  blue: new THREE.MeshStandardMaterial({ color: 0x168de0 }),
  red: new THREE.MeshStandardMaterial({ color: 0xff4d58 }),
  yellow: new THREE.MeshStandardMaterial({ color: 0xffd52a }),
  purple: new THREE.MeshStandardMaterial({ color: 0x884dff }),
  green: new THREE.MeshStandardMaterial({ color: 0x24d18c }),
  skin: new THREE.MeshStandardMaterial({ color: 0xffc28c }),
  dark: new THREE.MeshStandardMaterial({ color: 0x182238 }),
  white: new THREE.MeshStandardMaterial({ color: 0xffffff }),
  gold: new THREE.MeshStandardMaterial({
    color: 0xffc928, metalness: 0.7, roughness: 0.2
  })
};

// ---------- GROUND ----------
const ground = new THREE.Mesh(
  new THREE.BoxGeometry(9, 0.3, 150),
  materials.road
);
ground.position.set(0, -0.2, -40);
ground.receiveShadow = true;
scene.add(ground);

// Lane markings
for (const x of [-1.25, 1.25]) {
  for (let z = 10; z > -100; z -= 8) {
    const stripe = new THREE.Mesh(
      new THREE.BoxGeometry(0.06, 0.03, 3),
      materials.line
    );
    stripe.position.set(x, -0.025, z);
    stripe.userData.movingStripe = true;
    scene.add(stripe);
    scenery.push(stripe);
  }
}

// Track rails
for (const x of [-3.9, 3.9]) {
  const rail = new THREE.Mesh(
    new THREE.BoxGeometry(0.16, 0.35, 150),
    materials.rail
  );
  rail.position.set(x, 0.05, -40);
  scene.add(rail);
}

// ---------- CITY SCENERY ----------
function addBuilding(x, z) {
  const height = 7 + Math.random() * 17;
  const width = 4 + Math.random() * 3;
  const building = new THREE.Group();

  const body = new THREE.Mesh(
    new THREE.BoxGeometry(width, height, 5),
    new THREE.MeshStandardMaterial({
      color: new THREE.Color().setHSL(
        0.55 + Math.random() * 0.25, 0.35, 0.3
      )
    })
  );
  body.position.y = height / 2;
  body.castShadow = true;
  building.add(body);

  const windowMaterial = new THREE.MeshStandardMaterial({
    color: 0xffe59a,
    emissive: 0x775522,
    emissiveIntensity: 0.4
  });

  for (let y = 2; y < height - 1; y += 2.5) {
    for (let wx = -width / 2 + 0.8; wx < width / 2 - 0.4; wx += 1.3) {
      const windowMesh = new THREE.Mesh(
        new THREE.BoxGeometry(0.55, 0.9, 0.08),
        windowMaterial
      );
      windowMesh.position.set(wx, y, 2.53);
      building.add(windowMesh);
    }
  }

  building.position.set(x, 0, z);
  scene.add(building);
  scenery.push(building);
}

for (let z = -100; z < 25; z += 13) {
  addBuilding(-9, z);
  addBuilding(9, z - 5);
}

// ---------- PLAYER ----------
const player = new THREE.Group();
scene.add(player);
player.position.set(0, 0, playerZ);

const body = new THREE.Mesh(
  new THREE.BoxGeometry(0.8, 1.1, 0.48),
  materials.blue
);
body.position.y = 1.15;
body.castShadow = true;
player.add(body);

const head = new THREE.Mesh(
  new THREE.SphereGeometry(0.36, 20, 16),
  materials.skin
);
head.position.y = 1.95;
head.castShadow = true;
player.add(head);

const hair = new THREE.Mesh(
  new THREE.SphereGeometry(0.37, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2),
  materials.dark
);
hair.position.y = 2.08;
player.add(hair);

const leftLeg = new THREE.Mesh(
  new THREE.BoxGeometry(0.28, 0.65, 0.3),
  materials.dark
);
leftLeg.position.set(-0.23, 0.35, 0);
player.add(leftLeg);

const rightLeg = leftLeg.clone();
rightLeg.position.x = 0.23;
player.add(rightLeg);

const leftArm = new THREE.Mesh(
  new THREE.BoxGeometry(0.22, 0.7, 0.25),
  materials.blue
);
leftArm.position.set(-0.52, 1.2, 0);
player.add(leftArm);

const rightArm = leftArm.clone();
rightArm.position.x = 0.52;
player.add(rightArm);

// ---------- OBSTACLES AND COINS ----------
function createObstacle(lane, z, type) {
  const group = new THREE.Group();
  let mesh;

  if (type === "barrier") {
    mesh = new THREE.Mesh(
      new THREE.BoxGeometry(1.7, 0.85, 0.7),
      materials.red
    );
    mesh.position.y = 0.5;
  } else if (type === "arch") {
    // A high overhead obstacle: slide underneath it.
    const top = new THREE.Mesh(
      new THREE.BoxGeometry(1.8, 0.45, 0.7),
      materials.purple
    );
    top.position.y = 2.15;
    group.add(top);

    for (const x of [-0.72, 0.72]) {
      const leg = new THREE.Mesh(
        new THREE.BoxGeometry(0.22, 2.2, 0.7),
        materials.purple
      );
      leg.position.set(x, 1.1, 0);
      group.add(leg);
    }
  } else {
    mesh = new THREE.Mesh(
      new THREE.BoxGeometry(1.8, 2.6, 2.1),
      materials.green
    );
    mesh.position.y = 1.3;
  }

  if (mesh) {
    mesh.castShadow = true;
    group.add(mesh);
  }

  group.position.set(laneX[lane], 0, z);
  scene.add(group);
  objects.push({ mesh: group, type, lane, z, kind: "obstacle" });
}

function createCoin(lane, z, height = 1.3) {
  const coin = new THREE.Mesh(
    new THREE.TorusGeometry(0.35, 0.12, 10, 20),
    materials.gold
  );
  coin.position.set(laneX[lane], height, z);
  coin.rotation.y = Math.PI / 2;
  coin.castShadow = true;
  scene.add(coin);
  objects.push({
    mesh: coin,
    type: "coin",
    lane,
    z,
    kind: "coin",
    baseY: height
  });
}

function spawnRow() {
  const blockedLane = Math.floor(Math.random() * 3);
  const obstacleTypes = ["barrier", "arch", "block"];
  const type = obstacleTypes[Math.floor(Math.random() * 3)];

  createObstacle(blockedLane, -90, type);

  // Add a coin trail in one of the other lanes.
  let coinLane = (blockedLane + 1 + Math.floor(Math.random() * 2)) % 3;

  for (let i = 0; i < 5; i++) {
    createCoin(coinLane, -90 - i * 2.2, 1.2);
  }

  // Sometimes add coins in the remaining lane too.
  if (Math.random() < 0.45) {
    const otherLane = 3 - blockedLane - coinLane;
    for (let i = 0; i < 3; i++) {
      createCoin(otherLane, -93 - i * 2.2, 1.2);
    }
  }
}

// ---------- CONTROLS ----------
function moveLeft() {
  if (!running) return;
  playerLane = Math.max(0, playerLane - 1);
  targetX = laneX[playerLane];
}
function moveRight() {
  if (!running) return;
  playerLane = Math.min(2, playerLane + 1);
  targetX = laneX[playerLane];
}
function jump() {
  if (!running || jumping || sliding) return;
  jumping = true;
  jumpTime = 0;
}
function slide() {
  if (!running || sliding || jumping) return;
  sliding = true;
  slideTime = 0;
}

document.addEventListener("keydown", (e) => {
  if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", " "].includes(e.key)) {
    e.preventDefault();
  }
  if (e.repeat) return;

  if (e.key === "ArrowLeft" || e.key.toLowerCase() === "a") moveLeft();
  if (e.key === "ArrowRight" || e.key.toLowerCase() === "d") moveRight();
  if (e.key === "ArrowUp" || e.key === " ") jump();
  if (e.key === "ArrowDown" || e.key.toLowerCase() === "s") slide();
});

function bindButton(id, action) {
  const button = document.getElementById(id);
  button.addEventListener("pointerdown", (e) => {
    e.preventDefault();
    action();
  });
}
bindButton("left", moveLeft);
bindButton("right", moveRight);
bindButton("jump", jump);
bindButton("slide", slide);

// ---------- GAME LOGIC ----------
function updateHUD() {
  document.getElementById("score").textContent = Math.floor(score);
  document.getElementById("coins").textContent = coins;
  document.getElementById("speed").textContent =
    (speed / 22).toFixed(1) + "x";
}

function endGame() {
  running = false;
  gameOver = true;
  document.getElementById("title").textContent = "GAME OVER!";
  document.getElementById("description").innerHTML =
    "⭐ Score: " + Math.floor(score) +
    "<br>🪙 Coins collected: " + coins +
    "<br><br>Can you beat your score?";
  document.getElementById("startButton").textContent = "PLAY AGAIN";
  document.getElementById("message").style.display = "flex";
}

function startGame() {
  for (const obj of objects) scene.remove(obj.mesh);
  objects = [];

  score = 0;
  coins = 0;
  speed = 22;
  distance = 0;
  spawnDistance = 0;
  playerLane = 1;
  targetX = 0;
  player.position.set(0, 0, playerZ);
  player.scale.set(1, 1, 1);
  jumping = false;
  sliding = false;
  gameOver = false;
  running = true;

  document.getElementById("message").style.display = "none";
  updateHUD();
}

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

// ---------- ANIMATION ----------
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const delta = Math.min(clock.getDelta(), 0.04);

  if (running) {
    distance += speed * delta;
    score += speed * delta * 0.5;
    speed = Math.min(42, 22 + distance * 0.018);

    // Move player smoothly between lanes.
    player.position.x += (targetX - player.position.x) *
      Math.min(1, delta * 13);

    // Jump animation.
    if (jumping) {
      jumpTime += delta;
      const t = jumpTime / 0.85;
      player.position.y = Math.sin(Math.PI * t) * 2.3;
      if (t >= 1) {
        jumping = false;
        player.position.y = 0;
      }
    }

    // Slide animation.
    if (sliding) {
      slideTime += delta;
      player.scale.y = 0.48;
      player.position.y = 0;
      if (slideTime >= 0.75) {
        sliding = false;
        player.scale.y = 1;
      }
    } else if (!jumping) {
      player.scale.y = 1;
    }

    // Running animation.
    if (!jumping && !sliding) {
      leftLeg.rotation.x = Math.sin(distance * 0.35) * 0.65;
      rightLeg.rotation.x = -leftLeg.rotation.x;
      leftArm.rotation.x = -leftLeg.rotation.x;
      rightArm.rotation.x = leftLeg.rotation.x;
    }

    // Move lane markings.
    for (const item of scenery) {
      if (item.userData.movingStripe) {
        item.position.z += speed * delta;
        if (item.position.z > 14) item.position.z -= 112;
      } else {
        item.position.z += speed * delta;
        if (item.position.z > 30) {
          item.position.z -= 130;
        }
      }
    }

    // Spawn obstacle rows.
    spawnDistance += speed * delta;
    if (spawnDistance > 23) {
      spawnDistance = 0;
      spawnRow();
    }

    // Move obstacles and coins toward the player.
    for (let i = objects.length - 1; i >= 0; i--) {
      const obj = objects[i];
      obj.z += speed * delta;
      obj.mesh.position.z = obj.z;

      if (obj.kind === "coin") {
        obj.mesh.rotation.x += delta * 3;
        obj.mesh.position.y = obj.baseY + Math.sin(distance * 0.1 + i) * 0.12;

        // Collect coin.
        if (
          Math.abs(obj.z - playerZ) < 1.25 &&
          Math.abs(player.position.x - laneX[obj.lane]) < 0.85
        ) {
          coins++;
          score += 10;
          scene.remove(obj.mesh);
          objects.splice(i, 1);
          continue;
        }
      } else {
        // Check collision with an obstacle.
        if (
          Math.abs(obj.z - playerZ) < 1.0 &&
          Math.abs(player.position.x - laneX[obj.lane]) < 0.85
        ) {
          let hit = true;

          if (obj.type === "barrier" && jumping && player.position.y > 0.9) {
            hit = false;
          }
          if (obj.type === "arch" && sliding) {
            hit = false;
          }

          if (hit) {
            endGame();
          }
        }
      }

      // Remove objects behind the player.
      if (obj.z > 18) {
        scene.remove(obj.mesh);
        objects.splice(i, 1);
      }
    }

    updateHUD();
  }

  // Camera follows player gently.
  camera.position.x += (player.position.x * 0.25 - camera.position.x) *
    Math.min(1, delta * 3);
  camera.lookAt(player.position.x * 0.15, 1.1, -8);

  renderer.render(scene, camera);
}

animate();

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

Game Source: Neon Dash 3D

Creator: AtomicBolt46

Libraries: three

Complexity: complex (655 lines, 15.6 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: neon-dash-3d-atomicbolt46" to link back to the original. Then publish at arcadelab.ai/publish.