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 TANK_HEIGHT = 0.5; // ํฌํ ๋์ด ์กฐ์ | |
| 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 | |
| }; | |
| // TankPlayer ํด๋์ค ์ ์ | |
| class TankPlayer { | |
| constructor() { | |
| this.body = null; | |
| this.turret = null; | |
| this.position = new THREE.Vector3(0, 0, 0); | |
| this.rotation = new THREE.Euler(0, 0, 0); | |
| this.turretRotation = 0; | |
| this.moveSpeed = 0.5; | |
| this.turnSpeed = 0.03; | |
| this.turretGroup = new THREE.Group(); // ํฌํ ๊ทธ๋ฃน ์ถ๊ฐ | |
| } | |
| async initialize(scene, loader) { | |
| try { | |
| // ๋ชธ์ฒด ๋ก๋ | |
| const bodyResult = await loader.loadAsync('/models/abramsBody.glb'); | |
| this.body = bodyResult.scene; | |
| this.body.position.copy(this.position); | |
| // ํฌํ ๋ก๋ | |
| const turretResult = await loader.loadAsync('/models/abramsTurret.glb'); | |
| this.turret = turretResult.scene; | |
| // ํฌํ ๊ทธ๋ฃน ์ค์ | |
| this.turretGroup.position.y = TANK_HEIGHT; | |
| this.turretGroup.add(this.turret); | |
| this.body.add(this.turretGroup); | |
| // ๊ทธ๋ฆผ์ ์ค์ | |
| this.body.traverse((child) => { | |
| if (child.isMesh) { | |
| child.castShadow = true; | |
| child.receiveShadow = true; | |
| } | |
| }); | |
| this.turret.traverse((child) => { | |
| if (child.isMesh) { | |
| child.castShadow = true; | |
| child.receiveShadow = true; | |
| } | |
| }); | |
| scene.add(this.body); | |
| } catch (error) { | |
| console.error('Error loading tank models:', error); | |
| } | |
| } | |
| update(mouseX, mouseY) { | |
| if (!this.body || !this.turretGroup) return; | |
| // ๋ง์ฐ์ค ์์น๋ฅผ ์ด์ฉํ ํฌํ ํ์ ๊ณ์ฐ | |
| const targetAngle = Math.atan2(mouseX, mouseY); | |
| // ํฌํ ๋ถ๋๋ฌ์ด ํ์ | |
| const currentRotation = this.turretGroup.rotation.y; | |
| const rotationDiff = targetAngle - currentRotation; | |
| this.turretGroup.rotation.y += rotationDiff * 0.1; | |
| } | |
| move(direction) { | |
| if (!this.body) return; | |
| const moveVector = new THREE.Vector3(); | |
| moveVector.x = direction.x * this.moveSpeed; | |
| moveVector.z = direction.z * this.moveSpeed; | |
| moveVector.applyEuler(this.body.rotation); | |
| this.body.position.add(moveVector); | |
| } | |
| rotate(angle) { | |
| if (!this.body) return; | |
| this.body.rotation.y += angle * this.turnSpeed; | |
| } | |
| } | |
| // TankPlayer ํด๋์ค ์์ | |
| class TankPlayer { | |
| constructor() { | |
| this.body = null; | |
| this.turret = null; | |
| this.position = new THREE.Vector3(0, 0, 0); | |
| this.rotation = new THREE.Euler(0, 0, 0); | |
| this.turretRotation = 0; | |
| this.moveSpeed = 0.5; | |
| this.turnSpeed = 0.03; | |
| this.turretGroup = new THREE.Group(); | |
| this.health = MAX_HEALTH; | |
| } | |
| async initialize(scene, loader) { | |
| try { | |
| const bodyResult = await loader.loadAsync('/models/abramsBody.glb'); | |
| this.body = bodyResult.scene; | |
| this.body.position.copy(this.position); | |
| const turretResult = await loader.loadAsync('/models/abramsTurret.glb'); | |
| this.turret = turretResult.scene; | |
| // ํฌํ ์์น ์กฐ์ | |
| this.turretGroup.position.y = 0.2; // ํฌํ ๋์ด ์กฐ์ | |
| this.turretGroup.add(this.turret); | |
| this.body.add(this.turretGroup); | |
| this.body.traverse((child) => { | |
| if (child.isMesh) { | |
| child.castShadow = true; | |
| child.receiveShadow = true; | |
| } | |
| }); | |
| this.turret.traverse((child) => { | |
| if (child.isMesh) { | |
| child.castShadow = true; | |
| child.receiveShadow = true; | |
| } | |
| }); | |
| scene.add(this.body); | |
| } catch (error) { | |
| console.error('Error loading tank models:', error); | |
| } | |
| } | |
| update(mouseX, mouseY) { | |
| if (!this.body || !this.turretGroup) return; | |
| const targetAngle = Math.atan2(mouseX, mouseY); | |
| const currentRotation = this.turretGroup.rotation.y; | |
| const rotationDiff = targetAngle - currentRotation; | |
| // ํฌํ ํ์ ๊ฐ๋ ์ ๊ทํ | |
| let normalizedDiff = rotationDiff; | |
| while (normalizedDiff > Math.PI) normalizedDiff -= Math.PI * 2; | |
| while (normalizedDiff < -Math.PI) normalizedDiff += Math.PI * 2; | |
| this.turretGroup.rotation.y += normalizedDiff * 0.1; | |
| } | |
| move(direction) { | |
| if (!this.body) return; | |
| const moveVector = new THREE.Vector3(); | |
| moveVector.x = direction.x * this.moveSpeed; | |
| moveVector.z = direction.z * this.moveSpeed; | |
| moveVector.applyEuler(this.body.rotation); | |
| this.body.position.add(moveVector); | |
| } | |
| rotate(angle) { | |
| if (!this.body) return; | |
| this.body.rotation.y += angle * this.turnSpeed; | |
| } | |
| getPosition() { | |
| return this.body ? this.body.position : new THREE.Vector3(); | |
| } | |
| takeDamage(damage) { | |
| this.health -= damage; | |
| return this.health <= 0; | |
| } | |
| } | |
| // Enemy ํด๋์ค ์ ์ | |
| class Enemy { | |
| constructor(scene, position) { | |
| this.scene = scene; | |
| this.position = position; | |
| this.mesh = null; | |
| this.health = 100; | |
| this.lastAttackTime = 0; | |
| this.bullets = []; | |
| } | |
| async initialize(loader) { | |
| try { | |
| const result = await loader.loadAsync('/models/enemy.glb'); | |
| this.mesh = result.scene; | |
| this.mesh.position.copy(this.position); | |
| this.mesh.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE); | |
| this.mesh.traverse((child) => { | |
| if (child.isMesh) { | |
| child.castShadow = true; | |
| child.receiveShadow = true; | |
| } | |
| }); | |
| this.scene.add(this.mesh); | |
| } catch (error) { | |
| console.error('Error loading enemy model:', error); | |
| } | |
| } | |
| update(playerPosition) { | |
| if (!this.mesh) return; | |
| // ํ๋ ์ด์ด ๋ฐฉํฅ์ผ๋ก ํ์ | |
| const direction = new THREE.Vector3() | |
| .subVectors(playerPosition, this.mesh.position) | |
| .normalize(); | |
| this.mesh.lookAt(playerPosition); | |
| // ํ๋ ์ด์ด ๋ฐฉํฅ์ผ๋ก ์ด๋ | |
| this.mesh.position.add(direction.multiplyScalar(ENEMY_MOVE_SPEED)); | |
| // ์ด์ ์ ๋ฐ์ดํธ | |
| for (let i = this.bullets.length - 1; i >= 0; i--) { | |
| const bullet = this.bullets[i]; | |
| bullet.position.add(bullet.velocity); | |
| // ์ด์์ด ๋งต ๋ฐ์ผ๋ก ๋๊ฐ๋ฉด ์ ๊ฑฐ | |
| if (Math.abs(bullet.position.x) > MAP_SIZE || | |
| Math.abs(bullet.position.z) > MAP_SIZE) { | |
| this.scene.remove(bullet); | |
| this.bullets.splice(i, 1); | |
| } | |
| } | |
| } | |
| shoot(playerPosition) { | |
| const currentTime = Date.now(); | |
| if (currentTime - this.lastAttackTime < ENEMY_CONFIG.ATTACK_INTERVAL) return; | |
| const bulletGeometry = new THREE.SphereGeometry(0.2); | |
| const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); | |
| const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); | |
| bullet.position.copy(this.mesh.position); | |
| const direction = new THREE.Vector3() | |
| .subVectors(playerPosition, this.mesh.position) | |
| .normalize(); | |
| bullet.velocity = direction.multiplyScalar(ENEMY_CONFIG.BULLET_SPEED); | |
| this.scene.add(bullet); | |
| this.bullets.push(bullet); | |
| this.lastAttackTime = currentTime; | |
| } | |
| takeDamage(damage) { | |
| this.health -= damage; | |
| return this.health <= 0; | |
| } | |
| destroy() { | |
| if (this.mesh) { | |
| this.scene.remove(this.mesh); | |
| this.bullets.forEach(bullet => this.scene.remove(bullet)); | |
| this.bullets = []; | |
| } | |
| } | |
| } | |
| // Particle ํด๋์ค ์ ์ | |
| class Particle { | |
| constructor(scene, position) { | |
| const geometry = new THREE.SphereGeometry(0.1); | |
| const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); | |
| this.mesh = new THREE.Mesh(geometry, material); | |
| this.mesh.position.copy(position); | |
| this.velocity = new THREE.Vector3( | |
| (Math.random() - 0.5) * 0.3, | |
| Math.random() * 0.2, | |
| (Math.random() - 0.5) * 0.3 | |
| ); | |
| this.gravity = -0.01; | |
| this.lifetime = 60; | |
| this.age = 0; | |
| scene.add(this.mesh); | |
| } | |
| update() { | |
| this.velocity.y += this.gravity; | |
| this.mesh.position.add(this.velocity); | |
| this.age++; | |
| return this.age < this.lifetime; | |
| } | |
| destroy(scene) { | |
| scene.remove(this.mesh); | |
| } | |
| } | |
| // Game ํด๋์ค ์ ์ | |
| class Game { | |
| constructor() { | |
| // ๊ธฐ๋ณธ Three.js ์ค์ | |
| this.scene = new THREE.Scene(); | |
| this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| this.renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| this.renderer.shadowMap.enabled = true; | |
| document.body.appendChild(this.renderer.domElement); | |
| // ๊ฒ์ ์์ ์ด๊ธฐํ | |
| this.tank = new TankPlayer(); | |
| this.enemies = []; | |
| this.particles = []; | |
| this.obstacles = []; | |
| this.loader = new GLTFLoader(); | |
| this.controls = null; | |
| this.gameTime = GAME_DURATION; | |
| this.score = 0; | |
| this.isGameOver = false; | |
| // ๋ง์ฐ์ค ์ํ | |
| this.mouse = { | |
| x: 0, | |
| y: 0 | |
| }; | |
| // ํค๋ณด๋ ์ํ | |
| this.keys = { | |
| forward: false, | |
| backward: false, | |
| left: false, | |
| right: false | |
| }; | |
| // ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ค์ | |
| this.setupEventListeners(); | |
| // ๊ฒ์ ์ด๊ธฐํ | |
| this.initialize(); | |
| } | |
| async initialize() { | |
| // ์กฐ๋ช ์ค์ | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
| this.scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(50, 50, 50); | |
| directionalLight.castShadow = true; | |
| this.scene.add(directionalLight); | |
| // ๋ฐ๋ฅ ์์ฑ | |
| const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE); | |
| const groundMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x808080, | |
| roughness: 0.8, | |
| metalness: 0.2 | |
| }); | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.receiveShadow = true; | |
| this.scene.add(ground); | |
| // ํฑํฌ ์ด๊ธฐํ | |
| await this.tank.initialize(this.scene, this.loader); | |
| // ์ฅ์ ๋ฌผ ์์ฑ | |
| this.createObstacles(); | |
| // ์นด๋ฉ๋ผ ์์น ์ค์ | |
| this.camera.position.set(0, 10, -10); | |
| this.camera.lookAt(0, 0, 0); | |
| // ํฌ์ธํฐ ๋ฝ ์ปจํธ๋กค ์ค์ | |
| this.controls = new PointerLockControls(this.camera, document.body); | |
| // ๊ฒ์ ์์ | |
| this.animate(); | |
| this.spawnEnemies(); | |
| this.startGameTimer(); | |
| } | |
| createObstacles() { | |
| for (let i = 0; i < OBSTACLE_COUNT; i++) { | |
| const geometry = new THREE.BoxGeometry(2, 2, 2); | |
| const material = new THREE.MeshStandardMaterial({ color: 0x808080 }); | |
| const obstacle = new THREE.Mesh(geometry, material); | |
| obstacle.position.x = (Math.random() - 0.5) * MAP_SIZE; | |
| obstacle.position.z = (Math.random() - 0.5) * MAP_SIZE; | |
| obstacle.position.y = 1; | |
| obstacle.castShadow = true; | |
| obstacle.receiveShadow = true; | |
| this.obstacles.push(obstacle); | |
| this.scene.add(obstacle); | |
| } | |
| } | |
| spawnEnemies() { | |
| const spawnEnemy = () => { | |
| if (this.enemies.length < ENEMY_COUNT_MAX && !this.isGameOver) { | |
| const position = new THREE.Vector3( | |
| (Math.random() - 0.5) * MAP_SIZE, | |
| ENEMY_GROUND_HEIGHT, | |
| (Math.random() - 0.5) * MAP_SIZE | |
| ); | |
| const enemy = new Enemy(this.scene, position); | |
| enemy.initialize(this.loader); | |
| this.enemies.push(enemy); | |
| } | |
| setTimeout(spawnEnemy, 3000); | |
| }; | |
| spawnEnemy(); | |
| } | |
| startGameTimer() { | |
| const timer = setInterval(() => { | |
| this.gameTime--; | |
| if (this.gameTime <= 0 || this.isGameOver) { | |
| clearInterval(timer); | |
| this.endGame(); | |
| } | |
| }, 1000); | |
| } | |
| setupEventListeners() { | |
| // ํค๋ณด๋ ์ด๋ฒคํธ | |
| document.addEventListener('keydown', (event) => { | |
| switch(event.code) { | |
| case 'KeyW': this.keys.forward = true; break; | |
| case 'KeyS': this.keys.backward = true; break; | |
| case 'KeyA': this.keys.left = true; break; | |
| case 'KeyD': this.keys.right = true; break; | |
| } | |
| }); | |
| document.addEventListener('keyup', (event) => { | |
| switch(event.code) { | |
| case 'KeyW': this.keys.forward = false; break; | |
| case 'KeyS': this.keys.backward = false; break; | |
| case 'KeyA': this.keys.left = false; break; | |
| case 'KeyD': this.keys.right = false; break; | |
| } | |
| }); | |
| // ๋ง์ฐ์ค ์ด๋ฒคํธ | |
| document.addEventListener('mousemove', (event) => { | |
| this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| }); | |
| // ์ฐฝ ํฌ๊ธฐ ๋ณ๊ฒฝ ์ด๋ฒคํธ | |
| window.addEventListener('resize', () => { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| } | |
| handleMovement() { | |
| const direction = new THREE.Vector3(); | |
| if (this.keys.forward) direction.z += 1; | |
| if (this.keys.backward) direction.z -= 1; | |
| if (this.keys.left) this.tank.rotate(-1); | |
| if (this.keys.right) this.tank.rotate(1); | |
| if (direction.length() > 0) { | |
| direction.normalize(); | |
| this.tank.move(direction); | |
| } | |
| } | |
| updateParticles() { | |
| for (let i = this.particles.length - 1; i >= 0; i--) { | |
| const particle = this.particles[i]; | |
| if (!particle.update()) { | |
| particle.destroy(this.scene); | |
| this.particles.splice(i, 1); | |
| } | |
| } | |
| } | |
| createExplosion(position) { | |
| for (let i = 0; i < PARTICLE_COUNT; i++) { | |
| this.particles.push(new Particle(this.scene, position)); | |
| } | |
| } | |
| checkCollisions() { | |
| const tankPosition = this.tank.getPosition(); | |
| // ์ ๊ณผ์ ์ถฉ๋ ์ฒดํฌ | |
| this.enemies.forEach(enemy => { | |
| if (!enemy.mesh) return; | |
| enemy.bullets.forEach(bullet => { | |
| const distance = bullet.position.distanceTo(tankPosition); | |
| if (distance < 1) { | |
| if (this.tank.takeDamage(10)) { | |
| this.endGame(); | |
| } | |
| this.scene.remove(bullet); | |
| enemy.bullets = enemy.bullets.filter(b => b !== bullet); | |
| } | |
| }); | |
| }); | |
| } | |
| endGame() { | |
| this.isGameOver = true; | |
| // ๊ฒ์ ์ค๋ฒ UI ํ์ | |
| const gameOverDiv = document.createElement('div'); | |
| gameOverDiv.style.position = 'absolute'; | |
| gameOverDiv.style.top = '50%'; | |
| gameOverDiv.style.left = '50%'; | |
| gameOverDiv.style.transform = 'translate(-50%, -50%)'; | |
| gameOverDiv.style.color = 'white'; | |
| gameOverDiv.style.fontSize = '48px'; | |
| gameOverDiv.innerHTML = `Game Over<br>Score: ${this.score}`; | |
| document.body.appendChild(gameOverDiv); | |
| } | |
| animate() { | |
| if (this.isGameOver) return; | |
| requestAnimationFrame(() => this.animate()); | |
| // ํฑํฌ ์ ๋ฐ์ดํธ | |
| this.tank.update(this.mouse.x, this.mouse.y); | |
| this.handleMovement(); | |
| // ์ ์ ๋ฐ์ดํธ | |
| const tankPosition = this.tank.getPosition(); | |
| this.enemies.forEach(enemy => { | |
| enemy.update(tankPosition); | |
| const distance = enemy.mesh?.position.distanceTo(tankPosition) || Infinity; | |
| if (distance < ENEMY_CONFIG.ATTACK_RANGE) { | |
| enemy.shoot(tankPosition); | |
| } | |
| }); | |
| // ํํฐํด ์ ๋ฐ์ดํธ | |
| this.updateParticles(); | |
| // ์ถฉ๋ ์ฒดํฌ | |
| this.checkCollisions(); | |
| // ๋ ๋๋ง | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| } | |
| // ๊ฒ์ ์์ | |
| const game = new Game(); |