3D Football Game
by BlazingKnight42578 lines12.8 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Football Game</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
background: #153d20;
}
#hud {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
background: #111d;
color: white;
padding: 12px 20px;
border-radius: 12px;
text-align: center;
z-index: 2;
}
#score { font-size: 25px; font-weight: bold; }
#controls {
position: fixed;
bottom: 12px;
left: 12px;
color: white;
background: #111d;
padding: 12px;
border-radius: 8px;
line-height: 1.6;
}
#message {
position: fixed;
top: 25%;
width: 100%;
text-align: center;
color: white;
font-size: 30px;
font-weight: bold;
text-shadow: 2px 2px 5px black;
pointer-events: none;
}
button {
cursor: pointer;
padding: 8px 12px;
border-radius: 6px;
border: none;
}
</style>
</head>
<body>
<div id="hud">
<div id="score">BLUE 0 - 0 RED</div>
<div id="timer">2:00</div>
<button onclick="restart()">Restart Match</button>
</div>
<div id="message">KICK OFF!</div>
<div id="controls">
WASD / Arrow keys: Move<br>
SPACE: Shoot<br>
E: Pass<br>
SHIFT: Sprint<br>
R: Restart
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script>
if (!window.THREE) {
document.getElementById("message").textContent =
"Please connect to the internet and refresh.";
} else {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
const camera = new THREE.PerspectiveCamera(
60, innerWidth / innerHeight, 0.1, 300
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff, 0x446644, 2));
const sunlight = new THREE.DirectionalLight(0xffffff, 2);
sunlight.position.set(-20, 40, 20);
scene.add(sunlight);
function material(color) {
return new THREE.MeshStandardMaterial({ color });
}
const blueMat = material(0x1676ff);
const redMat = material(0xef3434);
const whiteMat = material(0xffffff);
const darkMat = material(0x222222);
const skinMat = material(0xf1c7a4);
// Football pitch
const field = new THREE.Mesh(
new THREE.PlaneGeometry(50, 90),
material(0x258039)
);
field.rotation.x = -Math.PI / 2;
field.position.y = -0.1;
scene.add(field);
// Pitch markings
function makeLine(x1, z1, x2, z2) {
const dx = x2 - x1;
const dz = z2 - z1;
const length = Math.hypot(dx, dz);
const line = new THREE.Mesh(
new THREE.BoxGeometry(length, 0.04, 0.12),
whiteMat
);
line.position.set((x1+x2)/2, 0, (z1+z2)/2);
line.rotation.y = -Math.atan2(dz, dx);
scene.add(line);
}
makeLine(-25,-45,25,-45);
makeLine(-25,45,25,45);
makeLine(-25,-45,-25,45);
makeLine(25,-45,25,45);
makeLine(-25,0,25,0);
const circle = new THREE.Mesh(
new THREE.TorusGeometry(8, 0.07, 8, 64),
whiteMat
);
circle.rotation.x = Math.PI / 2;
circle.position.y = 0.04;
scene.add(circle);
// Goals
function createGoal(z) {
for (const x of [-5, 5]) {
const post = new THREE.Mesh(
new THREE.CylinderGeometry(0.12,0.12,3,12),
whiteMat
);
post.position.set(x,1.5,z);
scene.add(post);
}
const crossbar = new THREE.Mesh(
new THREE.BoxGeometry(10,0.15,0.15),
whiteMat
);
crossbar.position.set(0,3,z);
scene.add(crossbar);
}
createGoal(-44);
createGoal(44);
// Players
function createPlayer(team, x, z, isUser=false, goalkeeper=false) {
const group = new THREE.Group();
const body = new THREE.Mesh(
new THREE.CylinderGeometry(0.4,0.5,1,12),
team
);
body.position.y = 1.35;
group.add(body);
const head = new THREE.Mesh(
new THREE.SphereGeometry(0.27,12,12),
skinMat
);
head.position.y = 2.05;
group.add(head);
const shorts = new THREE.Mesh(
new THREE.CylinderGeometry(0.3,0.35,0.35,10),
darkMat
);
shorts.position.y = 0.65;
group.add(shorts);
for (const side of [-1,1]) {
const leg = new THREE.Mesh(
new THREE.BoxGeometry(0.16,0.55,0.18),
darkMat
);
leg.position.set(side*0.18,0.28,0);
group.add(leg);
}
group.position.set(x,0,z);
group.userData = {
homeX:x, homeZ:z, isUser, goalkeeper, team
};
scene.add(group);
return group;
}
const user = createPlayer(blueMat,0,15,true);
const teammates = [
[-12,22],[12,22],[-18,7],[18,7],
[-8,-2],[8,-2],[-15,-20],[15,-20]
].map(p => createPlayer(blueMat,p[0],p[1]));
const opponents = [
[0,-12],[-12,-23],[12,-23],[-18,-5],
[18,-5],[-8,5],[8,5],[-14,20],[14,20]
].map(p => createPlayer(redMat,p[0],p[1]));
const keeperBlue = createPlayer(blueMat,0,40,false,true);
const keeperRed = createPlayer(redMat,0,-40,false,true);
const players = [
user,...teammates,...opponents,keeperBlue,keeperRed
];
// Ball
const ball = new THREE.Mesh(
new THREE.SphereGeometry(0.42,20,16),
material(0xffffff)
);
ball.position.set(0,0.45,13);
scene.add(ball);
let ballVelocity = new THREE.Vector3();
let keys = {};
let blueScore = 0;
let redScore = 0;
let timeLeft = 120;
let cooldown = 0;
let pause = 1.5;
let finished = false;
let passTarget = null;
const scoreEl = document.getElementById("score");
const timerEl = document.getElementById("timer");
const messageEl = document.getElementById("message");
function updateScore() {
scoreEl.textContent =
`BLUE ${blueScore} - ${redScore} RED`;
}
function say(text) {
messageEl.textContent = text;
}
function clampPlayer(p) {
p.position.x = THREE.MathUtils.clamp(p.position.x,-24,24);
p.position.z = THREE.MathUtils.clamp(p.position.z,-42,42);
}
function nearBall() {
return Math.hypot(
user.position.x-ball.position.x,
user.position.z-ball.position.z
) < 2.2;
}
// Pass to nearest teammate
function passBall() {
if (pause > 0 || finished || cooldown > 0) return;
if (!nearBall()) {
say("GET CLOSER TO THE BALL!");
return;
}
let target = teammates[0];
let bestDistance = Infinity;
for (const p of teammates) {
const d = Math.hypot(
p.position.x-user.position.x,
p.position.z-user.position.z
);
if (d < bestDistance) {
bestDistance = d;
target = p;
}
}
const dx = target.position.x-ball.position.x;
const dz = target.position.z-ball.position.z;
const d = Math.hypot(dx,dz) || 1;
ballVelocity.set(dx/d*15,0,dz/d*15);
passTarget = target;
cooldown = 0.4;
say("PASS!");
}
// Shoot toward the red goal
function shootBall() {
if (pause > 0 || finished || cooldown > 0) return;
if (!nearBall()) {
say("GET CLOSER TO THE BALL!");
return;
}
const power = keys.Shift ? 30 : 23;
ballVelocity.set(
(user.position.x-ball.position.x)*0.25,
0,
-power
);
cooldown = 0.5;
say("SHOT!");
}
// Keyboard controls
window.addEventListener("keydown", e => {
const key = e.key === " " ? "Space" : e.key;
keys[key] = true;
if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "]
.includes(e.key)) e.preventDefault();
if (key === "Space") shootBall();
if (key.toLowerCase() === "e") passBall();
if (key.toLowerCase() === "r") restart();
});
window.addEventListener("keyup", e => {
keys[e.key === " " ? "Space" : e.key] = false;
});
function restart() {
blueScore = 0;
redScore = 0;
timeLeft = 120;
finished = false;
cooldown = 0;
passTarget = null;
ballVelocity.set(0,0,0);
user.position.set(0,0,15);
[...teammates,...opponents].forEach(p => {
p.position.set(p.userData.homeX,0,p.userData.homeZ);
});
keeperBlue.position.set(0,0,40);
keeperRed.position.set(0,0,-40);
ball.position.set(0,0.45,13);
updateScore();
pause = 1.5;
say("KICK OFF!");
}
function separatePlayers() {
for (let i=0; i<players.length; i++) {
for (let j=i+1; j<players.length; j++) {
const a = players[i];
const b = players[j];
const dx = b.position.x-a.position.x;
const dz = b.position.z-a.position.z;
const d = Math.hypot(dx,dz);
const minDistance = 1.1;
if (d > 0 && d < minDistance) {
const push = (minDistance-d)/2;
const nx = dx/d;
const nz = dz/d;
if (!a.userData.goalkeeper) {
a.position.x -= nx*push;
a.position.z -= nz*push;
}
if (!b.userData.goalkeeper) {
b.position.x += nx*push;
b.position.z += nz*push;
}
clampPlayer(a);
clampPlayer(b);
}
}
}
}
function moveAI(p,dt) {
const dx = ball.position.x-p.position.x;
const dz = ball.position.z-p.position.z;
const d = Math.hypot(dx,dz);
let tx = p.userData.homeX;
let tz = p.userData.homeZ;
if (d < 12) {
tx = ball.position.x;
tz = ball.position.z;
}
const mx = tx-p.position.x;
const mz = tz-p.position.z;
const n = Math.hypot(mx,mz);
if (n > 0.2) {
const speed = 4*dt;
p.position.x += mx/n*Math.min(speed,n);
p.position.z += mz/n*Math.min(speed,n);
}
clampPlayer(p);
}
function resetBall() {
ball.position.set(0,0.45,0);
ballVelocity.set(0,0,0);
passTarget = null;
pause = 1.5;
}
let lastTime = performance.now();
function animate(now) {
requestAnimationFrame(animate);
const dt = Math.min((now-lastTime)/1000,0.035);
lastTime = now;
if (pause > 0) {
pause -= dt;
if (pause <= 0 && !finished) say("");
} else if (!finished) {
timeLeft -= dt;
if (timeLeft <= 0) {
timeLeft = 0;
finished = true;
say("FULL TIME!");
}
cooldown = Math.max(0,cooldown-dt);
let mx = 0, mz = 0;
if (keys.KeyA || keys.a || keys.ArrowLeft) mx--;
if (keys.KeyD || keys.d || keys.ArrowRight) mx++;
if (keys.KeyW || keys.w || keys.ArrowUp) mz--;
if (keys.KeyS || keys.s || keys.ArrowDown) mz++;
if (mx || mz) {
const n = Math.hypot(mx,mz);
const speed = (keys.Shift ? 11 : 7)*dt;
user.position.x += mx/n*speed;
user.position.z += mz/n*speed;
clampPlayer(user);
}
[...teammates,...opponents].forEach(p => moveAI(p,dt));
// Keep the pass moving toward the teammate.
if (passTarget) {
const d = Math.hypot(
ball.position.x-passTarget.position.x,
ball.position.z-passTarget.position.z
);
if (d < 1.4) {
ball.position.x = passTarget.position.x;
ball.position.z = passTarget.position.z;
ballVelocity.set(0,0,0);
passTarget = null;
say("PASS RECEIVED!");
}
}
// Dribbling
if (nearBall() && ballVelocity.length() < 4 && !passTarget) {
ballVelocity.x +=
(user.position.x-ball.position.x)*dt*6;
ballVelocity.z +=
(user.position.z-1-ball.position.z)*dt*6;
}
// Ball physics
ball.position.x += ballVelocity.x*dt;
ball.position.z += ballVelocity.z*dt;
ballVelocity.multiplyScalar(Math.pow(0.985,dt*60));
if (ballVelocity.length() < 0.15) {
ballVelocity.set(0,0,0);
}
// Goal detection
if (Math.abs(ball.position.z) > 44 &&
Math.abs(ball.position.x) < 5) {
if (ball.position.z < 0) {
blueScore++;
say("GOAL FOR BLUE!");
} else {
redScore++;
say("GOAL FOR RED!");
}
updateScore();
resetBall();
}
// Ball goes out of the sideline
if (Math.abs(ball.position.x) > 25) {
ball.position.x = Math.sign(ball.position.x)*24;
ballVelocity.x *= -0.5;
say("THROW-IN");
}
// Goalkeepers follow the ball
for (const keeper of [keeperBlue,keeperRed]) {
keeper.position.x += (
THREE.MathUtils.clamp(ball.position.x,-4,4)
-keeper.position.x
)*dt*2;
if (Math.hypot(
keeper.position.x-ball.position.x,
keeper.position.z-ball.position.z
) < 1.3) {
ballVelocity.z *= -0.8;
}
}
separatePlayers();
timerEl.textContent =
`${Math.floor(timeLeft/60)}:`+
`${String(Math.floor(timeLeft%60)).padStart(2,"0")}`;
}
camera.position.lerp(
new THREE.Vector3(user.position.x*0.4,14,user.position.z+21),
0.08
);
camera.lookAt(user.position.x*0.3,0,user.position.z-5);
renderer.render(scene,camera);
}
window.addEventListener("resize", () => {
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
updateScore();
requestAnimationFrame(animate);
}
</script>
</body>
</html>
Game Source: 3D Football Game
Creator: BlazingKnight42
Libraries: three
Complexity: complex (578 lines, 12.8 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: 3d-football-game-blazingknight42" to link back to the original. Then publish at arcadelab.ai/publish.