第50节:构建元宇宙沙盒 - 多人协作3D创作平台 - 实践
概述
元宇宙沙盒是一个集成了3D建模、物理模拟、实时协作和社交互动的综合性平台。本节将指导你构建一个功能完整的元宇宙沙盒,支持多用户实时协作创建和编辑3D内容。
元宇宙沙盒系统架构:
核心功能模块
实时协作系统
| 功能模块 | 技术实现 | 性能优化 | 用户体验 |
|---|---|---|---|
| 对象同步 | 差分状态同步 | 数据压缩 | 实时更新 |
| 用户表示 | 3D虚拟形象 | LOD优化 | 个性化 |
| 聊天系统 | WebSocket + Redis | 消息队列 | 实时对话 |
| 权限控制 | 角色权限系统 | 缓存策略 | 灵活管理 |
3D创作工具集
基础建模工具
- 几何体创建(立方体、球体、圆柱体等)
- 变换操作(移动、旋转、缩放)
- 组合与分组
高级编辑功能
- 顶点级编辑
- 材质与纹理应用
- 光照与阴影设置
场景管理
- 图层系统
- 对象库
- 场景快照
完整代码实现
后端服务器(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
性能优化策略
网络优化
- 使用Protocol Buffers替代JSON
- 实现数据差分同步
- 添加数据压缩
渲染优化
- 实现对象LOD系统
- 使用实例化渲染
- 添加视锥体剔除
内存管理
- 对象池重用
- 自动资源清理
- 内存泄漏检测
这个元宇宙沙盒平台为多用户3D创作提供了完整的基础设施,可以进一步扩展为教育、设计、游戏等多种应用场景。
浙公网安备 33010602011771号