web client

This commit is contained in:
2025-07-04 23:42:27 +08:00
parent f0fd00d706
commit 027f582d4e
13 changed files with 19795 additions and 85 deletions

216
Client/web/README.md Normal file
View File

@@ -0,0 +1,216 @@
# 游戏方块容器 - 优化版本
## 🚀 项目概述
这是一个基于Vue 3和WebSocket的实时游戏监控系统用于显示4个游戏方块的连接状态和动态圆点。
## ✨ 优化内容
### 1. **性能优化**
-**WebSocket连接管理优化**:使用连接池和自动重连机制
-**DOM操作优化**使用Vue响应式数据替代直接DOM操作
-**内存管理**:限制圆点数量,防止内存泄漏
-**动画性能**使用CSS3硬件加速和will-change属性
### 2. **代码结构优化**
-**组件化设计**:将方块组件化,提高代码复用性
-**服务类分离**WebSocket逻辑独立到服务类
-**配置管理**:统一的配置文件管理
-**工具类**:通用工具函数和日志系统
### 3. **用户体验优化**
-**响应式设计**:支持不同屏幕尺寸
-**加载状态**:连接状态指示器和加载动画
-**键盘快捷键**:快速操作功能
-**状态栏**:实时显示连接状态和统计信息
### 4. **错误处理增强**
-**完善的错误处理**WebSocket连接错误处理
-**自动重连机制**:指数退避算法
-**日志系统**:分级日志记录
-**性能监控**:性能测量和分析
### 5. **安全性优化**
-**输入验证**:坐标范围验证
-**连接验证**WebSocket连接状态检查
-**错误边界**:防止应用崩溃
## 🛠️ 技术栈
- **前端框架**Vue 3
- **通信协议**WebSocket
- **样式**CSS3 + 响应式设计
- **协议缓冲**Protobuf.js
- **工具库**:自定义工具类
## 📁 项目结构
```
web/
├── index.html # 主页面
├── game.js # 主应用逻辑
├── config.js # 配置文件
├── utils.js # 工具类
├── style.css # 样式文件
├── proto/ # 协议文件
│ ├── action.proto # 动作协议
│ └── define.proto # 定义协议
└── README.md # 项目说明
```
## 🚀 快速开始
### 1. 启动项目
```bash
# 使用任何HTTP服务器启动项目
python -m http.server 8000
# 或
npx serve .
```
### 2. 访问应用
打开浏览器访问 `http://localhost:8000`
## ⌨️ 键盘快捷键
| 快捷键 | 功能 |
|--------|------|
| `H` | 隐藏/显示状态栏 |
| `C` | 清除所有圆点 |
| `R` | 重新连接所有WebSocket |
## ⚙️ 配置说明
### WebSocket配置
```javascript
websocket: {
baseUrl: 'ws://localhost:8501', // WebSocket服务器地址
reconnectAttempts: 5, // 重连次数
reconnectDelay: 1000, // 重连延迟
heartbeatInterval: 30000, // 心跳间隔
connectionTimeout: 10000 // 连接超时
}
```
### 游戏配置
```javascript
game: {
maxDotsPerSquare: 100, // 每个方块最大圆点数
dotLifetime: 30000, // 圆点生命周期
gridSize: { width: 400, height: 400 }, // 网格大小
animationDuration: 300 // 动画持续时间
}
```
## 🔧 自定义配置
### 修改WebSocket服务器地址
`config.js` 中修改 `websocket.baseUrl`
```javascript
websocket: {
baseUrl: 'ws://your-server:port',
// ... 其他配置
}
```
### 调整性能参数
```javascript
performance: {
enableAnimations: true, // 启用动画
enableHoverEffects: true, // 启用悬停效果
enableBackdropFilter: true, // 启用背景模糊
maxFPS: 60 // 最大帧率
}
```
## 📊 性能特性
### 1. **响应式布局**
- 桌面端2x2网格布局
- 平板端2x2自适应布局
- 移动端1x4垂直布局
### 2. **动画优化**
- CSS3硬件加速
- 帧率控制
- 动画性能监控
### 3. **内存管理**
- 圆点数量限制
- 自动清理机制
- 内存泄漏防护
## 🐛 调试功能
### 日志系统
```javascript
// 在浏览器控制台查看详细日志
utils.debug('调试信息');
utils.info('一般信息');
utils.warn('警告信息');
utils.error('错误信息');
```
### 性能监控
```javascript
// 测量函数执行时间
const { result, duration } = utils.measurePerformance('函数名', () => {
// 要测量的代码
});
```
## 🔒 安全特性
### 1. **输入验证**
- 坐标范围检查
- 数据类型验证
- 恶意数据过滤
### 2. **连接安全**
- WebSocket状态验证
- 连接超时处理
- 错误恢复机制
## 📱 移动端支持
- 响应式设计
- 触摸友好的界面
- 性能优化
- 电池友好的动画
## 🎨 主题定制
### 颜色方案
可以通过修改CSS变量来自定义主题
```css
:root {
--primary-color: #ff6b6b;
--secondary-color: #4ecdc4;
--background-color: #f5f5f5;
--border-color: #555;
}
```
## 🤝 贡献指南
1. Fork 项目
2. 创建功能分支
3. 提交更改
4. 推送到分支
5. 创建 Pull Request
## 📄 许可证
MIT License
## 📞 支持
如有问题或建议,请提交 Issue 或联系开发团队。
---
**版本**: 2.0.0
**最后更新**: 2024年
**维护者**: 游戏开发团队

71
Client/web/config.js Normal file
View File

@@ -0,0 +1,71 @@
// 游戏配置文件
const GameConfig = {
// WebSocket配置
websocket: {
baseUrl: 'ws://localhost:8501',
reconnectAttempts: 5,
reconnectDelay: 1000,
heartbeatInterval: 30000, // 心跳间隔(毫秒)
connectionTimeout: 10000, // 连接超时(毫秒)
},
// 游戏配置
game: {
maxDotsPerSquare: 100, // 每个方块最大圆点数
dotLifetime: 30000, // 圆点生命周期(毫秒)
gridSize: {
width: 400,
height: 400
},
animationDuration: 300, // 动画持续时间(毫秒)
},
// UI配置
ui: {
showStatusBar: true,
showKeyboardHints: true,
autoHideHints: true,
hintsHideDelay: 5000, // 提示自动隐藏延迟(毫秒)
},
// 性能配置
performance: {
enableAnimations: true,
enableHoverEffects: true,
enableBackdropFilter: true,
maxFPS: 60,
},
// 调试配置
debug: {
enableLogging: true,
logLevel: 'info', // 'debug', 'info', 'warn', 'error'
showConnectionDetails: false,
}
};
// 环境检测
const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// 根据环境调整配置
if (isDevelopment) {
GameConfig.debug.enableLogging = true;
GameConfig.debug.logLevel = 'debug';
} else {
GameConfig.debug.enableLogging = false;
GameConfig.performance.enableAnimations = true;
}
if (isMobile) {
GameConfig.ui.showKeyboardHints = false;
GameConfig.performance.enableHoverEffects = false;
GameConfig.game.maxDotsPerSquare = 50; // 移动端减少圆点数量
}
// 导出配置
if (typeof module !== 'undefined' && module.exports) {
module.exports = GameConfig;
} else {
window.GameConfig = GameConfig;
}

View File

@@ -1,11 +1,206 @@
const { createApp } = Vue;
// WebSocket服务类
class WebSocketService {
constructor() {
this.connections = new Map();
this.reconnectAttempts = new Map();
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
}
connect(squareId, onMessage, onStatusChange) {
const wsUrl = `ws://localhost:8501/?token=${squareId}`;
try {
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
console.log(`WebSocket ${squareId} connected`);
onStatusChange(true);
this.reconnectAttempts.set(squareId, 0);
// 发送进入实例消息protobuf
protoUtils.readyPromise.then(() => {
const enterInstanceBuffer = protoUtils.createEnterInstanceMessage(1);
if (enterInstanceBuffer) {
this.sendMessage(ws, enterInstanceBuffer);
}
});
};
ws.onclose = (e) => {
console.log(`WebSocket ${squareId} disconnected:`, e.code, e.reason);
onStatusChange(false);
this.connections.delete(squareId);
// 自动重连
this.scheduleReconnect(squareId, onMessage, onStatusChange);
};
ws.onerror = (error) => {
console.error(`WebSocket ${squareId} error:`, error);
};
ws.onmessage = (event) => {
try {
onMessage(event.data, squareId);
} catch (error) {
console.error('Error processing message:', error);
}
};
this.connections.set(squareId, ws);
} catch (error) {
console.error(`WebSocket ${squareId} init error:`, error);
}
}
sendMessage(ws, data) {
if (ws.readyState === WebSocket.OPEN) {
try {
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
ws.send(data);
} else {
console.error('Attempted to send non-binary data through binary channel.');
}
} catch (error) {
console.error('Failed to send message:', error);
}
}
}
// 发送动作消息
sendAction(squareId, actionId, x, y) {
const ws = this.connections.get(squareId);
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.warn(`Cannot send action: WebSocket ${squareId} not connected`);
return false;
}
protoUtils.readyPromise.then(() => {
const actionBuffer = protoUtils.createActionMessage(actionId, x, y);
if (actionBuffer) {
this.sendMessage(ws, actionBuffer);
console.log(`Action sent to square ${squareId}: actionId=${actionId}, x=${x}, y=${y}`);
return true;
}
});
return false;
}
// 发送方向动作消息
sendDirectionAction(squareId, directionBits) {
const ws = this.connections.get(squareId);
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.warn(`Cannot send direction action: WebSocket ${squareId} not connected`);
return false;
}
protoUtils.readyPromise.then(() => {
const actionBuffer = protoUtils.createDirectionActionMessage(directionBits);
if (actionBuffer) {
this.sendMessage(ws, actionBuffer);
console.log(`Direction action sent to square ${squareId}: directionBits=${directionBits}`);
return true;
}
});
return false;
}
scheduleReconnect(squareId, onMessage, onStatusChange) {
const attempts = this.reconnectAttempts.get(squareId) || 0;
if (attempts < this.maxReconnectAttempts) {
const delay = this.reconnectDelay * Math.pow(2, attempts);
this.reconnectAttempts.set(squareId, attempts + 1);
setTimeout(() => {
console.log(`Attempting to reconnect WebSocket ${squareId} (attempt ${attempts + 1})`);
this.connect(squareId, onMessage, onStatusChange);
}, delay);
} else {
console.error(`Max reconnection attempts reached for WebSocket ${squareId}`);
}
}
disconnect(squareId) {
const ws = this.connections.get(squareId);
if (ws) {
ws.close();
this.connections.delete(squareId);
}
}
disconnectAll() {
this.connections.forEach((ws, squareId) => {
this.disconnect(squareId);
});
}
}
// 方块组件
const SquareComponent = {
props: ['squareId', 'connected', 'dots'],
template: `
<div class="square">
<div class="connection-dot" :class="{ connected: connected }"></div>
<div class="loading-indicator" v-if="!connected">
<div class="spinner"></div>
</div>
<div
v-for="(dot, index) in dots"
:key="dot.uid"
class="game-dot"
:class="{ 'player-controlled': dot.isPlayerControlled }"
:style="{ left: dot.x + 'px', top: transformY(dot.y) + 'px' }"
></div>
</div>
`,
methods: {
// 转换Y坐标使左下角为原点
transformY(y) {
// 假设方块高度为400px从GameConfig获取
const height = GameConfig.game.gridSize.height;
// 将y坐标从"从下到上"转换为"从上到下"
return height - y;
}
}
};
// 主应用组件
const App = {
components: {
'square-component': SquareComponent
},
template: `
<div class="app-container">
<div class="grid-container">
<div class="square" v-for="(square, index) in squares" :key="index">
<div class="connection-dot" :class="{ connected: square.connected }"></div>
<square-component
v-for="(square, index) in squares"
:key="index"
:square-id="index + 1"
:connected="square.connected"
:dots="square.dots"
@mouseenter="setActiveSquare(index + 1)"
@mouseleave="clearActiveSquare()"
></square-component>
</div>
<div class="status-bar" v-if="showStatusBar">
<div class="status-item">
连接状态: {{ connectedCount }}/{{ squares.length }}
</div>
<div class="status-item">
总圆点数: {{ totalDots }}
</div>
<div class="status-item" v-if="activeSquareId">
当前方块: {{ activeSquareId }}
</div>
<div class="status-item" v-if="directionState.active">
方向键: {{ directionStateText }}
</div>
</div>
</div>
@@ -13,89 +208,310 @@ const App = {
data() {
return {
squares: [
{ connected: false, ws: null },
{ connected: false, ws: null },
{ connected: false, ws: null },
{ connected: false, ws: null }
]
{ connected: false, dots: [] },
{ connected: false, dots: [] },
{ connected: false, dots: [] },
{ connected: false, dots: [] }
],
wsService: null,
showStatusBar: true,
activeSquareId: null,
directionState: {
active: false,
w: false, // 1
s: false, // 2
a: false, // 4
d: false // 8
},
directionKeyCodes: {
'KeyW': 'w',
'KeyS': 's',
'KeyA': 'a',
'KeyD': 'd',
// 兼容箭头键
'ArrowUp': 'w',
'ArrowDown': 's',
'ArrowLeft': 'a',
'ArrowRight': 'd'
}
}
},
computed: {
connectedCount() {
return this.squares.filter(square => square.connected).length;
},
totalDots() {
return this.squares.reduce((total, square) => total + square.dots.length, 0);
},
// 计算方向状态文本
directionStateText() {
const keys = [];
if (this.directionState.w) keys.push('W');
if (this.directionState.s) keys.push('S');
if (this.directionState.a) keys.push('A');
if (this.directionState.d) keys.push('D');
return keys.join('+') || '无';
},
// 计算方向位
directionBits() {
let bits = 0;
if (this.directionState.w) bits |= 1; // W = 1
if (this.directionState.s) bits |= 2; // S = 2
if (this.directionState.a) bits |= 4; // A = 4
if (this.directionState.d) bits |= 8; // D = 8
return bits;
}
},
mounted() {
// 为每个正方形建立独立的WebSocket连接
this.squares.forEach((square, index) => {
this.connectWebSocket(square, index);
});
this.wsService = new WebSocketService();
this.initializeConnections();
// 添加键盘事件监听
window.addEventListener('keydown', this.handleKeyDown);
window.addEventListener('keyup', this.handleKeyUp);
// 添加键盘快捷键
this.addKeyboardShortcuts();
},
beforeUnmount() {
if (this.wsService) {
this.wsService.disconnectAll();
}
// 移除键盘事件监听
window.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener('keyup', this.handleKeyUp);
},
methods: {
connectWebSocket(square, index) {
// 替换为实际的WebSocket服务器地址
const wsUrl = `ws://localhost:8501/?token=${index + 1}`;
initializeConnections() {
this.squares.forEach((square, index) => {
const squareId = index + 1;
this.wsService.connect(
squareId,
this.handleMessage,
(connected) => this.updateConnectionStatus(index, connected)
);
});
},
updateConnectionStatus(index, connected) {
this.squares[index].connected = connected;
},
handleMessage(rawData, squareId) {
const squareIndex = squareId - 1;
try {
square.ws = new WebSocket(wsUrl);
// 仅处理二进制 protobuf 消息
if (rawData instanceof ArrayBuffer || rawData instanceof Uint8Array) {
const arrayBuf = rawData instanceof Uint8Array ? rawData : new Uint8Array(rawData);
const outerMsg = protoUtils.encoder.decodeMessage(arrayBuf);
if (!outerMsg) return;
square.ws.onopen = () => {
square.connected = true;
console.log(`WebSocket ${index + 1} connected`);
const msgId = outerMsg.ID;
const msgEnum = protoUtils.encoder.MessageID;
// 连接建立后发送初始消息
try {
square.ws.send(JSON.stringify({
type: "init"
}));
console.log(`Initial message sent to WebSocket ${index + 1}`);
} catch (error) {
console.error(`Failed to send initial message to WebSocket ${index + 1}:`, error);
}
};
// 根据消息ID处理不同类型的消息
switch (msgId) {
case msgEnum.MESSAGE_ID_POSITION: {
// 处理位置更新消息
const S2C_Position = protoUtils.encoder.root.lookupType('S2C_Position');
const positionMsg = S2C_Position.decode(outerMsg.Payload);
square.ws.onclose = (e) => {
square.connected = false;
console.log(`WebSocket ${index + 1} disconnected`);
console.log(e.code, e.reason, e.wasClean)
// 尝试重新连接
// setTimeout(() => this.connectWebSocket(square, index), 1000);
};
// 处理所有位置信息
if (positionMsg.Info && positionMsg.Info.length > 0) {
positionMsg.Info.forEach(info => {
// 注意:坐标已经是以左下角为原点,保持原样
if (utils.validateCoordinates(info.X, info.Y)) {
// 查找是否已存在该UID的点
const existingDotIndex = this.squares[squareIndex].dots.findIndex(dot => dot.uid === info.UID);
square.ws.onerror = (error) => {
console.error(`WebSocket ${index + 1} error:`, error);
};
if (existingDotIndex !== -1) {
// 已存在,更新位置
this.squares[squareIndex].dots[existingDotIndex].x = info.X;
this.squares[squareIndex].dots[existingDotIndex].y = info.Y;
this.squares[squareIndex].dots[existingDotIndex].timestamp = Date.now();
console.log(`Dot updated for square ${squareId}, UID ${info.UID}: (${info.X}, ${info.Y})`);
} else {
// 不存在,创建新点
this.squares[squareIndex].dots.push({
x: info.X,
y: info.Y,
timestamp: Date.now(),
uid: info.UID,
// 判断是否是玩家控制的点假设UID为当前squareId的是玩家控制的
isPlayerControlled: info.UID === squareId
});
square.ws.onmessage = (event) => {
console.log(`WebSocket ${index + 1} message:`, event.data);
// 处理接收到的消息
try {
const message = JSON.parse(event.data);
const arr = JSON.parse(message.data);
// 限制每个方块的圆点数量,避免内存泄漏
if (this.squares[squareIndex].dots.length > GameConfig.game.maxDotsPerSquare) {
this.squares[squareIndex].dots.shift();
}
if (Array.isArray(arr) && arr.length === 2) {
const [x, y] = arr;
console.log(`Creating dot at (${x}, ${y}) for square ${index}`);
const squareElement = document.querySelectorAll('.square')[index];
if (!squareElement) {
console.error('Square element not found');
return;
console.log(`New dot added to square ${squareId} at (${info.X}, ${info.Y}) for UID ${info.UID}`);
}
}
});
}
// 创建圆点元素
const dot = document.createElement('div');
dot.className = 'game-dot';
dot.style.left = `${x}px`;
dot.style.top = `${y}px`;
dot.style.zIndex = '10';
// 添加到游戏场景
squareElement.appendChild(dot);
console.log('Dot added successfully');
break;
}
} catch (error) {
console.error('Error processing message:', error);
case msgEnum.MESSAGE_ID_ENTER_INSTANCE: {
// 处理进入实例响应消息
const S2C_EnterInstance = protoUtils.encoder.root.lookupType('S2C_EnterInstance');
const enterInstanceMsg = S2C_EnterInstance.decode(outerMsg.Payload);
// 如果有位置信息,添加到对应方块
if (enterInstanceMsg.Info) {
const info = enterInstanceMsg.Info;
// 注意:坐标已经是以左下角为原点,保持原样
if (utils.validateCoordinates(info.X, info.Y)) {
// 查找是否已存在该UID的点
const existingDotIndex = this.squares[squareIndex].dots.findIndex(dot => dot.uid === info.UID);
if (existingDotIndex !== -1) {
// 已存在,更新位置
this.squares[squareIndex].dots[existingDotIndex].x = info.X;
this.squares[squareIndex].dots[existingDotIndex].y = info.Y;
this.squares[squareIndex].dots[existingDotIndex].timestamp = Date.now();
this.squares[squareIndex].dots[existingDotIndex].isInitial = true;
this.squares[squareIndex].dots[existingDotIndex].isPlayerControlled = info.UID === squareId;
console.log(`Initial position updated for square ${squareId}, UID ${info.UID}: (${info.X}, ${info.Y})`);
} else {
// 不存在,创建新点
this.squares[squareIndex].dots.push({
x: info.X,
y: info.Y,
timestamp: Date.now(),
uid: info.UID,
isInitial: true, // 标记为初始位置
// 判断是否是玩家控制的点假设UID为当前squareId的是玩家控制的
isPlayerControlled: info.UID === squareId
});
console.log(`Initial position created for square ${squareId}: (${info.X}, ${info.Y}) UID: ${info.UID}`);
}
}
}
break;
}
default:
console.warn('Unhandled message ID:', msgId);
break;
}
};
return; // 已处理protobuf
}
} catch (error) {
console.error(`WebSocket ${index + 1} init error:`, error);
console.error('Error processing message:', error);
}
},
// 设置当前活动方块
setActiveSquare(squareId) {
if (this.squares[squareId - 1].connected) {
this.activeSquareId = squareId;
this.directionState.active = true;
console.log(`Active square set to ${squareId}`);
}
},
// 清除当前活动方块
clearActiveSquare() {
if (this.activeSquareId) {
// 在清除之前发送一个id=0的action消息
this.sendDirectionAction(this.activeSquareId, 0);
// 然后清除状态
this.activeSquareId = null;
this.directionState.active = false;
// 重置所有方向键状态
this.directionState.w = false;
this.directionState.s = false;
this.directionState.a = false;
this.directionState.d = false;
}
},
// 处理键盘按下事件
handleKeyDown(event) {
if (!this.activeSquareId) return;
const key = this.directionKeyCodes[event.code];
if (key && this.directionState.hasOwnProperty(key)) {
// 只有当状态真正变化时才发送
if (!this.directionState[key]) {
this.directionState[key] = true;
// 计算新的方向位并发送
const directionBits = this.directionBits;
this.sendDirectionAction(this.activeSquareId, directionBits);
}
event.preventDefault(); // 防止页面滚动
}
},
// 处理键盘释放事件
handleKeyUp(event) {
if (!this.activeSquareId) return;
const key = this.directionKeyCodes[event.code];
if (key && this.directionState.hasOwnProperty(key)) {
// 只有当状态真正变化时才发送
if (this.directionState[key]) {
this.directionState[key] = false;
// 计算新的方向位并发送
const directionBits = this.directionBits;
this.sendDirectionAction(this.activeSquareId, directionBits);
}
event.preventDefault();
}
},
addKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'h':
this.showStatusBar = !this.showStatusBar;
break;
case 'c':
this.clearAllDots();
break;
case 'r':
this.reconnectAll();
break;
}
});
},
clearAllDots() {
this.squares.forEach(square => {
square.dots = [];
});
console.log('All dots cleared');
},
reconnectAll() {
this.wsService.disconnectAll();
setTimeout(() => {
this.initializeConnections();
}, 1000);
console.log('Reconnecting all WebSockets...');
},
// 发送动作消息到服务器
sendAction(squareId, actionId, x, y) {
if (this.wsService) {
return this.wsService.sendAction(squareId, actionId, x, y);
}
return false;
},
// 发送方向动作消息到服务器
sendDirectionAction(squareId, directionBits) {
if (this.wsService) {
return this.wsService.sendDirectionAction(squareId, directionBits);
}
return false;
}
}
};

View File

@@ -4,13 +4,31 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="游戏方块容器 - 实时WebSocket连接监控">
<meta name="keywords" content="游戏,WebSocket,实时监控,Vue.js">
<meta name="author" content="游戏开发团队">
<title>游戏方块容器</title>
<link rel="stylesheet" href="style.css">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="lib/vue.global.js"></script>
<script src="lib/protobuf.min.js"></script>
<script src="config.js"></script>
<script src="utils.js"></script>
<script src="proto.js"></script>
</head>
<body>
<div id="app"></div>
<!-- 键盘快捷键提示 -->
<div class="keyboard-hints">
<h4>键盘快捷键</h4>
<ul>
<li><strong>H</strong> - 隐藏/显示状态栏</li>
<li><strong>C</strong> - 清除所有圆点</li>
<li><strong>R</strong> - 重新连接所有WebSocket</li>
</ul>
</div>
<script src="game.js"></script>
</body>

8
Client/web/lib/protobuf.min.js vendored Normal file

File diff suppressed because one or more lines are too long

18193
Client/web/lib/vue.global.js Normal file

File diff suppressed because it is too large Load Diff

303
Client/web/proto.js Normal file
View File

@@ -0,0 +1,303 @@
// Proto.js - 游戏协议处理
// 基于 proto/action.proto 和 proto/define.proto 生成
// 协议定义(将由本地 .proto 文件动态填充枚举值)
const GameProto = {};
// 协议编码器
class ProtoEncoder {
constructor() {
this.protobuf = window.protobuf;
this.root = null;
this.readyPromise = this.initProtobuf();
}
async initProtobuf() {
try {
// 使用本地 .proto 文件,避免硬编码
const root = await this.protobuf.load([
"proto/define.proto",
"proto/action.proto"
]);
this.root = root;
// 缓存枚举,供其他模块使用
this.MessageID = this.root.lookupEnum('MessageID').values;
this.ActionID = this.root.lookupEnum('ActionID').values;
// 更新全局GameProto枚举避免硬编码
GameProto.MessageID = this.MessageID;
GameProto.ActionID = this.ActionID;
utils.info('Protobuf loaded from local files successfully');
} catch (error) {
utils.error('Failed to load protobuf files:', error);
}
}
// 编码消息
encodeMessage(messageId, payload) {
try {
if (!this.root) {
throw new Error('Protobuf not initialized');
}
const Message = this.root.lookupType('Message');
const message = {
ID: messageId,
Payload: payload
};
const buffer = Message.encode(message).finish();
return buffer;
} catch (error) {
utils.error('Failed to encode message:', error);
return null;
}
}
// 编码动作
encodeAction(actionId, payload) {
try {
if (!this.root) {
throw new Error('Protobuf not initialized');
}
const C2S_Action = this.root.lookupType('C2S_Action');
const action = {
Action: actionId,
Payload: payload
};
const buffer = C2S_Action.encode(action).finish();
return buffer;
} catch (error) {
utils.error('Failed to encode action:', error);
return null;
}
}
// 解码消息
decodeMessage(buffer) {
try {
if (!this.root) {
throw new Error('Protobuf not initialized');
}
const Message = this.root.lookupType('Message');
const message = Message.decode(buffer);
return message;
} catch (error) {
utils.error('Failed to decode message:', error);
return null;
}
}
// 解码动作
decodeAction(buffer) {
try {
if (!this.root) {
throw new Error('Protobuf not initialized');
}
const C2S_Action = this.root.lookupType('C2S_Action');
const action = C2S_Action.decode(buffer);
return action;
} catch (error) {
utils.error('Failed to decode action:', error);
return null;
}
}
}
// 协议工具类
class ProtoUtils {
constructor() {
this.encoder = new ProtoEncoder();
this.readyPromise = this.encoder.readyPromise;
}
// 创建移动指令
createMoveAction(x, y) {
const moveData = new Uint8Array([x, y]);
return this.encoder.encodeAction(this.encoder.ActionID.ACTION_ID_MOVE, moveData);
}
// 创建攻击指令
createAttackAction(targetId) {
const attackData = new Uint8Array([targetId]);
return this.encoder.encodeAction(this.encoder.ActionID.ACTION_ID_ATTACK, attackData);
}
// 创建消息
createMessage(messageId, payload) {
return this.encoder.encodeMessage(messageId, payload);
}
// 解析接收到的数据
parseReceivedData(data) {
try {
// 如果是字符串尝试解析为JSON
if (typeof data === 'string') {
const jsonData = JSON.parse(data);
return {
type: 'json',
data: jsonData
};
}
// 如果是二进制数据尝试解析为protobuf
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
const message = this.encoder.decodeMessage(data);
return {
type: 'protobuf',
data: message
};
}
return {
type: 'unknown',
data: data
};
} catch (error) {
utils.error('Failed to parse received data:', error);
return {
type: 'error',
data: null,
error: error.message
};
}
}
// 验证坐标数据
validateCoordinateData(data) {
if (!Array.isArray(data) || data.length !== 2) {
return false;
}
const [x, y] = data;
return utils.validateCoordinates(x, y);
}
// 格式化坐标数据
formatCoordinateData(data) {
if (this.validateCoordinateData(data)) {
const [x, y] = data;
return {
x: Math.round(x),
y: Math.round(y),
timestamp: Date.now()
};
}
return null;
}
// 创建进入实例消息外层Message
createEnterInstanceMessage(instanceId = 0) {
try {
if (!this.encoder.root) {
utils.warn('Protobuf root not ready');
return null;
}
const C2S_EnterInstance = this.encoder.root.lookupType('C2S_EnterInstance');
const enterInstance = {
InstanceID: instanceId
};
const innerBuffer = C2S_EnterInstance.encode(enterInstance).finish();
return this.encoder.encodeMessage(
this.encoder.MessageID.MESSAGE_ID_ENTER_INSTANCE,
innerBuffer
);
} catch (error) {
utils.error('Failed to create EnterInstance message:', error);
return null;
}
}
// 创建动作消息外层Message
createActionMessage(actionId, x, y) {
try {
if (!this.encoder.root) {
utils.warn('Protobuf root not ready');
return null;
}
// 创建移动指令的payload
let payload;
if (actionId === this.encoder.ActionID.ACTION_ID_MOVE) {
// 创建包含x,y坐标的二进制数据
// 注意这里使用Float64Array来存储双精度浮点数因为proto中X和Y是double类型
const buffer = new ArrayBuffer(16); // 8字节 * 2 = 16字节
const view = new Float64Array(buffer);
view[0] = x;
view[1] = y;
payload = new Uint8Array(buffer);
} else {
// 其他类型的动作可以在这里添加
utils.warn('Unsupported action ID:', actionId);
return null;
}
// 创建C2S_Action消息
const C2S_Action = this.encoder.root.lookupType('C2S_Action');
const action = {
Action: actionId,
Payload: payload
};
const actionBuffer = C2S_Action.encode(action).finish();
// 封装到外层Message
return this.encoder.encodeMessage(
this.encoder.MessageID.MESSAGE_ID_ACTION,
actionBuffer
);
} catch (error) {
utils.error('Failed to create Action message:', error);
return null;
}
}
// 创建WASD方向动作消息外层Message
createDirectionActionMessage(directionBits) {
try {
if (!this.encoder.root) {
utils.warn('Protobuf root not ready');
return null;
}
// 创建C2S_Action消息payload为空
const C2S_Action = this.encoder.root.lookupType('C2S_Action');
const action = {
Action: directionBits, // 使用位运算结果作为动作ID
Payload: new Uint8Array(0) // 空payload
};
const actionBuffer = C2S_Action.encode(action).finish();
// 封装到外层Message
return this.encoder.encodeMessage(
this.encoder.MessageID.MESSAGE_ID_ACTION,
actionBuffer
);
} catch (error) {
utils.error('Failed to create Direction Action message:', error);
return null;
}
}
}
// 创建全局协议工具实例
const protoUtils = new ProtoUtils();
// 导出到全局
window.GameProto = GameProto;
window.ProtoEncoder = ProtoEncoder;
window.ProtoUtils = ProtoUtils;
window.protoUtils = protoUtils;
// 兼容性处理
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
GameProto,
ProtoEncoder,
ProtoUtils,
protoUtils
};
}

View File

@@ -0,0 +1,33 @@
syntax = "proto3";
option go_package = "common/proto/gen/cs";
import "common.proto";
// MESSAGE_ID_ENTER_INSTANCE
message C2S_EnterInstance {
int32 InstanceID = 1;
}
message S2C_EnterInstance {
PositionInfo Info = 1;
}
// MESSAGE_ID_ACTION
enum ActionID {
ACTION_ID_INVALID = 0;
ACTION_ID_MOVE = 1; // 1-15都是移动指令
ACTION_ID_ATTACK = 16; // 攻击指令
}
message C2S_Action {
ActionID Action = 1; // 指令ID
bytes Payload = 2; // 指令数据
}
// MESSAGE_ID_POSITION
message PositionInfo {
int32 UID = 1;
double X = 2;
double Y = 3;
}
message S2C_Position {
repeated PositionInfo Info = 1;
}

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
// 通用占位符文件避免import错误
message Placeholder {
int32 id = 1;
}

View File

@@ -0,0 +1,16 @@
syntax = "proto3";
option go_package = "common/proto/gen/cs";
import "common.proto";
enum MessageID {
MESSAGE_ID_INVALID = 0;
MESSAGE_ID_ENTER_INSTANCE = 1; // 进入副本
MESSAGE_ID_ACTION = 2; // 指令
MESSAGE_ID_POSITION = 3; // 位置更新
}
message Message {
MessageID ID = 1;
bytes Payload = 2;
}

View File

@@ -4,6 +4,7 @@ body, html {
padding: 0;
height: 100%;
background-color: #f5f5f5;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 应用容器填满整个视口并居中 */
@@ -11,8 +12,10 @@ body, html {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
/* 重置body样式 */
@@ -22,25 +25,49 @@ body {
overflow: hidden;
}
/* 网格容器 - 固定2x2布局 */
/* 网格容器 - 响应式布局 */
.grid-container {
display: grid;
grid-template-columns: 400px 400px;
grid-template-rows: 400px 400px;
gap: 30px;
padding: 40px;
grid-template-columns: repeat(auto-fit, minmax(300px, 400px));
grid-template-rows: repeat(auto-fit, minmax(300px, 400px));
gap: 20px;
padding: 20px;
background-color: #e0e0e0;
min-height: 100vh;
width: 100vw;
box-sizing: border-box;
margin: 0 auto;
place-content: center;
transition: all 0.3s ease;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 15px;
padding: 15px;
}
}
@media (max-width: 768px) {
.grid-container {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, 1fr);
gap: 10px;
padding: 10px;
}
}
/* 正方形样式 */
.square {
width: 400px;
height: 400px;
width: 100%;
height: 100%;
min-width: 300px;
min-height: 300px;
max-width: 400px;
max-height: 400px;
background-color: #ffffff;
background-image:
linear-gradient(to right, #ddd 1px, transparent 1px),
@@ -50,6 +77,13 @@ body {
border-radius: 15px;
position: relative;
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
transition: all 0.3s ease;
overflow: hidden;
}
.square:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
/* 连接状态圆点 */
@@ -61,20 +95,199 @@ body {
height: 20px;
border-radius: 50%;
background-color: #ff4444; /* 默认断开状态-红色 */
transition: background-color 0.3s;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
z-index: 10;
}
.connection-dot.connected {
background-color: #44ff44; /* 连接状态-绿色 */
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* 加载指示器 */
.loading-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 游戏动态圆点 */
.game-dot {
position: absolute;
width: 10px;
height: 10px;
background-color: red;
width: 12px;
height: 12px;
background: linear-gradient(45deg, #ff6b6b, #ff8e8e);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
animation: dotAppear 0.3s ease-out;
z-index: 5;
/* 平滑移动效果 */
transition: left 0.08s linear, top 0.08s linear;
will-change: left, top, transform;
/* 启用GPU加速 */
transform: translate3d(-50%, -50%, 0);
backface-visibility: hidden;
}
/* 标记为玩家控制的点 */
.game-dot.player-controlled {
background: linear-gradient(45deg, #4a90e2, #63b3ed);
box-shadow: 0 0 10px rgba(74, 144, 226, 0.6);
width: 14px;
height: 14px;
/* 为玩家控制的点设置更快的响应速度 */
transition: left 0.06s linear, top 0.06s linear;
}
@keyframes dotAppear {
0% {
transform: translate3d(-50%, -50%, 0) scale(0);
opacity: 0;
}
50% {
transform: translate3d(-50%, -50%, 0) scale(1.2);
opacity: 0.8;
}
100% {
transform: translate3d(-50%, -50%, 0) scale(1);
opacity: 1;
}
}
/* 状态栏 */
.status-bar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 20px;
border-radius: 25px;
display: flex;
gap: 20px;
font-size: 14px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
z-index: 100;
transition: all 0.3s ease;
}
.status-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-item::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #44ff44;
}
/* 键盘快捷键提示 */
.keyboard-hints {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px;
border-radius: 10px;
font-size: 12px;
backdrop-filter: blur(10px);
z-index: 100;
opacity: 0.8;
transition: opacity 0.3s ease;
}
.keyboard-hints:hover {
opacity: 1;
}
.keyboard-hints h4 {
margin: 0 0 10px 0;
font-size: 14px;
}
.keyboard-hints ul {
margin: 0;
padding-left: 20px;
}
.keyboard-hints li {
margin: 5px 0;
}
/* 错误状态样式 */
.square.error {
border-color: #ff4444;
animation: errorShake 0.5s ease-in-out;
}
@keyframes errorShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* 性能优化 */
.square {
will-change: transform;
}
.game-dot {
will-change: transform, opacity;
}
/* 无障碍支持 */
@media (prefers-reduced-motion: reduce) {
.spinner,
.game-dot,
.connection-dot.connected {
animation: none;
}
.square:hover {
transform: none;
}
}
/* 高对比度模式支持 */
@media (prefers-contrast: high) {
.square {
border-width: 3px;
border-color: #000;
}
.game-dot {
background: #000;
box-shadow: 0 0 0 2px #fff;
}
}

214
Client/web/utils.js Normal file
View File

@@ -0,0 +1,214 @@
// 工具类
class Utils {
constructor() {
this.logLevels = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
this.currentLogLevel = this.logLevels[GameConfig.debug.logLevel];
}
// 日志记录
log(level, message, ...args) {
if (!GameConfig.debug.enableLogging) return;
const levelNum = this.logLevels[level];
if (levelNum >= this.currentLogLevel) {
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
switch (level) {
case 'debug':
console.debug(prefix, message, ...args);
break;
case 'info':
console.info(prefix, message, ...args);
break;
case 'warn':
console.warn(prefix, message, ...args);
break;
case 'error':
console.error(prefix, message, ...args);
break;
}
}
}
debug(message, ...args) {
this.log('debug', message, ...args);
}
info(message, ...args) {
this.log('info', message, ...args);
}
warn(message, ...args) {
this.log('warn', message, ...args);
}
error(message, ...args) {
this.log('error', message, ...args);
}
// 性能监控
measurePerformance(name, fn) {
const start = performance.now();
const result = fn();
const end = performance.now();
const duration = end - start;
this.debug(`Performance [${name}]: ${duration.toFixed(2)}ms`);
return { result, duration };
}
// 防抖函数
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 坐标验证
validateCoordinates(x, y, maxX = 400, maxY = 400) {
// 坐标系以左下角为原点x轴从左到右y轴从下到上
return x >= 0 && x <= maxX && y >= 0 && y <= maxY;
}
// 随机颜色生成
generateRandomColor() {
const colors = [
'#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4',
'#feca57', '#ff9ff3', '#54a0ff', '#5f27cd'
];
return colors[Math.floor(Math.random() * colors.length)];
}
// 格式化时间
formatTime(ms) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${(ms / 60000).toFixed(1)}m`;
}
// 格式化字节大小
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 深拷贝
deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof Array) return obj.map(item => this.deepClone(item));
if (typeof obj === 'object') {
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = this.deepClone(obj[key]);
}
}
return clonedObj;
}
}
// 本地存储工具
storage = {
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Failed to save to localStorage:', error);
}
},
get(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('Failed to read from localStorage:', error);
return defaultValue;
}
},
remove(key) {
try {
localStorage.removeItem(key);
} catch (error) {
console.error('Failed to remove from localStorage:', error);
}
}
};
// 网络状态检测
checkNetworkStatus() {
return navigator.onLine;
}
// 设备信息
getDeviceInfo() {
return {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
cookieEnabled: navigator.cookieEnabled,
onLine: navigator.onLine,
screenWidth: screen.width,
screenHeight: screen.height,
windowWidth: window.innerWidth,
windowHeight: window.innerHeight
};
}
// 错误处理
handleError(error, context = '') {
this.error(`Error in ${context}:`, error);
// 可以在这里添加错误上报逻辑
if (GameConfig.debug.showConnectionDetails) {
console.group('Error Details');
console.error('Error:', error);
console.error('Context:', context);
console.error('Stack:', error.stack);
console.error('Device Info:', this.getDeviceInfo());
console.groupEnd();
}
}
}
// 创建全局工具实例
const utils = new Utils();
// 导出工具类
if (typeof module !== 'undefined' && module.exports) {
module.exports = Utils;
} else {
window.Utils = Utils;
window.utils = utils;
}

View File

@@ -5,3 +5,6 @@ cd ../../Server/common
protoc --proto_path=. --go_out=.. ./proto/common.proto
protoc --proto_path=./proto --go_out=.. ./proto/grpc/*.proto
protoc --proto_path=./proto --go_out=.. --go-grpc_out=.. ./proto/grpc/*.proto
protoc --proto_path=./proto --go_out=.. ./proto/cs/*.proto
xcopy ./proto/cs/*.* ../../Client/web/proto/