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 = 30; | |
| 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 OBSTACLE_COUNT = 50; | |
| 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(); | |
| let lastRender = 0; | |
| // μ€μ€λ μ΄ν° κΈ°λ° μ΄μ리 μμ±κΈ° | |
| class GunSoundGenerator { | |
| constructor() { | |
| this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| createGunshot() { | |
| const currentTime = this.audioContext.currentTime; | |
| // λ©μΈ μ€μ€λ μ΄ν° | |
| const osc = this.audioContext.createOscillator(); | |
| const gainNode = this.audioContext.createGain(); | |
| osc.type = 'square'; | |
| osc.frequency.setValueAtTime(200, currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(50, currentTime + 0.1); | |
| gainNode.gain.setValueAtTime(0.5, currentTime); | |
| gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.1); | |
| osc.connect(gainNode); | |
| gainNode.connect(this.audioContext.destination); | |
| osc.start(currentTime); | |
| osc.stop(currentTime + 0.1); | |
| // λ Έμ΄μ¦ μΆκ° | |
| const bufferSize = this.audioContext.sampleRate * 0.1; | |
| const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); | |
| const data = buffer.getChannelData(0); | |
| for (let i = 0; i < bufferSize; i++) { | |
| data[i] = Math.random() * 2 - 1; | |
| } | |
| const noise = this.audioContext.createBufferSource(); | |
| const noiseGain = this.audioContext.createGain(); | |
| noise.buffer = buffer; | |
| noiseGain.gain.setValueAtTime(0.2, currentTime); | |
| noiseGain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.05); | |
| noise.connect(noiseGain); | |
| noiseGain.connect(this.audioContext.destination); | |
| noise.start(currentTime); | |
| } | |
| resume() { | |
| if (this.audioContext.state === 'suspended') { | |
| this.audioContext.resume(); | |
| } | |
| } | |
| } | |
| // μ¬μ΄λ μμ€ν μ΄κΈ°ν | |
| const gunSound = new GunSoundGenerator(); | |
| async function init() { | |
| document.getElementById('loading').style.display = 'block'; | |
| try { | |
| // Scene μ΄κΈ°ν | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87ceeb); | |
| scene.fog = new THREE.Fog(0x87ceeb, 0, 1000); | |
| // Renderer μ΅μ ν | |
| renderer = new THREE.WebGLRenderer({ | |
| antialias: false, | |
| powerPreference: "high-performance" | |
| }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.BasicShadowMap; | |
| document.body.appendChild(renderer.domElement); | |
| // Camera μ€μ | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, HELICOPTER_HEIGHT, 0); | |
| // κΈ°λ³Έ μ‘°λͺ | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.6)); | |
| const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| dirLight.position.set(100, 100, 50); | |
| dirLight.castShadow = true; | |
| dirLight.shadow.mapSize.width = 1024; | |
| dirLight.shadow.mapSize.height = 1024; | |
| scene.add(dirLight); | |
| // Controls μ€μ | |
| controls = new PointerLockControls(camera, document.body); | |
| // μ΄λ²€νΈ 리μ€λ | |
| setupEventListeners(); | |
| // λͺ¨λΈ ν μ€νΈ λ¨Όμ μ€ν | |
| await testModelLoading(); | |
| // κ²μ μμ μ΄κΈ°ν | |
| await Promise.all([ | |
| createTerrain(), | |
| createEnemies() | |
| ]); | |
| document.getElementById('loading').style.display = 'none'; | |
| console.log('Game initialized successfully'); | |
| } catch (error) { | |
| console.error('Initialization error:', error); | |
| document.getElementById('loading').innerHTML = ` | |
| <div class="loading-text" style="color: #ff0000;"> | |
| Error loading models. Please check console and file paths. | |
| </div> | |
| `; | |
| throw error; | |
| } | |
| } | |
| function setupEventListeners() { | |
| document.addEventListener('click', onClick); | |
| document.addEventListener('keydown', onKeyDown); | |
| document.addEventListener('keyup', onKeyUp); | |
| window.addEventListener('resize', onWindowResize); | |
| } | |
| async function testModelLoading() { | |
| const loader = new GLTFLoader(); | |
| try { | |
| const modelPath = 'models/enemy1.glb'; | |
| console.log('Testing model loading:', modelPath); | |
| const gltf = await loader.loadAsync(modelPath); | |
| console.log('Test model loaded successfully:', gltf); | |
| } catch (error) { | |
| console.error('Test model loading failed:', error); | |
| throw error; | |
| } | |
| } | |
| function createTerrain() { | |
| return new Promise((resolve) => { | |
| const geometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100); | |
| 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 < OBSTACLE_COUNT; 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 createEnemies() { | |
| console.log('Creating enemies...'); | |
| const loader = new GLTFLoader(); | |
| const enemyCount = Math.min(3 + currentStage, ENEMY_COUNT_MAX); | |
| 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.model); | |
| enemies.push(tempEnemy); | |
| // GLB λͺ¨λΈ λ‘λ | |
| try { | |
| const modelIndex = i % 4 + 1; | |
| const modelPath = `models/enemy${modelIndex}.glb`; | |
| console.log(`Loading model: ${modelPath}`); | |
| const gltf = await loader.loadAsync(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.model); | |
| scene.add(model); | |
| enemies[enemies.indexOf(tempEnemy)].model = model; | |
| console.log(`Successfully loaded enemy model ${modelIndex}`); | |
| } catch (error) { | |
| console.error(`Error loading enemy model:`, error); | |
| } | |
| } | |
| } | |
| function createTemporaryEnemy(position) { | |
| const geometry = new THREE.BoxGeometry(5, 10, 5); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: 0xff0000, | |
| transparent: true, | |
| opacity: 0.8 | |
| }); | |
| const model = new THREE.Mesh(geometry, material); | |
| model.position.copy(position); | |
| model.castShadow = true; | |
| model.receiveShadow = true; | |
| return { | |
| model: model, | |
| health: 100, | |
| speed: ENEMY_MOVE_SPEED, | |
| lastAttackTime: 0 | |
| }; | |
| } | |
| function createExplosion(position) { | |
| const particles = []; | |
| for (let i = 0; i < PARTICLE_COUNT; 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 explosionLight = new THREE.PointLight(0xff4400, 2, 20); | |
| explosionLight.position.copy(position); | |
| scene.add(explosionLight); | |
| let opacity = 1; | |
| const animate = () => { | |
| opacity -= 0.05; | |
| if (opacity <= 0) { | |
| particles.forEach(p => scene.remove(p)); | |
| scene.remove(explosionLight); | |
| return; | |
| } | |
| particles.forEach(particle => { | |
| particle.position.add(particle.velocity); | |
| particle.material.opacity = opacity; | |
| }); | |
| requestAnimationFrame(animate); | |
| }; | |
| animate(); | |
| } | |
| function onClick() { | |
| if (!controls.isLocked) { | |
| controls.lock(); | |
| gunSound.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); | |
| } | |
| // μ΄λ μν | |
| const moveState = { | |
| forward: false, | |
| backward: false, | |
| left: false, | |
| right: false | |
| }; | |
| function shoot() { | |
| if (ammo <= 0) return; | |
| ammo--; | |
| updateAmmoDisplay(); | |
| const bullet = createBullet(); | |
| bullets.push(bullet); | |
| gunSound.createGunshot(); | |
| // μ΄κ΅¬ νμΌ ν¨κ³Ό | |
| const muzzleFlash = new THREE.PointLight(0xffff00, 3, 10); | |
| muzzleFlash.position.copy(camera.position); | |
| scene.add(muzzleFlash); | |
| setTimeout(() => scene.remove(muzzleFlash), 50); | |
| } | |
| function createBullet() { | |
| const bullet = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.5), | |
| new THREE.MeshBasicMaterial({ | |
| color: 0xffff00, | |
| emissive: 0xffff00, | |
| emissiveIntensity: 1 | |
| }) | |
| ); | |
| 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 bullet = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.5), | |
| new THREE.MeshBasicMaterial({ | |
| color: 0xff0000, | |
| emissive: 0xff0000, | |
| emissiveIntensity: 1 | |
| }) | |
| ); | |
| 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); | |
| // μ κ³Όμ μΆ©λ κ²μ¬ | |
| for (let j = enemies.length - 1; j >= 0; j--) { | |
| const enemy = enemies[j]; | |
| if (!enemy || !enemy.model) continue; | |
| 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()); | |
| scene.remove(enemy.model); | |
| enemies.splice(j, 1); | |
| } | |
| break; | |
| } | |
| } | |
| // λ²μ λ²μ΄λ μ΄μ μ κ±° | |
| 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) return; | |
| // μ μ΄λ λ‘μ§ | |
| const direction = new THREE.Vector3(); | |
| direction.subVectors(camera.position, enemy.model.position); | |
| direction.y = 0; | |
| 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) { | |
| 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() { | |
| document.querySelector('#altitude-indicator span').textContent = | |
| Math.round(camera.position.y); | |
| 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'; | |
| createEnemies(); | |
| }, 2000); | |
| } | |
| } | |
| function cleanupResources() { | |
| bullets.forEach(bullet => scene.remove(bullet)); | |
| bullets = []; | |
| enemyBullets.forEach(bullet => scene.remove(bullet)); | |
| enemyBullets = []; | |
| enemies.forEach(enemy => { | |
| if (enemy && enemy.model) { | |
| scene.remove(enemy.model); | |
| } | |
| }); | |
| enemies = []; | |
| } | |
| function gameOver(won) { | |
| isGameOver = true; | |
| controls.unlock(); | |
| cleanupResources(); | |
| setTimeout(() => { | |
| alert(won ? 'Mission Complete!' : 'Game Over!'); | |
| location.reload(); | |
| }, 100); | |
| } | |
| function gameLoop(timestamp) { | |
| requestAnimationFrame(gameLoop); | |
| // νλ μ μ ν (60fps) | |
| if (timestamp - lastRender < 16) { | |
| return; | |
| } | |
| lastRender = timestamp; | |
| if (controls.isLocked && !isGameOver) { | |
| updateMovement(); | |
| updateBullets(); | |
| updateEnemies(); | |
| updateEnemyBullets(); | |
| updateHelicopterHUD(); | |
| checkGameStatus(); | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| // μ±λ₯ λͺ¨λν°λ§ | |
| let lastFpsUpdate = 0; | |
| let frameCount = 0; | |
| function updateFPS(timestamp) { | |
| frameCount++; | |
| if (timestamp - lastFpsUpdate >= 1000) { | |
| const fps = Math.round(frameCount * 1000 / (timestamp - lastFpsUpdate)); | |
| console.log('FPS:', fps); | |
| frameCount = 0; | |
| lastFpsUpdate = timestamp; | |
| } | |
| requestAnimationFrame(updateFPS); | |
| } | |
| // κ²μ μμ | |
| window.addEventListener('load', async () => { | |
| try { | |
| await init(); | |
| console.log('Game started'); | |
| console.log('Active enemies:', enemies.length); | |
| gameLoop(performance.now()); | |
| updateFPS(performance.now()); | |
| } catch (error) { | |
| console.error('Game initialization error:', error); | |
| document.getElementById('loading').innerHTML = ` | |
| <div class="loading-text" style="color: #ff0000;"> | |
| Error loading game. Please check console and file paths. | |
| </div> | |
| `; | |
| } | |
| }); | |
| // λλ²κΉ μ μν μ μ μ κ·Ό | |
| window.debugGame = { | |
| scene, | |
| camera, | |
| enemies, | |
| gunSound, | |
| reloadEnemies: createEnemies | |
| }; |