You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Node服务器搭建无头Three.js,实现FPS游戏服务端碰撞检测?

Great question—building an authoritative backend for a Three.js FPS with client prediction is a smart move for balanced, low-latency gameplay. Let’s walk through how to tackle each part of your setup:

1. Headless Three.js Environment (Alternative to Babylon's NullEngine)

Three.js doesn’t have an official "NullEngine" like Babylon, but you can easily set up a headless environment on Node.js by simulating the browser APIs it needs. Here’s how:

First, install the required dependencies to mock the DOM and canvas:

npm install three jsdom canvas

Then, set up a simulated browser context in your server code so Three.js can initialize without a real browser:

const { JSDOM } = require('jsdom');
const THREE = require('three');

// Mock browser environment
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
global.window = dom.window;
global.document = dom.window.document;
global.navigator = dom.window.navigator;
global.HTMLElement = dom.window.HTMLElement;
global.WebGLRenderingContext = dom.window.WebGLRenderingContext;

// Now you can initialize Three.js components just like the client
const scene = new THREE.Scene();
const raycaster = new THREE.Raycaster();

// Add static collision objects (walls, obstacles, etc.)
const wall = new THREE.Mesh(new THREE.BoxGeometry(10, 2, 1), new THREE.MeshBasicMaterial());
wall.position.set(0, 1, 0);
scene.add(wall);

For raycasting collisions, use Three.js’s built-in Raycaster exactly like you would on the client. The server maintains the authoritative scene, so all bullet hits and line-of-sight checks happen here—no client-side collision logic counts for scoring or gameplay state.

2. Tracking Game State with Socket.io

Your backend needs to own the single source of truth for all game data. Here’s a straightforward approach:

  • Initialize a centralized game state: Use a map for players (keyed by Socket.io ID) and an array for bullets/projectiles. Store only critical data (position, rotation, health, bullet direction/speed) to keep payloads small.
  • Validate and process inputs: When clients send movement or shooting events, validate them first (e.g., prevent players from moving faster than the max allowed speed) before updating the state.
  • Broadcast incremental updates: Instead of sending the entire state every tick, send only changes (or the full state at fixed intervals, like 60 times per second). Serialize Three.js objects (like Vector3) to plain JSON before sending, since Socket.io can’t transmit class instances directly.

Example state management code:

const io = require('socket.io')(server);

// Authoritative game state
const gameState = {
  players: new Map(), // { socketId: { position: {x,y,z}, rotation: {x,y,z}, health: 100 } }
  bullets: []
};

io.on('connection', (socket) => {
  // Add new player to state
  gameState.players.set(socket.id, {
    position: { x: 0, y: 1, z: 0 },
    rotation: { x: 0, y: 0, z: 0 },
    health: 100
  });

  // Handle movement input
  socket.on('player-move', (input) => {
    const player = gameState.players.get(socket.id);
    if (!player) return;

    // Calculate desired position (with speed cap)
    const moveSpeed = 0.1;
    const desiredPos = {
      x: player.position.x + (input.x * moveSpeed),
      y: player.position.y,
      z: player.position.z + (input.z * moveSpeed)
    };

    // Run collision check here (we'll cover this next)
    if (!checkPlayerCollision(desiredPos)) {
      player.position = desiredPos;
    }
  });

  // Broadcast state updates every ~16ms (60 FPS)
  const stateInterval = setInterval(() => {
    const serializedState = {
      players: Array.from(gameState.players.entries()).map(([id, p]) => ({ id, ...p }))
    };
    io.emit('game-state-update', serializedState);
  }, 16);

  socket.on('disconnect', () => {
    gameState.players.delete(socket.id);
    clearInterval(stateInterval);
  });
});
3. Handling Bullet & Movement Collisions

Movement Collisions

For player movement, use Three.js’s bounding volume classes (Box3, Sphere) to detect intersections with static scene objects. Extend the headless scene with collision shapes (you don’t need to render them, just use their geometry for checks):

// Reuse the wall from the headless scene
const wallCollider = new THREE.Box3().setFromObject(wall);

function checkPlayerCollision(pos) {
  const playerCollider = new THREE.Sphere(new THREE.Vector3(pos.x, pos.y, pos.z), 0.5); // Player hitbox
  return playerCollider.intersectsBox(wallCollider);
}

If a collision is detected, block the movement or add slide logic to let players move along walls instead of stopping dead.

Bullet Collisions

For instant-hit weapons (like rifles), use Raycaster on the server when a player shoots:

function handlePlayerShoot(socketId, playerData) {
  const origin = new THREE.Vector3(playerData.position.x, playerData.position.y, playerData.position.z);
  const direction = new THREE.Vector3();
  direction.setFromEuler(new THREE.Euler(playerData.rotation.x, playerData.rotation.y, playerData.rotation.z)).normalize();

  raycaster.set(origin, direction);
  const intersects = raycaster.intersectObjects(scene.children);

  if (intersects.length > 0) {
    const hit = intersects[0];
    // Check if hit is a player (by checking distance to nearby player positions)
    const hitPlayer = Array.from(gameState.players.values()).find(p => {
      const playerPos = new THREE.Vector3(p.position.x, p.position.y, p.position.z);
      return playerPos.distanceTo(hit.point) < 0.8;
    });

    if (hitPlayer) {
      hitPlayer.health -= 25;
      // Broadcast hit event to all clients to update visuals
      io.emit('player-hit', { shooterId: socketId, targetId: hitPlayer.id, health: hitPlayer.health });
    }
  }
}

For projectile-based weapons (like grenades), simulate the bullet’s trajectory server-side each tick, checking for collisions with objects or players at each step.

Key Optimizations & Tips
  • Use BVH for fast raycasts: For complex scenes, use a library like three-mesh-bvh to add bounding volume hierarchies to your meshes. This drastically speeds up Raycaster checks, which is critical when handling multiple players and bullets.
  • Validate all inputs: Never trust client data—cap movement speed, limit fire rate, and reject any input that would break game rules (e.g., teleporting through walls).
  • Smooth client prediction corrections: When the client receives an authoritative state update, if its predicted state differs, interpolate to the server’s state over a few frames to avoid jarring jumps.

内容的提问来源于stack exchange,提问作者Bancksy

火山引擎 最新活动