WEBLEB
Accueil
Éditeur
Connexion
Pro
Français
English
Français
Español
Português
Deutsch
Italiano
हिंदी
Crossy Road avec three.js
545
zegarkidawida
Ouvrir dans l'éditeur
Publiez votre code
Recommandé
27 December 2024
Une page d'accueil créée avec des annuaires
21 September 2024
Ajouter un bouton pour ouvrir l'ajout de données de profil avec validation
7 November 2024
basculer avec animation lumineuse
HTML
Copy
▲
◀
▼
▶
0
Game Over
Your score:
Retry
Learn Three.js while building this game on YouTube
Learn Three.js while building this game on YouTube
CSS
Copy
@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; } }
JS
Copy
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); }