|
8 | 8 | <head> |
9 | 9 | <meta charset="UTF-8"> |
10 | 10 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
11 | | - <title>Firebase Cube Game</title> |
| 11 | + <title>Firebase Ball Game</title> |
12 | 12 | <style> |
13 | 13 | /* Global styles for the page */ |
14 | 14 | body { |
|
144 | 144 | // Three.js and Cannon.js variables |
145 | 145 | let scene, camera, renderer, world; |
146 | 146 | let playerBody, playerMesh; |
| 147 | + let cameraPivot; // New pivot for camera orbit |
| 148 | + |
| 149 | + // Camera control state |
| 150 | + let isRightMouseDown = false; |
| 151 | + let lastMouseX = 0; |
| 152 | + let lastMouseY = 0; |
147 | 153 |
|
148 | 154 | // Keyboard input state |
149 | 155 | const keyboard = {}; |
|
186 | 192 | } |
187 | 193 | } |
188 | 194 |
|
| 195 | + /** |
| 196 | + * Creates a new ball mesh with an attached face. |
| 197 | + * @param {number} radius The radius of the ball. |
| 198 | + * @param {string} color The hex color for the ball material. |
| 199 | + * @returns {THREE.Mesh} The new mesh object. |
| 200 | + */ |
| 201 | + function createPlayerMesh(radius, color) { |
| 202 | + const ballGeometry = new THREE.SphereGeometry(radius, 32, 32); |
| 203 | + const ballMaterial = new THREE.MeshStandardMaterial({ color: color, metalness: 0.5, roughness: 0.5 }); |
| 204 | + const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial); |
| 205 | + ballMesh.castShadow = true; |
| 206 | + |
| 207 | + // Create the cute face texture using an SVG data URL |
| 208 | + const faceSvg = ` |
| 209 | + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> |
| 210 | + <!-- Eyes --> |
| 211 | + <circle cx="30" cy="40" r="10" fill="#000"/> |
| 212 | + <circle cx="70" cy="40" r="10" fill="#000"/> |
| 213 | + <!-- Mouth --> |
| 214 | + <path d="M 35 65 Q 50 85, 65 65" stroke="#000" stroke-width="5" fill="none" stroke-linecap="round"/> |
| 215 | + </svg> |
| 216 | + `; |
| 217 | + const faceDataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(faceSvg)}`; |
| 218 | + const faceTexture = new THREE.TextureLoader().load(faceDataUrl); |
| 219 | + const faceMaterial = new THREE.MeshBasicMaterial({ map: faceTexture, transparent: true }); |
| 220 | + |
| 221 | + // Create a plane for the face |
| 222 | + const facePlane = new THREE.Mesh(new THREE.PlaneGeometry(1.5, 1.5), faceMaterial); |
| 223 | + |
| 224 | + // Position the face in front of the ball |
| 225 | + facePlane.position.z = radius; |
| 226 | + facePlane.rotation.y = Math.PI; |
| 227 | + |
| 228 | + // Add the face to the ball mesh as a child |
| 229 | + ballMesh.add(facePlane); |
| 230 | + |
| 231 | + return ballMesh; |
| 232 | + } |
| 233 | + |
189 | 234 | /** |
190 | 235 | * Sets up the initial game world, including the 3D scene, physics, |
191 | 236 | * and the player's cube. This runs only after authentication is complete. |
|
197 | 242 |
|
198 | 243 | // 2. Camera setup |
199 | 244 | camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
200 | | - camera.position.set(0, 10, 20); |
201 | | - camera.lookAt(new THREE.Vector3(0, 0, 0)); |
| 245 | + camera.position.set(0, 5, 15); |
| 246 | + |
| 247 | + // Create a pivot for the camera and add it to the scene |
| 248 | + cameraPivot = new THREE.Object3D(); |
| 249 | + scene.add(cameraPivot); |
| 250 | + cameraPivot.add(camera); |
202 | 251 |
|
203 | 252 | // 3. Renderer setup |
204 | 253 | renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('gameCanvas'), antialias: true }); |
|
266 | 315 | wallRight.quaternion.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), -Math.PI / 2); |
267 | 316 | world.addBody(wallRight); |
268 | 317 |
|
269 | | - // 9. Player's cube (3D and Physics) |
270 | | - const playerShape = new CANNON.Box(new CANNON.Vec3(1, 1, 1)); |
| 318 | + // 9. Player's ball (3D and Physics) |
| 319 | + const playerShape = new CANNON.Sphere(1); // Radius of 1 |
271 | 320 | playerBody = new CANNON.Body({ mass: 5, shape: playerShape, position: new CANNON.Vec3(0, 10, 0) }); |
272 | 321 | world.addBody(playerBody); |
273 | 322 |
|
274 | | - const playerGeometry = new THREE.BoxGeometry(2, 2, 2); |
275 | | - const playerMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffaa, metalness: 0.5, roughness: 0.5 }); |
276 | | - playerMesh = new THREE.Mesh(playerGeometry, playerMaterial); |
277 | | - playerMesh.castShadow = true; |
| 323 | + // Create the player's mesh using the new function |
| 324 | + playerMesh = createPlayerMesh(1, 0x00ffaa); |
278 | 325 | scene.add(playerMesh); |
279 | 326 |
|
280 | 327 | // Start the animation loop |
|
300 | 347 | } |
301 | 348 |
|
302 | 349 | if (change.type === "added" || change.type === "modified") { |
303 | | - // Add or update the player's cube |
| 350 | + // Add or update the player's cube/ball |
304 | 351 | if (!playerCubes[playerId]) { |
305 | | - const otherPlayerGeometry = new THREE.BoxGeometry(2, 2, 2); |
306 | | - const otherPlayerMaterial = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff, metalness: 0.5, roughness: 0.5 }); |
307 | | - const otherPlayerMesh = new THREE.Mesh(otherPlayerGeometry, otherPlayerMaterial); |
| 352 | + // Create a sphere with a random color for other players |
| 353 | + const randomColor = Math.random() * 0xffffff; |
| 354 | + const otherPlayerMesh = createPlayerMesh(1, randomColor); |
308 | 355 | scene.add(otherPlayerMesh); |
309 | 356 | playerCubes[playerId] = otherPlayerMesh; |
310 | 357 | } |
|
328 | 375 | } |
329 | 376 |
|
330 | 377 | /** |
331 | | - * Updates the current user's cube position and rotation in Firestore. |
| 378 | + * Updates the current user's ball position and rotation in Firestore. |
332 | 379 | */ |
333 | 380 | function updateMyPositionInFirestore() { |
334 | 381 | if (!authReady) return; |
|
360 | 407 | // Update physics world |
361 | 408 | world.step(1/60); |
362 | 409 |
|
363 | | - // Update player cube position from physics body |
| 410 | + // Update player ball position from physics body |
364 | 411 | playerMesh.position.copy(playerBody.position); |
365 | | - playerMesh.quaternion.copy(playerBody.quaternion); |
| 412 | + |
| 413 | + // Update the player mesh's rotation to match the camera's Y-axis rotation |
| 414 | + playerMesh.rotation.y = cameraPivot.rotation.y; |
| 415 | + |
| 416 | + // Update the camera pivot's position to follow the player |
| 417 | + cameraPivot.position.copy(playerMesh.position); |
366 | 418 |
|
367 | 419 | // Handle keyboard input and apply forces |
368 | 420 | const force = 50; |
369 | 421 | const jumpForce = 400; |
| 422 | + |
| 423 | + // Get the camera's forward and right directions based on its rotation |
| 424 | + const forwardVector = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion); |
| 425 | + const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion); |
| 426 | + |
| 427 | + // Normalize these vectors to ensure consistent speed |
| 428 | + forwardVector.normalize(); |
| 429 | + rightVector.normalize(); |
370 | 430 |
|
371 | 431 | if (keyboard['ArrowUp'] || keyboard['KeyW']) { |
372 | | - playerBody.applyLocalForce(new CANNON.Vec3(0, 0, -force), playerBody.position); |
| 432 | + playerBody.applyImpulse(new CANNON.Vec3(forwardVector.x * force, 0, forwardVector.z * force), playerBody.position); |
373 | 433 | } |
374 | 434 | if (keyboard['ArrowDown'] || keyboard['KeyS']) { |
375 | | - playerBody.applyLocalForce(new CANNON.Vec3(0, 0, force), playerBody.position); |
| 435 | + playerBody.applyImpulse(new CANNON.Vec3(-forwardVector.x * force, 0, -forwardVector.z * force), playerBody.position); |
376 | 436 | } |
377 | 437 | if (keyboard['ArrowLeft'] || keyboard['KeyA']) { |
378 | | - playerBody.applyLocalForce(new CANNON.Vec3(-force, 0, 0), playerBody.position); |
| 438 | + playerBody.applyImpulse(new CANNON.Vec3(-rightVector.x * force, 0, -rightVector.z * force), playerBody.position); |
379 | 439 | } |
380 | 440 | if (keyboard['ArrowRight'] || keyboard['KeyD']) { |
381 | | - playerBody.applyLocalForce(new CANNON.Vec3(force, 0, 0), playerBody.position); |
| 441 | + playerBody.applyImpulse(new CANNON.Vec3(rightVector.x * force, 0, rightVector.z * force), playerBody.position); |
382 | 442 | } |
383 | 443 | if (keyboard['Space']) { |
384 | 444 | // Simple jump logic: check if on the ground |
|
387 | 447 | } |
388 | 448 | } |
389 | 449 |
|
390 | | - // Update camera position to follow the player |
391 | | - const cameraOffset = new THREE.Vector3(0, 5, 15); |
392 | | - camera.position.copy(playerMesh.position).add(cameraOffset); |
393 | | - camera.lookAt(playerMesh.position); |
394 | | - |
395 | 450 | // Render the scene |
396 | 451 | renderer.render(scene, camera); |
397 | 452 |
|
|
428 | 483 | } |
429 | 484 | }); |
430 | 485 |
|
| 486 | + // Camera control event listeners |
| 487 | + window.addEventListener('contextmenu', (event) => { |
| 488 | + event.preventDefault(); |
| 489 | + }); |
| 490 | + |
| 491 | + window.addEventListener('mousedown', (event) => { |
| 492 | + if (event.button === 2) { // Right mouse button |
| 493 | + isRightMouseDown = true; |
| 494 | + lastMouseX = event.clientX; |
| 495 | + lastMouseY = event.clientY; |
| 496 | + } |
| 497 | + }); |
| 498 | + |
| 499 | + window.addEventListener('mouseup', (event) => { |
| 500 | + if (event.button === 2) { |
| 501 | + isRightMouseDown = false; |
| 502 | + } |
| 503 | + }); |
| 504 | + |
| 505 | + window.addEventListener('mousemove', (event) => { |
| 506 | + if (!isRightMouseDown) return; |
| 507 | + |
| 508 | + const deltaX = event.clientX - lastMouseX; |
| 509 | + const deltaY = event.clientY - lastMouseY; |
| 510 | + const rotationSpeed = 0.005; |
| 511 | + |
| 512 | + // Rotate the camera pivot around the Y-axis |
| 513 | + cameraPivot.rotation.y -= deltaX * rotationSpeed; |
| 514 | + |
| 515 | + // Rotate the camera up and down, but clamp the values |
| 516 | + camera.rotation.x -= deltaY * rotationSpeed; |
| 517 | + camera.rotation.x = Math.max(-Math.PI / 4, Math.min(Math.PI / 4, camera.rotation.x)); |
| 518 | + |
| 519 | + lastMouseX = event.clientX; |
| 520 | + lastMouseY = event.clientY; |
| 521 | + }); |
| 522 | + |
| 523 | + |
431 | 524 | // Handle window resize |
432 | 525 | window.addEventListener('resize', () => { |
433 | 526 | if (renderer && camera) { |
|
448 | 541 | <body> |
449 | 542 | <!-- UI overlay for instructions and user ID --> |
450 | 543 | <div id="ui"> |
451 | | - <h1>3D Cube Game</h1> |
452 | | - <p>Move with Arrow Keys or WASD, Jump with Space</p> |
| 544 | + <h1>3D Ball Game</h1> |
| 545 | + <p>Move with Arrow Keys or WASD (relative to camera), Jump with Space</p> |
| 546 | + <p>Drag with Right Mouse to look around</p> |
453 | 547 | <button id="tpButton">Teleport to Spawn</button> |
454 | 548 | <p id="userIdDisplay">Connecting...</p> |
455 | 549 | </div> |
|
0 commit comments