Sky Hopper 3D
by AtomicBolt46524 lines11.2 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sky Hopper 3D</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
background: #75d5ff;
color: white;
}
canvas { display: block; }
#hud {
position: absolute;
top: 15px;
left: 15px;
display: flex;
gap: 10px;
z-index: 2;
}
.stat {
background: #102a49dd;
border: 2px solid #ffffff;
border-radius: 12px;
padding: 10px 15px;
font-size: 17px;
font-weight: bold;
}
#overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #061b32aa;
z-index: 5;
text-align: center;
}
.panel {
width: min(90%, 420px);
background: #12385b;
border: 3px solid #ffffff;
border-radius: 22px;
padding: 28px;
box-shadow: 0 0 30px #003c6a88;
}
h1 {
margin: 0 0 10px;
font-size: 38px;
color: #ffe66d;
}
p { line-height: 1.5; }
button {
padding: 14px 25px;
background: #ffe66d;
color: #172b42;
font-weight: bold;
border: 0;
border-radius: 12px;
font-size: 18px;
cursor: pointer;
}
button:hover { background: white; }
.hint {
font-size: 14px;
color: #d6edff;
}
#flapButton {
position: absolute;
bottom: 25px;
right: 25px;
z-index: 3;
width: 78px;
height: 78px;
border-radius: 50%;
font-size: 30px;
border: 3px solid white;
background: #ffe66ddd;
touch-action: none;
}
</style>
</head>
<body>
<div id="hud">
<div class="stat">⭐ Score: <span id="score">0</span></div>
<div class="stat">🏆 Best: <span id="best">0</span></div>
</div>
<div id="overlay">
<div class="panel">
<h1 id="heading">SKY HOPPER 3D</h1>
<p id="description">
Fly through the gaps between the pipes!
Don't hit the pipes or fall down.
</p>
<p class="hint">
SPACE / UP ARROW = FLAP<br>
MOUSE CLICK = FLAP<br>
MOBILE = TAP THE SCREEN
</p>
<button id="start">START GAME</button>
</div>
</div>
<button id="flapButton">↑</button>
<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";
// ---------------- SCENE ----------------
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x78d8ff);
scene.fog = new THREE.Fog(0x78d8ff, 25, 80);
const camera = new THREE.PerspectiveCamera(
60, innerWidth / innerHeight, 0.1, 150
);
camera.position.set(0, 0, 15);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff, 0x526c88, 2));
const sunlight = new THREE.DirectionalLight(0xffffff, 2);
sunlight.position.set(-5, 10, 12);
sunlight.castShadow = true;
scene.add(sunlight);
// ---------------- MATERIALS ----------------
const birdMat = new THREE.MeshStandardMaterial({
color: 0xffd32a,
roughness: 0.5
});
const orangeMat = new THREE.MeshStandardMaterial({
color: 0xff781f
});
const whiteMat = new THREE.MeshStandardMaterial({
color: 0xffffff
});
const blackMat = new THREE.MeshStandardMaterial({
color: 0x17202b
});
const pipeMat = new THREE.MeshStandardMaterial({
color: 0x18a94e,
roughness: 0.5
});
const pipeEdgeMat = new THREE.MeshStandardMaterial({
color: 0x087b36
});
// ---------------- SKY DECORATION ----------------
const clouds = [];
function makeCloud(x, y, z) {
const cloud = new THREE.Group();
for (let i = 0; i < 4; i++) {
const puff = new THREE.Mesh(
new THREE.SphereGeometry(0.65, 16, 12),
whiteMat
);
puff.position.set(i * 0.65 - 0.8, Math.sin(i) * 0.15, 0);
puff.scale.set(1.3, 0.8, 0.5);
cloud.add(puff);
}
cloud.position.set(x, y, z);
scene.add(cloud);
clouds.push(cloud);
}
for (let i = 0; i < 12; i++) {
makeCloud(
-18 + Math.random() * 40,
-1 + Math.random() * 9,
-8 - Math.random() * 5
);
}
// ---------------- GROUND ----------------
const ground = new THREE.Mesh(
new THREE.BoxGeometry(100, 1.2, 12),
new THREE.MeshStandardMaterial({ color: 0x5bbd45 })
);
ground.position.set(0, -7.2, 0);
ground.receiveShadow = true;
scene.add(ground);
const groundTop = new THREE.Mesh(
new THREE.BoxGeometry(100, 0.25, 12),
new THREE.MeshStandardMaterial({ color: 0x9cdb55 })
);
groundTop.position.set(0, -6.55, 0);
scene.add(groundTop);
// ---------------- BIRD ----------------
const bird = new THREE.Group();
scene.add(bird);
const body = new THREE.Mesh(
new THREE.SphereGeometry(0.55, 24, 18),
birdMat
);
body.scale.set(1.2, 0.9, 0.8);
body.castShadow = true;
bird.add(body);
// Wing
const wing = new THREE.Mesh(
new THREE.BoxGeometry(0.65, 0.16, 0.65),
orangeMat
);
wing.position.set(-0.25, -0.1, 0.45);
bird.add(wing);
// Beak
const beak = new THREE.Mesh(
new THREE.ConeGeometry(0.25, 0.65, 4),
orangeMat
);
beak.rotation.z = -Math.PI / 2;
beak.position.set(0.72, -0.03, 0);
bird.add(beak);
// Eye
const eyeWhite = new THREE.Mesh(
new THREE.SphereGeometry(0.14, 16, 12),
whiteMat
);
eyeWhite.position.set(0.32, 0.25, 0.53);
bird.add(eyeWhite);
const pupil = new THREE.Mesh(
new THREE.SphereGeometry(0.07, 12, 10),
blackMat
);
pupil.position.set(0.37, 0.25, 0.64);
bird.add(pupil);
// ---------------- PIPE CREATION ----------------
let pipes = [];
function makePipePart(x, y, height, isTop) {
const group = new THREE.Group();
const body = new THREE.Mesh(
new THREE.BoxGeometry(1.25, height, 1.5),
pipeMat
);
body.castShadow = true;
body.receiveShadow = true;
group.add(body);
const cap = new THREE.Mesh(
new THREE.BoxGeometry(1.55, 0.32, 1.7),
pipeEdgeMat
);
cap.position.y = isTop ? -height / 2 + 0.16 : height / 2 - 0.16;
group.add(cap);
group.position.set(x, y, 0);
scene.add(group);
return group;
}
function createPipePair(x) {
const gap = Math.max(3.3, 4.2 - score * 0.015);
const center = -0.2 + Math.random() * 2.2;
const topHeight = 13 - (center + gap / 2);
const bottomHeight = center - gap / 2 + 7;
const topY = center + gap / 2 + topHeight / 2;
const bottomY = -7 + bottomHeight / 2;
const top = makePipePart(x, topY, topHeight, true);
const bottom = makePipePart(x, bottomY, bottomHeight, false);
pipes.push({
top,
bottom,
x,
gapCenter: center,
gap,
scored: false
});
}
// ---------------- GAME STATE ----------------
let playing = false;
let score = 0;
let best = Number(localStorage.getItem("skyHopperBest") || 0);
let velocity = 0;
let pipeTimer = 0;
let worldSpeed = 6;
let elapsed = 0;
const scoreElement = document.getElementById("score");
const bestElement = document.getElementById("best");
const overlay = document.getElementById("overlay");
bestElement.textContent = best;
// ---------------- CONTROLS ----------------
function flap() {
if (!playing) return;
velocity = 8.5;
bird.rotation.z = 0.35;
}
document.addEventListener("keydown", (e) => {
if (
e.code === "Space" ||
e.code === "ArrowUp" ||
e.code === "KeyW"
) {
e.preventDefault();
if (!e.repeat) flap();
}
});
renderer.domElement.addEventListener("pointerdown", flap);
document.getElementById("flapButton").addEventListener(
"pointerdown",
(e) => {
e.preventDefault();
flap();
}
);
// ---------------- COLLISION ----------------
function checkCollision() {
const birdY = bird.position.y;
// Top and bottom boundaries
if (birdY > 6.2 || birdY < -6.2) {
return true;
}
const birdRadius = 0.48;
for (const pipe of pipes) {
if (Math.abs(pipe.x) < 1.0) {
const gapTop = pipe.gapCenter + pipe.gap / 2;
const gapBottom = pipe.gapCenter - pipe.gap / 2;
if (
birdY + birdRadius > gapTop ||
birdY - birdRadius < gapBottom
) {
return true;
}
}
}
return false;
}
// ---------------- GAME OVER ----------------
function endGame() {
playing = false;
if (score > best) {
best = score;
localStorage.setItem("skyHopperBest", best);
}
bestElement.textContent = best;
document.getElementById("heading").textContent = "GAME OVER!";
document.getElementById("description").innerHTML =
"⭐ Your score: " + score +
"<br>🏆 Best score: " + best +
"<br><br>Can you fly further next time?";
document.getElementById("start").textContent = "PLAY AGAIN";
overlay.style.display = "flex";
}
// ---------------- START GAME ----------------
function startGame() {
pipes.forEach(pipe => {
scene.remove(pipe.top);
scene.remove(pipe.bottom);
});
pipes = [];
score = 0;
velocity = 0;
pipeTimer = 0;
worldSpeed = 6;
elapsed = 0;
bird.position.set(-3, 0, 0);
bird.rotation.set(0, 0, 0);
scoreElement.textContent = score;
playing = true;
document.getElementById("heading").textContent = "SKY HOPPER 3D";
document.getElementById("description").textContent =
"Fly through the gaps between the pipes!";
document.getElementById("start").textContent = "START GAME";
overlay.style.display = "none";
createPipePair(13);
}
// ---------------- BUTTON ----------------
document.getElementById("start").addEventListener("click", startGame);
// ---------------- ANIMATION ----------------
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.04);
if (playing) {
elapsed += dt;
// Gravity
velocity -= 18 * dt;
bird.position.y += velocity * dt;
// Tilt bird based on movement
bird.rotation.z += (
Math.max(-0.7, Math.min(0.5, velocity * 0.06)) -
bird.rotation.z
) * Math.min(1, dt * 6);
// Wing animation
wing.rotation.z = Math.sin(elapsed * 18) * 0.5;
// Gradually increase speed
worldSpeed = Math.min(12, 6 + score * 0.07);
// Move pipes
pipeTimer += dt;
if (pipeTimer > Math.max(1.45, 2.4 - score * 0.008)) {
pipeTimer = 0;
createPipePair(13);
}
for (let i = pipes.length - 1; i >= 0; i--) {
const pipe = pipes[i];
pipe.x -= worldSpeed * dt;
pipe.top.position.x = pipe.x;
pipe.bottom.position.x = pipe.x;
// Score when passing a pipe
if (!pipe.scored && pipe.x < -3) {
pipe.scored = true;
score++;
scoreElement.textContent = score;
}
// Remove pipes behind the bird
if (pipe.x < -18) {
scene.remove(pipe.top);
scene.remove(pipe.bottom);
pipes.splice(i, 1);
}
}
// Move clouds
for (const cloud of clouds) {
cloud.position.x -= worldSpeed * dt * 0.18;
if (cloud.position.x < -23) {
cloud.position.x = 23;
}
}
if (checkCollision()) {
endGame();
}
} else {
// Gentle idle animation
if (pipes.length === 0) {
bird.position.y = Math.sin(clock.elapsedTime * 2) * 0.2;
wing.rotation.z = Math.sin(clock.elapsedTime * 5) * 0.25;
}
}
renderer.render(scene, camera);
}
animate();
// ---------------- RESIZE ----------------
window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>Game Source: Sky Hopper 3D
Creator: AtomicBolt46
Libraries: three
Complexity: complex (524 lines, 11.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: sky-hopper-3d-atomicbolt46" to link back to the original. Then publish at arcadelab.ai/publish.