- Updated Player class to use maxSpeed, acceleration, and deceleration for smoother movement. - Introduced velocity vector for more responsive control and smoother diagonal movement. - Enhanced update method to calculate target velocity based on input and apply easing for acceleration and deceleration. - Implemented boundary checks to stop movement when hitting the edges of the play area. This change improves the overall player control experience, making movement more fluid and intuitive.
694 lines
No EOL
25 KiB
HTML
694 lines
No EOL
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>3D Coin Collector Game</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
overflow: hidden;
|
|
font-family: 'Arial', sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
#gameCanvas {
|
|
display: block;
|
|
}
|
|
#ui {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: white;
|
|
font-size: 24px;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
|
z-index: 100;
|
|
}
|
|
#gameOver {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(0,0,0,0.8);
|
|
padding: 40px;
|
|
border-radius: 20px;
|
|
text-align: center;
|
|
color: white;
|
|
display: none;
|
|
z-index: 200;
|
|
}
|
|
#gameOver h1 {
|
|
font-size: 48px;
|
|
margin-bottom: 20px;
|
|
color: #ff6b6b;
|
|
}
|
|
#gameOver p {
|
|
font-size: 24px;
|
|
margin-bottom: 30px;
|
|
}
|
|
#restartBtn {
|
|
background: #4CAF50;
|
|
border: none;
|
|
color: white;
|
|
padding: 15px 40px;
|
|
font-size: 20px;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
#restartBtn:hover {
|
|
background: #45a049;
|
|
transform: scale(1.1);
|
|
}
|
|
#instructions {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
color: white;
|
|
text-align: center;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
|
}
|
|
@media (max-width: 768px) {
|
|
#instructions {
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="ui">
|
|
<div>Score: <span id="score">0</span></div>
|
|
<div>Health: <span id="health">100</span></div>
|
|
</div>
|
|
|
|
<div id="gameOver">
|
|
<h1>Game Over!</h1>
|
|
<p>Final Score: <span id="finalScore">0</span></p>
|
|
<button id="restartBtn">Play Again</button>
|
|
</div>
|
|
|
|
<div id="instructions">
|
|
<p><strong>Controls:</strong> WASD or Arrow Keys to move | Touch and drag to move (mobile) | Collect yellow coins | Avoid red obstacles!</p>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script>
|
|
// Base GameObject class
|
|
class GameObject {
|
|
constructor(scene, groundSize) {
|
|
this.scene = scene;
|
|
this.groundSize = groundSize;
|
|
this.mesh = this.createMesh();
|
|
this.initialize();
|
|
scene.add(this.mesh);
|
|
}
|
|
|
|
// Override in child classes
|
|
createMesh() {
|
|
throw new Error('createMesh() must be implemented by child class');
|
|
}
|
|
|
|
// Override in child classes for initialization
|
|
initialize() {
|
|
// Default implementation - can be overridden
|
|
}
|
|
|
|
// Override in child classes for update logic
|
|
update(...args) {
|
|
// Default implementation - can be overridden
|
|
}
|
|
|
|
// Common collision detection
|
|
checkCollision(otherPosition, collisionRadius) {
|
|
const distance = this.mesh.position.distanceTo(otherPosition);
|
|
return distance < collisionRadius;
|
|
}
|
|
|
|
// Common position getter
|
|
getPosition() {
|
|
return this.mesh.position;
|
|
}
|
|
|
|
// Remove from scene
|
|
remove() {
|
|
this.scene.remove(this.mesh);
|
|
}
|
|
|
|
// Set position
|
|
setPosition(x, y, z) {
|
|
this.mesh.position.set(x, y, z);
|
|
}
|
|
|
|
// Get position vector
|
|
getPositionVector() {
|
|
return this.mesh.position.clone();
|
|
}
|
|
}
|
|
|
|
// Player class extends GameObject
|
|
class Player extends GameObject {
|
|
constructor(scene, groundSize) {
|
|
super(scene, groundSize);
|
|
this.maxSpeed = 0.15;
|
|
this.acceleration = 0.08;
|
|
this.deceleration = 0.12;
|
|
this.velocity = new THREE.Vector3(0, 0, 0);
|
|
}
|
|
|
|
createMesh() {
|
|
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0x4169E1,
|
|
metalness: 0.3,
|
|
roughness: 0.4
|
|
});
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
return mesh;
|
|
}
|
|
|
|
initialize() {
|
|
// Initialize velocity (needed because initialize is called from parent constructor)
|
|
if (!this.velocity) {
|
|
this.velocity = new THREE.Vector3(0, 0, 0);
|
|
}
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
this.setPosition(0, 0.5, 0);
|
|
this.mesh.rotation.y = 0;
|
|
if (this.velocity) {
|
|
this.velocity.set(0, 0, 0);
|
|
}
|
|
}
|
|
|
|
update(keys) {
|
|
// Calculate target velocity based on input
|
|
const targetVelocity = new THREE.Vector3(0, 0, 0);
|
|
|
|
if (keys['w'] || keys['arrowup']) targetVelocity.z -= this.maxSpeed;
|
|
if (keys['s'] || keys['arrowdown']) targetVelocity.z += this.maxSpeed;
|
|
if (keys['a'] || keys['arrowleft']) targetVelocity.x -= this.maxSpeed;
|
|
if (keys['d'] || keys['arrowright']) targetVelocity.x += this.maxSpeed;
|
|
|
|
// Smoothly interpolate velocity towards target
|
|
const isMoving = targetVelocity.length() > 0;
|
|
const accelRate = isMoving ? this.acceleration : this.deceleration;
|
|
|
|
// Calculate velocity difference for each axis
|
|
const velDiffX = targetVelocity.x - this.velocity.x;
|
|
const velDiffZ = targetVelocity.z - this.velocity.z;
|
|
|
|
// Apply easing to each axis independently for smoother diagonal movement
|
|
if (Math.abs(velDiffX) > 0.001) {
|
|
this.velocity.x += velDiffX * accelRate;
|
|
} else {
|
|
this.velocity.x = targetVelocity.x;
|
|
}
|
|
|
|
if (Math.abs(velDiffZ) > 0.001) {
|
|
this.velocity.z += velDiffZ * accelRate;
|
|
} else {
|
|
this.velocity.z = targetVelocity.z;
|
|
}
|
|
|
|
// Apply velocity to position
|
|
this.mesh.position.add(this.velocity);
|
|
|
|
// Boundary checks
|
|
const boundary = this.groundSize / 2 - 0.5;
|
|
this.mesh.position.x = Math.max(-boundary, Math.min(boundary, this.mesh.position.x));
|
|
this.mesh.position.z = Math.max(-boundary, Math.min(boundary, this.mesh.position.z));
|
|
|
|
// Stop velocity if hitting boundary
|
|
if (Math.abs(this.mesh.position.x) >= boundary) {
|
|
this.velocity.x = 0;
|
|
}
|
|
if (Math.abs(this.mesh.position.z) >= boundary) {
|
|
this.velocity.z = 0;
|
|
}
|
|
|
|
// Rotate player based on movement
|
|
if (this.velocity.length() > 0.01) {
|
|
this.mesh.rotation.y += 0.1;
|
|
}
|
|
}
|
|
|
|
handleCollision(obstaclePosition) {
|
|
const pushDirection = this.mesh.position.clone().sub(obstaclePosition);
|
|
pushDirection.y = 0;
|
|
pushDirection.normalize();
|
|
this.mesh.position.add(pushDirection.multiplyScalar(0.3));
|
|
this.mesh.position.y = 0.5;
|
|
}
|
|
}
|
|
|
|
// Coin class extends GameObject
|
|
class Coin extends GameObject {
|
|
constructor(scene, groundSize, index = 0) {
|
|
super(scene, groundSize);
|
|
this.index = index;
|
|
this.rotationSpeed = 0.02;
|
|
this.collisionRadius = 0.8;
|
|
}
|
|
|
|
createMesh() {
|
|
const geometry = new THREE.SphereGeometry(0.3, 16, 16);
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0xFFD700,
|
|
metalness: 0.8,
|
|
roughness: 0.2,
|
|
emissive: 0xFFD700,
|
|
emissiveIntensity: 0.3
|
|
});
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
return mesh;
|
|
}
|
|
|
|
initialize() {
|
|
this.setRandomPosition();
|
|
}
|
|
|
|
setRandomPosition() {
|
|
const x = (Math.random() - 0.5) * (this.groundSize - 4);
|
|
const z = (Math.random() - 0.5) * (this.groundSize - 4);
|
|
this.setPosition(x, 0.5, z);
|
|
}
|
|
|
|
update() {
|
|
this.mesh.rotation.y += this.rotationSpeed;
|
|
this.mesh.position.y = 0.5 + Math.sin(Date.now() * 0.003 + this.index) * 0.2;
|
|
}
|
|
|
|
checkCollisionWithPlayer(playerPosition) {
|
|
return this.checkCollision(playerPosition, this.collisionRadius);
|
|
}
|
|
}
|
|
|
|
// Obstacle class extends GameObject
|
|
class Obstacle extends GameObject {
|
|
constructor(scene, groundSize) {
|
|
super(scene, groundSize);
|
|
this.direction = this.createRandomDirection();
|
|
this.collisionRadius = 1.5;
|
|
}
|
|
|
|
createMesh() {
|
|
const geometry = new THREE.BoxGeometry(1.5, 2, 1.5);
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0xFF4500,
|
|
metalness: 0.3,
|
|
roughness: 0.7
|
|
});
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
return mesh;
|
|
}
|
|
|
|
initialize() {
|
|
this.setRandomPosition();
|
|
this.mesh.position.y = 1;
|
|
}
|
|
|
|
createRandomDirection() {
|
|
return new THREE.Vector3(
|
|
(Math.random() - 0.5) * 0.05,
|
|
0,
|
|
(Math.random() - 0.5) * 0.05
|
|
);
|
|
}
|
|
|
|
setRandomPosition() {
|
|
let posX, posZ;
|
|
do {
|
|
posX = (Math.random() - 0.5) * (this.groundSize - 4);
|
|
posZ = (Math.random() - 0.5) * (this.groundSize - 4);
|
|
} while (Math.abs(posX) < 3 && Math.abs(posZ) < 3);
|
|
|
|
this.setPosition(posX, 1, posZ);
|
|
}
|
|
|
|
update() {
|
|
this.mesh.position.add(this.direction);
|
|
|
|
// Bounce off boundaries
|
|
const boundary = this.groundSize / 2 - 1;
|
|
if (Math.abs(this.mesh.position.x) > boundary) {
|
|
this.direction.x *= -1;
|
|
}
|
|
if (Math.abs(this.mesh.position.z) > boundary) {
|
|
this.direction.z *= -1;
|
|
}
|
|
}
|
|
|
|
checkCollisionWithPlayer(playerPosition) {
|
|
return this.checkCollision(playerPosition, this.collisionRadius);
|
|
}
|
|
}
|
|
|
|
// Game class
|
|
class Game {
|
|
constructor() {
|
|
this.groundSize = 30;
|
|
this.score = 0;
|
|
this.health = 100;
|
|
this.gameActive = true;
|
|
this.keys = {};
|
|
this.coins = [];
|
|
this.obstacles = [];
|
|
this.touchActive = false;
|
|
this.touchStartX = 0;
|
|
this.touchStartY = 0;
|
|
this.touchCurrentX = 0;
|
|
this.touchCurrentY = 0;
|
|
this.touchId = null;
|
|
|
|
this.init();
|
|
this.setupEventListeners();
|
|
this.animate();
|
|
}
|
|
|
|
init() {
|
|
this.setupScene();
|
|
this.setupCamera();
|
|
this.setupRenderer();
|
|
this.setupLights();
|
|
this.setupGround();
|
|
this.setupPlayer();
|
|
this.createGameObjects();
|
|
}
|
|
|
|
setupScene() {
|
|
this.scene = new THREE.Scene();
|
|
this.scene.background = new THREE.Color(0x87CEEB);
|
|
this.scene.fog = new THREE.Fog(0x87CEEB, 0, 50);
|
|
}
|
|
|
|
setupCamera() {
|
|
this.camera = new THREE.PerspectiveCamera(
|
|
75,
|
|
window.innerWidth / window.innerHeight,
|
|
0.1,
|
|
1000
|
|
);
|
|
this.camera.position.set(0, 10, 15);
|
|
this.camera.lookAt(0, 0, 0);
|
|
}
|
|
|
|
setupRenderer() {
|
|
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
this.renderer.shadowMap.enabled = true;
|
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
document.body.appendChild(this.renderer.domElement);
|
|
}
|
|
|
|
setupLights() {
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
this.scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
directionalLight.position.set(10, 20, 10);
|
|
directionalLight.castShadow = true;
|
|
directionalLight.shadow.mapSize.width = 2048;
|
|
directionalLight.shadow.mapSize.height = 2048;
|
|
directionalLight.shadow.camera.left = -20;
|
|
directionalLight.shadow.camera.right = 20;
|
|
directionalLight.shadow.camera.top = 20;
|
|
directionalLight.shadow.camera.bottom = -20;
|
|
directionalLight.shadow.camera.near = 0.5;
|
|
directionalLight.shadow.camera.far = 50;
|
|
this.scene.add(directionalLight);
|
|
}
|
|
|
|
setupGround() {
|
|
const groundGeometry = new THREE.PlaneGeometry(this.groundSize, this.groundSize);
|
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x90EE90,
|
|
roughness: 0.8
|
|
});
|
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
ground.rotation.x = -Math.PI / 2;
|
|
ground.receiveShadow = true;
|
|
this.scene.add(ground);
|
|
|
|
const gridHelper = new THREE.GridHelper(this.groundSize, 20, 0x000000, 0x000000);
|
|
gridHelper.material.opacity = 0.2;
|
|
gridHelper.material.transparent = true;
|
|
this.scene.add(gridHelper);
|
|
}
|
|
|
|
setupPlayer() {
|
|
this.player = new Player(this.scene, this.groundSize);
|
|
}
|
|
|
|
createGameObjects() {
|
|
this.createCoins(10);
|
|
this.createObstacles(8);
|
|
}
|
|
|
|
createCoins(count) {
|
|
for (let i = 0; i < count; i++) {
|
|
const coin = new Coin(this.scene, this.groundSize, this.coins.length);
|
|
this.coins.push(coin);
|
|
}
|
|
}
|
|
|
|
createObstacles(count) {
|
|
for (let i = 0; i < count; i++) {
|
|
const obstacle = new Obstacle(this.scene, this.groundSize);
|
|
this.obstacles.push(obstacle);
|
|
}
|
|
}
|
|
|
|
setupEventListeners() {
|
|
window.addEventListener('keydown', (e) => {
|
|
this.keys[e.key.toLowerCase()] = true;
|
|
});
|
|
window.addEventListener('keyup', (e) => {
|
|
this.keys[e.key.toLowerCase()] = false;
|
|
});
|
|
window.addEventListener('resize', () => this.onWindowResize());
|
|
document.getElementById('restartBtn').addEventListener('click', () => this.restart());
|
|
|
|
// Touch event listeners
|
|
this.setupTouchControls();
|
|
}
|
|
|
|
setupTouchControls() {
|
|
const canvas = this.renderer.domElement;
|
|
|
|
// Prevent default touch behaviors
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
this.handleTouchStart(e);
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchmove', (e) => {
|
|
e.preventDefault();
|
|
this.handleTouchMove(e);
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchend', (e) => {
|
|
e.preventDefault();
|
|
this.handleTouchEnd(e);
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchcancel', (e) => {
|
|
e.preventDefault();
|
|
this.handleTouchEnd(e);
|
|
}, { passive: false });
|
|
}
|
|
|
|
handleTouchStart(e) {
|
|
if (this.touchActive) return;
|
|
|
|
const touch = e.touches[0];
|
|
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
|
|
// Use center of screen as reference point
|
|
this.touchStartX = rect.left + rect.width / 2;
|
|
this.touchStartY = rect.top + rect.height / 2;
|
|
|
|
this.touchId = touch.identifier;
|
|
this.touchCurrentX = touch.clientX;
|
|
this.touchCurrentY = touch.clientY;
|
|
this.touchActive = true;
|
|
}
|
|
|
|
handleTouchMove(e) {
|
|
if (!this.touchActive) return;
|
|
|
|
const touch = Array.from(e.touches).find(t => t.identifier === this.touchId);
|
|
if (!touch) return;
|
|
|
|
this.touchCurrentX = touch.clientX;
|
|
this.touchCurrentY = touch.clientY;
|
|
|
|
this.updateTouchMovement();
|
|
}
|
|
|
|
handleTouchEnd(e) {
|
|
if (!this.touchActive) return;
|
|
|
|
// Check if the touch that ended is our tracked touch
|
|
const touch = Array.from(e.changedTouches).find(t => t.identifier === this.touchId);
|
|
if (!touch) return;
|
|
|
|
this.touchActive = false;
|
|
this.touchId = null;
|
|
this.touchCurrentX = this.touchStartX;
|
|
this.touchCurrentY = this.touchStartY;
|
|
this.updateTouchMovement();
|
|
}
|
|
|
|
updateTouchMovement() {
|
|
if (!this.touchActive) {
|
|
// Reset all movement keys
|
|
this.keys['w'] = false;
|
|
this.keys['s'] = false;
|
|
this.keys['a'] = false;
|
|
this.keys['d'] = false;
|
|
return;
|
|
}
|
|
|
|
const deltaX = this.touchCurrentX - this.touchStartX;
|
|
const deltaY = this.touchCurrentY - this.touchStartY;
|
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
|
|
// Normalize movement direction
|
|
const threshold = 10; // Minimum movement to register
|
|
if (distance < threshold) {
|
|
this.keys['w'] = false;
|
|
this.keys['s'] = false;
|
|
this.keys['a'] = false;
|
|
this.keys['d'] = false;
|
|
return;
|
|
}
|
|
|
|
// Calculate direction
|
|
const normalizedX = deltaX / distance;
|
|
const normalizedY = deltaY / distance;
|
|
|
|
// Update movement keys based on direction
|
|
this.keys['w'] = normalizedY < -0.3;
|
|
this.keys['s'] = normalizedY > 0.3;
|
|
this.keys['a'] = normalizedX < -0.3;
|
|
this.keys['d'] = normalizedX > 0.3;
|
|
}
|
|
|
|
updatePlayer() {
|
|
if (!this.gameActive) return;
|
|
this.player.update(this.keys);
|
|
}
|
|
|
|
updateCoins() {
|
|
this.coins.forEach((coin, index) => {
|
|
coin.update();
|
|
|
|
if (coin.checkCollisionWithPlayer(this.player.getPosition())) {
|
|
coin.remove();
|
|
this.coins.splice(index, 1);
|
|
this.score += 10;
|
|
this.updateUI();
|
|
this.createCoins(1);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateObstacles() {
|
|
if (!this.gameActive) return;
|
|
|
|
this.obstacles.forEach(obstacle => {
|
|
obstacle.update();
|
|
|
|
if (obstacle.checkCollisionWithPlayer(this.player.getPosition())) {
|
|
this.health -= 1;
|
|
this.updateUI();
|
|
this.player.handleCollision(obstacle.getPosition());
|
|
|
|
if (this.health <= 0) {
|
|
this.gameOver();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
updateCamera() {
|
|
const playerPos = this.player.getPosition();
|
|
this.camera.position.x = playerPos.x;
|
|
this.camera.position.z = playerPos.z + 15;
|
|
this.camera.lookAt(playerPos);
|
|
}
|
|
|
|
updateUI() {
|
|
document.getElementById('score').textContent = this.score;
|
|
document.getElementById('health').textContent = Math.max(0, this.health);
|
|
}
|
|
|
|
gameOver() {
|
|
this.gameActive = false;
|
|
document.getElementById('finalScore').textContent = this.score;
|
|
document.getElementById('gameOver').style.display = 'block';
|
|
}
|
|
|
|
restart() {
|
|
// Remove all coins and obstacles
|
|
this.coins.forEach(coin => coin.remove());
|
|
this.obstacles.forEach(obstacle => obstacle.remove());
|
|
this.coins = [];
|
|
this.obstacles = [];
|
|
|
|
// Reset player
|
|
this.player.reset();
|
|
|
|
// Reset game state
|
|
this.score = 0;
|
|
this.health = 100;
|
|
this.gameActive = true;
|
|
|
|
// Recreate game objects
|
|
this.createGameObjects();
|
|
|
|
// Hide game over screen
|
|
document.getElementById('gameOver').style.display = 'none';
|
|
this.updateUI();
|
|
}
|
|
|
|
onWindowResize() {
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
this.camera.updateProjectionMatrix();
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
animate() {
|
|
requestAnimationFrame(() => this.animate());
|
|
|
|
// Update touch movement if active
|
|
if (this.touchActive) {
|
|
this.updateTouchMovement();
|
|
}
|
|
|
|
this.updatePlayer();
|
|
this.updateCoins();
|
|
this.updateObstacles();
|
|
this.updateCamera();
|
|
|
|
this.renderer.render(this.scene, this.camera);
|
|
}
|
|
}
|
|
|
|
// Start the game
|
|
const game = new Game();
|
|
</script>
|
|
</body>
|
|
</html> |