import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; // 게임 상수 const GAME_DURATION = 180; const MAP_SIZE = 2000; const TANK_HEIGHT = 0.5; const ENEMY_GROUND_HEIGHT = 0; const ENEMY_SCALE = 10; const MAX_HEALTH = 1000; const ENEMY_MOVE_SPEED = 0.1; const ENEMY_COUNT_MAX = 5; const PARTICLE_COUNT = 15; const BUILDING_COUNT = 30; // 건물 수 추가 const ENEMY_CONFIG = { ATTACK_RANGE: 100, ATTACK_INTERVAL: 2000, BULLET_SPEED: 2 }; // TankPlayer 클래스 class TankPlayer { constructor() { this.body = null; this.turret = null; this.position = new THREE.Vector3(0, 0, 0); this.rotation = new THREE.Euler(0, 0, 0); this.turretRotation = 0; this.moveSpeed = 0.5; this.turnSpeed = 0.03; this.turretGroup = new THREE.Group(); this.health = MAX_HEALTH; this.isLoaded = false; this.ammo = 10; this.lastShootTime = 0; this.shootInterval = 1000; this.bullets = []; } async initialize(scene, loader) { try { const bodyResult = await loader.loadAsync('/models/abramsBody.glb'); this.body = bodyResult.scene; this.body.position.copy(this.position); const turretResult = await loader.loadAsync('/models/abramsTurret.glb'); this.turret = turretResult.scene; this.turretGroup.position.y = 0.2; this.turretGroup.add(this.turret); this.body.add(this.turretGroup); this.body.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }); this.turret.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }); scene.add(this.body); this.isLoaded = true; } catch (error) { console.error('Error loading tank models:', error); this.isLoaded = false; } } shoot(scene) { const currentTime = Date.now(); if (currentTime - this.lastShootTime < this.shootInterval || this.ammo <= 0) return null; // 총알 생성 const bulletGeometry = new THREE.SphereGeometry(0.2); const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); // 총알 시작 위치 (포탑 끝) const bulletOffset = new THREE.Vector3(0, 0.5, 2); // 포탑의 회전을 적용 bulletOffset.applyQuaternion(this.turretGroup.quaternion); bulletOffset.applyQuaternion(this.body.quaternion); bullet.position.copy(this.body.position).add(bulletOffset); // 총알 속도 (포탑 방향) const direction = new THREE.Vector3(0, 0, 1); direction.applyQuaternion(this.turretGroup.quaternion); direction.applyQuaternion(this.body.quaternion); bullet.velocity = direction.multiplyScalar(2); scene.add(bullet); this.bullets.push(bullet); this.ammo--; this.lastShootTime = currentTime; document.getElementById('ammo').textContent = `Ammo: ${this.ammo}/10`; return bullet; } update(mouseX, mouseY) { if (!this.body || !this.turretGroup) return; // 총알 업데이트만 수행하고 포탑 회전은 Game 클래스의 handleMovement에서 처리 for (let i = this.bullets.length - 1; i >= 0; i--) { const bullet = this.bullets[i]; bullet.position.add(bullet.velocity); // 총알이 맵 밖으로 나가면 제거 if (Math.abs(bullet.position.x) > MAP_SIZE/2 || Math.abs(bullet.position.z) > MAP_SIZE/2) { scene.remove(bullet); this.bullets.splice(i, 1); } } } move(direction) { if (!this.body) return; const moveVector = new THREE.Vector3(); moveVector.x = direction.x * this.moveSpeed; moveVector.z = direction.z * this.moveSpeed; this.body.position.add(moveVector); } rotate(angle) { if (!this.body) return; this.body.rotation.y += angle * this.turnSpeed; } getPosition() { return this.body ? this.body.position : new THREE.Vector3(); } takeDamage(damage) { this.health -= damage; return this.health <= 0; } } // Enemy 클래스 수정 class Enemy { constructor(scene, position, type = 'tank') { this.scene = scene; this.position = position; this.mesh = null; this.type = type; // 'tank' 또는 'heavy' this.health = type === 'tank' ? 100 : 200; // heavy는 체력이 더 높음 this.lastAttackTime = 0; this.bullets = []; this.isLoaded = false; this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7; // heavy는 더 느림 } async initialize(loader) { try { // 타입에 따라 다른 모델 로드 const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb'; const result = await loader.loadAsync(modelPath); this.mesh = result.scene; this.mesh.position.copy(this.position); this.mesh.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE); this.mesh.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }); this.scene.add(this.mesh); this.isLoaded = true; } catch (error) { console.error('Error loading enemy model:', error); this.isLoaded = false; } } update(playerPosition) { if (!this.mesh || !this.isLoaded) return; // 플레이어 방향으로 회전 const direction = new THREE.Vector3() .subVectors(playerPosition, this.mesh.position) .normalize(); this.mesh.lookAt(playerPosition); // 플레이어 방향으로 이동 (타입에 따라 다른 속도) this.mesh.position.add(direction.multiplyScalar(this.moveSpeed)); // 총알 업데이트 for (let i = this.bullets.length - 1; i >= 0; i--) { const bullet = this.bullets[i]; bullet.position.add(bullet.velocity); // 총알이 맵 밖으로 나가면 제거 if (Math.abs(bullet.position.x) > MAP_SIZE || Math.abs(bullet.position.z) > MAP_SIZE) { this.scene.remove(bullet); this.bullets.splice(i, 1); } } } shoot(playerPosition) { const currentTime = Date.now(); const attackInterval = this.type === 'tank' ? ENEMY_CONFIG.ATTACK_INTERVAL : ENEMY_CONFIG.ATTACK_INTERVAL * 1.5; // heavy는 발사 간격이 더 김 if (currentTime - this.lastAttackTime < attackInterval) return; const bulletGeometry = new THREE.SphereGeometry(this.type === 'tank' ? 0.2 : 0.3); const bulletMaterial = new THREE.MeshBasicMaterial({ color: this.type === 'tank' ? 0xff0000 : 0xff6600 }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); bullet.position.copy(this.mesh.position); const direction = new THREE.Vector3() .subVectors(playerPosition, this.mesh.position) .normalize(); const bulletSpeed = this.type === 'tank' ? ENEMY_CONFIG.BULLET_SPEED : ENEMY_CONFIG.BULLET_SPEED * 0.8; bullet.velocity = direction.multiplyScalar(bulletSpeed); this.scene.add(bullet); this.bullets.push(bullet); this.lastAttackTime = currentTime; } takeDamage(damage) { this.health -= damage; return this.health <= 0; } destroy() { if (this.mesh) { this.scene.remove(this.mesh); this.bullets.forEach(bullet => this.scene.remove(bullet)); this.bullets = []; this.isLoaded = false; } } } // Particle 클래스는 그대로 유지 class Particle { constructor(scene, position) { const geometry = new THREE.SphereGeometry(0.1); const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); this.mesh = new THREE.Mesh(geometry, material); this.mesh.position.copy(position); this.velocity = new THREE.Vector3( (Math.random() - 0.5) * 0.3, Math.random() * 0.2, (Math.random() - 0.5) * 0.3 ); this.gravity = -0.01; this.lifetime = 60; this.age = 0; scene.add(this.mesh); } update() { this.velocity.y += this.gravity; this.mesh.position.add(this.velocity); this.age++; return this.age < this.lifetime; } destroy(scene) { scene.remove(this.mesh); } } // Game 클래스 class Game { constructor() { // 기본 Three.js 설정 this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.shadowMap.enabled = true; document.getElementById('gameContainer').appendChild(this.renderer.domElement); // 게임 요소 초기화 this.tank = new TankPlayer(); this.enemies = []; this.particles = []; this.buildings = []; this.loader = new GLTFLoader(); this.controls = null; this.gameTime = GAME_DURATION; this.score = 0; this.isGameOver = false; this.isLoading = true; this.previousTankPosition = new THREE.Vector3(); this.lastTime = performance.now(); // 마우스/키보드 상태 this.mouse = { x: 0, y: 0 }; this.keys = { forward: false, backward: false, left: false, right: false }; // 이벤트 리스너 설정 this.setupEventListeners(); this.initialize(); this.disposedObjects = new Set(); // 제거된 객체 추적 } // 리소스 정리를 위한 메서드 dispose(object) { if (this.disposedObjects.has(object)) return; if (object.geometry) object.geometry.dispose(); if (object.material) { if (Array.isArray(object.material)) { object.material.forEach(material => material.dispose()); } else { object.material.dispose(); } } if (object.parent) object.parent.remove(object); this.disposedObjects.add(object); } // 총알 정리 cleanupBullets() { // 플레이어 총알 정리 for (let i = this.tank.bullets.length - 1; i >= 0; i--) { const bullet = this.tank.bullets[i]; if (Math.abs(bullet.position.x) > MAP_SIZE/2 || Math.abs(bullet.position.z) > MAP_SIZE/2) { this.dispose(bullet); this.tank.bullets.splice(i, 1); } } // 적 총알 정리 this.enemies.forEach(enemy => { for (let i = enemy.bullets.length - 1; i >= 0; i--) { const bullet = enemy.bullets[i]; if (Math.abs(bullet.position.x) > MAP_SIZE/2 || Math.abs(bullet.position.z) > MAP_SIZE/2) { this.dispose(bullet); enemy.bullets.splice(i, 1); } } }); } async initialize() { try { // 조명 설정 const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); this.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(50, 50, 50); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; this.scene.add(directionalLight); // 지형 생성 const ground = new THREE.Mesh( new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE), new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.9, metalness: 0.1 }) ); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; this.scene.add(ground); // 건물 생성 await this.createBuildings(); // 탱크 초기화 await this.tank.initialize(this.scene, this.loader); if (!this.tank.isLoaded) { throw new Error('Tank loading failed'); } // 카메라 초기 설정 const tankPosition = this.tank.getPosition(); this.camera.position.set( tankPosition.x, tankPosition.y + 15, tankPosition.z - 30 ); this.camera.lookAt(new THREE.Vector3( tankPosition.x, tankPosition.y + 2, tankPosition.z )); // 로딩 완료 this.isLoading = false; document.getElementById('loading').style.display = 'none'; // 게임 시작 this.animate(); this.spawnEnemies(); this.startGameTimer(); } catch (error) { console.error('Game initialization error:', error); this.handleLoadingError(); } } setupEventListeners() { document.addEventListener('keydown', (event) => { if (this.isLoading) return; switch(event.code) { case 'KeyW': this.keys.forward = true; break; case 'KeyS': this.keys.backward = true; break; case 'KeyA': this.keys.left = true; break; case 'KeyD': this.keys.right = true; break; } }); document.addEventListener('keyup', (event) => { if (this.isLoading) return; switch(event.code) { case 'KeyW': this.keys.forward = false; break; case 'KeyS': this.keys.backward = false; break; case 'KeyA': this.keys.left = false; break; case 'KeyD': this.keys.right = false; break; } }); document.addEventListener('mousemove', (event) => { if (this.isLoading || !document.pointerLockElement) return; this.mouse.x += event.movementX * 0.002; this.mouse.y += event.movementY * 0.002; }); document.addEventListener('click', () => { if (!document.pointerLockElement) { document.body.requestPointerLock(); } else { const bullet = this.tank.shoot(this.scene); if (bullet) { // 발사 효과 추가 가능 } } }); document.addEventListener('pointerlockchange', () => { if (!document.pointerLockElement) { this.mouse.x = 0; this.mouse.y = 0; } }); window.addEventListener('resize', () => { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); }); } handleMovement() { if (!this.tank.isLoaded) return; const direction = new THREE.Vector3(); if (this.keys.forward) direction.z += 1; if (this.keys.backward) direction.z -= 1; if (this.keys.left) direction.x -= 1; if (this.keys.right) direction.x += 1; if (direction.length() > 0) { direction.normalize(); if (this.keys.left) this.tank.rotate(-1); if (this.keys.right) this.tank.rotate(1); direction.applyEuler(this.tank.body.rotation); this.tank.move(direction); } const mouseVector = new THREE.Vector2(this.mouse.x, -this.mouse.y); const rotationAngle = Math.atan2(mouseVector.x, mouseVector.y); if (this.tank.turretGroup) { this.tank.turretGroup.rotation.y = rotationAngle; } const tankPos = this.tank.getPosition(); const cameraDistance = 30; const cameraHeight = 15; const lookAtHeight = 5; const tankRotation = this.tank.body.rotation.y; this.camera.position.set( tankPos.x - Math.sin(tankRotation) * cameraDistance, tankPos.y + cameraHeight, tankPos.z - Math.cos(tankRotation) * cameraDistance ); const lookAtPoint = new THREE.Vector3( tankPos.x + Math.sin(tankRotation) * 10, tankPos.y + lookAtHeight, tankPos.z + Math.cos(tankRotation) * 10 ); this.camera.lookAt(lookAtPoint); } animate() { if (this.isGameOver) return; requestAnimationFrame(() => this.animate()); const currentTime = performance.now(); const deltaTime = (currentTime - this.lastTime) / 1000; this.lastTime = currentTime; if (this.isLoading) { this.renderer.render(this.scene, this.camera); return; } if (deltaTime < 1/60) return; this.handleMovement(); this.tank.update(this.mouse.x, this.mouse.y); const tankPosition = this.tank.getPosition(); this.enemies.forEach(enemy => { if (!enemy.mesh || !enemy.isLoaded) return; const distance = enemy.mesh.position.distanceTo(tankPosition); if (distance > 200) return; enemy.update(tankPosition); if (distance < ENEMY_CONFIG.ATTACK_RANGE) { enemy.shoot(tankPosition); } }); this.updateParticles(); this.checkCollisions(); this.cleanupBullets(); this.updateUI(); this.renderer.render(this.scene, this.camera); } checkCollisions() { if (this.isLoading || !this.tank.isLoaded) return; const tankPosition = this.tank.getPosition(); const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body); for (const enemy of this.enemies) { if (!enemy.mesh || !enemy.isLoaded) continue; for (let i = enemy.bullets.length - 1; i >= 0; i--) { const bullet = enemy.bullets[i]; const distance = bullet.position.distanceTo(tankPosition); if (distance > 50) continue; if (distance < 1) { if (this.tank.takeDamage(10)) { this.endGame(); return; } this.dispose(bullet); enemy.bullets.splice(i, 1); this.createExplosion(bullet.position); document.getElementById('health').style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`; } } } for (const building of this.buildings) { const buildingBox = new THREE.Box3().setFromObject(building); if (tankBoundingBox.intersectsBox(buildingBox)) { this.tank.body.position.copy(this.previousTankPosition); break; } } this.previousTankPosition = this.tank.body.position.clone(); } updateUI() { const healthBar = document.getElementById('health'); if (healthBar) { healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`; } const timeElement = document.getElementById('time'); if (timeElement) { timeElement.textContent = `Time: ${this.gameTime}s`; } const scoreElement = document.getElementById('score'); if (scoreElement) { scoreElement.textContent = `Score: ${this.score}`; } } endGame() { this.isGameOver = true; const gameOverDiv = document.createElement('div'); gameOverDiv.style.position = 'absolute'; gameOverDiv.style.top = '50%'; gameOverDiv.style.left = '50%'; gameOverDiv.style.transform = 'translate(-50%, -50%)'; gameOverDiv.style.color = 'white'; gameOverDiv.style.fontSize = '48px'; gameOverDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; gameOverDiv.style.padding = '20px'; gameOverDiv.style.borderRadius = '10px'; gameOverDiv.innerHTML = ` Game Over
Score: ${this.score}
Time Survived: ${GAME_DURATION - this.gameTime}s
`; document.body.appendChild(gameOverDiv); } animate() { if (this.isGameOver) return; requestAnimationFrame(() => this.animate()); const currentTime = performance.now(); const deltaTime = (currentTime - this.lastTime) / 1000; this.lastTime = currentTime; if (this.isLoading) { this.renderer.render(this.scene, this.camera); return; } // 프레임 제한 (60fps) if (deltaTime < 1/60) return; this.handleMovement(); this.tank.update(this.mouse.x, this.mouse.y); const tankPosition = this.tank.getPosition(); // 화면에 보이는 적만 업데이트 this.enemies.forEach(enemy => { if (!enemy.mesh || !enemy.isLoaded) return; const distance = enemy.mesh.position.distanceTo(tankPosition); if (distance > 200) return; // 먼 적은 업데이트 스킵 enemy.update(tankPosition); if (distance < ENEMY_CONFIG.ATTACK_RANGE) { enemy.shoot(tankPosition); } }); this.updateParticles(); this.checkCollisions(); this.cleanupBullets(); this.updateUI(); this.renderer.render(this.scene, this.camera); } updateUI() { const healthBar = document.getElementById('health'); if (healthBar) { healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`; } const timeElement = document.getElementById('time'); if (timeElement) { timeElement.textContent = `Time: ${this.gameTime}s`; } const scoreElement = document.getElementById('score'); if (scoreElement) { scoreElement.textContent = `Score: ${this.score}`; } } } // HTML의 startGame 함수와 연결 window.startGame = function() { document.getElementById('startScreen').style.display = 'none'; document.body.requestPointerLock(); }; // 게임 인스턴스 생성 const game = new Game();