Gunship-3D-FPS / game.js
gunship999's picture
Update game.js
08e0a03 verified
raw
history blame
10.5 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;
// μ‚¬μš΄λ“œ ν’€ 클래슀
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();