<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>3D 过马路游戏</title> <style> @import url('https://fonts.googleapis.com/css?family=Press+Start+2P'); body { margin: 0; display: flex; font-family: 'Press Start 2P', cursive; } #controls { position: absolute; bottom: 20px; min-width: 100%; display: flex; align-items: flex-end; justify-content: center; } #controls div { display: grid; grid-template-columns: 50px 50px 50px; gap: 10px; } #controls button { width: 100%; height: 40px; background-color: white; border: 1px solid lightgray; box-shadow: 3px 5px 0px 0px rgba(0, 0, 0, 0.75); cursor: pointer; outline: none; } #controls button:first-of-type { grid-column: 1/-1; } #score { position: absolute; top: 20px; left: 20px; font-size: 2em; color: white; } #result-container { position: absolute; min-width: 100%; min-height: 100%; top: 0; display: flex; align-items: center; justify-content: center; visibility: hidden; #result { display: flex; flex-direction: column; align-items: center; background-color: white; padding: 20px; } button { background-color: red; padding: 20px 50px 20px 50px; font-family: inherit; font-size: inherit; cursor: pointer; } } #youtube, #youtube-card { display: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: black; } @media (min-height: 425px) { /** Youtube logo by https://codepen.io/alvaromontoro */ #youtube { z-index: 50; width: 100px; display: block; height: 70px; position: fixed; bottom: 20px; right: 20px; background: red; border-radius: 50% / 11%; transform: scale(0.8); transition: transform 0.5s; } #youtube:hover, #youtube:focus { transform: scale(0.9); color: black; } #youtube::before { content: ''; display: block; position: absolute; top: 7.5%; left: -6%; width: 112%; height: 85%; background: red; border-radius: 9% / 50%; } #youtube::after { content: ''; display: block; position: absolute; top: 20px; left: 40px; width: 45px; height: 30px; border: 15px solid transparent; box-sizing: border-box; border-left: 30px solid white; } #youtube span { font-size: 0; position: absolute; width: 0; height: 0; overflow: hidden; } #youtube:hover + #youtube-card { z-index: 49; display: block; position: fixed; bottom: 12px; right: 10px; padding: 25px 130px 25px 25px; width: 300px; background-color: white; } } </style> </head> <body> <canvas></canvas> <div id="controls"> <div> <button id="forward">▲</button> <button id="left">◀</button> <button id="backward">▼</button> <button id="right">▶</button> </div> </div> <div id="score">0</div> <div id="result-container"> <div id="result"> <h1>Game Over</h1> <p>Your score: <span id="final-score"></span></p> <button id="retry">Retry</button> </div> </div> <script type="module"> import * as THREE from 'https://esm.sh/three'; const minTileIndex = -8; const maxTileIndex = 8; const tilesPerRow = maxTileIndex - minTileIndex + 1; const tileSize = 42; function Camera() { const size = 300; const viewRatio = window.innerWidth / window.innerHeight; const width = viewRatio < 1 ? size : size * viewRatio; const height = viewRatio < 1 ? size / viewRatio : size; const camera = new THREE.OrthographicCamera( width / -2, // left width / 2, // right height / 2, // top height / -2, // bottom 100, // near 900 // far ); camera.up.set(0, 0, 1); camera.position.set(300, -300, 300); camera.lookAt(0, 0, 0); return camera; } function Texture(width, height, rects) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d'); context.fillStyle = '#ffffff'; context.fillRect(0, 0, width, height); context.fillStyle = 'rgba(0,0,0,0.6)'; rects.forEach((rect) => { context.fillRect(rect.x, rect.y, rect.w, rect.h); }); return new THREE.CanvasTexture(canvas); } const carFrontTexture = new Texture(40, 80, [ { x: 0, y: 10, w: 30, h: 60 }, ]); const carBackTexture = new Texture(40, 80, [ { x: 10, y: 10, w: 30, h: 60 }, ]); const carRightSideTexture = new Texture(110, 40, [ { x: 10, y: 0, w: 50, h: 30 }, { x: 70, y: 0, w: 30, h: 30 }, ]); const carLeftSideTexture = new Texture(110, 40, [ { x: 10, y: 10, w: 50, h: 30 }, { x: 70, y: 10, w: 30, h: 30 }, ]); export const truckFrontTexture = Texture(30, 30, [ { x: 5, y: 0, w: 10, h: 30 }, ]); export const truckRightSideTexture = Texture(25, 30, [ { x: 15, y: 5, w: 10, h: 10 }, ]); export const truckLeftSideTexture = Texture(25, 30, [ { x: 15, y: 15, w: 10, h: 10 }, ]); function Car(initialTileIndex, direction, color) { const car = new THREE.Group(); car.position.x = initialTileIndex * tileSize; if (!direction) car.rotation.z = Math.PI; const main = new THREE.Mesh( new THREE.BoxGeometry(60, 30, 15), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); main.position.z = 12; main.castShadow = true; main.receiveShadow = true; car.add(main); const cabin = new THREE.Mesh(new THREE.BoxGeometry(33, 24, 12), [ new THREE.MeshPhongMaterial({ color: 0xcccccc, flatShading: true, map: carBackTexture, }), new THREE.MeshPhongMaterial({ color: 0xcccccc, flatShading: true, map: carFrontTexture, }), new THREE.MeshPhongMaterial({ color: 0xcccccc, flatShading: true, map: carRightSideTexture, }), new THREE.MeshPhongMaterial({ color: 0xcccccc, flatShading: true, map: carLeftSideTexture, }), new THREE.MeshPhongMaterial({ color: 0xcccccc, flatShading: true }), // top new THREE.MeshPhongMaterial({ color: 0xcccccc, flatShading: true }), // bottom ]); cabin.position.x = -6; cabin.position.z = 25.5; cabin.castShadow = true; cabin.receiveShadow = true; car.add(cabin); const frontWheel = Wheel(18); car.add(frontWheel); const backWheel = Wheel(-18); car.add(backWheel); return car; } function DirectionalLight() { const dirLight = new THREE.DirectionalLight(); dirLight.position.set(-100, -100, 200); dirLight.up.set(0, 0, 1); dirLight.castShadow = true; dirLight.shadow.mapSize.width = 2048; dirLight.shadow.mapSize.height = 2048; dirLight.shadow.camera.up.set(0, 0, 1); dirLight.shadow.camera.left = -400; dirLight.shadow.camera.right = 400; dirLight.shadow.camera.top = 400; dirLight.shadow.camera.bottom = -400; dirLight.shadow.camera.near = 50; dirLight.shadow.camera.far = 400; return dirLight; } function Grass(rowIndex) { const grass = new THREE.Group(); grass.position.y = rowIndex * tileSize; const createSection = (color) => new THREE.Mesh( new THREE.BoxGeometry(tilesPerRow * tileSize, tileSize, 3), new THREE.MeshLambertMaterial({ color }) ); const middle = createSection(0xbaf455); middle.receiveShadow = true; grass.add(middle); const left = createSection(0x99c846); left.position.x = -tilesPerRow * tileSize; grass.add(left); const right = createSection(0x99c846); right.position.x = tilesPerRow * tileSize; grass.add(right); return grass; } const metadata = []; const map = new THREE.Group(); function initializeMap() { // Remove all rows metadata.length = 0; map.remove(...map.children); // Add new rows for (let rowIndex = 0; rowIndex > -10; rowIndex--) { const grass = Grass(rowIndex); map.add(grass); } addRows(); } function addRows() { const newMetadata = generateRows(20); const startIndex = metadata.length; metadata.push(...newMetadata); newMetadata.forEach((rowData, index) => { const rowIndex = startIndex + index + 1; if (rowData.type === 'forest') { const row = Grass(rowIndex); rowData.trees.forEach(({ tileIndex, height }) => { const three = Tree(tileIndex, height); row.add(three); }); map.add(row); } if (rowData.type === 'car') { const row = Road(rowIndex); rowData.vehicles.forEach((vehicle) => { const car = Car( vehicle.initialTileIndex, rowData.direction, vehicle.color ); vehicle.ref = car; row.add(car); }); map.add(row); } if (rowData.type === 'truck') { const row = Road(rowIndex); rowData.vehicles.forEach((vehicle) => { const truck = Truck( vehicle.initialTileIndex, rowData.direction, vehicle.color ); vehicle.ref = truck; row.add(truck); }); map.add(row); } }); } const player = Player(); function Player() { const player = new THREE.Group(); const body = new THREE.Mesh( new THREE.BoxGeometry(15, 15, 20), new THREE.MeshLambertMaterial({ color: 'white', flatShading: true, }) ); body.position.z = 10; body.castShadow = true; body.receiveShadow = true; player.add(body); const cap = new THREE.Mesh( new THREE.BoxGeometry(2, 4, 2), new THREE.MeshLambertMaterial({ color: 0xf0619a, flatShading: true, }) ); cap.position.z = 21; cap.castShadow = true; cap.receiveShadow = true; player.add(cap); const playerContainer = new THREE.Group(); playerContainer.add(player); return playerContainer; } const position = { currentRow: 0, currentTile: 0, }; const movesQueue = []; function initializePlayer() { // Initialize the Three.js player object player.position.x = 0; player.position.y = 0; player.children[0].position.z = 0; // Initialize metadata position.currentRow = 0; position.currentTile = 0; // Clear the moves queue movesQueue.length = 0; } function queueMove(direction) { const isValidMove = endsUpInValidPosition( { rowIndex: position.currentRow, tileIndex: position.currentTile, }, [...movesQueue, direction] ); if (!isValidMove) return; movesQueue.push(direction); } function stepCompleted() { const direction = movesQueue.shift(); if (direction === 'forward') position.currentRow += 1; if (direction === 'backward') position.currentRow -= 1; if (direction === 'left') position.currentTile -= 1; if (direction === 'right') position.currentTile += 1; // Add new rows if the player is running out of them if (position.currentRow > metadata.length - 10) addRows(); const scoreDOM = document.getElementById('score'); if (scoreDOM) scoreDOM.innerText = position.currentRow.toString(); } function Renderer() { const canvas = document.querySelector('canvas.game'); if (!canvas) throw new Error('Canvas not found'); const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, canvas: canvas, }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; return renderer; } function Road(rowIndex) { const road = new THREE.Group(); road.position.y = rowIndex * tileSize; const createSection = (color) => new THREE.Mesh( new THREE.PlaneGeometry(tilesPerRow * tileSize, tileSize), new THREE.MeshLambertMaterial({ color }) ); const middle = createSection(0x454a59); middle.receiveShadow = true; road.add(middle); const left = createSection(0x393d49); left.position.x = -tilesPerRow * tileSize; road.add(left); const right = createSection(0x393d49); right.position.x = tilesPerRow * tileSize; road.add(right); return road; } function Tree(tileIndex, height) { const tree = new THREE.Group(); tree.position.x = tileIndex * tileSize; const trunk = new THREE.Mesh( new THREE.BoxGeometry(15, 15, 20), new THREE.MeshLambertMaterial({ color: 0x4d2926, flatShading: true, }) ); trunk.position.z = 10; tree.add(trunk); const crown = new THREE.Mesh( new THREE.BoxGeometry(30, 30, height), new THREE.MeshLambertMaterial({ color: 0x7aa21d, flatShading: true, }) ); crown.position.z = height / 2 + 20; crown.castShadow = true; crown.receiveShadow = true; tree.add(crown); return tree; } function Truck(initialTileIndex, direction, color) { const truck = new THREE.Group(); truck.position.x = initialTileIndex * tileSize; if (!direction) truck.rotation.z = Math.PI; const cargo = new THREE.Mesh( new THREE.BoxGeometry(70, 35, 35), new THREE.MeshLambertMaterial({ color: 0xb4c6fc, flatShading: true, }) ); cargo.position.x = -15; cargo.position.z = 25; cargo.castShadow = true; cargo.receiveShadow = true; truck.add(cargo); const cabin = new THREE.Mesh(new THREE.BoxGeometry(30, 30, 30), [ new THREE.MeshLambertMaterial({ color, flatShading: true, map: truckFrontTexture, }), // front new THREE.MeshLambertMaterial({ color, flatShading: true, }), // back new THREE.MeshLambertMaterial({ color, flatShading: true, map: truckLeftSideTexture, }), new THREE.MeshLambertMaterial({ color, flatShading: true, map: truckRightSideTexture, }), new THREE.MeshPhongMaterial({ color, flatShading: true }), // top new THREE.MeshPhongMaterial({ color, flatShading: true }), // bottom ]); cabin.position.x = 35; cabin.position.z = 20; cabin.castShadow = true; cabin.receiveShadow = true; truck.add(cabin); const frontWheel = Wheel(37); truck.add(frontWheel); const middleWheel = Wheel(5); truck.add(middleWheel); const backWheel = Wheel(-35); truck.add(backWheel); return truck; } function Wheel(x) { const wheel = new THREE.Mesh( new THREE.BoxGeometry(12, 33, 12), new THREE.MeshLambertMaterial({ color: 0x333333, flatShading: true, }) ); wheel.position.x = x; wheel.position.z = 6; return wheel; } function calculateFinalPosition(currentPosition, moves) { return moves.reduce((position, direction) => { if (direction === 'forward') return { rowIndex: position.rowIndex + 1, tileIndex: position.tileIndex, }; if (direction === 'backward') return { rowIndex: position.rowIndex - 1, tileIndex: position.tileIndex, }; if (direction === 'left') return { rowIndex: position.rowIndex, tileIndex: position.tileIndex - 1, }; if (direction === 'right') return { rowIndex: position.rowIndex, tileIndex: position.tileIndex + 1, }; return position; }, currentPosition); } function endsUpInValidPosition(currentPosition, moves) { // Calculate where the player would end up after the move const finalPosition = calculateFinalPosition(currentPosition, moves); // Detect if we hit the edge of the board if ( finalPosition.rowIndex === -1 || finalPosition.tileIndex === minTileIndex - 1 || finalPosition.tileIndex === maxTileIndex + 1 ) { // Invalid move, ignore move command return false; } // Detect if we hit a tree const finalRow = metadata[finalPosition.rowIndex - 1]; if ( finalRow && finalRow.type === 'forest' && finalRow.trees.some( (tree) => tree.tileIndex === finalPosition.tileIndex ) ) { // Invalid move, ignore move command return false; } return true; } function generateRows(amount) { const rows = []; for (let i = 0; i < amount; i++) { const rowData = generateRow(); rows.push(rowData); } return rows; } function generateRow() { const type = randomElement(['car', 'truck', 'forest']); if (type === 'car') return generateCarLaneMetadata(); if (type === 'truck') return generateTruckLaneMetadata(); return generateForesMetadata(); } function randomElement(array) { return array[Math.floor(Math.random() * array.length)]; } function generateForesMetadata() { const occupiedTiles = new Set(); const trees = Array.from({ length: 4 }, () => { let tileIndex; do { tileIndex = THREE.MathUtils.randInt(minTileIndex, maxTileIndex); } while (occupiedTiles.has(tileIndex)); occupiedTiles.add(tileIndex); const height = randomElement([20, 45, 60]); return { tileIndex, height }; }); return { type: 'forest', trees }; } function generateCarLaneMetadata() { const direction = randomElement([true, false]); const speed = randomElement([125, 156, 188]); const occupiedTiles = new Set(); const vehicles = Array.from({ length: 3 }, () => { let initialTileIndex; do { initialTileIndex = THREE.MathUtils.randInt( minTileIndex, maxTileIndex ); } while (occupiedTiles.has(initialTileIndex)); occupiedTiles.add(initialTileIndex - 1); occupiedTiles.add(initialTileIndex); occupiedTiles.add(initialTileIndex + 1); const color = randomElement([0xa52523, 0xbdb638, 0x78b14b]); return { initialTileIndex, color }; }); return { type: 'car', direction, speed, vehicles }; } function generateTruckLaneMetadata() { const direction = randomElement([true, false]); const speed = randomElement([125, 156, 188]); const occupiedTiles = new Set(); const vehicles = Array.from({ length: 2 }, () => { let initialTileIndex; do { initialTileIndex = THREE.MathUtils.randInt( minTileIndex, maxTileIndex ); } while (occupiedTiles.has(initialTileIndex)); occupiedTiles.add(initialTileIndex - 2); occupiedTiles.add(initialTileIndex - 1); occupiedTiles.add(initialTileIndex); occupiedTiles.add(initialTileIndex + 1); occupiedTiles.add(initialTileIndex + 2); const color = randomElement([0xa52523, 0xbdb638, 0x78b14b]); return { initialTileIndex, color }; }); return { type: 'truck', direction, speed, vehicles }; } const moveClock = new THREE.Clock(false); function animatePlayer() { if (!movesQueue.length) return; if (!moveClock.running) moveClock.start(); const stepTime = 0.2; // Seconds it takes to take a step const progress = Math.min(1, moveClock.getElapsedTime() / stepTime); setPosition(progress); setRotation(progress); // Once a step has ended if (progress >= 1) { stepCompleted(); moveClock.stop(); } } function setPosition(progress) { const startX = position.currentTile * tileSize; const startY = position.currentRow * tileSize; let endX = startX; let endY = startY; if (movesQueue[0] === 'left') endX -= tileSize; if (movesQueue[0] === 'right') endX += tileSize; if (movesQueue[0] === 'forward') endY += tileSize; if (movesQueue[0] === 'backward') endY -= tileSize; player.position.x = THREE.MathUtils.lerp(startX, endX, progress); player.position.y = THREE.MathUtils.lerp(startY, endY, progress); player.children[0].position.z = Math.sin(progress * Math.PI) * 8; } function setRotation(progress) { let endRotation = 0; if (movesQueue[0] == 'forward') endRotation = 0; if (movesQueue[0] == 'left') endRotation = Math.PI / 2; if (movesQueue[0] == 'right') endRotation = -Math.PI / 2; if (movesQueue[0] == 'backward') endRotation = Math.PI; player.children[0].rotation.z = THREE.MathUtils.lerp( player.children[0].rotation.z, endRotation, progress ); } const clock = new THREE.Clock(); function animateVehicles() { const delta = clock.getDelta(); // Animate cars and trucks metadata.forEach((rowData) => { if (rowData.type === 'car' || rowData.type === 'truck') { const beginningOfRow = (minTileIndex - 2) * tileSize; const endOfRow = (maxTileIndex + 2) * tileSize; rowData.vehicles.forEach(({ ref }) => { if (!ref) throw Error('Vehicle reference is missing'); if (rowData.direction) { ref.position.x = ref.position.x > endOfRow ? beginningOfRow : ref.position.x + rowData.speed * delta; } else { ref.position.x = ref.position.x < beginningOfRow ? endOfRow : ref.position.x - rowData.speed * delta; } }); } }); } document .getElementById('forward') ?.addEventListener('click', () => queueMove('forward')); document .getElementById('backward') ?.addEventListener('click', () => queueMove('backward')); document .getElementById('left') ?.addEventListener('click', () => queueMove('left')); document .getElementById('right') ?.addEventListener('click', () => queueMove('right')); window.addEventListener('keydown', (event) => { if (event.key === 'ArrowUp') { event.preventDefault(); // Avoid scrolling the page queueMove('forward'); } else if (event.key === 'ArrowDown') { event.preventDefault(); // Avoid scrolling the page queueMove('backward'); } else if (event.key === 'ArrowLeft') { event.preventDefault(); // Avoid scrolling the page queueMove('left'); } else if (event.key === 'ArrowRight') { event.preventDefault(); // Avoid scrolling the page queueMove('right'); } }); function hitTest() { const row = metadata[position.currentRow - 1]; if (!row) return; if (row.type === 'car' || row.type === 'truck') { const playerBoundingBox = new THREE.Box3(); playerBoundingBox.setFromObject(player); row.vehicles.forEach(({ ref }) => { if (!ref) throw Error('Vehicle reference is missing'); const vehicleBoundingBox = new THREE.Box3(); vehicleBoundingBox.setFromObject(ref); if (playerBoundingBox.intersectsBox(vehicleBoundingBox)) { if (!resultDOM || !finalScoreDOM) return; resultDOM.style.visibility = 'visible'; finalScoreDOM.innerText = position.currentRow.toString(); } }); } } const scene = new THREE.Scene(); scene.add(player); scene.add(map); const ambientLight = new THREE.AmbientLight(); scene.add(ambientLight); const dirLight = DirectionalLight(); dirLight.target = player; player.add(dirLight); const camera = Camera(); player.add(camera); const scoreDOM = document.getElementById('score'); const resultDOM = document.getElementById('result-container'); const finalScoreDOM = document.getElementById('final-score'); initializeGame(); document .querySelector('#retry') ?.addEventListener('click', initializeGame); function initializeGame() { initializePlayer(); initializeMap(); // Initialize UI if (scoreDOM) scoreDOM.innerText = '0'; if (resultDOM) resultDOM.style.visibility = 'hidden'; } const renderer = Renderer(); renderer.setAnimationLoop(animate); function animate() { animateVehicles(); animatePlayer(); hitTest(); renderer.render(scene, camera); } </script> </body> </html>
推荐本站淘宝优惠价购买喜欢的宝贝:
本文链接:https://www.hqyman.cn/post/10948.html 非本站原创文章欢迎转载,原创文章需保留本站地址!
休息一下~~