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 = { | |
TANK: { | |
MODEL: '/models/enemy1.glb', | |
HEALTH: 120, | |
SPEED: 0.1, | |
ATTACK_RANGE: 100, | |
ATTACK_INTERVAL: 2000, | |
BULLET_SPEED: 2 | |
}, | |
HEAVY_TANK: { | |
MODEL: '/models/enemy4.glb', | |
HEALTH: 200, | |
SPEED: 0.05, | |
ATTACK_RANGE: 150, | |
ATTACK_INTERVAL: 3000, | |
BULLET_SPEED: 1.5 | |
} | |
}; | |
// Building ν΄λμ€ μΆκ° | |
class Building { | |
constructor(scene, position, size) { | |
const geometry = new THREE.BoxGeometry(size.width, size.height, size.depth); | |
const material = new THREE.MeshStandardMaterial({ | |
color: 0x808080, | |
roughness: 0.7, | |
metalness: 0.3 | |
}); | |
this.mesh = new THREE.Mesh(geometry, material); | |
this.mesh.position.copy(position); | |
this.mesh.position.y = size.height / 2; | |
this.mesh.castShadow = true; | |
this.mesh.receiveShadow = true; | |
scene.add(this.mesh); | |
} | |
} | |
// Enemy ν΄λμ€ μμ | |
class Enemy { | |
constructor(scene, position, type) { | |
this.scene = scene; | |
this.position = position; | |
this.type = type; | |
this.mesh = null; | |
this.health = type === 'TANK' ? ENEMY_CONFIG.TANK.HEALTH : ENEMY_CONFIG.HEAVY_TANK.HEALTH; | |
this.lastAttackTime = 0; | |
this.bullets = []; | |
this.config = ENEMY_CONFIG[type]; | |
} | |
async initialize(loader) { | |
try { | |
const modelPath = this.type === 'TANK' ? ENEMY_CONFIG.TANK.MODEL : ENEMY_CONFIG.HEAVY_TANK.MODEL; | |
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); | |
} catch (error) { | |
console.error('Error loading enemy model:', error); | |
} | |
} | |
update(playerPosition) { | |
if (!this.mesh) return; | |
const direction = new THREE.Vector3() | |
.subVectors(playerPosition, this.mesh.position) | |
.normalize(); | |
this.mesh.lookAt(playerPosition); | |
// νμ λ³ μ΄λ μλ μ μ© | |
const moveSpeed = this.type === 'TANK' ? ENEMY_CONFIG.TANK.SPEED : ENEMY_CONFIG.HEAVY_TANK.SPEED; | |
this.mesh.position.add(direction.multiplyScalar(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.TANK.ATTACK_INTERVAL : | |
ENEMY_CONFIG.HEAVY_TANK.ATTACK_INTERVAL; | |
if (currentTime - this.lastAttackTime < attackInterval) return; | |
const bulletGeometry = new THREE.SphereGeometry(0.2); | |
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.TANK.BULLET_SPEED : | |
ENEMY_CONFIG.HEAVY_TANK.BULLET_SPEED; | |
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 = []; | |
} | |
} | |
} | |
// Game ν΄λμ€ μμ | |
class Game { | |
constructor() { | |
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.body.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.mouse = { | |
x: 0, | |
y: 0 | |
}; | |
this.keys = { | |
forward: false, | |
backward: false, | |
left: false, | |
right: false | |
}; | |
this.setupEventListeners(); | |
this.initialize(); | |
} | |
async initialize() { | |
// μ‘°λͺ μ€μ | |
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; | |
this.scene.add(directionalLight); | |
// λμ λ°λ₯ μμ± | |
const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE); | |
const groundMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x333333, // μ΄λμ΄ νμ (μμ€ννΈ) | |
roughness: 0.9, | |
metalness: 0.1 | |
}); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
this.scene.add(ground); | |
// 건물 μμ± | |
this.createBuildings(); | |
// ν±ν¬ μ΄κΈ°ν | |
await this.tank.initialize(this.scene, this.loader); | |
// μΉ΄λ©λΌ μ€μ | |
this.camera.position.set(0, 10, -10); | |
this.camera.lookAt(0, 0, 0); | |
this.controls = new PointerLockControls(this.camera, document.body); | |
this.animate(); | |
this.spawnEnemies(); | |
this.startGameTimer(); | |
} | |
createBuildings() { | |
for (let i = 0; i < BUILDING_COUNT; i++) { | |
const size = { | |
width: 10 + Math.random() * 20, | |
height: 20 + Math.random() * 80, | |
depth: 10 + Math.random() * 20 | |
}; | |
const position = new THREE.Vector3( | |
(Math.random() - 0.5) * (MAP_SIZE - size.width), | |
0, | |
(Math.random() - 0.5) * (MAP_SIZE - size.depth) | |
); | |
const building = new Building(this.scene, position, size); | |
this.buildings.push(building); | |
} | |
} | |
spawnEnemies() { | |
const spawnEnemy = () => { | |
if (this.enemies.length < ENEMY_COUNT_MAX && !this.isGameOver) { | |
const position = new THREE.Vector3( | |
(Math.random() - 0.5) * MAP_SIZE, | |
ENEMY_GROUND_HEIGHT, | |
(Math.random() - 0.5) * MAP_SIZE | |
); | |
// λλ€νκ² μ μ ν μ ν | |
const type = Math.random() > 0.7 ? 'HEAVY_TANK' : 'TANK'; | |
const enemy = new Enemy(this.scene, position, type); | |
enemy.initialize(this.loader); | |
this.enemies.push(enemy); | |
} | |
setTimeout(spawnEnemy, 3000); | |
}; | |
spawnEnemy(); | |
} | |
// Game ν΄λμ€ κ³μ... | |
startGameTimer() { | |
const timer = setInterval(() => { | |
this.gameTime--; | |
if (this.gameTime <= 0 || this.isGameOver) { | |
clearInterval(timer); | |
this.endGame(); | |
} | |
}, 1000); | |
} | |
setupEventListeners() { | |
document.addEventListener('keydown', (event) => { | |
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) => { | |
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) => { | |
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
}); | |
window.addEventListener('resize', () => { | |
this.camera.aspect = window.innerWidth / window.innerHeight; | |
this.camera.updateProjectionMatrix(); | |
this.renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
} | |
handleMovement() { | |
const direction = new THREE.Vector3(); | |
if (this.keys.forward) direction.z += 1; | |
if (this.keys.backward) direction.z -= 1; | |
if (this.keys.left) this.tank.rotate(-1); | |
if (this.keys.right) this.tank.rotate(1); | |
if (direction.length() > 0) { | |
direction.normalize(); | |
this.tank.move(direction); | |
} | |
} | |
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() { | |
const tankPosition = this.tank.getPosition(); | |
// μ κ³Όμ μΆ©λ μ²΄ν¬ | |
this.enemies.forEach(enemy => { | |
if (!enemy.mesh) return; | |
// μ΄μ μΆ©λ μ²΄ν¬ | |
enemy.bullets.forEach(bullet => { | |
const distance = bullet.position.distanceTo(tankPosition); | |
if (distance < 1) { | |
if (this.tank.takeDamage(enemy.type === 'HEAVY_TANK' ? 15 : 10)) { | |
this.endGame(); | |
} | |
this.scene.remove(bullet); | |
enemy.bullets = enemy.bullets.filter(b => b !== bullet); | |
} | |
}); | |
// 건물과μ μΆ©λ μ²΄ν¬ | |
this.buildings.forEach(building => { | |
// κ°λ¨ν μΆ©λ μ²΄ν¬ (μ€μ κ²μμμλ λ μ κ΅ν μΆ©λ 체ν¬κ° νμν μ μμ΅λλ€) | |
const distance = enemy.mesh.position.distanceTo(building.mesh.position); | |
if (distance < 5) { // μμμ μΆ©λ 거리 | |
const pushDirection = new THREE.Vector3() | |
.subVectors(enemy.mesh.position, building.mesh.position) | |
.normalize(); | |
enemy.mesh.position.add(pushDirection); | |
} | |
}); | |
}); | |
} | |
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.innerHTML = `Game Over<br>Score: ${this.score}`; | |
document.body.appendChild(gameOverDiv); | |
} | |
animate() { | |
if (this.isGameOver) return; | |
requestAnimationFrame(() => this.animate()); | |
this.tank.update(this.mouse.x, this.mouse.y); | |
this.handleMovement(); | |
const tankPosition = this.tank.getPosition(); | |
this.enemies.forEach(enemy => { | |
enemy.update(tankPosition); | |
const distance = enemy.mesh?.position.distanceTo(tankPosition) || Infinity; | |
if (distance < enemy.config.ATTACK_RANGE) { | |
enemy.shoot(tankPosition); | |
} | |
}); | |
this.updateParticles(); | |
this.checkCollisions(); | |
this.renderer.render(this.scene, this.camera); | |
} | |
} | |
// κ²μ μμ | |
const game = new Game(); |