web client
This commit is contained in:
216
Client/web/README.md
Normal file
216
Client/web/README.md
Normal 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
71
Client/web/config.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,11 +1,206 @@
|
|||||||
const { createApp } = Vue;
|
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 = {
|
const App = {
|
||||||
|
components: {
|
||||||
|
'square-component': SquareComponent
|
||||||
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<div class="square" v-for="(square, index) in squares" :key="index">
|
<square-component
|
||||||
<div class="connection-dot" :class="{ connected: square.connected }"></div>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -13,89 +208,310 @@ const App = {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
squares: [
|
squares: [
|
||||||
{ connected: false, ws: null },
|
{ connected: false, dots: [] },
|
||||||
{ connected: false, ws: null },
|
{ connected: false, dots: [] },
|
||||||
{ connected: false, ws: null },
|
{ connected: false, dots: [] },
|
||||||
{ connected: false, ws: null }
|
{ 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() {
|
mounted() {
|
||||||
// 为每个正方形建立独立的WebSocket连接
|
this.wsService = new WebSocketService();
|
||||||
this.squares.forEach((square, index) => {
|
this.initializeConnections();
|
||||||
this.connectWebSocket(square, index);
|
|
||||||
});
|
// 添加键盘事件监听
|
||||||
|
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: {
|
methods: {
|
||||||
connectWebSocket(square, index) {
|
initializeConnections() {
|
||||||
// 替换为实际的WebSocket服务器地址
|
this.squares.forEach((square, index) => {
|
||||||
const wsUrl = `ws://localhost:8501/?token=${index + 1}`;
|
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 {
|
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 = () => {
|
const msgId = outerMsg.ID;
|
||||||
square.connected = true;
|
const msgEnum = protoUtils.encoder.MessageID;
|
||||||
console.log(`WebSocket ${index + 1} connected`);
|
|
||||||
|
|
||||||
// 连接建立后发送初始消息
|
// 根据消息ID处理不同类型的消息
|
||||||
try {
|
switch (msgId) {
|
||||||
square.ws.send(JSON.stringify({
|
case msgEnum.MESSAGE_ID_POSITION: {
|
||||||
type: "init"
|
// 处理位置更新消息
|
||||||
}));
|
const S2C_Position = protoUtils.encoder.root.lookupType('S2C_Position');
|
||||||
console.log(`Initial message sent to WebSocket ${index + 1}`);
|
const positionMsg = S2C_Position.decode(outerMsg.Payload);
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to send initial message to WebSocket ${index + 1}:`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
square.ws.onclose = (e) => {
|
// 处理所有位置信息
|
||||||
square.connected = false;
|
if (positionMsg.Info && positionMsg.Info.length > 0) {
|
||||||
console.log(`WebSocket ${index + 1} disconnected`);
|
positionMsg.Info.forEach(info => {
|
||||||
console.log(e.code, e.reason, e.wasClean)
|
// 注意:坐标已经是以左下角为原点,保持原样
|
||||||
// 尝试重新连接
|
if (utils.validateCoordinates(info.X, info.Y)) {
|
||||||
// setTimeout(() => this.connectWebSocket(square, index), 1000);
|
// 查找是否已存在该UID的点
|
||||||
};
|
const existingDotIndex = this.squares[squareIndex].dots.findIndex(dot => dot.uid === info.UID);
|
||||||
|
|
||||||
square.ws.onerror = (error) => {
|
if (existingDotIndex !== -1) {
|
||||||
console.error(`WebSocket ${index + 1} error:`, error);
|
// 已存在,更新位置
|
||||||
};
|
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);
|
if (this.squares[squareIndex].dots.length > GameConfig.game.maxDotsPerSquare) {
|
||||||
// 处理接收到的消息
|
this.squares[squareIndex].dots.shift();
|
||||||
try {
|
}
|
||||||
const message = JSON.parse(event.data);
|
|
||||||
const arr = JSON.parse(message.data);
|
|
||||||
|
|
||||||
if (Array.isArray(arr) && arr.length === 2) {
|
console.log(`New dot added to square ${squareId} at (${info.X}, ${info.Y}) for UID ${info.UID}`);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
// 创建圆点元素
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
} 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) {
|
} 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,13 +4,31 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
<title>游戏方块容器</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<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>
|
<script src="game.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
8
Client/web/lib/protobuf.min.js
vendored
Normal file
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
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
303
Client/web/proto.js
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
33
Client/web/proto/action.proto
Normal file
33
Client/web/proto/action.proto
Normal 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;
|
||||||
|
}
|
||||||
6
Client/web/proto/common.proto
Normal file
6
Client/web/proto/common.proto
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
// 通用占位符文件,避免import错误
|
||||||
|
message Placeholder {
|
||||||
|
int32 id = 1;
|
||||||
|
}
|
||||||
16
Client/web/proto/define.proto
Normal file
16
Client/web/proto/define.proto
Normal 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;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ body, html {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 应用容器填满整个视口并居中 */
|
/* 应用容器填满整个视口并居中 */
|
||||||
@@ -11,8 +12,10 @@ body, html {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 重置body样式 */
|
/* 重置body样式 */
|
||||||
@@ -22,25 +25,49 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 网格容器 - 固定2x2布局 */
|
/* 网格容器 - 响应式布局 */
|
||||||
.grid-container {
|
.grid-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 400px 400px;
|
grid-template-columns: repeat(auto-fit, minmax(300px, 400px));
|
||||||
grid-template-rows: 400px 400px;
|
grid-template-rows: repeat(auto-fit, minmax(300px, 400px));
|
||||||
gap: 30px;
|
gap: 20px;
|
||||||
padding: 40px;
|
padding: 20px;
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
place-content: center;
|
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 {
|
.square {
|
||||||
width: 400px;
|
width: 100%;
|
||||||
height: 400px;
|
height: 100%;
|
||||||
|
min-width: 300px;
|
||||||
|
min-height: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 400px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(to right, #ddd 1px, transparent 1px),
|
linear-gradient(to right, #ddd 1px, transparent 1px),
|
||||||
@@ -50,6 +77,13 @@ body {
|
|||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
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;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #ff4444; /* 默认断开状态-红色 */
|
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 {
|
.connection-dot.connected {
|
||||||
background-color: #44ff44; /* 连接状态-绿色 */
|
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 {
|
.game-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 10px;
|
width: 12px;
|
||||||
height: 10px;
|
height: 12px;
|
||||||
background-color: red;
|
background: linear-gradient(45deg, #ff6b6b, #ff8e8e);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
pointer-events: none;
|
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
214
Client/web/utils.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -5,3 +5,6 @@ cd ../../Server/common
|
|||||||
protoc --proto_path=. --go_out=.. ./proto/common.proto
|
protoc --proto_path=. --go_out=.. ./proto/common.proto
|
||||||
protoc --proto_path=./proto --go_out=.. ./proto/grpc/*.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=.. --go-grpc_out=.. ./proto/grpc/*.proto
|
||||||
|
protoc --proto_path=./proto --go_out=.. ./proto/cs/*.proto
|
||||||
|
|
||||||
|
xcopy ./proto/cs/*.* ../../Client/web/proto/
|
||||||
Reference in New Issue
Block a user