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

news/2025/11/6 20:32:55/文章来源:https://www.cnblogs.com/gccbuaa/p/19197679

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

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

在这里插入图片描述

概述

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

多人同步系统架构:

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

核心原理深度解析

网络同步模型

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

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

同步策略选择

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

  1. 状态同步

  2. 输入同步

完整代码实现

多人在线3D场景框架


<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);}}// 生成序列IDgenerateSequenceId() {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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/958075.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

别再选错!5分钟掌握AI Agent框架选型的方法

本文介绍AI Agent框架选型指南,阐述了框架对AI应用开发的重要性,将主流框架分为入门友好型、多智能体协作、复杂流程建模、自主决策能力和企业级应用五大类9种框架,从技术复杂度、功能特性、行业适用性等维度进行对…

完整教程:【Qt MOC预处理器解读与使用指南】

完整教程:【Qt MOC预处理器解读与使用指南】pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &qu…

Linux - 7 磁盘管理篇

1)tree 可视化目录 tree 是 Linux 中可视化目录结构的实用工具,能以树形图形式递归展示目录下的文件和子目录,清晰直观,常用于文档整理、项目结构查看、运维排查等场景。以下从「安装、基础用法、进阶参数、实用场…

java word转 pdf

<!-- https://mvnrepository.com/artifact/com.luhuiguo/aspose-words --><dependency><groupId>com.luhuiguo</groupId><artifactId>aspose-words</artifactId><version>2…

11-05 题

11-05 题P7468 [NOI Online 2021 提高组] 愤怒的小 N - 洛谷 CF1938M - 2024 ICPC Asia Pacific Championship CF1466H Finding satisfactory solutions - 洛谷 P8147 [JRKSJ R4] Salieri - 洛谷 CF1770F Koxia and Se…

Markdown之Typora语法

Markdown之Typora语法 标题 #+空格:一级标题 ##+空格:二级标题 ...... 最多支持六级标题 字体 *** +正文+ * :粗体(星星之间无空格) *** +正文+ :斜体* *** * * +正文+ * * :粗斜体* **~ +正文+ :画线**(正文…

运维审计/堡垒机选型 2025:从 SSH 直连|堡垒机绕行的可见性到“命令+返回文本”的内容级证据

运维审计/堡垒机选型 2025:从 SSH 直连|堡垒机绕行的可见性到“命令+返回文本”的内容级证据在 2025 年多云、外包协同成为常态的运维环境下,企业在选型运维堡垒机/运维审计系统时,已不再仅仅关注“谁登录、从哪台…

[题解]P12025 [USACO25OPEN] Sequence Construction S

P12025 [USACO25OPEN] Sequence Construction S Ref:P12025 [USACO25OPEN] Sequence Construction S 题解 - Little_x_starTYJ 我们的构造要满足三个条件:\(1\le N\le 100\) \(\sum_i^N A_i=M\) \(\bigoplus_i^N \te…

【日记】我居然解决了三家运营商都没解决的问题(539 字)

正文这则日记是在家里写的,难得的,我出差回家了。并且,还和兄长的假期凑巧到了一起。这次出差负责解决一个支行的问题。说实话,我心里没什么底,因为支行反馈上来的情况相当怪。总体而言,就是十分正常的操作,而且…

深入解析:Jackson 入门:为什么它是 Java JSON 处理的首选?

深入解析:Jackson 入门:为什么它是 Java JSON 处理的首选?pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Cons…

大模型在流行性乙型脑炎极重型预测及个体化诊疗专业的方案中的应用研究

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

markdown入门(复盘)

markdown入门(复盘)markdown入门(复盘) 标题 一级 ##二级 ###三级。。。(最多六级) 字体 hello hello hello ~~hello~~ 注意⚠️:都要用英文打符号 hello 插图超链接 链接 表格 直接右键生成吧! |xingming…

卡尔算法哈希表

一:有效的字母异位词part1题意:给出两个字符串,判断是否可以更改字符串内字母的顺序,从而使得两个字符串的单词一样。注意该题目认为两个相同字母顺序的字符串也是相同的。同样也返回true。即题目让我们判断两个字…

Rust 之二 各组件工具的源码、构建、配置、使用 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

java第三天

random生成随机数

新东方听力day2

同义替换1词性替换,2短语单词替换3否定替换

P9596 [JOI Open 2018] 冒泡排序 2 做题记录

P9596 [JOI Open 2018] 冒泡排序 2 做题记录 P9596 [JOI Open 2018] 冒泡排序 2 / Bubble Sort 2 - 洛谷 (luogu.com.cn) Solution 1 结论:设 \(v_i=\sum_{j\le i} [a_j>a_i]\),序列 \(a\) 的代价为 \(\max\{v_i\…

超级管理员目录索引的Google搜索技巧

本文介绍了一个特定的Google搜索语法"intitle:index of inurl:superadmin",用于发现公开在互联网上的敏感目录信息。这种技术属于Google Hacking范畴,可帮助安全研究人员识别配置不当的系统和敏感数据泄露…