修复提交成绩报错
This commit is contained in:
@@ -1,167 +0,0 @@
|
||||
# 多人游戏观战和实时排行榜改进说明
|
||||
|
||||
## 主要改进
|
||||
|
||||
### 1. 游戏状态缓冲区 (GameplayStateBuffer)
|
||||
- **实时分数缓冲**: 为每个房间的每个玩家维护最多50帧的分数数据
|
||||
- **实时排行榜**: 自动计算和维护实时排行榜数据
|
||||
- **游戏状态快照**: 为新加入的观众创建完整的游戏状态快照
|
||||
- **观战者状态缓存**: 跟踪观战者状态以优化同步
|
||||
|
||||
### 2. 观战同步管理器 (SpectatorSyncManager)
|
||||
- **跨Hub通信**: 通过Redis在MultiplayerHub和SpectatorHub之间同步状态
|
||||
- **事件通知**: 游戏开始/结束、用户状态变化等事件的实时通知
|
||||
- **异步消息处理**: 订阅和处理观战相关事件
|
||||
|
||||
### 3. 增强的MultiplayerHub功能
|
||||
|
||||
#### 新增方法:
|
||||
- `UpdateScore(client, score_data)`: 接收实时分数更新
|
||||
- `GetLeaderboard(client)`: 获取当前排行榜
|
||||
- `RequestSpectatorSync(client)`: 观战者请求状态同步
|
||||
|
||||
#### 改进的方法:
|
||||
- `JoinRoomWithPassword`: 增强新用户加入时的状态同步
|
||||
- `ChangeState`: 添加观战状态处理和分数缓冲区管理
|
||||
- `start_gameplay`: 启动实时排行榜广播和创建游戏快照
|
||||
- `change_room_state`: 处理游戏结束时的清理工作
|
||||
|
||||
### 4. 实时排行榜系统
|
||||
- **自动广播**: 每秒更新一次实时排行榜
|
||||
- **智能启停**: 根据游戏状态自动启动/停止广播任务
|
||||
- **最终排行榜**: 游戏结束时发送最终排行榜
|
||||
|
||||
## 客户端集成示例
|
||||
|
||||
### JavaScript客户端示例
|
||||
```javascript
|
||||
// 连接到MultiplayerHub
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl("/multiplayer")
|
||||
.build();
|
||||
|
||||
// 监听实时排行榜更新
|
||||
connection.on("LeaderboardUpdate", (leaderboard) => {
|
||||
updateLeaderboardUI(leaderboard);
|
||||
});
|
||||
|
||||
// 监听游戏状态同步(观战者)
|
||||
connection.on("GameplayStateSync", (snapshot) => {
|
||||
syncSpectatorUI(snapshot);
|
||||
});
|
||||
|
||||
// 监听最终排行榜
|
||||
connection.on("FinalLeaderboard", (finalLeaderboard) => {
|
||||
showFinalResults(finalLeaderboard);
|
||||
});
|
||||
|
||||
// 发送分数更新(玩家)
|
||||
async function updateScore(scoreData) {
|
||||
try {
|
||||
await connection.invoke("UpdateScore", scoreData);
|
||||
} catch (err) {
|
||||
console.error("Error updating score:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 请求观战同步(观战者)
|
||||
async function requestSpectatorSync() {
|
||||
try {
|
||||
await connection.invoke("RequestSpectatorSync");
|
||||
} catch (err) {
|
||||
console.error("Error requesting sync:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前排行榜
|
||||
async function getCurrentLeaderboard() {
|
||||
try {
|
||||
return await connection.invoke("GetLeaderboard");
|
||||
} catch (err) {
|
||||
console.error("Error getting leaderboard:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python客户端示例
|
||||
```python
|
||||
import signalrcore
|
||||
|
||||
# 创建连接
|
||||
connection = signalrcore.HubConnectionBuilder() \
|
||||
.with_url("ws://localhost:8000/multiplayer") \
|
||||
.build()
|
||||
|
||||
# 监听排行榜更新
|
||||
def on_leaderboard_update(leaderboard):
|
||||
print("Leaderboard update:", leaderboard)
|
||||
# 更新UI显示排行榜
|
||||
|
||||
connection.on("LeaderboardUpdate", on_leaderboard_update)
|
||||
|
||||
# 监听游戏状态同步
|
||||
def on_gameplay_state_sync(snapshot):
|
||||
print("Gameplay state sync:", snapshot)
|
||||
# 同步观战界面
|
||||
|
||||
connection.on("GameplayStateSync", on_gameplay_state_sync)
|
||||
|
||||
# 发送分数更新
|
||||
async def send_score_update(score, combo, accuracy):
|
||||
await connection.send("UpdateScore", {
|
||||
"score": score,
|
||||
"combo": combo,
|
||||
"accuracy": accuracy,
|
||||
"completed": False
|
||||
})
|
||||
|
||||
# 启动连接
|
||||
connection.start()
|
||||
```
|
||||
|
||||
## 配置要求
|
||||
|
||||
### Redis配置
|
||||
确保Redis服务器运行并配置正确的连接参数:
|
||||
```python
|
||||
# 在app/dependencies/database.py中
|
||||
REDIS_CONFIG = {
|
||||
'host': 'localhost',
|
||||
'port': 6379,
|
||||
'db': 0,
|
||||
'decode_responses': True
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库表结构
|
||||
确保`multiplayer_event`表包含以下字段:
|
||||
- `event_detail`: JSON字段,用于存储事件详细信息
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **缓冲区大小调整**: 根据实际需求调整分数帧缓冲区大小(默认50帧)
|
||||
2. **广播频率调整**: 可以根据网络条件调整排行榜广播频率(默认1秒)
|
||||
3. **内存清理**: 定期清理过期的游戏状态快照和观战者状态
|
||||
4. **连接池优化**: 配置Redis连接池以处理高并发请求
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
1. **排行榜不更新**: 检查Redis连接和广播任务状态
|
||||
2. **观战者状态不同步**: 确认SpectatorSyncManager已正确初始化
|
||||
3. **分数数据丢失**: 检查缓冲区大小和清理逻辑
|
||||
|
||||
### 日志监控
|
||||
关键日志点:
|
||||
- `[MultiplayerHub] Synced gameplay state for user X`
|
||||
- `[MultiplayerHub] Broadcasted leaderboard update to room X`
|
||||
- `Error updating score for user X`
|
||||
- `Error in leaderboard broadcast loop`
|
||||
|
||||
### 调试模式
|
||||
在开发环境中启用详细日志:
|
||||
```python
|
||||
import logging
|
||||
logging.getLogger("app.signalr.hub.multiplayer").setLevel(logging.DEBUG)
|
||||
```
|
||||
@@ -108,17 +108,72 @@ class GameplayStateBuffer:
|
||||
|
||||
async def create_gameplay_snapshot(self, room_id: int, room_data: Dict):
|
||||
"""创建游戏状态快照用于新加入的观众"""
|
||||
# 序列化复杂对象
|
||||
serialized_room_data = self._serialize_room_data(room_data)
|
||||
|
||||
snapshot = {
|
||||
'room_id': room_id,
|
||||
'state': room_data.get('state'),
|
||||
'current_item': room_data.get('current_item'),
|
||||
'users': room_data.get('users', []),
|
||||
'state': serialized_room_data.get('state'),
|
||||
'current_item': serialized_room_data.get('current_item'),
|
||||
'users': serialized_room_data.get('users', []),
|
||||
'leaderboard': self.get_leaderboard(room_id),
|
||||
'created_at': datetime.now(UTC)
|
||||
'created_at': datetime.now(UTC).isoformat()
|
||||
}
|
||||
self.gameplay_snapshots[room_id] = snapshot
|
||||
return snapshot
|
||||
|
||||
def _serialize_room_data(self, room_data: Dict) -> Dict:
|
||||
"""序列化房间数据"""
|
||||
result = {}
|
||||
for key, value in room_data.items():
|
||||
if hasattr(value, 'value') and hasattr(value, 'name'):
|
||||
# 枚举类型
|
||||
result[key] = {'name': value.name, 'value': value.value}
|
||||
elif hasattr(value, '__dict__'):
|
||||
# 复杂对象
|
||||
if hasattr(value, 'model_dump'):
|
||||
result[key] = value.model_dump()
|
||||
elif hasattr(value, 'dict'):
|
||||
result[key] = value.dict()
|
||||
else:
|
||||
# 手动序列化
|
||||
obj_dict = {}
|
||||
for attr_name, attr_value in value.__dict__.items():
|
||||
if not attr_name.startswith('_'):
|
||||
obj_dict[attr_name] = self._serialize_value(attr_value)
|
||||
result[key] = obj_dict
|
||||
elif isinstance(value, (list, tuple)):
|
||||
result[key] = [self._serialize_value(item) for item in value]
|
||||
else:
|
||||
result[key] = self._serialize_value(value)
|
||||
return result
|
||||
|
||||
def _serialize_value(self, value):
|
||||
"""序列化单个值"""
|
||||
if hasattr(value, 'value') and hasattr(value, 'name'):
|
||||
# 枚举类型
|
||||
return {'name': value.name, 'value': value.value}
|
||||
elif hasattr(value, '__dict__'):
|
||||
# 复杂对象
|
||||
if hasattr(value, 'model_dump'):
|
||||
return value.model_dump()
|
||||
elif hasattr(value, 'dict'):
|
||||
return value.dict()
|
||||
else:
|
||||
obj_dict = {}
|
||||
for attr_name, attr_value in value.__dict__.items():
|
||||
if not attr_name.startswith('_'):
|
||||
obj_dict[attr_name] = self._serialize_value(attr_value)
|
||||
return obj_dict
|
||||
elif isinstance(value, (list, tuple)):
|
||||
return [self._serialize_value(item) for item in value]
|
||||
elif isinstance(value, dict):
|
||||
return {k: self._serialize_value(v) for k, v in value.items()}
|
||||
elif isinstance(value, (str, int, float, bool, type(None))):
|
||||
return value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
def get_gameplay_snapshot(self, room_id: int) -> Optional[Dict]:
|
||||
"""获取游戏状态快照"""
|
||||
return self.gameplay_snapshots.get(room_id)
|
||||
@@ -155,12 +210,48 @@ class SpectatorSyncManager:
|
||||
self.redis = redis_client
|
||||
self.channel_prefix = "multiplayer_spectator"
|
||||
|
||||
def _serialize_for_json(self, obj):
|
||||
"""递归序列化对象为JSON兼容格式"""
|
||||
if hasattr(obj, '__dict__'):
|
||||
# 如果对象有__dict__属性,将其转换为字典
|
||||
if hasattr(obj, 'model_dump'):
|
||||
# 对于Pydantic模型
|
||||
return obj.model_dump()
|
||||
elif hasattr(obj, 'dict'):
|
||||
# 对于较旧的Pydantic模型
|
||||
return obj.dict()
|
||||
else:
|
||||
# 对于普通对象
|
||||
result = {}
|
||||
for key, value in obj.__dict__.items():
|
||||
if not key.startswith('_'): # 跳过私有属性
|
||||
result[key] = self._serialize_for_json(value)
|
||||
return result
|
||||
elif isinstance(obj, dict):
|
||||
return {key: self._serialize_for_json(value) for key, value in obj.items()}
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
return [self._serialize_for_json(item) for item in obj]
|
||||
elif isinstance(obj, datetime):
|
||||
# 处理datetime对象
|
||||
return obj.isoformat()
|
||||
elif hasattr(obj, 'value') and hasattr(obj, 'name'):
|
||||
# 对于枚举类型
|
||||
return {'name': obj.name, 'value': obj.value}
|
||||
elif isinstance(obj, (str, int, float, bool, type(None))):
|
||||
return obj
|
||||
else:
|
||||
# 对于其他类型,尝试转换为字符串
|
||||
return str(obj)
|
||||
|
||||
async def notify_spectator_hubs(self, room_id: int, event_type: str, data: Dict):
|
||||
"""通知观战Hub游戏状态变化"""
|
||||
# 序列化复杂对象为JSON兼容格式
|
||||
serialized_data = self._serialize_for_json(data)
|
||||
|
||||
message = {
|
||||
'room_id': room_id,
|
||||
'event_type': event_type,
|
||||
'data': data,
|
||||
'data': serialized_data,
|
||||
'timestamp': datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user