第28节:网络同步与多人在线3D场景

第28节:网络同步与多人在线3D场景

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,可以分享一下给大家。点击跳转到网站。
https://www.captainbed.cn/ccc

概述

多人在线3D场景是现代Web应用的重要发展方向,涉及实时网络通信、状态同步、冲突检测等复杂技术。本节将深入探索WebSocket通信架构、权威服务器模式、预测与调和算法,构建稳定可靠的多人交互体验。

多人同步系统架构:
多人在线系统 网络层 同步层 表现层 WebSocket连接 数据传输 连接管理 状态同步 预测算法 冲突解决 实体渲染 动画同步 特效管理 心跳检测 插值计算 视觉平滑

核心原理深度解析

网络同步模型

多人游戏常用的同步架构对比:

模型类型 架构特点 适用场景 延迟处理
权威服务器 服务器验证所有操作 竞技游戏、MMO 客户端预测+服务器调和
P2P对等 节点间直接通信 小规模联机 锁步同步、帧同步
混合模式 区域服务器+中继 大型开放世界 分区分层同步

同步策略选择

根据应用需求选择合适的同步粒度:

  1. 状态同步

    • 全量状态定期同步
    • 增量状态实时同步
    • 关键事件立即同步
  2. 输入同步

    • 只同步用户输入
    • 服务器计算确定结果
    • 客户端预测显示

完整代码实现

多人在线3D场景系统

vue 复制代码
<template>
  <div class="multiplayer-container">
    <!-- 主渲染区域 -->
    <canvas ref="renderCanvas" class="render-canvas"></canvas>
    
    <!-- 连接状态面板 -->
    <div class="connection-panel" :class="connectionStatus">
      <div class="status-indicator"></div>
      <span class="status-text">{{ connectionText }}</span>
      <span class="ping-display">Ping: {{ currentPing }}ms</span>
    </div>

    <!-- 玩家信息面板 -->
    <div class="players-panel">
      <h4>在线玩家 ({{ playerCount }})</h4>
      <div class="players-list">
        <div 
          v-for="player in connectedPlayers" 
          :key="player.id"
          class="player-item"
          :class="{ local: player.isLocal }"
        >
          <span class="player-name">{{ player.name }}</span>
          <span class="player-ping">{{ player.ping }}ms</span>
        </div>
      </div>
    </div>

    <!-- 控制面板 -->
    <div class="control-panel">
      <div class="panel-section">
        <h4>连接设置</h4>
        <div class="connection-controls">
          <input 
            v-model="serverAddress" 
            placeholder="服务器地址"
            class="server-input"
          >
          <button 
            @click="connectToServer" 
            :disabled="isConnected"
            class="connect-button"
          >
            {{ isConnected ? '已连接' : '连接服务器' }}
          </button>
          <button 
            @click="disconnectFromServer" 
            :disabled="!isConnected"
            class="disconnect-button"
          >
            断开连接
          </button>
        </div>
      </div>

      <div class="panel-section">
        <h4>同步设置</h4>
        <div class="sync-settings">
          <label class="setting-item">
            <input type="checkbox" v-model="enablePrediction">
            客户端预测
          </label>
          <label class="setting-item">
            <input type="checkbox" v-model="enableInterpolation">
            插值平滑
          </label>
          <label class="setting-item">
            <input type="checkbox" v-model="enableReconciliation">
            状态调和
          </label>
        </div>
      </div>

      <div class="panel-section">
        <h4>网络统计</h4>
        <div class="network-stats">
          <div class="stat-item">
            <span>上行: {{ formatBytes(uploadRate) }}/s</span>
          </div>
          <div class="stat-item">
            <span>下行: {{ formatBytes(downloadRate) }}/s</span>
          </div>
          <div class="stat-item">
            <span>丢包率: {{ packetLoss }}%</span>
          </div>
          <div class="stat-item">
            <span>抖动: {{ networkJitter }}ms</span>
          </div>
        </div>
      </div>
    </div>

    <!-- 聊天面板 -->
    <div class="chat-panel">
      <div class="chat-messages">
        <div 
          v-for="message in chatMessages" 
          :key="message.id"
          class="chat-message"
          :class="message.type"
        >
          <span class="message-sender">{{ message.sender }}:</span>
          <span class="message-content">{{ message.content }}</span>
          <span class="message-time">{{ formatTime(message.timestamp) }}</span>
        </div>
      </div>
      <div class="chat-input-container">
        <input 
          v-model="chatInput" 
          @keyup.enter="sendChatMessage"
          placeholder="输入聊天消息..."
          class="chat-input"
        >
        <button @click="sendChatMessage" class="send-button">发送</button>
      </div>
    </div>

    <!-- 连接状态遮罩 -->
    <div v-if="showLoading" class="loading-overlay">
      <div class="loading-content">
        <div class="spinner"></div>
        <p>{{ loadingMessage }}</p>
      </div>
    </div>
  </div>
</template>

<script>
import { onMounted, onUnmounted, ref, reactive, computed } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 网络连接管理器
class NetworkManager {
  constructor() {
    this.socket = null;
    this.isConnected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.reconnectInterval = 2000;
    
    this.messageHandlers = new Map();
    this.pendingMessages = new Map();
    
    this.setupMessageHandlers();
  }

  // 连接到服务器
  async connect(serverUrl) {
    return new Promise((resolve, reject) => {
      try {
        this.socket = new WebSocket(serverUrl);
        
        this.socket.onopen = () => {
          this.isConnected = true;
          this.reconnectAttempts = 0;
          console.log('WebSocket连接已建立');
          resolve();
        };
        
        this.socket.onmessage = (event) => {
          this.handleMessage(JSON.parse(event.data));
        };
        
        this.socket.onclose = () => {
          this.isConnected = false;
          console.log('WebSocket连接已关闭');
          this.handleDisconnection();
        };
        
        this.socket.onerror = (error) => {
          console.error('WebSocket错误:', error);
          reject(error);
        };
        
      } catch (error) {
        reject(error);
      }
    });
  }

  // 处理断开连接
  handleDisconnection() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      setTimeout(() => {
        this.reconnectAttempts++;
        this.connect(this.socket.url);
      }, this.reconnectInterval);
    }
  }

  // 发送消息
  send(messageType, data, reliable = true) {
    if (!this.isConnected) return false;
    
    const message = {
      type: messageType,
      data: data,
      timestamp: Date.now(),
      sequence: this.generateSequenceId()
    };
    
    if (reliable) {
      this.pendingMessages.set(message.sequence, message);
    }
    
    this.socket.send(JSON.stringify(message));
    return true;
  }

  // 注册消息处理器
  on(messageType, handler) {
    if (!this.messageHandlers.has(messageType)) {
      this.messageHandlers.set(messageType, []);
    }
    this.messageHandlers.get(messageType).push(handler);
  }

  // 处理接收到的消息
  handleMessage(message) {
    const handlers = this.messageHandlers.get(message.type) || [];
    handlers.forEach(handler => handler(message.data));
    
    // 确认可靠消息
    if (message.ack) {
      this.pendingMessages.delete(message.ack);
    }
  }

  // 生成序列ID
  generateSequenceId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }

  // 断开连接
  disconnect() {
    if (this.socket) {
      this.socket.close();
      this.socket = null;
    }
    this.isConnected = false;
  }
}

// 实体同步管理器
class EntitySyncManager {
  constructor(networkManager, scene) {
    this.networkManager = networkManager;
    this.scene = scene;
    this.entities = new Map();
    this.localEntities = new Map();
    this.predictionBuffer = new Map();
    
    this.setupNetworkHandlers();
  }

  // 设置网络处理器
  setupNetworkHandlers() {
    this.networkManager.on('entityCreate', this.handleEntityCreate.bind(this));
    this.networkManager.on('entityUpdate', this.handleEntityUpdate.bind(this));
    this.networkManager.on('entityDestroy', this.handleEntityDestroy.bind(this));
    this.networkManager.on('worldState', this.handleWorldState.bind(this));
  }

  // 处理实体创建
  handleEntityCreate(entityData) {
    const entity = this.createEntity(entityData);
    this.entities.set(entityData.id, entity);
    
    if (entityData.owner === this.networkManager.clientId) {
      this.localEntities.set(entityData.id, entity);
    }
  }

  // 处理实体更新
  handleEntityUpdate(updateData) {
    const entity = this.entities.get(updateData.id);
    if (!entity) return;
    
    // 如果是本地实体,进行预测调和
    if (this.localEntities.has(updateData.id)) {
      this.reconcileEntity(entity, updateData);
    } else {
      this.applyEntityUpdate(entity, updateData);
    }
  }

  // 预测调和
  reconcileEntity(entity, serverState) {
    const predictedStates = this.predictionBuffer.get(entity.userData.id) || [];
    
    // 找到对应的预测状态
    const matchingStateIndex = predictedStates.findIndex(
      state => state.sequence === serverState.sequence
    );
    
    if (matchingStateIndex !== -1) {
      // 移除已确认的状态
      predictedStates.splice(0, matchingStateIndex + 1);
      
      // 如果有未确认的状态,重新应用
      if (predictedStates.length > 0) {
        this.reapplyPredictedStates(entity, predictedStates);
      }
    } else {
      // 没有匹配的预测状态,强制同步到服务器状态
      this.applyEntityUpdate(entity, serverState);
    }
  }

  // 重新应用预测状态
  reapplyPredictedStates(entity, predictedStates) {
    predictedStates.forEach(state => {
      this.applyEntityUpdate(entity, state, true);
    });
  }

  // 应用实体更新
  applyEntityUpdate(entity, updateData, isPrediction = false) {
    if (updateData.position) {
      if (isPrediction) {
        entity.position.lerp(
          new THREE.Vector3().fromArray(updateData.position),
          0.3
        );
      } else {
        entity.position.fromArray(updateData.position);
      }
    }
    
    if (updateData.rotation) {
      entity.rotation.fromArray(updateData.rotation);
    }
    
    if (updateData.animation) {
      this.updateEntityAnimation(entity, updateData.animation);
    }
    
    // 保存预测状态
    if (isPrediction && this.localEntities.has(entity.userData.id)) {
      this.savePredictionState(entity, updateData.sequence);
    }
  }

  // 创建实体
  createEntity(entityData) {
    let mesh;
    
    switch (entityData.type) {
      case 'player':
        mesh = this.createPlayerEntity(entityData);
        break;
      case 'npc':
        mesh = this.createNPCEntity(entityData);
        break;
      case 'item':
        mesh = this.createItemEntity(entityData);
        break;
      default:
        mesh = this.createDefaultEntity(entityData);
    }
    
    mesh.userData = {
      id: entityData.id,
      type: entityData.type,
      owner: entityData.owner,
      lastUpdate: Date.now()
    };
    
    this.scene.add(mesh);
    return mesh;
  }

  // 创建玩家实体
  createPlayerEntity(entityData) {
    const geometry = new THREE.CapsuleGeometry(0.5, 1, 4, 8);
    const material = new THREE.MeshStandardMaterial({
      color: entityData.color || 0x00ff00,
      roughness: 0.7,
      metalness: 0.3
    });
    
    const mesh = new THREE.Mesh(geometry, material);
    mesh.castShadow = true;
    
    // 添加玩家标签
    const nameLabel = this.createNameLabel(entityData.name);
    mesh.add(nameLabel);
    
    return mesh;
  }

  // 创建名称标签
  createNameLabel(name) {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    canvas.width = 256;
    canvas.height = 64;
    
    context.fillStyle = 'rgba(0, 0, 0, 0.7)';
    context.fillRect(0, 0, canvas.width, canvas.height);
    
    context.font = '24px Arial';
    context.fillStyle = 'white';
    context.textAlign = 'center';
    context.fillText(name, canvas.width / 2, canvas.height / 2 + 8);
    
    const texture = new THREE.CanvasTexture(canvas);
    const material = new THREE.SpriteMaterial({ map: texture });
    const sprite = new THREE.Sprite(material);
    
    sprite.scale.set(2, 0.5, 1);
    sprite.position.y = 2;
    
    return sprite;
  }

  // 更新实体动画
  updateEntityAnimation(entity, animationData) {
    // 实现动画状态同步
    if (entity.userData.animationMixer) {
      entity.userData.animationMixer.update(animationData.deltaTime);
    }
  }

  // 保存预测状态
  savePredictionState(entity, sequence) {
    if (!this.predictionBuffer.has(entity.userData.id)) {
      this.predictionBuffer.set(entity.userData.id, []);
    }
    
    const buffer = this.predictionBuffer.get(entity.userData.id);
    buffer.push({
      sequence: sequence,
      position: entity.position.toArray(),
      rotation: entity.rotation.toArray(),
      timestamp: Date.now()
    });
    
    // 限制缓冲区大小
    if (buffer.length > 60) { // 保持1秒的预测数据
      buffer.shift();
    }
  }
}

// 输入预测系统
class InputPredictionSystem {
  constructor(networkManager, entitySyncManager) {
    this.networkManager = networkManager;
    this.entitySyncManager = entitySyncManager;
    this.inputBuffer = [];
    this.lastProcessedInput = 0;
    
    this.setupInputHandlers();
  }

  // 设置输入处理器
  setupInputHandlers() {
    document.addEventListener('keydown', this.handleKeyDown.bind(this));
    document.addEventListener('keyup', this.handleKeyUp.bind(this));
    document.addEventListener('mousemove', this.handleMouseMove.bind(this));
  }

  // 处理按键按下
  handleKeyDown(event) {
    if (!this.shouldProcessInput(event)) return;
    
    const input = {
      type: 'keydown',
      key: event.key,
      code: event.code,
      timestamp: Date.now(),
      sequence: this.networkManager.generateSequenceId()
    };
    
    this.processInput(input);
  }

  // 处理按键释放
  handleKeyUp(event) {
    if (!this.shouldProcessInput(event)) return;
    
    const input = {
      type: 'keyup',
      key: event.key,
      code: event.code,
      timestamp: Date.now(),
      sequence: this.networkManager.generateSequenceId()
    };
    
    this.processInput(input);
  }

  // 处理鼠标移动
  handleMouseMove(event) {
    const input = {
      type: 'mousemove',
      movementX: event.movementX,
      movementY: event.movementY,
      timestamp: Date.now(),
      sequence: this.networkManager.generateSequenceId()
    };
    
    this.processInput(input);
  }

  // 处理输入
  processInput(input) {
    // 本地预测
    this.applyInputPrediction(input);
    
    // 发送到服务器
    this.networkManager.send('playerInput', input);
    
    // 保存到缓冲区
    this.inputBuffer.push(input);
    
    // 限制缓冲区大小
    if (this.inputBuffer.length > 120) { // 保持2秒的输入数据
      this.inputBuffer.shift();
    }
  }

  // 应用输入预测
  applyInputPrediction(input) {
    // 根据输入类型更新本地实体状态
    const localEntities = Array.from(this.entitySyncManager.localEntities.values());
    
    localEntities.forEach(entity => {
      this.updateEntityFromInput(entity, input);
    });
  }

  // 根据输入更新实体
  updateEntityFromInput(entity, input) {
    const speed = 0.1;
    const rotationSpeed = 0.02;
    
    switch (input.type) {
      case 'keydown':
        switch (input.code) {
          case 'KeyW':
            entity.position.z -= speed;
            break;
          case 'KeyS':
            entity.position.z += speed;
            break;
          case 'KeyA':
            entity.position.x -= speed;
            break;
          case 'KeyD':
            entity.position.x += speed;
            break;
        }
        break;
        
      case 'mousemove':
        entity.rotation.y -= input.movementX * rotationSpeed;
        break;
    }
    
    // 保存预测状态
    this.entitySyncManager.savePredictionState(entity, input.sequence);
  }

  // 检查是否应该处理输入
  shouldProcessInput(event) {
    // 忽略组合键和系统快捷键
    if (event.ctrlKey || event.altKey || event.metaKey) return false;
    
    // 忽略输入框中的输入
    if (event.target.tagName === 'INPUT') return false;
    
    return true;
  }
}

export default {
  name: 'MultiplayerScene',
  setup() {
    const renderCanvas = ref(null);
    const serverAddress = ref('ws://localhost:8080');
    const isConnected = ref(false);
    const connectionStatus = ref('disconnected');
    const currentPing = ref(0);
    const playerCount = ref(0);
    const connectedPlayers = ref([]);
    const chatMessages = ref([]);
    const chatInput = ref('');
    const showLoading = ref(false);
    const loadingMessage = ref('');
    
    const enablePrediction = ref(true);
    const enableInterpolation = ref(true);
    const enableReconciliation = ref(true);
    
    const uploadRate = ref(0);
    const downloadRate = ref(0);
    const packetLoss = ref(0);
    const networkJitter = ref(0);
    
    let scene, camera, renderer, controls;
    let networkManager, entitySyncManager, inputPredictionSystem;
    let localPlayerId = null;
    let pingInterval = null;

    // 初始化场景
    const initScene = async () => {
      // 创建场景
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0x87CEEB);
      scene.fog = new THREE.Fog(0x87CEEB, 10, 100);
      
      // 创建相机
      camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.set(0, 10, 10);
      
      // 创建渲染器
      renderer = new THREE.WebGLRenderer({
        canvas: renderCanvas.value,
        antialias: true
      });
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      renderer.shadowMap.enabled = true;
      renderer.shadowMap.type = THREE.PCFSoftShadowMap;
      
      // 添加控制器
      controls = new OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
      
      // 创建环境
      createEnvironment();
      
      // 初始化网络系统
      initNetworkSystems();
      
      // 启动渲染循环
      animate();
    };

    // 创建环境
    const createEnvironment = () => {
      // 添加环境光
      const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
      scene.add(ambientLight);
      
      // 添加方向光
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(50, 50, 25);
      directionalLight.castShadow = true;
      directionalLight.shadow.mapSize.set(2048, 2048);
      scene.add(directionalLight);
      
      // 创建地面
      const groundGeometry = new THREE.PlaneGeometry(100, 100);
      const groundMaterial = new THREE.MeshStandardMaterial({
        color: 0x90EE90,
        roughness: 0.8,
        metalness: 0.2
      });
      const ground = new THREE.Mesh(groundGeometry, groundMaterial);
      ground.rotation.x = -Math.PI / 2;
      ground.receiveShadow = true;
      scene.add(ground);
      
      // 添加一些障碍物
      addEnvironmentObjects();
    };

    // 添加环境物体
    const addEnvironmentObjects = () => {
      const obstacleGeometry = new THREE.BoxGeometry(2, 2, 2);
      const obstacleMaterial = new THREE.MeshStandardMaterial({
        color: 0x8B4513,
        roughness: 0.7
      });
      
      for (let i = 0; i < 10; i++) {
        const obstacle = new THREE.Mesh(obstacleGeometry, obstacleMaterial);
        obstacle.position.set(
          (Math.random() - 0.5) * 80,
          1,
          (Math.random() - 0.5) * 80
        );
        obstacle.castShadow = true;
        scene.add(obstacle);
      }
    };

    // 初始化网络系统
    const initNetworkSystems = () => {
      networkManager = new NetworkManager();
      entitySyncManager = new EntitySyncManager(networkManager, scene);
      inputPredictionSystem = new InputPredictionSystem(networkManager, entitySyncManager);
      
      setupNetworkEventHandlers();
    };

    // 设置网络事件处理器
    const setupNetworkEventHandlers = () => {
      networkManager.on('connectionEstablished', (data) => {
        console.log('连接已建立:', data);
        isConnected.value = true;
        connectionStatus.value = 'connected';
        localPlayerId = data.clientId;
        
        startPingMeasurement();
      });
      
      networkManager.on('playerJoined', (playerData) => {
        console.log('玩家加入:', playerData);
        addOrUpdatePlayer(playerData);
      });
      
      networkManager.on('playerLeft', (playerId) => {
        console.log('玩家离开:', playerId);
        removePlayer(playerId);
      });
      
      networkManager.on('chatMessage', (messageData) => {
        addChatMessage(messageData);
      });
      
      networkManager.on('pingResponse', (data) => {
        currentPing.value = Date.now() - data.sendTime;
      });
    };

    // 连接到服务器
    const connectToServer = async () => {
      showLoading.value = true;
      loadingMessage.value = '正在连接服务器...';
      
      try {
        await networkManager.connect(serverAddress.value);
        loadingMessage.value = '连接成功,正在初始化...';
        
        // 模拟加载过程
        setTimeout(() => {
          showLoading.value = false;
        }, 2000);
        
      } catch (error) {
        console.error('连接失败:', error);
        loadingMessage.value = `连接失败: ${error.message}`;
        
        setTimeout(() => {
          showLoading.value = false;
        }, 3000);
      }
    };

    // 断开连接
    const disconnectFromServer = () => {
      if (pingInterval) {
        clearInterval(pingInterval);
        pingInterval = null;
      }
      
      networkManager.disconnect();
      isConnected.value = false;
      connectionStatus.value = 'disconnected';
      connectedPlayers.value = [];
      playerCount.value = 0;
    };

    // 开始ping测量
    const startPingMeasurement = () => {
      pingInterval = setInterval(() => {
        networkManager.send('ping', { sendTime: Date.now() });
      }, 1000);
    };

    // 添加或更新玩家
    const addOrUpdatePlayer = (playerData) => {
      const existingIndex = connectedPlayers.value.findIndex(p => p.id === playerData.id);
      
      if (existingIndex !== -1) {
        connectedPlayers.value[existingIndex] = {
          ...connectedPlayers.value[existingIndex],
          ...playerData
        };
      } else {
        connectedPlayers.value.push({
          ...playerData,
          isLocal: playerData.id === localPlayerId
        });
      }
      
      playerCount.value = connectedPlayers.value.length;
    };

    // 移除玩家
    const removePlayer = (playerId) => {
      connectedPlayers.value = connectedPlayers.value.filter(p => p.id !== playerId);
      playerCount.value = connectedPlayers.value.length;
    };

    // 发送聊天消息
    const sendChatMessage = () => {
      if (!chatInput.value.trim() || !isConnected.value) return;
      
      const messageData = {
        content: chatInput.value,
        sender: '本地玩家', // 实际应该从服务器获取玩家名称
        timestamp: Date.now()
      };
      
      networkManager.send('chatMessage', messageData);
      chatInput.value = '';
    };

    // 添加聊天消息
    const addChatMessage = (messageData) => {
      chatMessages.value.push({
        ...messageData,
        id: Date.now().toString(),
        type: messageData.sender === '本地玩家' ? 'local' : 'remote'
      });
      
      // 限制消息数量
      if (chatMessages.value.length > 50) {
        chatMessages.value.shift();
      }
    };

    // 格式化时间
    const formatTime = (timestamp) => {
      return new Date(timestamp).toLocaleTimeString();
    };

    // 格式化字节大小
    const formatBytes = (bytes) => {
      if (bytes === 0) return '0 B';
      
      const k = 1024;
      const sizes = ['B', 'KB', 'MB', 'GB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      
      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    };

    // 连接状态文本
    const connectionText = computed(() => {
      switch (connectionStatus.value) {
        case 'connected': return '已连接';
        case 'connecting': return '连接中...';
        case 'disconnected': return '未连接';
        default: return '未知状态';
      }
    });

    // 动画循环
    const animate = () => {
      requestAnimationFrame(animate);
      
      // 更新控制器
      controls.update();
      
      // 更新网络统计(模拟数据)
      updateNetworkStats();
      
      // 渲染场景
      renderer.render(scene, camera);
    };

    // 更新网络统计
    const updateNetworkStats = () => {
      // 模拟网络统计数据
      if (isConnected.value) {
        uploadRate.value = Math.random() * 1024 * 10;
        downloadRate.value = Math.random() * 1024 * 50;
        packetLoss.value = Math.random() * 2;
        networkJitter.value = Math.random() * 10;
      }
    };

    onMounted(() => {
      initScene();
      window.addEventListener('resize', handleResize);
    });

    onUnmounted(() => {
      if (networkManager) {
        networkManager.disconnect();
      }
      if (pingInterval) {
        clearInterval(pingInterval);
      }
      window.removeEventListener('resize', handleResize);
    });

    const handleResize = () => {
      if (!camera || !renderer) return;
      
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    };

    return {
      renderCanvas,
      serverAddress,
      isConnected,
      connectionStatus,
      currentPing,
      playerCount,
      connectedPlayers,
      chatMessages,
      chatInput,
      showLoading,
      loadingMessage,
      enablePrediction,
      enableInterpolation,
      enableReconciliation,
      uploadRate,
      downloadRate,
      packetLoss,
      networkJitter,
      connectionText,
      connectToServer,
      disconnectFromServer,
      sendChatMessage,
      formatTime,
      formatBytes
    };
  }
};
</script>

<style scoped>
.multiplayer-container {
  width: 100%;
  height: 100vh;
  position: relative;
  background: #000;
}

.render-canvas {
  width: 100%;
  height: 100%;
  display: block;
}

.connection-panel {
  position: absolute;
  top: 20px;
  left: 20px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 15px;
  background: rgba(0, 0, 0, 0.8);
  border-radius: 8px;
  color: white;
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.connection-panel.connected {
  border-color: #00ff00;
}

.connection-panel.connecting {
  border-color: #ffff00;
}

.connection-panel.disconnected {
  border-color: #ff0000;
}

.status-indicator {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.connected .status-indicator {
  background: #00ff00;
  box-shadow: 0 0 10px #00ff00;
}

.connecting .status-indicator {
  background: #ffff00;
  box-shadow: 0 0 10px #ffff00;
}

.disconnected .status-indicator {
  background: #ff0000;
  box-shadow: 0 0 10px #ff0000;
}

.status-text {
  font-weight: bold;
}

.ping-display {
  color: #ccc;
  font-size: 12px;
}

.players-panel {
  position: absolute;
  top: 80px;
  left: 20px;
  width: 200px;
  background: rgba(0, 0, 0, 0.8);
  border-radius: 8px;
  padding: 15px;
  color: white;
  backdrop-filter: blur(10px);
}

.players-panel h4 {
  margin: 0 0 10px 0;
  color: #00ffff;
  font-size: 14px;
}

.players-list {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.player-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 5px 8px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  font-size: 12px;
}

.player-item.local {
  background: rgba(0, 255, 255, 0.2);
  border: 1px solid #00ffff;
}

.player-name {
  font-weight: bold;
}

.player-ping {
  color: #ccc;
  font-size: 10px;
}

.control-panel {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 250px;
  background: rgba(0, 0, 0, 0.8);
  border-radius: 8px;
  padding: 15px;
  color: white;
  backdrop-filter: blur(10px);
}

.panel-section {
  margin-bottom: 15px;
}

.panel-section h4 {
  margin: 0 0 10px 0;
  color: #00ff88;
  font-size: 14px;
}

.connection-controls {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.server-input {
  padding: 8px;
  border: 1px solid #444;
  border-radius: 4px;
  background: #333;
  color: white;
  font-size: 12px;
}

.connect-button, .disconnect-button {
  padding: 8px;
  border: none;
  border-radius: 4px;
  font-size: 12px;
  cursor: pointer;
  transition: background 0.3s;
}

.connect-button {
  background: #00aa00;
  color: white;
}

.connect-button:disabled {
  background: #666;
  cursor: not-allowed;
}

.disconnect-button {
  background: #aa0000;
  color: white;
}

.disconnect-button:disabled {
  background: #666;
  cursor: not-allowed;
}

.sync-settings {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.setting-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  cursor: pointer;
}

.network-stats {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.stat-item {
  font-size: 11px;
  color: #ccc;
}

.chat-panel {
  position: absolute;
  bottom: 20px;
  left: 20px;
  width: 300px;
  background: rgba(0, 0, 0, 0.8);
  border-radius: 8px;
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.chat-messages {
  height: 200px;
  overflow-y: auto;
  padding: 10px;
}

.chat-message {
  margin-bottom: 8px;
  padding: 5px 8px;
  border-radius: 4px;
  font-size: 12px;
}

.chat-message.local {
  background: rgba(0, 255, 255, 0.2);
}

.chat-message.remote {
  background: rgba(255, 255, 255, 0.1);
}

.message-sender {
  font-weight: bold;
  color: #00ffff;
}

.message-content {
  color: white;
  margin: 0 5px;
}

.message-time {
  color: #ccc;
  font-size: 10px;
}

.chat-input-container {
  display: flex;
  padding: 10px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.chat-input {
  flex: 1;
  padding: 8px;
  border: 1px solid #444;
  border-radius: 4px;
  background: #333;
  color: white;
  font-size: 12px;
}

.send-button {
  margin-left: 8px;
  padding: 8px 12px;
  border: none;
  border-radius: 4px;
  background: #00aa00;
  color: white;
  cursor: pointer;
  font-size: 12px;
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  justify-content: center
相关推荐
寒月霜华9 小时前
java-网络编程-UDP,TCP通信
java·网络·tcp/ip·udp
HappyGame0210 小时前
Linux网络编程(上)
linux·网络
Nimsolax11 小时前
Linux网络Socket编程TCP
linux·网络·tcp/ip
YoungLime12 小时前
DVWA靶场之十六:未验证的重定向漏洞(Open HTTP Redirect)
网络·安全·web安全
XUE-521131418 小时前
路由策略与路由控制实验
运维·网络·网络协议·智能路由器
加油201918 小时前
如何快速学习一个网络协议?
网络·网络协议·学习·方法论
爱奥尼欧1 天前
【Linux】网络部分——Socket编程 UDP实现网络云服务器与本地虚拟机的基本通信
linux·服务器·网络
十碗饭吃不饱1 天前
WebClient工具调用HTTP接口报错远程主机断开连接
网络·网络协议·http
liu****1 天前
基于websocket的多用户网页五子棋(九)
服务器·网络·数据库·c++·websocket·网络协议·个人开发