Spaces:
Running
Running
<html> | |
<head> | |
<title>3D Shooter Game</title> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
margin: 0; | |
overflow: hidden; | |
} | |
#info { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
color: white; | |
background: rgba(0,0,0,0.7); | |
padding: 10px; | |
font-family: Arial; | |
font-size: 14px; | |
z-index: 100; | |
border-radius: 5px; | |
user-select: none; | |
} | |
#timer { | |
position: absolute; | |
top: 10px; | |
right: 10px; | |
color: white; | |
background: rgba(0,0,0,0.7); | |
padding: 10px; | |
font-family: Arial; | |
font-size: 14px; | |
z-index: 100; | |
border-radius: 5px; | |
} | |
#crosshair { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
color: rgba(255, 255, 255, 0.8); | |
font-size: 24px; | |
z-index: 100; | |
user-select: none; | |
pointer-events: none; | |
} | |
#healthBar { | |
position: absolute; | |
bottom: 20px; | |
left: 20px; | |
width: 200px; | |
height: 20px; | |
background: rgba(0,0,0,0.5); | |
border: 2px solid white; | |
z-index: 100; | |
border-radius: 10px; | |
overflow: hidden; | |
} | |
#health { | |
width: 100%; | |
height: 100%; | |
background: #ff3333; | |
transition: width 0.3s; | |
} | |
#ammo { | |
position: absolute; | |
bottom: 20px; | |
right: 20px; | |
color: white; | |
background: rgba(0,0,0,0.7); | |
padding: 10px; | |
font-family: Arial; | |
font-size: 18px; | |
z-index: 100; | |
border-radius: 5px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="info"> | |
Click to start<br> | |
WASD - Move<br> | |
Mouse - Look around<br> | |
Left Click - Shoot<br> | |
R - Reload | |
</div> | |
<div id="timer">Safe Time: 10s</div> | |
<div id="crosshair">+</div> | |
<div id="healthBar"><div id="health"></div></div> | |
<div id="ammo">Ammo: 30/30</div> | |
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"three": "https://unpkg.com/three@0.157.0/build/three.module.js", | |
"three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/" | |
} | |
} | |
</script> | |
<script type="module"> | |
import * as THREE from 'three'; | |
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; | |
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; | |
// ๊ธฐ๋ณธ ๋ณ์ | |
let scene, camera, renderer, controls; | |
let enemies = [], bullets = []; | |
let enemyBullets = []; | |
let isGameOver = false; | |
let isSafePeriod = true; | |
let startTime = 0; | |
let playerHealth = 100; | |
let ammo = 30; | |
const maxAmmo = 30; | |
const SAFE_TIME = 10; | |
const raycaster = new THREE.Raycaster(); | |
const mouse = new THREE.Vector2(); | |
// Scene ์ด๊ธฐํ | |
function initScene() { | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x87ceeb); | |
scene.fog = new THREE.Fog(0x87ceeb, 0, 500); | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.set(0, 5, 0); | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
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, 1.0); | |
scene.add(ambientLight); | |
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2); | |
dirLight.position.set(50, 50, 0); | |
dirLight.castShadow = true; | |
dirLight.shadow.mapSize.width = 2048; | |
dirLight.shadow.mapSize.height = 2048; | |
dirLight.shadow.camera.far = 300; | |
scene.add(dirLight); | |
// ๋ฐ์ฌ๊ด | |
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6); | |
scene.add(hemiLight); | |
} | |
// ์ด์ ์์ฑ | |
function createBullet(isEnemy = false) { | |
const bulletGeometry = new THREE.SphereGeometry(0.1, 8, 8); | |
const bulletMaterial = new THREE.MeshBasicMaterial({ | |
color: isEnemy ? 0xff0000 : 0xffff00 | |
}); | |
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); | |
if (!isEnemy) { | |
bullet.position.copy(camera.position); | |
const direction = new THREE.Vector3(); | |
camera.getWorldDirection(direction); | |
bullet.velocity = direction.multiplyScalar(2); | |
} | |
scene.add(bullet); | |
return bullet; | |
} | |
// ๋ฐ์ฌ | |
function shoot() { | |
if (ammo <= 0) return; | |
ammo--; | |
updateAmmoDisplay(); | |
const bullet = createBullet(); | |
bullets.push(bullet); | |
// ๋ฐ์ฌ ์ฌ์ด๋๋ ์ดํํธ๋ฅผ ์ถ๊ฐํ ์ ์์ | |
} | |
// ์ ๋ฐ์ฌ | |
function enemyShoot(enemy) { | |
const bullet = createBullet(true); | |
bullet.position.copy(enemy.model.position); | |
const direction = new THREE.Vector3(); | |
direction.subVectors(camera.position, enemy.model.position); | |
direction.normalize(); | |
bullet.velocity = direction.multiplyScalar(1); | |
enemyBullets.push(bullet); | |
} | |
// ์ด์ ์ ๋ฐ์ดํธ | |
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) < 2) { | |
scene.remove(bullets[i]); | |
bullets.splice(i, 1); | |
enemy.health -= 25; | |
if (enemy.health <= 0) { | |
scene.remove(enemy.model); | |
enemies = enemies.filter(e => e !== enemy); | |
} | |
return; | |
} | |
}); | |
// ์๋ช ์ ํ | |
if (bullets[i] && bullets[i].position.distanceTo(camera.position) > 100) { | |
scene.remove(bullets[i]); | |
bullets.splice(i, 1); | |
} | |
} | |
// ์ ์ด์ | |
for (let i = enemyBullets.length - 1; i >= 0; i--) { | |
enemyBullets[i].position.add(enemyBullets[i].velocity); | |
// ํ๋ ์ด์ด์์ ์ถฉ๋ ์ฒดํฌ | |
if (enemyBullets[i].position.distanceTo(camera.position) < 2) { | |
scene.remove(enemyBullets[i]); | |
enemyBullets.splice(i, 1); | |
playerHealth -= 10; | |
updateHealthBar(); | |
if (playerHealth <= 0) { | |
gameOver(false); | |
} | |
continue; | |
} | |
// ์๋ช ์ ํ | |
if (enemyBullets[i] && enemyBullets[i].position.distanceTo(camera.position) > 100) { | |
scene.remove(enemyBullets[i]); | |
enemyBullets.splice(i, 1); | |
} | |
} | |
} | |
function updateHealthBar() { | |
const healthBar = document.getElementById('health'); | |
healthBar.style.width = `${playerHealth}%`; | |
} | |
function updateAmmoDisplay() { | |
document.getElementById('ammo').textContent = `Ammo: ${ammo}/${maxAmmo}`; | |
} | |
// ์ฌ์ฅ์ | |
function reload() { | |
ammo = maxAmmo; | |
updateAmmoDisplay(); | |
} | |
// ์งํ ์์ฑ | |
function createTerrain() { | |
const geometry = new THREE.PlaneGeometry(300, 300, 50, 50); | |
const material = new THREE.MeshStandardMaterial({ | |
color: 0x3a8c3a, | |
roughness: 0.6, | |
metalness: 0.1 | |
}); | |
const vertices = geometry.attributes.position.array; | |
for (let i = 0; i < vertices.length; i += 3) { | |
vertices[i + 2] = Math.random() * 3; | |
} | |
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); | |
} | |
// ์ ๋ก๋ | |
function loadEnemies() { | |
const loader = new GLTFLoader(); | |
loader.load('enemy.glb', (gltf) => { | |
console.log('Enemy model loaded successfully'); | |
const enemyModel = gltf.scene; | |
for (let i = 0; i < 5; i++) { | |
const enemy = enemyModel.clone(); | |
enemy.scale.set(1, 1, 1); // ํฌ๊ธฐ ์ฆ๊ฐ | |
const angle = (i / 5) * Math.PI * 2; | |
const radius = 50; // ๋ ๊ฐ๊น๊ฒ ์์ | |
enemy.position.set( | |
Math.cos(angle) * radius, | |
2, // ์ง๋ฉด์์ ์ฝ๊ฐ ์๋ก | |
Math.sin(angle) * radius | |
); | |
enemy.traverse((node) => { | |
if (node.isMesh) { | |
node.material = node.material.clone(); // ์ฌ์ง ๋ณต์ | |
node.material.metalness = 0.3; | |
node.material.roughness = 0.5; | |
node.castShadow = true; | |
node.receiveShadow = true; | |
} | |
}); | |
scene.add(enemy); | |
enemies.push({ | |
model: enemy, | |
health: 100, | |
speed: 0.2, | |
velocity: new THREE.Vector3(), | |
lastShot: 0 | |
}); | |
} | |
}, | |
// ๋ก๋ฉ ์งํ์ํฉ | |
(xhr) => { | |
console.log((xhr.loaded / xhr.total * 100) + '% loaded'); | |
}, | |
// ์๋ฌ ์ฒ๋ฆฌ | |
(error) => { | |
console.error('Error loading enemy model:', error); | |
}); | |
} | |
// ์ปจํธ๋กค ์ด๊ธฐํ | |
function initControls() { | |
controls = new PointerLockControls(camera, document.body); | |
const moveState = { | |
forward: false, | |
backward: false, | |
left: false, | |
right: false | |
}; | |
// ํค๋ณด๋ ์ด๋ฒคํธ | |
document.addEventListener('keydown', (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; | |
} | |
}); | |
document.addEventListener('keyup', (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; | |
} | |
}); | |
// ๋ง์ฐ์ค ์ด๋ฒคํธ | |
document.addEventListener('click', () => { | |
if (controls.isLocked) { | |
shoot(); | |
} else { | |
controls.lock(); | |
} | |
}); | |
controls.addEventListener('lock', () => { | |
document.getElementById('info').style.display = 'none'; | |
if (!startTime) { | |
startTime = Date.now(); | |
} | |
}); | |
controls.addEventListener('unlock', () => { | |
document.getElementById('info').style.display = 'block'; | |
}); | |
return moveState; | |
} | |
// ์ ์ ๋ฐ์ดํธ | |
function updateEnemies() { | |
if (!isSafePeriod) { | |
enemies.forEach(enemy => { | |
// ์ด๋ ๋ก์ง | |
const direction = new THREE.Vector3(); | |
direction.subVectors(camera.position, enemy.model.position); | |
direction.y = 0; | |
direction.normalize(); | |
enemy.velocity.lerp(direction.multiplyScalar(enemy.speed), 0.02); | |
enemy.model.position.add(enemy.velocity); | |
// ํ์ | |
enemy.model.lookAt(camera.position); | |
// ๋ฐ์ฌ ๋ก์ง | |
const now = Date.now(); | |
if (now - enemy.lastShot > 2000) { // 2์ด๋ง๋ค ๋ฐ์ฌ | |
enemyShoot(enemy); | |
enemy.lastShot = now; | |
} | |
}); | |
} | |
} | |
// ๊ฒ์ ์ค๋ฒ ์ฒ๋ฆฌ | |
function gameOver(won) { | |
if (!isGameOver) { | |
isGameOver = true; | |
controls.unlock(); | |
setTimeout(() => { | |
alert(won ? 'You won!' : 'Game Over! You were eliminated!'); | |
location.reload(); | |
}, 100); | |
} | |
} | |
// ๊ฒ์ ์ด๊ธฐํ | |
function init() { | |
initScene(); | |
createTerrain(); | |
loadEnemies(); | |
const moveState = initControls(); | |
// ๊ฒ์ ๋ฃจํ | |
function animate() { | |
requestAnimationFrame(animate); | |
if (controls.isLocked && !isGameOver) { | |
// ์์ ์๊ฐ ์ฒดํฌ | |
if (startTime) { | |
const elapsed = Math.floor((Date.now() - startTime) / 1000); | |
const remaining = SAFE_TIME - elapsed; | |
if (remaining > 0) { | |
document.getElementById('timer').textContent = `Safe Time: ${remaining}s`; | |
} else { | |
document.getElementById('timer').textContent = ''; | |
isSafePeriod = false; | |
} | |
} | |
// ์ด๋ ์ฒ๋ฆฌ | |
const speed = 0.3; | |
if (moveState.forward) controls.moveForward(speed); | |
if (moveState.backward) controls.moveForward(-speed); | |
if (moveState.left) controls.moveRight(-speed); | |
if (moveState.right) controls.moveRight(speed); | |
// ์ด์ ์ ๋ฐ์ดํธ | |
updateBullets(); | |
// ์ ์ ๋ฐ์ดํธ | |
updateEnemies(); | |
// ์น๋ฆฌ ์กฐ๊ฑด ์ฒดํฌ | |
if (!isSafePeriod && enemies.length === 0) { | |
gameOver(true); | |
} | |
} | |
renderer.render(scene, camera); | |
} | |
animate(); | |
} | |
// ํ๋ฉด ํฌ๊ธฐ ์กฐ์ ์ฒ๋ฆฌ | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
// ๊ฒ์ ์์ | |
init(); | |
</script> | |
</body> | |
</html> |