# snake-game **Repository Path**: hackerSnail/snake-game ## Basic Information - **Project Name**: snake-game - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-08-06 - **Last Updated**: 2026-08-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 贪吃蛇大战 (Snake Battle) — 项目实现文档 > **最新更新(2026-08-06)**:新增单人模式支持,方便开发测试。详见 [第九章 · 单人模式改造](#九单人模式改造)。 ![游戏截图](doc/screenshot.png) ## 一、项目概述 基于设计大纲实现的贪吃蛇多人在线对战游戏,采用前后端分离架构: | 层级 | 技术栈 | |------|--------| | 前端 | Vue 3 + TypeScript + Vite + Pinia + Canvas + ECharts | | 后端 | Spring Boot 3.2 + WebSocket + Redis + JPA + H2 | | 通信 | REST API(认证/排行) + WebSocket(实时对战) | | 存储 | H2 内存数据库(开发)+ Redis ZSet(排行榜)+ Caffeine(本地缓存) | --- ## 二、项目结构 ``` e:\snake\ ├── doc/ # 设计文档 │ ├── back.md # 后端设计大纲 │ └── front.md # 前端设计大纲 ├── frontend/ # 前端项目 │ ├── package.json │ ├── tsconfig.json │ ├── vite.config.ts │ ├── index.html │ └── src/ │ ├── main.ts # 入口文件 │ ├── App.vue # 根组件 │ ├── router/index.ts # 路由 + 导航守卫(JWT鉴权) │ ├── types/index.ts # 全部 TypeScript 类型定义 │ ├── api/index.ts # Axios封装(拦截器自动带Token) │ ├── store/ │ │ ├── user.ts # Pinia - 用户登录态 │ │ └── gameState.ts # Pinia - 游戏实时状态 │ ├── composables/ │ │ ├── useAuth.ts # 登录/注册逻辑封装 │ │ ├── useWebSocket.ts # Socket.IO WebSocket连接管理 │ │ └── useGameLoop.ts # Canvas渲染主循环(requestAnimationFrame) │ └── views/ │ ├── LoginView.vue # 登录/注册页面 │ ├── GameView.vue # 游戏主界面 │ └── RankingView.vue # 排行榜页面 └── backend/ # 后端项目 ├── pom.xml └── src/main/ ├── resources/application.yml └── java/com/snake/ ├── SnakeApplication.java ├── model/ │ ├── User.java # 用户实体 │ ├── GameRoom.java # 游戏房间 │ ├── Player.java # 玩家蛇(移动/碰撞检测) │ ├── Food.java # 食物实体 │ ├── GameRecord.java # 游戏记录(MySQL持久化) │ ├── Point.java / Direction.java │ └── dto/ # 请求/响应DTO ├── repository/ │ ├── UserRepository.java │ └── GameRecordRepository.java ├── security/ │ ├── JwtUtils.java # JWT令牌生成/验证 │ └── JwtAuthFilter.java # 请求认证过滤器 ├── config/ │ ├── SecurityConfig.java # Spring Security配置 │ ├── WebSocketConfig.java # WebSocket端点注册 │ └── RedisConfig.java # Redis序列化配置 ├── service/ │ ├── UserService.java # 用户注册/登录(BCrypt) │ ├── GameService.java # 游戏核心逻辑(Tick驱动) │ └── RankingService.java # Redis ZSet排行榜 ├── websocket/ │ ├── WebSocketHandshakeInterceptor.java │ └── GameWebSocketHandler.java └── controller/ ├── AuthController.java # /api/auth/* ├── RankingController.java # /api/ranking/* └── GameController.java # /api/game/* ``` --- ## 三、前端实现细节 ### 3.1 用户登录与认证 - **登录流程**:用户输入账号密码 → `axios.post('/api/auth/login')` → 后端返回 JWT → 存入 `localStorage` → 路由跳转 `/game` - **Token 管理**:`api/index.ts` 请求拦截器自动从 `localStorage` 读取 Token 并附加 `Authorization: Bearer xxx` 头 - **路由守卫**:`router/index.ts` 中 `beforeEach` 检查登录态,未登录跳转至 `/login` - **注册支持**:`LoginView.vue` 通过开关切换登录/注册模式 ### 3.2 Canvas 游戏渲染 - **主循环**:`useGameLoop` 基于 `requestAnimationFrame` 驱动,避免 `setInterval` 卡顿 - **网格**:40×30 格子,每格 18px,深色背景 + 半透明网格线 - **蛇身渲染**:圆角矩形 + 渐变色彩,头部有双白色眼珠随方向移动 - **食物渲染**: - 普通食物 → 红色圆形 - 金色食物 → 金黄圆形 + 光晕特效 - **玩家标签**:蛇头上方显示用户名和分数 ### 3.3 实时对战 (WebSocket) - **连接建立**:`useWebSocket` 通过 `socket.io-client` 连接 `/ws/game` 端点,携带 JWT Token - **消息协议**: - **客户端→服务端**:`{ type: "MOVE", payload: { direction: "UP" } }` / `{ type: "JOIN" }` - **服务端→客户端**:`{ type: "UPDATE", players: [...], foods: [...], status: "..." }` - **按键处理**: - 方向键 `↑↓←→` 或 `WASD` - 空格键准备/重新开始 - 本地 + 服务端双重阻止反方向移动 ### 3.4 排行榜 (ECharts) - **数据来源**:`rankingApi.getDailyRanking()` / `rankingApi.getWeeklyRanking()` - **可视化**:ECharts 柱状图展示前10名(前3名金色渐变,其余蓝紫渐变) - **表格**:右侧完整展示前100名,带奖牌 🥇🥈🥉 - **我的排名**:底部状态栏显示当前用户排名和分数 ### 3.5 状态管理 (Pinia) | Store | 职责 | |-------|------| | `useUserStore` | Token、用户信息、登录态、localStorage 同步 | | `useGameStore` | 游戏状态(玩家列表、食物、状态)、我的ID、当前方向 | --- ## 四、后端实现细节 ### 4.1 用户认证与JWT - **密码加密**:`BCryptPasswordEncoder`(Spring Security 内置) - **JWT生成**:`JwtUtils.generateToken()` — 包含 `userId` + `username`,默认24小时过期 - **JWT验证**:`JwtAuthFilter` 从 `Authorization` 头提取 Bearer Token,验证后设置 `SecurityContext` - **密钥管理**:Base64 编码的 HMAC-SHA 密钥,配置于 `application.yml` ### 4.2 实时对战 (WebSocket) - **握手验证**:`WebSocketHandshakeInterceptor` 从连接参数提取 JWT Token 并验证 - **房间管理**:`GameService` 维护 `ConcurrentHashMap`,最多6人/房间 - **游戏循环**:`ScheduledExecutorService` 每 120ms 执行一次 `tick()` - 移动所有存活蛇 - 食物碰撞检测(吃到加分并从蛇尾生长) - 碰撞检测(墙、自身、其他蛇) - 补充食物至目标数量 - 检查结束条件: - 单人模式:玩家死亡 → 游戏结束 - 多人模式:存活玩家 ≤ 1 → 游戏结束 - **消息广播**:`GameWebSocketHandler.broadcastRoomState()` 向房间内所有玩家推送 UPDATE - **反作弊**:服务端 `handleMove()` 二次验证方向有效性,禁止瞬间反向 ### 4.3 排行榜 (Redis ZSet) - **数据结构**: - `ranking:daily:{YYYYMMDD}` — 日排行榜 - `ranking:weekly:{YYYY}W{week}` — 周排行榜 - **分数更新**:游戏结束时 `ZSetOperations.incrementScore()` 累加分数 - **Top N查询**:`ZSetOperations.reverseRangeWithScores(0, 99)` 获取前100名 - **缓存加速**:Caffeine 本地缓存 Top100,3秒过期,降低 Redis 压力 - **定时归档**:每日0点 `@Scheduled` 清理缓存,生产环境可归档至 MySQL ### 4.4 数据持久化 - **H2 内存数据库**:开发环境使用,自动建表(JPA `ddl-auto: update`) - **GameRecord**:每局结束写入 MySQL/H2,用于历史统计和反作弊分析 - **生产切换**:将 `spring.datasource` 改为 MySQL 连接即可 ### 4.5 安全防护 | 措施 | 实现方式 | |------|---------| | JWT 无状态认证 | `jjwt` 库,HMAC 签名验证 | | 密码加密 | BCrypt(`BCryptPasswordEncoder`) | | 方向反作弊 | 服务端 `Direction.isOpposite()` 二次验证 | | CORS 防护 | `SecurityConfig` 配置 `CorsConfigurationSource` | | Session 禁用 | `SessionCreationPolicy.STATELESS` | --- ## 五、API 接口清单 ### 认证 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/api/auth/login` | 登录,返回 JWT Token + 用户信息 | | POST | `/api/auth/register` | 注册,返回 JWT Token + 用户信息 | ### 排行榜 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/ranking/daily` | 获取日排行榜 Top100 + 我的排名 | | GET | `/api/ranking/weekly` | 获取周排行榜 Top100 + 我的排名 | ### 游戏记录 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/api/game/score` | 提交游戏分数 | | GET | `/api/game/history` | 获取历史游戏记录(分页) | ### WebSocket | 端点 | 说明 | |------|------| | `/ws/game` | 游戏实时通信端点(需携带 JWT Token) | --- ## 六、WebSocket 消息协议 ### 客户端 → 服务端 ```json { "type": "JOIN", "payload": {} } { "type": "MOVE", "payload": { "direction": "UP" } } ``` ### 服务端 → 客户端 ```json { "type": "UPDATE", "players": [ { "id": 1, "username": "player1", "body": [{ "x": 10, "y": 5 }, ...], "direction": "RIGHT", "score": 42, "alive": true, "color": "#55ff88" } ], "foods": [ { "x": 5, "y": 8, "type": "normal" }, { "x": 20, "y": 3, "type": "golden" } ], "gridWidth": 40, "gridHeight": 30, "status": "playing" } ``` --- ## 七、启动方式 ### 前置条件 - **后端**:JDK 17+、Maven 3.8+、Redis(默认 `localhost:6379`) - **前端**:Node.js 18+ ### 后端启动 ```bash cd backend mvn spring-boot:run # 服务运行在 http://localhost:8080 # H2 控制台: http://localhost:8080/h2-console ``` ### 前端启动 ```bash cd frontend npm install npm run dev # 服务运行在 http://localhost:3000 # Vite代理自动转发 /api 和 /ws 到后端 ``` --- ## 八、扩展方向 | 方向 | 说明 | |------|------| | **数据库升级** | 将 H2 替换为 MySQL/PostgreSQL | | **消息队列** | 引入 RabbitMQ/Kafka 解耦游戏逻辑与排行榜更新 | | **微服务拆分** | 将匹配服务、房间服务、排行榜服务独立部署 | | **断线重连** | WebSocket 断线后保留玩家状态 N 秒,允许重连恢复 | | **观战模式** | 已死亡玩家可切换视角观战存活玩家 | | **道具系统** | 增加加速、穿墙、护盾等特殊道具 | | **音效** | 添加吃食物、死亡、胜利等音效反馈 | --- ## 九、单人模式改造 > **目的**:让游戏无需等待其他玩家即可单人游玩,方便开发调试和功能测试。 ### 9.1 改动概览 | 文件 | 改动 | 说明 | |------|------|------| | `backend/.../service/GameService.java` | 修复游戏结束条件 | 单人模式下玩家死亡后游戏正常结束 | | `frontend/src/views/GameView.vue` | 改进 UI、修复重连流程 | 单人友好的界面文案 + 稳定的重连逻辑 | ### 9.2 后端改动 **文件**:`backend/src/main/java/com/snake/service/GameService.java`(第 188-194 行) **问题**:原结束条件为 `alive <= 1 && room.getPlayers().size() > 1`——单人模式下 `playerCount` 恒为 1,导致玩家死后游戏永不结束,界面卡死。 **修复**: ```java // 原代码 if (alive <= 1 && room.getPlayers().size() > 1) { endGame(room); } // 新代码 // 单人模式:自己死了就结束 // 多人模式:只剩 ≤1 人存活时结束 if (alive == 0 || (alive <= 1 && room.getPlayers().size() > 1)) { endGame(room); } ``` - 新增 `alive == 0` 分支:所有蛇死亡时(含单人)直接结束 - 保留原多人逻辑:最后一人存活时结束 ### 9.3 前端改动 **文件**:`frontend/src/views/GameView.vue` | 改动项 | 原实现 | 新实现 | |--------|--------|--------| | `readyPlayers` | `computed(() => 1)` 硬编码 | 根据实际玩家数动态计算 | | 等待遮罩 | "等待开始" + "按空格键准备就绪" + "X/Y 人已准备" | "正在连接服务器..." + 连接成功后"已连接,即将开始..." + 增加"手动开始"备用按钮 | | 结束遮罩 | 仅显示分数 | 额外显示蛇长度 | | `handleRestart` | 直接调用 `wsConnect()` + 立即发送 JOIN | 先 `wsDisconnect()` → `reset()` → `stopLoop()` → 延迟 300ms → `wsConnect()` + `startLoop()`,确保旧连接完全释放 | | 在线标签 | 始终 "在线 X" | 单人时显示 "单人模式",多人时显示 "在线 X 人" | **新增**:`handleManualStart()` 函数,用于手动触发 JOIN(作为自动连接的备用方案)。 ### 9.4 单人游戏流程 ``` 打开页面 ──► WebSocket 自动连接 ──► onopen 自动发送 JOIN ──► 游戏立即开始 │ ↑↓←→ / WASD 操控 │ 撞墙/咬到自己 ──► 游戏结束 │ 显示分数 + 蛇长 │ 点击 "再来一局" ``` - **操控**:方向键 `↑↓←→` 或 `WASD` - **无需**:登录(当前前端未强制鉴权)、等待其他玩家、按空格准备 - **断线重连**:`handleRestart` 先断开旧连接再建立新连接,避免新旧 WebSocket 冲突