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

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

概述
多人在线3D场景是现代Web应用的重要发展方向,涉及实时网络通信、状态同步、冲突检测等复杂技术。本节将深入探索WebSocket通信架构、权威服务器模式、预测与调和算法,构建稳定可靠的多人交互体验。
多人同步系统架构:
多人在线系统 网络层 同步层 表现层 WebSocket连接 数据传输 连接管理 状态同步 预测算法 冲突解决 实体渲染 动画同步 特效管理 心跳检测 插值计算 视觉平滑
核心原理深度解析
网络同步模型
多人游戏常用的同步架构对比:
模型类型 | 架构特点 | 适用场景 | 延迟处理 |
---|---|---|---|
权威服务器 | 服务器验证所有操作 | 竞技游戏、MMO | 客户端预测+服务器调和 |
P2P对等 | 节点间直接通信 | 小规模联机 | 锁步同步、帧同步 |
混合模式 | 区域服务器+中继 | 大型开放世界 | 分区分层同步 |
同步策略选择
根据应用需求选择合适的同步粒度:
-
状态同步
- 全量状态定期同步
- 增量状态实时同步
- 关键事件立即同步
-
输入同步
- 只同步用户输入
- 服务器计算确定结果
- 客户端预测显示
完整代码实现
多人在线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