Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>3D Battle Royale</title>
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
body {
margin: 0;
overflow: hidden;
background: #000;
font-family: Arial, sans-serif;
touch-action: none;
}
#game {
width: 100vw;
height: 100vh;
}
#hud {
position: fixed;
top: 15px;
left: 15px;
color: white;
z-index: 10;
font-size: 18px;
text-shadow: 2px 2px 4px black;
}
#healthBox {
width: 180px;
height: 20px;
background: #333;
border: 2px solid white;
border-radius: 10px;
overflow: hidden;
margin-top: 5px;
}
#health {
width: 100%;
height: 100%;
background: #20e020;
}
#crosshair {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 30px;
z-index: 5;
text-shadow: 0 0 5px black;
}
#message {
display: none;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: white;
text-align: center;
z-index: 50;
font-size: 35px;
font-weight: bold;
}
#message button {
padding: 12px 25px;
font-size: 18px;
margin-top: 20px;
border: 0;
border-radius: 10px;
}
#mobileControls {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 20;
}
.control {
pointer-events: auto;
position: absolute;
width: 65px;
height: 65px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,.7);
background: rgba(0,0,0,.4);
color: white;
font-size: 25px;
display: flex;
justify-content: center;
align-items: center;
}
#left {
left: 25px;
bottom: 75px;
}
#right {
left: 170px;
bottom: 75px;
}
#forward {
left: 98px;
bottom: 145px;
}
#back {
left: 98px;
bottom: 5px;
}
#shoot {
right: 30px;
bottom: 80px;
width: 90px;
height: 90px;
background: rgba(200,0,0,.65);
}
#aim {
right: 135px;
bottom: 35px;
}
#run {
right: 30px;
bottom: 190px;
}
@media (min-width: 900px) {
#mobileControls {
display: none;
}
}
</style>
</head>
<body>
<div id="game"></div>
<div id="hud">
β€οΈ HP
<div id="healthBox">
<div id="health"></div>
</div>
π« Ammo: <span id="ammo">30</span><br>
π― Kills: <span id="kills">0</span><br>
π Score: <span id="score">0</span>
</div>
<div id="crosshair">+</div>
<div id="message">
<div id="messageText">GAME OVER</div>
<button onclick="location.reload()">PLAY AGAIN</button>
</div>
<div id="mobileControls">
<div class="control" id="forward">β²</div>
<div class="control" id="left">β</div>
<div class="control" id="right">βΆ</div>
<div class="control" id="back">βΌ</div>
<div class="control" id="aim">π―</div>
<div class="control" id="shoot">π«</div>
<div class="control" id="run">π</div>
</div>
<script type="importmap">
{
"imports": {
"three":
"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
/* =========================
BASIC SETUP
========================= */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 40, 180);
const camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
0.1,
500
);
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(
window.innerWidth,
window.innerHeight
);
renderer.setPixelRatio(
Math.min(window.devicePixelRatio, 2)
);
renderer.shadowMap.enabled = true;
document.getElementById("game").appendChild(renderer.domElement);
/* =========================
LIGHT
========================= */
const sunlight = new THREE.DirectionalLight(
0xffffff,
2
);
sunlight.position.set(50, 100, 30);
sunlight.castShadow = true;
scene.add(sunlight);
scene.add(
new THREE.HemisphereLight(
0xffffff,
0x444444,
1.2
)
);
/* =========================
GROUND
========================= */
const groundGeometry =
new THREE.PlaneGeometry(300, 300);
const groundMaterial =
new THREE.MeshStandardMaterial({
color: 0x3d8b3d
});
const ground =
new THREE.Mesh(
groundGeometry,
groundMaterial
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
/* =========================
WATER
========================= */
const waterGeometry =
new THREE.PlaneGeometry(500, 500);
const waterMaterial =
new THREE.MeshStandardMaterial({
color: 0x2080c0,
transparent: true,
opacity: .8
});
const water =
new THREE.Mesh(
waterGeometry,
waterMaterial
);
water.rotation.x = -Math.PI / 2;
water.position.y = -0.2;
scene.add(water);
/* =========================
PLAYER
========================= */
const player = new THREE.Group();
scene.add(player);
player.position.set(
0,
1,
20
);
/* BODY */
const body =
new THREE.Mesh(
new THREE.BoxGeometry(
1,
1.5,
.6
),
new THREE.MeshStandardMaterial({
color: 0x2255ff
})
);
body.position.y = 1;
body.castShadow = true;
player.add(body);
/* HEAD */
const head =
new THREE.Mesh(
new THREE.SphereGeometry(
.38,
16,
16
),
new THREE.MeshStandardMaterial({
color: 0xffc49b
})
);
head.position.y = 2;
head.castShadow = true;
player.add(head);
/* GUN */
const gun =
new THREE.Mesh(
new THREE.BoxGeometry(
.2,
.2,
1.5
),
new THREE.MeshStandardMaterial({
color: 0x222222
})
);
gun.position.set(
.55,
1.25,
-.8
);
player.add(gun);
/* =========================
ENEMIES
========================= */
let enemies = [];
let kills = 0;
let score = 0;
function createEnemy() {
const enemy =
new THREE.Group();
enemy.position.set(
(Math.random() - .5) * 120,
0,
(Math.random() - .5) * 120
);
const enemyBody =
new THREE.Mesh(
new THREE.BoxGeometry(
1,
1.5,
.6
),
new THREE.MeshStandardMaterial({
color: 0xff2222
})
);
enemyBody.position.y = 1;
enemyBody.castShadow = true;
enemy.add(enemyBody);
const enemyHead =
new THREE.Mesh(
new THREE.SphereGeometry(
.38,
16,
16
),
new THREE.MeshStandardMaterial({
color: 0xffc49b
})
);
enemyHead.position.y = 2;
enemy.add(enemyHead);
enemy.userData.health = 100;
scene.add(enemy);
enemies.push(enemy);
}
for (let i = 0; i < 12; i++) {
createEnemy();
}
/* =========================
TREES
========================= */
function createTree(x, z) {
const tree = new THREE.Group();
const trunk =
new THREE.Mesh(
new THREE.CylinderGeometry(
.25,
.35,
3
),
new THREE.MeshStandardMaterial({
color: 0x75451f
})
);
trunk.position.y = 1.5;
tree.add(trunk);
const leaves =
new THREE.Mesh(
new THREE.SphereGeometry(
1.5,
12,
12
),
new THREE.MeshStandardMaterial({
color: 0x08752c
})
);
leaves.position.y = 3.5;
tree.add(leaves);
tree.position.set(x, 0, z);
scene.add(tree);
}
for (let i = 0; i < 80; i++) {
createTree(
(Math.random() - .5) * 250,
(Math.random() - .5) * 250
);
}
/* =========================
BUILDINGS
========================= */
function createBuilding(x, z) {
const building =
new THREE.Mesh(
new THREE.BoxGeometry(
5,
8 + Math.random() * 8,
5
),
new THREE.MeshStandardMaterial({
color:
new THREE.Color(
Math.random(),
Math.random(),
Math.random()
)
})
);
building.position.set(
x,
building.geometry.parameters.height / 2,
z
);
building.castShadow = true;
scene.add(building);
}
for (let i = 0; i < 20; i++) {
createBuilding(
(Math.random() - .5) * 150,
(Math.random() - .5) * 150
);
}
/* =========================
BULLETS
========================= */
let bullets = [];
let ammo = 30;
function shoot() {
if (ammo <= 0) return;
ammo--;
document.getElementById(
"ammo"
).textContent = ammo;
const bullet =
new THREE.Mesh(
new THREE.SphereGeometry(
.08,
8,
8
),
new THREE.MeshBasicMaterial({
color: 0xffff00
})
);
bullet.position.copy(
player.position
);
bullet.position.y += 1.4;
const direction =
new THREE.Vector3();
camera.getWorldDirection(
direction
);
bullet.userData.velocity =
direction.multiplyScalar(2);
scene.add(bullet);
bullets.push(bullet);
}
/* =========================
PLAYER HEALTH
========================= */
let health = 100;
function damagePlayer(amount) {
health -= amount;
if (health < 0)
health = 0;
document.getElementById(
"health"
).style.width = health + "%";
if (health <= 0) {
document.getElementById(
"message"
).style.display = "block";
document.getElementById(
"messageText"
).textContent = "π YOU DIED";
gameOver = true;
}
}
/* =========================
KEYBOARD
========================= */
const keys = {};
document.addEventListener(
"keydown",
e => {
keys[e.key.toLowerCase()] = true;
if (e.key === " ")
shoot();
}
);
document.addEventListener(
"keyup",
e => {
keys[e.key.toLowerCase()] = false;
}
);
/* =========================
MOBILE CONTROLS
========================= */
function mobileButton(
id,
key
) {
const button =
document.getElementById(id);
button.addEventListener(
"touchstart",
e => {
e.preventDefault();
keys[key] = true;
}
);
button.addEventListener(
"touchend",
e => {
e.preventDefault();
keys[key] = false;
}
);
}
mobileButton(
"forward",
"w"
);
mobileButton(
"back",
"s"
);
mobileButton(
"left",
"a"
);
mobileButton(
"right",
"d"
);
document.getElementById(
"shoot"
).addEventListener(
"touchstart",
e => {
e.preventDefault();
shoot();
}
);
/* =========================
PLAYER MOVEMENT
========================= */
let gameOver = false;
const clock =
new THREE.Clock();
function updatePlayer(delta) {
if (gameOver)
return;
let speed = 7;
if (keys["shift"])
speed = 13;
if (keys["w"])
player.translateZ(
-speed * delta
);
if (keys["s"])
player.translateZ(
speed * delta
);
if (keys["a"])
player.rotation.y +=
2.5 * delta;
if (keys["d"])
player.rotation.y -=
2.5 * delta;
/* keep player inside island */
player.position.x =
THREE.MathUtils.clamp(
player.position.x,
-140,
140
);
player.position.z =
THREE.MathUtils.clamp(
player.position.z,
-140,
140
);
}
/* =========================
ENEMY AI
========================= */
function updateEnemies(delta) {
enemies.forEach(
enemy => {
if (!enemy)
return;
const distance =
enemy.position.distanceTo(
player.position
);
if (distance < 45) {
enemy.lookAt(
player.position.x,
0,
player.position.z
);
if (distance > 5) {
enemy.translateZ(
-2 * delta
);
}
else {
damagePlayer(
5 * delta
);
}
}
}
);
}
/* =========================
BULLET SYSTEM
========================= */
function updateBullets(delta) {
bullets.forEach(
(bullet, bIndex) => {
bullet.position.add(
bullet.userData.velocity
.clone()
.multiplyScalar(
delta * 60
)
);
enemies.forEach(
(enemy, eIndex) => {
if (
bullet.position.distanceTo(
enemy.position
) < 1.5
) {
enemy.userData.health -= 50;
scene.remove(bullet);
bullets.splice(
bIndex,
1
);
if (
enemy.userData.health <= 0
) {
scene.remove(enemy);
enemies.splice(
eIndex,
1
);
kills++;
score += 100;
document.getElementById(
"kills"
).textContent = kills;
document.getElementById(
"score"
).textContent = score;
setTimeout(
createEnemy,
1500
);
}
}
}
);
if (
bullet.position.length() > 250
) {
scene.remove(bullet);
}
}
);
}
/* =========================
THIRD PERSON CAMERA
========================= */
function updateCamera() {
const cameraOffset =
new THREE.Vector3(
0,
5,
8
);
cameraOffset.applyQuaternion(
player.quaternion
);
const target =
player.position
.clone()
.add(cameraOffset);
camera.position.lerp(
target,
.12
);
camera.lookAt(
player.position.x,
player.position.y + 1,
player.position.z
);
}
/* =========================
GAME LOOP
========================= */
function animate() {
requestAnimationFrame(
animate
);
const delta =
Math.min(
clock.getDelta(),
.05
);
updatePlayer(delta);
updateEnemies(delta);
updateBullets(delta);
updateCamera();
renderer.render(
scene,
camera
);
}
animate();
/* =========================
RESIZE
========================= */
window.addEventListener(
"resize",
() => {
camera.aspect =
window.innerWidth /
window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(
window.innerWidth,
window.innerHeight
);
}
);
</script>
</body>
</html>3
2
385KB
1977KB
497.0ms
200.0ms
498.0ms