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 HELICOPTER_HEIGHT = 100; // ν¬λ¦¬μ½₯ν° κ³ λ μν₯ | |
const ENEMY_GROUND_HEIGHT = 5; // μ μ§μ λμ΄ | |
const ENEMY_SCALE = 3; | |
const MAX_HEALTH = 1000; | |
const ENEMY_MOVE_SPEED = 0.1; // μ μ΄λ μλ | |
const ENEMY_MODELS = [ | |
'./models/enemy1.glb', | |
'./models/enemy12.glb', | |
'./models/enemy13.glb', | |
'./models/enemy14.glb' | |
]; | |
const ENEMY_CONFIG = { | |
ATTACK_RANGE: 100, | |
ATTACK_INTERVAL: 2000, | |
BULLET_SPEED: 2 | |
}; | |
// κ²μ λ³μ | |
let scene, camera, renderer, controls; | |
let enemies = []; | |
let bullets = []; | |
let enemyBullets = []; | |
let playerHealth = MAX_HEALTH; | |
let ammo = 30; | |
let currentStage = 1; | |
let isGameOver = false; | |
// μ¬μ΄λ ν ν΄λμ€ | |
class SoundPool { | |
constructor(soundUrl, poolSize = 10) { | |
this.sounds = []; | |
this.currentIndex = 0; | |
this.poolSize = poolSize; | |
for (let i = 0; i < poolSize; i++) { | |
const sound = new Audio(soundUrl); | |
sound.preload = 'auto'; | |
this.sounds.push(sound); | |
} | |
} | |
play() { | |
const sound = this.sounds[this.currentIndex]; | |
sound.pause(); | |
sound.currentTime = 0; | |
const playPromise = sound.play(); | |
if (playPromise !== undefined) { | |
playPromise.catch(error => { | |
console.error("Sound play error:", error); | |
this.sounds[this.currentIndex] = new Audio(sound.src); | |
}); | |
} | |
this.currentIndex = (this.currentIndex + 1) % this.poolSize; | |
} | |
} | |
// μ¬μ΄λ μ΄κΈ°ν | |
const sounds = { | |
bgm: new Audio('Music.wav'), | |
gunshot: new SoundPool('gun.wav', 20), | |
explosion: new SoundPool('explosion.wav', 10) // νλ° μ¬μ΄λ μΆκ° | |
}; | |
sounds.bgm.loop = true; | |
// μ΄λ μν | |
const moveState = { | |
forward: false, | |
backward: false, | |
left: false, | |
right: false | |
}; | |
function init() { | |
console.log('Game initialized'); | |
console.log('Available enemy models:', ENEMY_MODELS); | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x87ceeb); | |
scene.fog = new THREE.Fog(0x87ceeb, 0, 1500); | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000); | |
camera.position.set(0, HELICOPTER_HEIGHT, 0); | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
document.body.appendChild(renderer.domElement); | |
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0); | |
scene.add(ambientLight); | |
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0); | |
dirLight.position.set(100, 100, 50); | |
dirLight.castShadow = true; | |
scene.add(dirLight); | |
controls = new PointerLockControls(camera, document.body); | |
document.addEventListener('click', onClick); | |
document.addEventListener('keydown', onKeyDown); | |
document.addEventListener('keyup', onKeyUp); | |
window.addEventListener('resize', onWindowResize); | |
createTerrain(); | |
console.log('Starting to load enemies...'); | |
loadEnemies(); | |
} | |
// νλ° ν¨κ³Ό μμ± ν¨μ | |
function createExplosion(position) { | |
const particleCount = 30; | |
const particles = []; | |
for (let i = 0; i < particleCount; i++) { | |
const particle = new THREE.Mesh( | |
new THREE.SphereGeometry(0.3), | |
new THREE.MeshBasicMaterial({ | |
color: 0xff4400, | |
transparent: true, | |
opacity: 1 | |
}) | |
); | |
particle.position.copy(position); | |
particle.velocity = new THREE.Vector3( | |
(Math.random() - 0.5) * 2, | |
Math.random() * 2, | |
(Math.random() - 0.5) * 2 | |
); | |
particles.push(particle); | |
scene.add(particle); | |
} | |
// νλ° μ λλ©μ΄μ | |
const animateExplosion = () => { | |
particles.forEach((particle, i) => { | |
particle.position.add(particle.velocity); | |
particle.material.opacity -= 0.02; | |
if (particle.material.opacity <= 0) { | |
scene.remove(particle); | |
particles.splice(i, 1); | |
} | |
}); | |
if (particles.length > 0) { | |
requestAnimationFrame(animateExplosion); | |
} | |
}; | |
animateExplosion(); | |
sounds.explosion.play(); | |
} | |
function createTerrain() { | |
const geometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 200, 200); | |
const material = new THREE.MeshStandardMaterial({ | |
color: 0xD2B48C, | |
roughness: 0.8, | |
metalness: 0.2 | |
}); | |
const vertices = geometry.attributes.position.array; | |
for (let i = 0; i < vertices.length; i += 3) { | |
vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20; | |
} | |
geometry.attributes.position.needsUpdate = true; | |
geometry.computeVertexNormals(); | |
const terrain = new THREE.Mesh(geometry, material); | |
terrain.rotation.x = -Math.PI / 2; | |
terrain.receiveShadow = true; | |
scene.add(terrain); | |
addObstacles(); | |
} | |
function createTemporaryEnemy(position) { | |
const geometry = new THREE.BoxGeometry(5, 5, 5); | |
const material = new THREE.MeshPhongMaterial({ color: 0xff0000 }); | |
const cube = new THREE.Mesh(geometry, material); | |
cube.position.copy(position); | |
return cube; | |
} | |
function loadEnemies() { | |
const loader = new GLTFLoader(); | |
const enemyCount = 3 + currentStage; | |
for (let i = 0; i < enemyCount; i++) { | |
const angle = (i / enemyCount) * Math.PI * 2; | |
const radius = 200; | |
const position = new THREE.Vector3( | |
Math.cos(angle) * radius, | |
ENEMY_GROUND_HEIGHT, | |
Math.sin(angle) * radius | |
); | |
const tempEnemy = createTemporaryEnemy(position); | |
scene.add(tempEnemy); | |
const modelPath = ENEMY_MODELS[i % ENEMY_MODELS.length]; | |
console.log('Loading enemy model:', modelPath); | |
loader.load( | |
modelPath, | |
(gltf) => { | |
console.log('Successfully loaded enemy model:', modelPath); | |
const enemy = gltf.scene; | |
enemy.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE); | |
enemy.position.copy(position); | |
enemy.rotation.y = Math.PI; | |
enemy.traverse((node) => { | |
if (node.isMesh) { | |
node.castShadow = true; | |
node.receiveShadow = true; | |
node.material.metalness = 0.2; | |
node.material.roughness = 0.8; | |
} | |
}); | |
scene.remove(tempEnemy); | |
scene.add(enemy); | |
const index = enemies.findIndex(e => e.model === tempEnemy); | |
if (index !== -1) { | |
enemies[index].model = enemy; | |
enemies[index].muzzleFlash = createMuzzleFlash(); | |
enemy.add(enemies[index].muzzleFlash); | |
} | |
}, | |
(xhr) => { | |
console.log(`${modelPath}: ${(xhr.loaded / xhr.total * 100)}% loaded`); | |
}, | |
(error) => { | |
console.error('Error loading enemy model:', modelPath, error); | |
} | |
); | |
enemies.push({ | |
model: tempEnemy, | |
health: 100, | |
speed: ENEMY_MOVE_SPEED, | |
lastAttackTime: 0 | |
}); | |
} | |
} | |
// λλ¨Έμ§ ν¨μλ€μ μ΄μ κ³Ό λμΌνκ² μ μ§λλ©°, updateEnemiesμ updateBullets ν¨μλ§ μμ λ©λλ€. | |
function updateEnemies() { | |
const currentTime = Date.now(); | |
enemies.forEach(enemy => { | |
const direction = new THREE.Vector3(); | |
direction.subVectors(camera.position, enemy.model.position); | |
direction.y = 0; // yμΆ μ΄λ μ ν | |
direction.normalize(); | |
const newPosition = enemy.model.position.clone() | |
.add(direction.multiplyScalar(enemy.speed)); | |
newPosition.y = ENEMY_GROUND_HEIGHT; | |
enemy.model.position.copy(newPosition); | |
// μ μ΄ νλ μ΄μ΄λ₯Ό λ°λΌλ³΄λλ‘ μ€μ | |
enemy.model.lookAt(new THREE.Vector3( | |
camera.position.x, | |
enemy.model.position.y, | |
camera.position.z | |
)); | |
const distanceToPlayer = enemy.model.position.distanceTo(camera.position); | |
if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE && | |
currentTime - enemy.lastAttackTime > ENEMY_CONFIG.ATTACK_INTERVAL) { | |
if (enemy.muzzleFlash) { | |
enemy.muzzleFlash.material.opacity = 1; | |
enemy.muzzleFlash.visible = true; | |
setTimeout(() => { | |
enemy.muzzleFlash.visible = false; | |
enemy.muzzleFlash.material.opacity = 0; | |
}, 100); | |
} | |
enemyBullets.push(createEnemyBullet(enemy)); | |
enemy.lastAttackTime = currentTime; | |
} | |
}); | |
} | |
function updateBullets() { | |
for (let i = bullets.length - 1; i >= 0; i--) { | |
bullets[i].position.add(bullets[i].velocity); | |
enemies.forEach(enemy => { | |
if (bullets[i].position.distanceTo(enemy.model.position) < 5) { | |
scene.remove(bullets[i]); | |
bullets.splice(i, 1); | |
enemy.health -= 25; | |
// νΌκ²© ν¨κ³Ό | |
createExplosion(enemy.model.position.clone()); | |
if (enemy.health <= 0) { | |
// νκ΄΄ ν¨κ³Ό | |
createExplosion(enemy.model.position.clone()); | |
createExplosion(enemy.model.position.clone().add(new THREE.Vector3(2, 0, 2))); | |
createExplosion(enemy.model.position.clone().add(new THREE.Vector3(-2, 0, -2))); | |
scene.remove(enemy.model); | |
enemies = enemies.filter(e => e !== enemy); | |
} | |
} | |
}); | |
if (bullets[i] && bullets[i].position.distanceTo(camera.position) > 1000) { | |
scene.remove(bullets[i]); | |
bullets.splice(i, 1); | |
} | |
} | |
} | |
// κ²μ μμ | |
init(); | |
gameLoop(); |