🎮ArcadeLab

3D Football Game

by BlazingKnight42
578 lines12.8 KB🛠️ Three.js (3D graphics)
▶ Play
<!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.