Gunship-3D-FPS / game.js
gunship999's picture
Update game.js
5326e18 verified
raw
history blame
21.1 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 = 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;
let lastTime = performance.now();
// μ‚¬μš΄λ“œ μ‹œμŠ€ν…œ
class SoundSystem {
constructor() {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
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);
}
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('gunshot', 'gun.wav');
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);
}
}
async function testModelLoading() {
const loader = new GLTFLoader();
const testPath = ENEMY_MODELS[0];
try {
const gltf = await new Promise((resolve, reject) => {
loader.load(testPath, resolve, undefined, reject);
});
console.log('Test model loaded successfully:', gltf);
} catch (error) {
console.error('Test model loading failed:', error);
console.error('Test path was:', testPath);
}
}
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);
}
}
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
});
const tempEnemy = new THREE.Mesh(tempGeometry, tempMaterial);
tempEnemy.position.copy(position);
tempEnemy.castShadow = true;
tempEnemy.receiveShadow = true;
scene.add(tempEnemy);
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;
}
});
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 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);
}
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.play('gunshot');
}
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);
bullet.position.copy(camera.position);
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
bullet.velocity = direction.multiplyScalar(3);
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);
bullet.position.copy(enemy.model.position);
bullet.position.y += 2; // 총ꡬ 높이 μ‘°μ •
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 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 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;
}
}
}
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) < 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);
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();
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;
}
});
}
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);
const time = performance.now();
const delta = (time - lastTime) / 1000;
lastTime = time;
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
};