Gunship-3D-FPS / game.js
gunship999's picture
Update game.js
7b3d86d verified
raw
history blame
24.8 kB
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 = 30; // 30m둜 μ‘°μ •
const ENEMY_GROUND_HEIGHT = 0;
const ENEMY_SCALE = 10; // 크기 λŒ€ν­ 증가
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;
let lastTime = performance.now();
// μ΄μ†Œλ¦¬ 생성기 클래슀
class GunSoundGenerator {
constructor(audioContext) {
this.audioContext = audioContext;
}
createGunshot() {
// 메인 μ˜€μ‹€λ ˆμ΄ν„° (μ΄μ†Œλ¦¬μ˜ μ£Όμš” 주파수)
const mainOsc = this.audioContext.createOscillator();
mainOsc.type = 'square';
mainOsc.frequency.setValueAtTime(100, this.audioContext.currentTime);
mainOsc.frequency.exponentialRampToValueAtTime(1, this.audioContext.currentTime + 0.1);
// λ…Έμ΄μ¦ˆ 생성
const noiseBuffer = this.createNoiseBuffer();
const noise = this.audioContext.createBufferSource();
noise.buffer = noiseBuffer;
// 게인 λ…Έλ“œλ“€
const mainGain = this.audioContext.createGain();
const noiseGain = this.audioContext.createGain();
// λ³Όλ₯¨ μ—”λ²¨λ‘œν”„ μ„€μ •
mainGain.gain.setValueAtTime(1, this.audioContext.currentTime);
mainGain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.1);
noiseGain.gain.setValueAtTime(1, this.audioContext.currentTime);
noiseGain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.05);
// μ—°κ²°
mainOsc.connect(mainGain);
noise.connect(noiseGain);
mainGain.connect(this.audioContext.destination);
noiseGain.connect(this.audioContext.destination);
// μž¬μƒ
mainOsc.start();
noise.start();
mainOsc.stop(this.audioContext.currentTime + 0.1);
noise.stop(this.audioContext.currentTime + 0.1);
}
createNoiseBuffer() {
const bufferSize = this.audioContext.sampleRate * 0.1;
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
const output = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
return buffer;
}
}
// μ‚¬μš΄λ“œ μ‹œμŠ€ν…œ
class SoundSystem {
constructor() {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.gunSoundGenerator = new GunSoundGenerator(this.audioContext);
this.buffers = new Map();
this.pools = new Map();
}
async loadSound(name, url) {
try {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
this.buffers.set(name, audioBuffer);
this.createPool(name, audioBuffer);
console.log(`Sound loaded: ${name}`);
} catch (error) {
console.error(`Error loading sound ${name}:`, error);
}
}
createPool(name, buffer, size = 10) {
const pool = [];
for (let i = 0; i < size; i++) {
pool.push({
source: null,
gainNode: this.audioContext.createGain(),
lastPlayed: 0
});
}
this.pools.set(name, pool);
}
playGunshot() {
this.gunSoundGenerator.createGunshot();
}
play(name) {
const pool = this.pools.get(name);
const buffer = this.buffers.get(name);
if (!pool || !buffer) {
console.warn(`Sound not found: ${name}`);
return;
}
const now = this.audioContext.currentTime;
const poolItem = pool.find(item => !item.source || item.lastPlayed + buffer.duration <= now);
if (poolItem) {
if (poolItem.source) {
poolItem.source.disconnect();
}
poolItem.source = this.audioContext.createBufferSource();
poolItem.source.buffer = buffer;
poolItem.source.connect(poolItem.gainNode);
poolItem.gainNode.connect(this.audioContext.destination);
poolItem.source.start(0);
poolItem.lastPlayed = now;
}
}
}
// μ‚¬μš΄λ“œ μ‹œμŠ€ν…œ μ΄ˆκΈ°ν™”
const soundSystem = new SoundSystem();
// 이동 μƒνƒœ
const moveState = {
forward: false,
backward: false,
left: false,
right: false
};
async function initSounds() {
try {
await soundSystem.loadSound('explosion', 'explosion.wav');
console.log('All sounds loaded successfully');
} catch (error) {
console.error('Error initializing sounds:', error);
}
}
async function init() {
console.log('Initializing game...');
// Scene μ΄ˆκΈ°ν™”
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 0, 1500);
// Camera μ„€μ •
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
camera.position.set(0, HELICOPTER_HEIGHT, 0);
// Renderer μ„€μ •
renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// μ‘°λͺ… μ„€μ •
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(100, 100, 50);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
scene.add(dirLight);
// Controls μ„€μ •
controls = new PointerLockControls(camera, document.body);
// 이벀트 λ¦¬μŠ€λ„ˆ
document.addEventListener('click', onClick);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
window.addEventListener('resize', onWindowResize);
try {
await initSounds();
await createTerrain();
await loadEnemies();
await testModelLoading();
document.getElementById('loading').style.display = 'none';
console.log('Game initialized successfully');
console.log('Active enemies:', enemies.length);
} catch (error) {
console.error('Initialization error:', error);
}
}
function createEnemyOutline(model) {
const outlineMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
side: THREE.BackSide
});
const outlineMesh = model.clone();
outlineMesh.traverse((node) => {
if (node.isMesh) {
node.material = outlineMaterial;
node.scale.multiplyScalar(1.05);
}
});
model.add(outlineMesh);
}
async function loadEnemies() {
console.log('Starting enemy loading...');
const loader = new GLTFLoader();
const enemyCount = 3 + currentStage;
const loadPromises = [];
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
);
console.log(`Creating enemy ${i} at position:`, position);
// μž„μ‹œ 적 생성
const tempGeometry = new THREE.BoxGeometry(5, 5, 5);
const tempMaterial = new THREE.MeshPhongMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.8,
emissive: 0xff0000,
emissiveIntensity: 0.5
});
const tempEnemy = new THREE.Mesh(tempGeometry, tempMaterial);
tempEnemy.position.copy(position);
tempEnemy.castShadow = true;
tempEnemy.receiveShadow = true;
scene.add(tempEnemy);
// μž„μ‹œ 적에 λΉ› μΆ”κ°€
const tempLight = new THREE.PointLight(0xff0000, 1, 20);
tempLight.position.set(0, 5, 0);
tempEnemy.add(tempLight);
const enemy = {
model: tempEnemy,
health: 100,
speed: ENEMY_MOVE_SPEED,
lastAttackTime: 0,
position: position.clone()
};
enemies.push(enemy);
// GLB λͺ¨λΈ λ‘œλ”©
const modelPath = ENEMY_MODELS[i % ENEMY_MODELS.length];
const loadPromise = new Promise((resolve, reject) => {
loader.load(
modelPath,
(gltf) => {
console.log(`Successfully loaded model: ${modelPath}`);
const model = gltf.scene;
model.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
model.position.copy(position);
model.traverse((node) => {
if (node.isMesh) {
node.castShadow = true;
node.receiveShadow = true;
node.material.metalness = 0.2;
node.material.roughness = 0.8;
node.material.emissive = new THREE.Color(0x444444);
node.material.emissiveIntensity = 0.5;
}
});
// 아웃라인 효과 μΆ”κ°€
createEnemyOutline(model);
// 적 주변에 λΉ› μΆ”κ°€
const enemyLight = new THREE.PointLight(0xff0000, 1, 20);
enemyLight.position.set(0, 5, 0);
model.add(enemyLight);
scene.remove(tempEnemy);
scene.add(model);
enemy.model = model;
resolve();
},
(xhr) => {
console.log(`${modelPath}: ${(xhr.loaded / xhr.total * 100)}% loaded`);
},
(error) => {
console.error(`Error loading model ${modelPath}:`, error);
reject(error);
}
);
});
loadPromises.push(loadPromise);
}
try {
await Promise.all(loadPromises);
console.log('All enemies loaded successfully');
} catch (error) {
console.error('Error loading enemies:', error);
}
}
function createTerrain() {
return new Promise((resolve) => {
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();
resolve();
});
}
function addObstacles() {
const rockGeometry = new THREE.DodecahedronGeometry(10);
const rockMaterial = new THREE.MeshStandardMaterial({
color: 0x8B4513,
roughness: 0.9
});
for (let i = 0; i < 100; i++) {
const rock = new THREE.Mesh(rockGeometry, rockMaterial);
rock.position.set(
(Math.random() - 0.5) * MAP_SIZE * 0.9,
Math.random() * 10,
(Math.random() - 0.5) * MAP_SIZE * 0.9
);
rock.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
rock.castShadow = true;
rock.receiveShadow = true;
scene.add(rock);
}
}
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,
emissive: 0xff4400,
emissiveIntensity: 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 explosionLight = new THREE.PointLight(0xff4400, 2, 20);
explosionLight.position.copy(position);
scene.add(explosionLight);
setTimeout(() => scene.remove(explosionLight), 100);
}
function 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();
soundSystem.play('explosion');
}
// 이벀트 ν•Έλ“€λŸ¬
function onClick() {
if (!controls.isLocked) {
controls.lock();
soundSystem.audioContext.resume();
} else if (ammo > 0) {
shoot();
}
}
function onKeyDown(event) {
switch(event.code) {
case 'KeyW': moveState.forward = true; break;
case 'KeyS': moveState.backward = true; break;
case 'KeyA': moveState.left = true; break;
case 'KeyD': moveState.right = true; break;
case 'KeyR': reload(); break;
}
}
function onKeyUp(event) {
switch(event.code) {
case 'KeyW': moveState.forward = false; break;
case 'KeyS': moveState.backward = false; break;
case 'KeyA': moveState.left = false; break;
case 'KeyD': moveState.right = false; break;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function shoot() {
if (ammo <= 0) return;
ammo--;
updateAmmoDisplay();
const bullet = createBullet();
bullets.push(bullet);
soundSystem.playGunshot();
// 총ꡬ ν™”μ—Ό 효과
const muzzleFlash = new THREE.PointLight(0xffff00, 3, 10);
muzzleFlash.position.copy(camera.position);
scene.add(muzzleFlash);
setTimeout(() => scene.remove(muzzleFlash), 50);
}
function createBullet() {
const bulletGeometry = new THREE.SphereGeometry(0.5);
const bulletMaterial = new THREE.MeshBasicMaterial({
color: 0xffff00,
emissive: 0xffff00,
emissiveIntensity: 1
});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
// μ΄μ•Œ 광원 효과 μΆ”κ°€
const bulletLight = new THREE.PointLight(0xffff00, 1, 5);
bullet.add(bulletLight);
bullet.position.copy(camera.position);
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
bullet.velocity = direction.multiplyScalar(5); // μ΄μ•Œ 속도 증가
scene.add(bullet);
return bullet;
}
function createEnemyBullet(enemy) {
const bulletGeometry = new THREE.SphereGeometry(0.5);
const bulletMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
emissive: 0xff0000,
emissiveIntensity: 1
});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
// 적 μ΄μ•Œ 광원 효과 μΆ”κ°€
const bulletLight = new THREE.PointLight(0xff0000, 1, 5);
bullet.add(bulletLight);
bullet.position.copy(enemy.model.position);
bullet.position.y += 5; // 총ꡬ 높이 μ‘°μ •
const direction = new THREE.Vector3();
direction.subVectors(camera.position, enemy.model.position).normalize();
bullet.velocity = direction.multiplyScalar(ENEMY_CONFIG.BULLET_SPEED);
scene.add(bullet);
return bullet;
}
function updateMovement() {
if (controls.isLocked) {
const speed = 2.0;
if (moveState.forward) controls.moveForward(speed);
if (moveState.backward) controls.moveForward(-speed);
if (moveState.left) controls.moveRight(-speed);
if (moveState.right) controls.moveRight(speed);
// 고도 μ œν•œ
if (camera.position.y < HELICOPTER_HEIGHT) {
camera.position.y = HELICOPTER_HEIGHT;
} else if (camera.position.y > HELICOPTER_HEIGHT + 10) {
camera.position.y = HELICOPTER_HEIGHT + 10;
}
}
}
function updateBullets() {
for (let i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i]) continue;
bullets[i].position.add(bullets[i].velocity);
enemies.forEach(enemy => {
if (!enemy || !enemy.model) return;
if (bullets[i] && bullets[i].position.distanceTo(enemy.model.position) < 10) { // 좩돌 λ²”μœ„ 증가
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(5, 0, 5)));
createExplosion(enemy.model.position.clone().add(new THREE.Vector3(-5, 0, -5)));
scene.remove(enemy.model);
enemies = enemies.filter(e => e !== enemy);
console.log('Enemy destroyed, remaining enemies:', enemies.length);
}
}
});
if (bullets[i] && bullets[i].position.distanceTo(camera.position) > 1000) {
scene.remove(bullets[i]);
bullets.splice(i, 1);
}
}
}
function updateEnemyBullets() {
for (let i = enemyBullets.length - 1; i >= 0; i--) {
if (!enemyBullets[i]) continue;
enemyBullets[i].position.add(enemyBullets[i].velocity);
if (enemyBullets[i].position.distanceTo(camera.position) < 3) {
playerHealth -= 10;
updateHealthBar();
createExplosion(enemyBullets[i].position.clone());
scene.remove(enemyBullets[i]);
enemyBullets.splice(i, 1);
if (playerHealth <= 0) {
gameOver(false);
}
continue;
}
if (enemyBullets[i].position.distanceTo(camera.position) > 1000) {
scene.remove(enemyBullets[i]);
enemyBullets.splice(i, 1);
}
}
}
function updateEnemies() {
const currentTime = Date.now();
enemies.forEach(enemy => {
if (!enemy || !enemy.model) {
console.warn('Invalid enemy detected');
return;
}
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) {
console.log('Enemy attacking!');
enemyBullets.push(createEnemyBullet(enemy));
enemy.lastAttackTime = currentTime;
// 곡격 μ‹œ λ°œκ΄‘ 효과
const attackFlash = new THREE.PointLight(0xff0000, 2, 20);
attackFlash.position.copy(enemy.model.position);
scene.add(attackFlash);
setTimeout(() => scene.remove(attackFlash), 100);
}
});
}
function reload() {
ammo = 30;
updateAmmoDisplay();
}
function updateAmmoDisplay() {
document.getElementById('ammo').textContent = `Ammo: ${ammo}/30`;
}
function updateHealthBar() {
const healthElement = document.getElementById('health');
const healthPercentage = (playerHealth / MAX_HEALTH) * 100;
healthElement.style.width = `${healthPercentage}%`;
}
function updateHelicopterHUD() {
const altitude = Math.round(camera.position.y);
document.querySelector('#altitude-indicator span').textContent = altitude;
const speed = Math.round(
Math.sqrt(
moveState.forward * moveState.forward +
moveState.right * moveState.right
) * 100
);
document.querySelector('#speed-indicator span').textContent = speed;
const heading = Math.round(
(camera.rotation.y * (180 / Math.PI) + 360) % 360
);
document.querySelector('#compass span').textContent = heading;
updateRadar();
}
function updateRadar() {
const radarTargets = document.querySelector('.radar-targets');
radarTargets.innerHTML = '';
enemies.forEach(enemy => {
if (!enemy || !enemy.model) return;
const relativePos = enemy.model.position.clone().sub(camera.position);
const distance = relativePos.length();
if (distance < 500) {
const playerAngle = camera.rotation.y;
const enemyAngle = Math.atan2(relativePos.x, relativePos.z);
const relativeAngle = enemyAngle - playerAngle;
const normalizedDistance = distance / 500;
const dot = document.createElement('div');
dot.className = 'radar-dot';
dot.style.left = `${50 + Math.sin(relativeAngle) * normalizedDistance * 45}%`;
dot.style.top = `${50 + Math.cos(relativeAngle) * normalizedDistance * 45}%`;
radarTargets.appendChild(dot);
}
});
}
function checkGameStatus() {
if (enemies.length === 0 && currentStage < 5) {
currentStage++;
document.getElementById('stage').style.display = 'block';
document.getElementById('stage').textContent = `Stage ${currentStage}`;
setTimeout(() => {
document.getElementById('stage').style.display = 'none';
loadEnemies();
}, 2000);
}
}
function gameOver(won) {
isGameOver = true;
controls.unlock();
soundSystem.audioContext.suspend();
alert(won ? 'Mission Complete!' : 'Game Over!');
location.reload();
}
function gameLoop() {
requestAnimationFrame(gameLoop);
if (controls.isLocked && !isGameOver) {
updateMovement();
updateBullets();
updateEnemies();
updateEnemyBullets();
updateHelicopterHUD();
checkGameStatus();
}
renderer.render(scene, camera);
}
// κ²Œμž„ μ‹œμž‘
window.addEventListener('load', async () => {
try {
await init();
console.log('Game started');
console.log('Active enemies:', enemies.length);
gameLoop();
} catch (error) {
console.error('Game initialization error:', error);
alert('Error loading game. Please check console for details.');
}
});
// 디버깅을 μœ„ν•œ μ „μ—­ μ ‘κ·Ό
window.debugGame = {
scene,
camera,
enemies,
soundSystem,
reloadEnemies: loadEnemies
};