第50节:构建元宇宙沙盒 - 多人协作3D创作平台 - 实践

概述

元宇宙沙盒是一个集成了3D建模、物理模拟、实时协作和社交互动的综合性平台。本节将指导你构建一个功能完整的元宇宙沙盒,支持多用户实时协作创建和编辑3D内容。
在这里插入图片描述

元宇宙沙盒系统架构:

元宇宙沙盒
网络同步层
3D编辑层
物理引擎层
用户管理层
WebSocket通信
状态同步
冲突解决
对象创建
变换工具
材质编辑
刚体物理
碰撞检测
约束系统
用户身份
权限管理
会话管理
实时协作
创意表达
真实交互
社交体验

核心功能模块

实时协作系统

功能模块技术实现性能优化用户体验
对象同步差分状态同步数据压缩实时更新
用户表示3D虚拟形象LOD优化个性化
聊天系统WebSocket + Redis消息队列实时对话
权限控制角色权限系统缓存策略灵活管理

3D创作工具集

  1. 基础建模工具

    • 几何体创建(立方体、球体、圆柱体等)
    • 变换操作(移动、旋转、缩放)
    • 组合与分组
  2. 高级编辑功能

    • 顶点级编辑
    • 材质与纹理应用
    • 光照与阴影设置
  3. 场景管理

    • 图层系统
    • 对象库
    • 场景快照

完整代码实现

后端服务器(Node.js + Socket.IO)

// server.js
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const cors = require('cors');
class MetaverseServer {
constructor() {
this.app = express();
this.server = http.createServer(this.app);
this.io = socketIo(this.server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
this.rooms = new Map();
this.users = new Map();
this.objects = new Map();
this.setupMiddleware();
this.setupSocketHandlers();
}
setupMiddleware() {
this.app.use(cors());
this.app.use(express.json());
this.app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
}
setupSocketHandlers() {
this.io.on('connection', (socket) => {
console.log('用户连接:', socket.id);
// 用户加入房间
socket.on('join-room', (data) => {
this.handleJoinRoom(socket, data);
});
// 创建对象
socket.on('create-object', (data) => {
this.handleCreateObject(socket, data);
});
// 更新对象
socket.on('update-object', (data) => {
this.handleUpdateObject(socket, data);
});
// 删除对象
socket.on('delete-object', (data) => {
this.handleDeleteObject(socket, data);
});
// 聊天消息
socket.on('chat-message', (data) => {
this.handleChatMessage(socket, data);
});
// 用户移动
socket.on('user-move', (data) => {
this.handleUserMove(socket, data);
});
// 断开连接
socket.on('disconnect', () => {
this.handleDisconnect(socket);
});
});
}
handleJoinRoom(socket, data) {
const { roomId, userInfo } = data;
socket.join(roomId);
// 初始化房间
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, {
id: roomId,
users: new Set(),
objects: new Map(),
createdAt: Date.now()
});
}
const room = this.rooms.get(roomId);
room.users.add(socket.id);
// 保存用户信息
this.users.set(socket.id, {
id: socket.id,
roomId,
userInfo,
position: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 },
joinedAt: Date.now()
});
// 通知房间内其他用户
socket.to(roomId).emit('user-joined', {
userId: socket.id,
userInfo,
position: { x: 0, y: 0, z: 0 }
});
// 发送当前房间状态给新用户
socket.emit('room-state', {
users: Array.from(room.users).map(userId => this.users.get(userId)),
objects: Array.from(room.objects.values())
});
console.log(`用户 ${socket.id} 加入房间 ${roomId}`);
}
handleCreateObject(socket, data) {
const user = this.users.get(socket.id);
if (!user) return;
const { roomId } = user;
const room = this.rooms.get(roomId);
if (!room) return;
const objectId = `obj_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const objectData = {
id: objectId,
type: data.type,
position: data.position,
rotation: data.rotation,
scale: data.scale,
properties: data.properties,
createdBy: socket.id,
createdAt: Date.now(),
lastModified: Date.now()
};
room.objects.set(objectId, objectData);
// 广播给房间内所有用户
this.io.to(roomId).emit('object-created', objectData);
}
handleUpdateObject(socket, data) {
const user = this.users.get(socket.id);
if (!user) return;
const { roomId } = user;
const room = this.rooms.get(roomId);
if (!room) return;
const object = room.objects.get(data.objectId);
if (!object) return;
// 更新对象属性
Object.assign(object, data.updates);
object.lastModified = Date.now();
object.lastModifiedBy = socket.id;
// 广播更新(排除发送者)
socket.to(roomId).emit('object-updated', {
objectId: data.objectId,
updates: data.updates,
updatedBy: socket.id
});
}
handleDeleteObject(socket, data) {
const user = this.users.get(socket.id);
if (!user) return;
const { roomId } = user;
const room = this.rooms.get(roomId);
if (!room) return;
if (room.objects.has(data.objectId)) {
room.objects.delete(data.objectId);
// 广播删除
this.io.to(roomId).emit('object-deleted', {
objectId: data.objectId,
deletedBy: socket.id
});
}
}
handleChatMessage(socket, data) {
const user = this.users.get(socket.id);
if (!user) return;
const message = {
id: `msg_${Date.now()}`,
userId: socket.id,
userInfo: user.userInfo,
content: data.content,
timestamp: Date.now(),
type: data.type || 'text'
};
// 广播消息
this.io.to(user.roomId).emit('chat-message', message);
}
handleUserMove(socket, data) {
const user = this.users.get(socket.id);
if (!user) return;
// 更新用户位置
user.position = data.position;
user.rotation = data.rotation;
// 广播移动(排除自己)
socket.to(user.roomId).emit('user-moved', {
userId: socket.id,
position: data.position,
rotation: data.rotation
});
}
handleDisconnect(socket) {
const user = this.users.get(socket.id);
if (user) {
const { roomId } = user;
const room = this.rooms.get(roomId);
if (room) {
room.users.delete(socket.id);
// 通知其他用户
socket.to(roomId).emit('user-left', {
userId: socket.id
});
// 如果房间为空,清理房间
if (room.users.size === 0) {
this.rooms.delete(roomId);
}
}
this.users.delete(socket.id);
}
console.log('用户断开连接:', socket.id);
}
start(port = 3000) {
this.server.listen(port, () => {
console.log(`元宇宙服务器运行在端口 ${port}`);
});
}
}
// 启动服务器
const server = new MetaverseServer();
server.start();

前端元宇宙客户端(Vue.js + Three.js)


<script>
import { onMounted, onUnmounted, ref, reactive, nextTick } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import io from 'socket.io-client';
export default {
  name: 'MetaverseSandbox',
  setup() {
    const metaverseCanvas = ref(null);
    const chatMessages = ref(null);
    // 状态管理
    const showToolbox = ref(true);
    const showChat = ref(false);
    const currentRoom = ref('default-room');
    const onlineUsers = ref(1);
    const editMode = ref('select');
    const connectionStatus = ref('connecting');
    // 材质编辑
    const materialColor = ref('#ff6b6b');
    const materialRoughness = ref(0.5);
    const materialMetalness = ref(0.0);
    // 聊天系统
    const chatInput = ref('');
    const chatMessages = ref([]);
    // 用户管理
    const socketId = ref('');
    const connectedUsers = ref([]);
    // Three.js 变量
    let scene, camera, renderer, controls;
    let socket;
    let selectedObject = null;
    let userAvatars = new Map();
    let sceneObjects = new Map();
    // 初始化场景
    const initScene = async () => {
      // 创建场景
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0x87CEEB);
      scene.fog = new THREE.Fog(0x87CEEB, 50, 200);
      // 创建相机
      camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
      camera.position.set(10, 10, 10);
      // 创建渲染器
      renderer = new THREE.WebGLRenderer({
        canvas: metaverseCanvas.value,
        antialias: true
      });
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.shadowMap.enabled = true;
      renderer.shadowMap.type = THREE.PCFSoftShadowMap;
      // 添加控制器
      controls = new OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
      // 设置光照
      setupLighting();
      // 创建地面
      createGround();
      // 连接服务器
      connectToServer();
      // 启动动画循环
      animate();
    };
    // 设置光照
    const setupLighting = () => {
      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.width = 2048;
      directionalLight.shadow.mapSize.height = 2048;
      scene.add(directionalLight);
    };
    // 创建地面
    const createGround = () => {
      const groundGeometry = new THREE.PlaneGeometry(100, 100);
      const groundMaterial = new THREE.MeshStandardMaterial({
        color: 0x3a6b3a,
        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);
    };
    // 连接服务器
    const connectToServer = () => {
      socket = io('http://localhost:3000');
      socket.on('connect', () => {
        connectionStatus.value = 'connected';
        socketId.value = socket.id;
        // 加入房间
        socket.emit('join-room', {
          roomId: currentRoom.value,
          userInfo: {
            username: `用户_${Math.random().toString(36).substr(2, 5)}`,
            avatar: 'default'
          }
        });
      });
      // 房间状态
      socket.on('room-state', (data) => {
        connectedUsers.value = data.users;
        onlineUsers.value = data.users.length;
        // 创建其他用户的虚拟形象
        data.users.forEach(user => {
          if (user.id !== socket.id) {
            createUserAvatar(user);
          }
        });
        // 创建现有对象
        data.objects.forEach(obj => {
          createObjectFromData(obj);
        });
      });
      // 用户加入
      socket.on('user-joined', (data) => {
        createUserAvatar(data);
        connectedUsers.value.push(data);
        onlineUsers.value = connectedUsers.value.length;
      });
      // 用户离开
      socket.on('user-left', (data) => {
        removeUserAvatar(data.userId);
        connectedUsers.value = connectedUsers.value.filter(user => user.id !== data.userId);
        onlineUsers.value = connectedUsers.value.length;
      });
      // 用户移动
      socket.on('user-moved', (data) => {
        updateUserAvatar(data);
      });
      // 对象创建
      socket.on('object-created', (data) => {
        createObjectFromData(data);
      });
      // 对象更新
      socket.on('object-updated', (data) => {
        updateObjectFromData(data);
      });
      // 对象删除
      socket.on('object-deleted', (data) => {
        deleteObject(data.objectId);
      });
      // 聊天消息
      socket.on('chat-message', (data) => {
        chatMessages.value.push(data);
        scrollChatToBottom();
      });
    };
    // 创建用户虚拟形象
    const createUserAvatar = (userData) => {
      const geometry = new THREE.CapsuleGeometry(0.5, 1, 4, 8);
      const material = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(Math.random(), 0.8, 0.6),
        roughness: 0.7
      });
      const avatar = new THREE.Mesh(geometry, material);
      avatar.position.set(
        userData.position.x,
        userData.position.y,
        userData.position.z
      );
      avatar.castShadow = true;
      scene.add(avatar);
      userAvatars.set(userData.userId, avatar);
    };
    // 更新用户虚拟形象
    const updateUserAvatar = (data) => {
      const avatar = userAvatars.get(data.userId);
      if (avatar) {
        avatar.position.set(
          data.position.x,
          data.position.y,
          data.position.z
        );
        avatar.rotation.set(
          data.rotation.x,
          data.rotation.y,
          data.rotation.z
        );
      }
    };
    // 移除用户虚拟形象
    const removeUserAvatar = (userId) => {
      const avatar = userAvatars.get(userId);
      if (avatar) {
        scene.remove(avatar);
        userAvatars.delete(userId);
      }
    };
    // 创建对象
    const createObject = (type) => {
      let geometry, material;
      switch (type) {
        case 'cube':
          geometry = new THREE.BoxGeometry(2, 2, 2);
          break;
        case 'sphere':
          geometry = new THREE.SphereGeometry(1, 16, 16);
          break;
        case 'cylinder':
          geometry = new THREE.CylinderGeometry(1, 1, 2, 16);
          break;
        case 'plane':
          geometry = new THREE.PlaneGeometry(4, 4);
          break;
        default:
          geometry = new THREE.BoxGeometry(2, 2, 2);
      }
      material = new THREE.MeshStandardMaterial({
        color: new THREE.Color(materialColor.value),
        roughness: parseFloat(materialRoughness.value),
        metalness: parseFloat(materialMetalness.value)
      });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.set(0, 2, 0);
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      scene.add(mesh);
      // 发送到服务器
      const objectData = {
        type: type,
        position: mesh.position,
        rotation: mesh.rotation,
        scale: mesh.scale,
        properties: {
          color: materialColor.value,
          roughness: materialRoughness.value,
          metalness: materialMetalness.value
        }
      };
      socket.emit('create-object', objectData);
    };
    // 从数据创建对象
    const createObjectFromData = (objectData) => {
      let geometry;
      switch (objectData.type) {
        case 'cube':
          geometry = new THREE.BoxGeometry(2, 2, 2);
          break;
        case 'sphere':
          geometry = new THREE.SphereGeometry(1, 16, 16);
          break;
        case 'cylinder':
          geometry = new THREE.CylinderGeometry(1, 1, 2, 16);
          break;
        case 'plane':
          geometry = new THREE.PlaneGeometry(4, 4);
          break;
        default:
          geometry = new THREE.BoxGeometry(2, 2, 2);
      }
      const material = new THREE.MeshStandardMaterial({
        color: new THREE.Color(objectData.properties?.color || '#ff6b6b'),
        roughness: objectData.properties?.roughness || 0.5,
        metalness: objectData.properties?.metalness || 0.0
      });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.set(
        objectData.position.x,
        objectData.position.y,
        objectData.position.z
      );
      mesh.rotation.set(
        objectData.rotation.x,
        objectData.rotation.y,
        objectData.rotation.z
      );
      mesh.scale.set(
        objectData.scale.x,
        objectData.scale.y,
        objectData.scale.z
      );
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      mesh.userData = { id: objectData.id };
      scene.add(mesh);
      sceneObjects.set(objectData.id, mesh);
    };
    // 更新对象
    const updateObjectFromData = (data) => {
      const mesh = sceneObjects.get(data.objectId);
      if (mesh && data.updates) {
        if (data.updates.position) {
          mesh.position.set(
            data.updates.position.x,
            data.updates.position.y,
            data.updates.position.z
          );
        }
        if (data.updates.rotation) {
          mesh.rotation.set(
            data.updates.rotation.x,
            data.updates.rotation.y,
            data.updates.rotation.z
          );
        }
        if (data.updates.scale) {
          mesh.scale.set(
            data.updates.scale.x,
            data.updates.scale.y,
            data.updates.scale.z
          );
        }
      }
    };
    // 删除对象
    const deleteObject = (objectId) => {
      const mesh = sceneObjects.get(objectId);
      if (mesh) {
        scene.remove(mesh);
        mesh.geometry.dispose();
        mesh.material.dispose();
        sceneObjects.delete(objectId);
      }
    };
    // 设置编辑模式
    const setEditMode = (mode) => {
      editMode.value = mode;
      // 这里应该更新控制器模式
    };
    // 更新选中对象的材质
    const updateSelectedMaterial = () => {
      if (selectedObject) {
        selectedObject.material.color.set(materialColor.value);
        selectedObject.material.roughness = parseFloat(materialRoughness.value);
        selectedObject.material.metalness = parseFloat(materialMetalness.value);
        selectedObject.material.needsUpdate = true;
      }
    };
    // 发送聊天消息
    const sendChatMessage = () => {
      if (chatInput.value.trim()) {
        socket.emit('chat-message', {
          content: chatInput.value,
          type: 'text'
        });
        chatInput.value = '';
      }
    };
    // 滚动聊天到底部
    const scrollChatToBottom = () => {
      nextTick(() => {
        if (chatMessages.value) {
          chatMessages.value.scrollTop = chatMessages.value.scrollHeight;
        }
      });
    };
    // 格式化时间
    const formatTime = (timestamp) => {
      return new Date(timestamp).toLocaleTimeString('zh-CN', {
        hour: '2-digit',
        minute: '2-digit'
      });
    };
    // 导出场景
    const exportScene = () => {
      const sceneData = {
        objects: Array.from(sceneObjects.entries()).map(([id, mesh]) => ({
          id,
          type: 'exported',
          position: mesh.position,
          rotation: mesh.rotation,
          scale: mesh.scale,
          material: {
            color: mesh.material.color.getHex(),
            roughness: mesh.material.roughness,
            metalness: mesh.material.metalness
          }
        })),
        exportedAt: Date.now()
      };
      const dataStr = JSON.stringify(sceneData, null, 2);
      const dataBlob = new Blob([dataStr], { type: 'application/json' });
      const link = document.createElement('a');
      link.href = URL.createObjectURL(dataBlob);
      link.download = `metaverse-scene-${Date.now()}.json`;
      link.click();
    };
    // 动画循环
    const animate = () => {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    };
    onMounted(() => {
      initScene();
      window.addEventListener('resize', handleResize);
    });
    onUnmounted(() => {
      if (socket) {
        socket.disconnect();
      }
      if (renderer) {
        renderer.dispose();
      }
      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 {
      metaverseCanvas,
      chatMessages,
      showToolbox,
      showChat,
      currentRoom,
      onlineUsers,
      editMode,
      connectionStatus,
      materialColor,
      materialRoughness,
      materialMetalness,
      chatInput,
      chatMessages,
      socketId,
      connectedUsers,
      createObject,
      setEditMode,
      updateSelectedMaterial,
      sendChatMessage,
      formatTime,
      exportScene
    };
  }
};
</script>

部署与扩展指南

生产环境部署

# docker-compose.yml
version: '3.8'
services:
metaverse-server:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
depends_on:
- redis
redis:
image: redis:alpine
ports:
- "6379:6379"
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./client-dist:/usr/share/nginx/html

性能优化策略

  1. 网络优化

    • 使用Protocol Buffers替代JSON
    • 实现数据差分同步
    • 添加数据压缩
  2. 渲染优化

    • 实现对象LOD系统
    • 使用实例化渲染
    • 添加视锥体剔除
  3. 内存管理

    • 对象池重用
    • 自动资源清理
    • 内存泄漏检测

这个元宇宙沙盒平台为多用户3D创作提供了完整的基础设施,可以进一步扩展为教育、设计、游戏等多种应用场景。

posted @ 2025-12-14 12:23  clnchanpin  阅读(0)  评论(0)    收藏  举报