TankWar3D / game.js
cutechicken's picture
Update game.js
96897f0 verified
raw
history blame
18 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 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();