<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Plane Flyer Game - Enhanced Scenery</title>
<style>
body {
margin: 0;
overflow: hidden; /* Prevent scrollbars */
background-color: #87CEEB; /* Sky Blue */
/* Prevent scrolling and zooming on touch devices */
touch-action: none;
overscroll-behavior: none;
}
canvas { display: block; }
#info {
position: absolute;
top: 10px;
width: 100%;
text-align: center;
z-index: 100;
display:block;
color: white;
font-family: Arial, sans-serif;
font-size: 24px;
text-shadow: 1px 1px 2px black;
}
#instructions {
position: absolute;
bottom: 10px;
width: 100%;
text-align: center;
z-index: 100;
display:block;
color: white;
font-family: Arial, sans-serif;
font-size: 16px;
text-shadow: 1px 1px 2px black;
}
#settings {
position: absolute;
top: 50px;
right: 20px;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
z-index: 100;
color: white;
font-family: Arial, sans-serif;
text-shadow: 1px 1px 2px black;
}
#settings label {
display: block;
margin-bottom: 5px;
}
#settings input {
width: 100%;
}
</style>
</head>
<body>
<div id="info">Score: 0</div>
<div id="settings">
<label for="touchSensitivity">Touch Sensitivity: <span id="sensitivityValue">5</span></label>
<input type="range" id="touchSensitivity" min="1" max="10" value="5">
</div>
<div id="instructions">Use Arrow Keys (Up, Down, Left, Right) to fly. On mobile, touch and drag to control the plane.</div>
<canvas id="gameCanvas"></canvas>
<!-- Load Three.js Library -->
<script>
// --- Basic Setup ---
let scene, camera, renderer;
let plane, clock, rings = [], mountains = [], skyscrapers = [];
let score = 0;
let scoreElement = document.getElementById('info');
let planeSpeed = 0.6; // Slightly increased speed for scenery effect
let planeTurnSpeed = 0.03;
// Input state
const keys = {
ArrowUp: false,
ArrowDown: false,
ArrowLeft: false,
ArrowRight: false
};
// Touch controls
let touchStartX = 0;
let touchStartY = 0;
let isTouching = false;
let touchDeltaX = 0;
let touchDeltaY = 0;
let touchSensitivity = 5; // Default sensitivity (1-10 scale)
// --- Initialization ---
function init() {
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB); // Sky blue background
scene.fog = new THREE.Fog(0x87CEEB, 20, 150); // Increase fog slightly for distance effect
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.5, 5);
camera.lookAt(0, 0, 0);
// Renderer
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('gameCanvas'), antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); // Slightly brighter ambient
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9); // Slightly stronger sun
directionalLight.position.set(10, 15, 10); // Adjust sun angle
scene.add(directionalLight);
// Clock
clock = new THREE.Clock();
// Create Game Objects
createPlane();
spawnInitialRings(10);
spawnInitialScenery(15, 'skyscraper'); // Add 15 skyscrapers
spawnInitialScenery(10, 'mountain'); // Add 10 mountains
// Event Listeners
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener('resize', onWindowResize);
// Touch event listeners
renderer.domElement.addEventListener('touchstart', handleTouchStart, { passive: false });
renderer.domElement.addEventListener('touchmove', handleTouchMove, { passive: false });
renderer.domElement.addEventListener('touchend', handleTouchEnd, { passive: false });
// Prevent default scrolling on the whole document for touch moves
document.addEventListener('touchmove', function(event) {
event.preventDefault();
}, { passive: false });
// Touch sensitivity slider
const sensitivitySlider = document.getElementById('touchSensitivity');
const sensitivityValue = document.getElementById('sensitivityValue');
sensitivitySlider.addEventListener('input', function() {
touchSensitivity = parseInt(this.value);
sensitivityValue.textContent = touchSensitivity;
});
// Start Animation Loop
animate();
}
// --- Game Objects ---
function createPlane() {
// (Same as before)
const planeGroup = new THREE.Group();
const bodyGeometry = new THREE.ConeGeometry(0.3, 1, 8);
const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, flatShading: true }); // Flat shading for low-poly look
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.rotation.x = Math.PI / 2;
planeGroup.add(body);
const wingGeometry = new THREE.BoxGeometry(1.5, 0.1, 0.5);
const wingMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc, flatShading: true });
const leftWing = new THREE.Mesh(wingGeometry, wingMaterial);
leftWing.position.set(-0.75, 0, 0);
planeGroup.add(leftWing);
const rightWing = new THREE.Mesh(wingGeometry, wingMaterial);
rightWing.position.set(0.75, 0, 0);
planeGroup.add(rightWing);
const tailGeometry = new THREE.BoxGeometry(0.1, 0.5, 0.3);
const tail = new THREE.Mesh(tailGeometry, wingMaterial);
tail.position.set(0, 0.25, -0.4);
planeGroup.add(tail);
plane = planeGroup;
plane.position.set(0, 0, 0);
scene.add(plane);
}
function createRing(zPosition) {
// (Same as before)
const ringGeometry = new THREE.TorusGeometry(1.5, 0.2, 16, 50);
const ringMaterial = new THREE.MeshStandardMaterial({ color: 0xFFD700, emissive: 0xccad00 });
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
ring.position.x = (Math.random() - 0.5) * 15;
ring.position.y = (Math.random() - 0.5) * 10 + 2;
ring.position.z = zPosition;
ring.userData = { passed: false };
rings.push(ring);
scene.add(ring);
}
function createSceneryObject(type, zPosition) {
let object;
const groundLevel = -5; // How low the base of the scenery sits
if (type === 'skyscraper') {
const height = Math.random() * 15 + 10; // Random height between 10 and 25
const width = Math.random() * 2 + 1; // Random width between 1 and 3
const depth = Math.random() * 2 + 1; // Random depth between 1 and 3
const geometry = new THREE.BoxGeometry(width, height, depth);
// Simple grey material, slightly varying shade
const color = new THREE.Color(0x606060 + Math.random() * 0x202020);
const material = new THREE.MeshStandardMaterial({ color: color, flatShading: true });
object = new THREE.Mesh(geometry, material);
object.position.y = groundLevel + height / 2; // Position base at ground level
// Place further out horizontally
object.position.x = (Math.random() < 0.5 ? -1 : 1) * (Math.random() * 15 + 15); // 15 to 30 units left or right
} else if (type === 'mountain') {
const radius = Math.random() * 5 + 5; // Base radius 5 to 10
const height = Math.random() * 10 + 8; // Height 8 to 18
const geometry = new THREE.ConeGeometry(radius, height, 8); // Low segment count for blocky look
// Simple brown/grey material, varying shade
const color = new THREE.Color(Math.random() < 0.6 ? 0x8B4513 : 0x696969).multiplyScalar(0.5 + Math.random() * 0.5); // Brown or Greyish
const material = new THREE.MeshStandardMaterial({ color: color, flatShading: true });
object = new THREE.Mesh(geometry, material);
object.position.y = groundLevel; // Base of cone sits at ground level
// Place even further out
object.position.x = (Math.random() < 0.5 ? -1 : 1) * (Math.random() * 20 + 30); // 30 to 50 units left or right
}
if (object) {
object.position.z = zPosition;
object.userData = { type: type }; // Store type for potential differentiation later
if (type === 'skyscraper') skyscrapers.push(object);
if (type === 'mountain') mountains.push(object);
scene.add(object);
}
return object; // Return the created object for recycling logic
}
function getFurthestZ(objectArrays) {
let furthestZ = 0;
objectArrays.forEach(arr => {
arr.forEach(obj => {
if (obj.position.z < furthestZ) {
furthestZ = obj.position.z;
}
});
});
return furthestZ;
}
function spawnInitialRings(count) {
let currentZ = -10; // Starting Z position
for (let i = 0; i < count; i++) {
createRing(currentZ);
currentZ -= (Math.random() * 10 + 10); // Space rings out randomly (10-20 units apart)
}
}
function spawnInitialScenery(count, type) {
let currentZ = -20; // Start scenery further back
const spacing = type === 'mountain' ? 30 : 15; // Space mountains further apart
for (let i = 0; i < count; i++) {
createSceneryObject(type, currentZ);
currentZ -= (Math.random() * spacing + spacing / 2);
}
}
// --- Game Logic ---
function updatePlaneMovement(deltaTime) {
const moveSpeed = 5 * deltaTime;
const turnAmount = planeTurnSpeed;
// Handle keyboard input
if (keys.ArrowUp) plane.position.y += moveSpeed;
if (keys.ArrowDown) plane.position.y -= moveSpeed;
if (keys.ArrowLeft) plane.position.x -= moveSpeed;
if (keys.ArrowRight) plane.position.x += moveSpeed;
// Handle touch input
if (isTouching) {
const sensitivityFactor = touchSensitivity / 5; // Convert 1-10 scale to a multiplier (1 = 0.2x, 5 = 1x, 10 = 2x)
plane.position.x += touchDeltaX * moveSpeed * 0.1 * sensitivityFactor;
plane.position.y += touchDeltaY * moveSpeed * 0.1 * sensitivityFactor;
}
plane.rotation.z = 0;
plane.rotation.y = 0;
plane.rotation.x = 0; // Reset pitch initially
// Apply rotations based on keyboard
if (keys.ArrowLeft) plane.rotation.z = turnAmount;
if (keys.ArrowRight) plane.rotation.z = -turnAmount;
if (keys.ArrowUp) plane.rotation.x = -turnAmount * 0.5;
else if (keys.ArrowDown) plane.rotation.x = turnAmount * 0.5;
// Apply rotations based on touch
if (isTouching) {
const sensitivityFactor = touchSensitivity / 5; // Convert 1-10 scale to a multiplier
plane.rotation.z = -touchDeltaX * turnAmount * 0.02 * sensitivityFactor;
plane.rotation.x = touchDeltaY * turnAmount * 0.02 * sensitivityFactor;
}
plane.position.x = Math.max(-12, Math.min(12, plane.position.x)); // Slightly wider bounds
plane.position.y = Math.max(-6, Math.min(10, plane.position.y));
}
function updateRings(deltaTime, moveDistance) {
const furthestZ = getFurthestZ([rings, skyscrapers, mountains]); // Consider all objects for Z placement
const ringSpacing = 15; // Average spacing
for (let i = rings.length - 1; i >= 0; i--) {
const ring = rings[i];
ring.position.z += moveDistance;
// Scoring Check (same as before)
if (!ring.userData.passed && ring.position.z > plane.position.z) {
const distanceX = Math.abs(plane.position.x - ring.position.x);
const distanceY = Math.abs(plane.position.y - ring.position.y);
const ringRadius = 1.5;
if (distanceX < ringRadius && distanceY < ringRadius) {
score++;
scoreElement.innerText = `Score: ${score}`;
ring.userData.passed = true;
ring.material.color.set(0x00ff00);
ring.material.emissive.set(0x00cc00);
} else {
ring.userData.passed = true;
ring.material.color.set(0x888888);
ring.material.emissive.set(0x333333);
}
}
// Recycle Ring
if (ring.position.z > camera.position.z + 10) {
ring.position.x = (Math.random() - 0.5) * 15;
ring.position.y = (Math.random() - 0.5) * 10 + 2;
ring.position.z = furthestZ - (Math.random() * 10 + ringSpacing/1.5); // Place behind furthest object, add random spacing
ring.userData.passed = false;
ring.material.color.set(0xFFD700);
ring.material.emissive.set(0xccad00);
}
}
}
function updateScenery(sceneryArray, deltaTime, moveDistance) {
const furthestZ = getFurthestZ([rings, skyscrapers, mountains]);
const recycleDistance = camera.position.z + 30; // Recycle when further behind camera
const baseSpacing = sceneryArray === mountains ? 30 : 15; // Use appropriate spacing
for (let i = sceneryArray.length - 1; i >= 0; i--) {
const sceneryObject = sceneryArray[i];
sceneryObject.position.z += moveDistance;
// Recycle Scenery Object
if (sceneryObject.position.z > recycleDistance) {
const type = sceneryObject.userData.type;
const groundLevel = -5;
// Reposition based on type
if (type === 'skyscraper') {
const height = Math.random() * 15 + 10;
sceneryObject.geometry = new THREE.BoxGeometry(Math.random() * 2 + 1, height, Math.random() * 2 + 1); // Regenerate geom for size variation
sceneryObject.position.y = groundLevel + height / 2;
sceneryObject.position.x = (Math.random() < 0.5 ? -1 : 1) * (Math.random() * 15 + 15); // 15 to 30 units left or right
} else if (type === 'mountain') {
const radius = Math.random() * 5 + 5;
const height = Math.random() * 10 + 8;
sceneryObject.geometry = new THREE.ConeGeometry(radius, height, 8); // Regenerate geom
sceneryObject.position.y = groundLevel; // Base at ground
sceneryObject.position.x = (Math.random() < 0.5 ? -1 : 1) * (Math.random() * 20 + 30); // 30 to 50 units left or right
}
// Common repositioning
sceneryObject.position.z = furthestZ - (Math.random() * baseSpacing + baseSpacing / 2); // Place far behind, add random spacing
}
}
}
function updateCamera() {
// (Same as before)
camera.position.x = plane.position.x * 0.2;
camera.position.y = plane.position.y * 0.5 + 1.5;
camera.position.z = plane.position.z + 5;
const lookAtPosition = plane.position.clone();
lookAtPosition.z -= 10;
camera.lookAt(lookAtPosition);
}
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
const moveDistance = planeSpeed * 60 * deltaTime; // Consistent movement speed
updatePlaneMovement(deltaTime);
updateRings(deltaTime, moveDistance);
updateScenery(skyscrapers, deltaTime, moveDistance); // Update skyscrapers
updateScenery(mountains, deltaTime, moveDistance); // Update mountains
updateCamera();
renderer.render(scene, camera);
}
// --- Event Handlers ---
function handleKeyDown(event) {
// (Same as before)
if (keys.hasOwnProperty(event.key)) {
keys[event.key] = true;
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)) {
event.preventDefault();
}
}
}
function handleKeyUp(event) {
// (Same as before)
if (keys.hasOwnProperty(event.key)) {
keys[event.key] = false;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Touch event handlers
function handleTouchStart(event) {
event.preventDefault();
isTouching = true;
if (event.touches.length > 0) {
touchStartX = event.touches[0].clientX;
touchStartY = event.touches[0].clientY;
touchDeltaX = 0;
touchDeltaY = 0;
}
}
function handleTouchMove(event) {
event.preventDefault();
if (event.touches.length > 0 && isTouching) {
const touchX = event.touches[0].clientX;
const touchY = event.touches[0].clientY;
// Calculate delta from start position
touchDeltaX = touchX - touchStartX;
touchDeltaY = touchStartY - touchY; // Invert Y for natural control
// Update start position for smoother control
touchStartX = touchX;
touchStartY = touchY;
}
}
function handleTouchEnd(event) {
event.preventDefault();
isTouching = false;
touchDeltaX = 0;
touchDeltaY = 0;
}
// --- Start the game ---
init();
</script>
</body>
</html>