Spaces:
Running
Running
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, // ν±ν¬λ³΄λ€ 15 μ λ μμ | |
tankPosition.z - 30 // ν±ν¬λ³΄λ€ 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; | |
// movementX/Yλ₯Ό μ¬μ©νμ¬ λ§μ°μ€ νμ κ³μ° | |
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(); | |
// A,D ν€λ‘ ν±ν¬ νμ | |
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, // ν±ν¬ μμͺ½ 10 μ λ | |
tankPos.y + lookAtHeight, // ν±ν¬λ³΄λ€ μ½κ° μ | |
tankPos.z + Math.cos(tankRotation) * 10 // ν±ν¬ μμͺ½ 10 μ λ | |
); | |
this.camera.lookAt(lookAtPoint); | |
} | |
createBuildings() { | |
const buildingTypes = [ | |
{ width: 10, height: 30, depth: 10, color: 0x808080 }, | |
{ width: 15, height: 40, depth: 15, color: 0x606060 }, | |
{ width: 20, height: 50, depth: 20, color: 0x404040 } | |
]; | |
for (let i = 0; i < BUILDING_COUNT; i++) { | |
const type = buildingTypes[Math.floor(Math.random() * buildingTypes.length)]; | |
const building = this.createBuilding(type); | |
let position; | |
let attempts = 0; | |
do { | |
position = new THREE.Vector3( | |
(Math.random() - 0.5) * (MAP_SIZE - type.width), | |
type.height / 2, | |
(Math.random() - 0.5) * (MAP_SIZE - type.depth) | |
); | |
attempts++; | |
} while (this.checkBuildingCollision(position, type) && attempts < 50); | |
if (attempts < 50) { | |
building.position.copy(position); | |
this.buildings.push(building); | |
this.scene.add(building); | |
} | |
} | |
} | |
createBuilding(type) { | |
const geometry = new THREE.BoxGeometry(type.width, type.height, type.depth); | |
const material = new THREE.MeshPhongMaterial({ | |
color: type.color, | |
emissive: 0x222222, | |
specular: 0x111111, | |
shininess: 30 | |
}); | |
const building = new THREE.Mesh(geometry, material); | |
building.castShadow = true; | |
building.receiveShadow = true; | |
return building; | |
} | |
checkBuildingCollision(position, type) { | |
const margin = 5; | |
const bbox = new THREE.Box3( | |
new THREE.Vector3( | |
position.x - (type.width / 2 + margin), | |
0, | |
position.z - (type.depth / 2 + margin) | |
), | |
new THREE.Vector3( | |
position.x + (type.width / 2 + margin), | |
type.height, | |
position.z + (type.depth / 2 + margin) | |
) | |
); | |
return this.buildings.some(building => { | |
const buildingBox = new THREE.Box3().setFromObject(building); | |
return bbox.intersectsBox(buildingBox); | |
}); | |
} | |
handleLoadingError() { | |
this.isLoading = false; | |
const loadingElement = document.getElementById('loading'); | |
if (loadingElement) { | |
loadingElement.innerHTML = ` | |
<div class="loading-text" style="color: red;"> | |
Loading failed. Please refresh the page. | |
</div> | |
`; | |
} | |
} | |
spawnEnemies() { | |
let spawnTimeout; | |
const spawnEnemy = () => { | |
if (this.isGameOver) { | |
clearTimeout(spawnTimeout); | |
return; | |
} | |
if (this.enemies.length < ENEMY_COUNT_MAX) { | |
const position = this.getValidEnemySpawnPosition(); | |
if (position) { | |
const type = Math.random() < 0.7 ? 'tank' : 'heavy'; | |
const enemy = new Enemy(this.scene, position, type); | |
enemy.initialize(this.loader).catch(console.error); | |
this.enemies.push(enemy); | |
} | |
} | |
spawnTimeout = setTimeout(spawnEnemy, 3000); | |
}; | |
spawnEnemy(); | |
} | |
getValidEnemySpawnPosition() { | |
const margin = 20; | |
let position; | |
let attempts = 0; | |
const maxAttempts = 50; | |
do { | |
position = new THREE.Vector3( | |
(Math.random() - 0.5) * (MAP_SIZE - margin * 2), | |
ENEMY_GROUND_HEIGHT, | |
(Math.random() - 0.5) * (MAP_SIZE - margin * 2) | |
); | |
const distanceToPlayer = position.distanceTo(this.tank.getPosition()); | |
if (distanceToPlayer < 100) continue; | |
let collisionFound = false; | |
for (const building of this.buildings) { | |
const buildingBox = new THREE.Box3().setFromObject(building); | |
if (buildingBox.containsPoint(position)) { | |
collisionFound = true; | |
break; | |
} | |
} | |
if (!collisionFound) return position; | |
attempts++; | |
} while (attempts < maxAttempts); | |
return null; | |
} | |
startGameTimer() { | |
const timer = setInterval(() => { | |
if (this.isLoading) return; | |
this.gameTime--; | |
if (this.gameTime <= 0 || this.isGameOver) { | |
clearInterval(timer); | |
this.endGame(); | |
} | |
}, 1000); | |
} | |
updateParticles() { | |
for (let i = this.particles.length - 1; i >= 0; i--) { | |
const particle = this.particles[i]; | |
if (!particle.update()) { | |
particle.destroy(this.scene); | |
this.particles.splice(i, 1); | |
} | |
} | |
} | |
createExplosion(position) { | |
for (let i = 0; i < PARTICLE_COUNT; i++) { | |
this.particles.push(new Particle(this.scene, position)); | |
} | |
} | |
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(); | |
} | |
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<br> | |
Score: ${this.score}<br> | |
Time Survived: ${GAME_DURATION - this.gameTime}s<br> | |
<button onclick="location.reload()" | |
style="font-size: 24px; padding: 10px; margin-top: 20px; | |
cursor: pointer; background: #4CAF50; border: none; | |
color: white; border-radius: 5px;"> | |
Play Again | |
</button> | |
`; | |
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(); |