Initial commit

This commit is contained in:
wfz
2026-07-26 14:41:36 +08:00
commit f741a0c8e8
90 changed files with 10774 additions and 0 deletions

597
docs/architecture.md Normal file
View File

@@ -0,0 +1,597 @@
# ZP2 全局架构文档
## 1. 项目概述
ZP2 是一款多人在线俯视角 3D 游戏,玩家在 32x32 网格地图上移动、实时同步位置。本项目是 ZP1 的精简版,仅保留房间管理、地图加载、玩家移动和基础 3D 显示功能。
### 技术栈
| 层级 | 技术 | 版本 |
|------|------|------|
| 后端 | Java | 17 |
| 构建 | Maven | - |
| 通信 | Java-WebSocket | 1.5.4 |
| JSON | Gson + Jackson | 2.10.1 / 2.15.2 |
| 前端 | JavaScript (ES Module) | - |
| 渲染 | Three.js | ^0.170.0 |
| 构建 | Vite | ^6.0.0 |
---
## 2. 系统架构总览
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Browser) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ main.js │───→│ engine.js│───→│ scene.js │ │ client.js │ │
│ │ (App) │ │ (Loop/ │ │ (Three.js│ │(WebSocket)│ │
│ │ │ │ Predict) │ │ Render) │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────┬────┘ │
│ │ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ lobby.js │ │ input.js │ │ grid.js │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │ │
└─────────────────────────────────────────────────────────┼────────┘
│ WebSocket
│ JSON
┌─────────────────────────────────────────────────────────┼────────┐
│ Backend (Java) │ │
│ ┌──────────────────────────────────────────────────────┤ │
│ │ GameWebSocketServer (8080) │←───────┘
│ │ onMessage 消息路由 → 房间CRUD / 玩家输入 / 状态广播 │
│ └──────┬───────────┬──────────────┬───────────────────┘
│ │ │ │
│ ┌──────┴───┐ ┌────┴─────┐ ┌────┴──────────┐
│ │RoomMgr │ │GameService│ │ GameLoop │
│ │(房间生命周期)│ │(游戏会话) │ │(30TPS固定帧) │
│ └──────────┘ └────┬─────┘ └───────────────┘
│ │
│ ┌───────────────────┴────────────────────────────┐
│ │ ECSWorld │
│ │ 实体(Entities) + 组件(组件) + 系统(Systems) │
│ │ ┌──────────────────────────────────────────┐ │
│ │ │ PlayerInputSystem: 移动 + 碰撞 + 朝向 │ │
│ │ │ StateSyncSystem: 构建状态快照 → 广播 │ │
│ │ └──────────────────────────────────────────┘ │
│ └────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────┘
```
---
## 3. 后端架构
### 3.1 启动流程
```
GameServerMain.main()
├── 解析端口参数 (默认 8080)
├── 创建 GameWebSocketServer
├── server.start() 启动 WebSocket 监听
└── 注册 shutdown hook优雅关闭
```
### 3.2 网络层 — GameWebSocketServer
核心职责:管理 WebSocket 连接、路由消息、广播状态。
**连接管理:**
- `connectionToPlayer`: WebSocket → playerId 映射
- `playerToConnection`: playerId → WebSocket 映射
**消息路由onMessage**
| 消息类型 | 处理方法 | 说明 |
|----------|----------|------|
| `create_room` | `handleCreateRoom` | 创建房间,生成 playerId 和 roomId |
| `join_room` | `handleJoinRoom` | 加入房间,广播新状态 |
| `leave_room` | `handleLeaveRoomByConn` | 离开房间,清理游戏会话 |
| `room_list` | `handleRoomList` | 返回可用房间列表 |
| `ready` | `handleReady` | 切换玩家准备状态 |
| `start_game` | `handleStartGame` | 房主开始游戏(需全员准备) |
| `player_input` | `handlePlayerInput` | 转发玩家输入到 GameService |
**定时任务:**
- 每 2 秒广播房间列表给未加入房间的玩家
### 3.3 房间管理 — RoomManager
使用 `ConcurrentHashMap` 保证线程安全:
- `rooms`: roomId → Room 映射
- `playerToRoom`: playerId → roomId 映射
- `getAvailableRooms()`: 返回未开始游戏的房间
**Room 实体:**
- `id`: 房间唯一标识UUID 前8位
- `hostId`: 房主 playerId
- `players`: LinkedHashMap 保持加入顺序最多4人
- `gameStarted`: 游戏是否已开始
### 3.4 游戏服务 — GameService
**startGame(room) 流程:**
```
1. 加载地图文件 (maps/d540209a.json)
2. 创建 ECSWorld
3. 注册 PlayerInputSystem
4. 为每个玩家创建 PlayerEntity分配到出生点
5. 创建并启动 GameLoop
6. 返回每个玩家的初始化数据mapData + players
```
**processPlayerInput**
- 查找玩家对应的 ECS 实体
- 更新 PlayerInput 组件dx, dy, aimX, aimY, seq
- 使用 `synchronized(world.lock)` 保证线程安全
### 3.5 游戏循环 — GameLoop
固定时间步长循环30 TPS
```
ScheduledExecutorService
└── scheduleAtFixedRate(tick, 0, 33ms)
├── synchronized(world.lock) { world.update(dt) }
└── broadcaster.broadcast(roomId, world)
```
- 每 33ms1000/30执行一次 tick
- tick 内更新所有 ECS 系统
- 更新完成后广播游戏状态
### 3.6 ECS 架构
#### ECSWorld
核心容器,管理所有实体和组件:
| 组件映射 | 类型 | 说明 |
|----------|------|------|
| `entities` | `LinkedHashSet<Integer>` | 所有活跃实体 |
| `players` | `LinkedHashSet<Integer>` | 玩家实体集合 |
| `positions` | `Map<Integer, Position>` | 位置组件 |
| `healths` | `Map<Integer, Health>` | 生命值组件 |
| `collisions` | `Map<Integer, Collision>` | 碰撞组件 |
| `renderInfos` | `Map<Integer, RenderInfo>` | 渲染组件 |
| `playerInputs` | `Map<Integer, PlayerInput>` | 输入组件 |
| `systems` | `List<System>` | 系统列表 |
| `playerIdToEntity` | `Map<String, Integer>` | 玩家ID到实体ID映射 |
**createPlayerEntity(playerId, name, x, y)**
```
1. createEntity() 获取新实体ID
2. 添加 Position(x, y)
3. 添加 Health(100)
4. 添加 Collision(PLAYER_SIZE=0.8)
5. 添加 RenderInfo(PLAYER)
6. 添加 PlayerInput()
7. 加入 players 集合
8. 建立 playerId → entityId 映射
```
#### System 接口
```java
public interface System {
void update(float dt, ECSWorld world);
}
```
#### PlayerInputSystem
每帧处理所有玩家输入:
```
对于每个玩家实体:
1. 获取 PlayerInput / Position / Health
2. 跳过死亡或缺少组件的实体
3. 计算移动newX = x + dx * PLAYER_SPEED * TICK_INTERVAL
4. 逐轴碰撞检测isWalkable(newX, y, PLAYER_SIZE)
5. 限制在地图边界 [0.5, GRID_SIZE-0.5]
6. 计算朝向atan2(aimX - x, aimY - y)
```
#### StateSyncSystem
构建游戏状态快照:
```json
{
"players": [
{
"id": "player-uuid",
"x": 12.5,
"y": 8.3,
"angle": 1.57,
"health": 100,
"lastProcessedSeq": 42
}
],
"gameTime": 12.5
}
```
### 3.7 地图系统
**GameMap** 从 JSON 文件加载:
```json
{
"width": 32,
"height": 32,
"walls": [{"x": 5, "y": 10, "type": "static"}],
"playerSpawns": [{"x": 2, "y": 2}],
"zombieSpawns": [{"x": 15, "y": 15}]
}
```
- `cells[y][x]`: 0=空地, 1=墙, 2=玩家出生点, 3=僵尸出生点
- `isWalkable(wx, wy, size)`: 检查实体四个角是否均可通行
- `isWall(gx, gy)`: 判断网格是否为墙或越界
**MapStorage**: 基于文件的地图持久化save/load/list/delete
---
## 4. 前端架构
### 4.1 应用入口 — main.js
```
App 构造函数
├── 创建 lobbyEl 和 gameCanvasEl
├── 实例化 LobbyUI
└── _setupLobby() 注册回调
```
**视图切换:**
- 默认显示 lobby隐藏 canvas
- `game_started` 消息后:隐藏 lobby显示 canvas
**连接管理_ensureConnection**
```
if (!engine):
创建 GameEngine → connect(WS_URL) → 注册网络处理器
else if (!connected):
重连
```
### 4.2 游戏引擎 — engine.js
#### 核心数据结构
```javascript
this.players = Map<playerId, {
id, name, x, y, angle, health, color, isLocal
}>
this.pendingInputs = [] // 待确认输入队列
```
#### 游戏循环(固定时间步长)
```
requestAnimationFrame(_loop)
delta = now - lastTick
accumulator += delta
while (accumulator >= TICK_INTERVAL):
_tick()
accumulator -= TICK_INTERVAL
updateCamera(localPlayer)
scene.render()
```
#### 客户端预测_applyLocalPrediction
```
1. 读取输入状态 (dx, dy)
2. 计算新位置: newX = x + dx * SPEED * dt
3. 逐轴碰撞检测
4. 限制在地图范围内 [0.5, 31.5]
5. 计算朝向: atan2(aimX - x, aimY - y)
6. 更新场景中的玩家模型
```
#### 服务器校正_reconcileLocalPlayer
```
1. 用服务器权威状态覆盖本地位置
2. 移除 seq <= lastProcessedSeq 的输入(已确认)
3. 对剩余未确认输入重新执行预测
4. 更新场景
```
这是经典的 **客户端预测 + 服务器校正** 模式:
- 客户端立即响应输入(零延迟手感)
- 服务器每帧广播权威状态
- 客户端用 seq 号匹配已处理输入,重新模拟未确认输入
### 4.3 3D 渲染 — scene.js
**场景设置:**
- 透视相机 (FOV=45°, 偏移量 (0, 25, 18))
- 环境光 + 方向光(带阴影)+ 点光源
- 窗口大小自适应
**地图渲染buildMap**
- 地板: 32x32 Plane (深灰色)
- 墙壁: BoxGeometry(1, 1.5, 1) (蓝灰色)
- 出生点: BoxGeometry(1, 0.1, 1) (绿色半透明)
**玩家模型createPlayerModel**
```
Group
├── 身体: Cylinder(半径 0.4, 高 0.8)
├── 头部: Sphere(半径 0.2, 肤色)
└── 武器: Box(0.08, 0.08, 0.5, 深色)
```
**鼠标射线投射getMouseGroundPos**
```
鼠标屏幕坐标 → NDC → Raycaster → 与 Y=0 平面相交 → 地面世界坐标
```
### 4.4 网络客户端 — client.js
WebSocket 封装,提供:
- 事件注册:`on(type, handler)`
- 消息发送:`send(type, data)`
- 便捷方法:`createRoom`, `joinRoom`, `sendInput`
消息格式:
```json
{
"type": "player_input",
"data": {"dx": 1, "dy": 0, "aimX": 10, "aimY": 5, "seq": 42}
}
```
### 4.5 输入管理 — input.js
**键盘输入:**
- WASD / 方向键 → 移动向量
- 对角线归一化×0.7071
**鼠标输入:**
- 位置追踪 (clientX, clientY)
- 左右键状态
**buildInputState**
```javascript
{
seq: sequenceNumber++, // 递增序列号
dx: movement.dx, // 水平输入 [-1, 1]
dy: movement.dy, // 垂直输入 [-1, 1]
aimX: mouseGroundPos.x, // 瞄准点 X
aimY: mouseGroundPos.y // 瞄准点 Y
}
```
### 4.6 大厅 UI — lobby.js
两个视图状态:
**房间列表视图:**
- 玩家名称输入框
- 创建房间 / 刷新按钮
- 房间列表(点击 Join 加入)
**房间视图:**
- 玩家列表(显示准备状态)
- Ready 按钮
- Start Game 按钮(仅房主可见)
- Leave 按钮
### 4.7 网格碰撞 — grid.js
**Grid 类:**
- `parseMap(mapData)`: 解析二维数组
- `isWall(gx, gy)`: 越界返回 true检查 cells == 1
- `worldToGrid(wx, wy)`: 世界坐标 → 网格坐标
- `isWalkable(wx, wy, size)`: 检查四个角是否可通行
**generateDefaultMap**
- 边界墙(最外层)
- 预设墙体段14段
- 4个角落出生点周围自动清理墙体
---
## 5. 网络协议
### 消息格式
所有消息使用统一的 `{type, data}` 信封格式:
```json
{"type": "xxx", "data": {...}}
```
### 消息类型汇总
| 方向 | type | 说明 |
|------|------|------|
| C→S | `create_room` | 创建房间 `{playerName}` |
| C→S | `join_room` | 加入房间 `{roomId, playerName}` |
| C→S | `leave_room` | 离开房间 |
| C→S | `room_list` | 请求房间列表 |
| C→S | `ready` | 切换准备状态 |
| C→S | `start_game` | 开始游戏(仅房主) |
| C→S | `player_input` | 玩家输入 `{dx, dy, aimX, aimY, seq}` |
| S→C | `room_list` | 房间列表 `{rooms[]}` |
| S→C | `room_state` | 房间状态 `{roomId, hostId, isHost, playerId, players[]}` |
| S→C | `game_started` | 游戏开始 `{playerId, mapData, players[]}` |
| S→C | `game_state` | 游戏状态 `{players[{id, x, y, angle, health, lastProcessedSeq}], gameTime}` |
| S→C | `error` | 错误 `{message}` |
### 通信时序
```
Client A Server Client B
| | |
|── create_room ──────→| |
|←── room_state ──────| |
| |←── join_room ────────|
|←── room_state ──────|─── room_state ──────→|
| | |
|── ready ───────────→| |
|←── room_state ──────|─── room_state ──────→|
| | |
|── start_game ──────→| |
|←── game_started ────|─── game_started ────→|
| | |
|═══ player_input ═══→| |
| |═══ game_state ══════→|
|←══ game_state ══════| |
```
---
## 6. 数据流
### 6.1 玩家移动完整数据流
```
InputManager (键盘/鼠标)
buildInputState() → {seq, dx, dy, aimX, aimY}
├──→ _applyLocalPrediction() [客户端预测]
│ ├── 新位置计算
│ ├── 碰撞检测
│ └── 更新 Three.js 模型
└──→ network.sendInput() ──→ WebSocket → Server
GameService.processPlayerInput()
ECSWorld.update(dt)
PlayerInputSystem.update()
├── 更新 Position 组件
└── 更新朝向角度
StateSyncSystem.buildGameState()
broadcast → WebSocket → Client
_processServerState()
├── 远程玩家: 直接更新
└── 本地玩家: _reconcileLocalPlayer()
├── 应用服务器状态
├── 移除已确认输入
└── 重放未确认输入
```
### 6.2 生命周期
```
[创建房间]
Client → create_room → Server → RoomManager.addRoom() → Room 实例
[加入房间]
Client → join_room → Server → RoomManager.joinRoom() → 广播 room_state
[开始游戏]
Client → start_game → Server → GameService.startGame()
├── 加载地图
├── 创建 ECSWorld + PlayerInputSystem
├── 创建 PlayerEntity分配出生点
├── 启动 GameLoop (30 TPS)
└── 广播 game_started含 mapData + players
[游戏进行中]
GameLoop.tick() → ECSWorld.update() → PlayerInputSystem → 广播 game_state
[玩家断开]
onClose → RoomManager.leaveRoom() → 清理游戏会话(如房间为空)
```
---
## 7. 关键设计模式
### 7.1 ECS (Entity-Component-System)
- **Entity**: 整数 ID仅作为标识符
- **Component**: 纯数据Position, Health, PlayerInput 等)
- **System**: 处理逻辑PlayerInputSystem, StateSyncSystem
优势:解耦数据与逻辑,便于扩展新实体类型。
### 7.2 客户端预测 + 服务器校正
- 客户端立即响应输入,不等待服务器
- 服务器每帧广播权威状态
- 客户端用序列号匹配已处理输入,重新模拟未确认输入
- 结果:零延迟手感 + 服务器权威防作弊
### 7.3 固定时间步长
- 客户端和服务器均使用 30 TPS
- 渲染帧率与逻辑帧率解耦(渲染用 requestAnimationFrame
- 保证确定性模拟
### 7.4 线程安全
- ECSWorld 使用 `synchronized(lock)` 保护
- RoomManager 使用 `ConcurrentHashMap`
- GameLoop 使用单线程 ScheduledExecutorService
---
## 8. 文件索引
### 后端
| 文件 | 职责 |
|------|------|
| `GameServerMain.java` | 服务器入口,启动 WebSocket |
| `model/Constants.java` | 游戏常量 + 消息类型 |
| `model/Room.java` | 房间实体 |
| `model/PlayerInfo.java` | 玩家信息 |
| `model/GameMap.java` | 地图加载 + 碰撞检测 |
| `model/MapData.java` | 地图序列化 DTO |
| `model/Wall.java` | 墙体抽象基类 |
| `model/StaticWall.java` | 不可破坏墙体 |
| `ecs/ECSWorld.java` | ECS 世界容器 |
| `ecs/System.java` | 系统接口 |
| `ecs/components/Position.java` | 位置组件 |
| `ecs/components/PlayerInput.java` | 输入组件 |
| `ecs/components/Health.java` | 生命值组件 |
| `ecs/components/Collision.java` | 碰撞组件 |
| `ecs/components/RenderInfo.java` | 渲染信息组件 |
| `server/GameWebSocketServer.java` | WebSocket 服务器 |
| `server/RoomManager.java` | 房间生命周期管理 |
| `server/GameService.java` | 游戏会话管理 |
| `server/GameLoop.java` | 30TPS 固定帧循环 |
| `server/MessageUtils.java` | JSON 安全提取工具 |
| `server/MapStorage.java` | 文件地图持久化 |
| `systems/PlayerInputSystem.java` | 移动 + 碰撞处理 |
| `systems/StateSyncSystem.java` | 状态快照构建 |
### 前端
| 文件 | 职责 |
|------|------|
| `index.html` | HTML 外壳 |
| `src/main.js` | 应用入口,大厅/游戏切换 |
| `src/style.css` | 全局样式 |
| `src/game/engine.js` | 游戏引擎,预测 + 校正 |
| `src/game/scene.js` | Three.js 3D 渲染 |
| `src/network/client.js` | WebSocket 客户端 |
| `src/ui/lobby.js` | 大厅 UI |
| `src/utils/constants.js` | 游戏常量 |
| `src/utils/grid.js` | 网格碰撞 + 默认地图 |
| `src/utils/input.js` | 键盘/鼠标输入 |

256
docs/migration-plan-zh.md Normal file
View File

@@ -0,0 +1,256 @@
# ZP1 最小化迁移计划
## 目标
将 zp1 重写为 zp2仅保留房间管理、地图加载、玩家移动、前端必要显示。技术栈不变Java + Vite/Three.js保留 ECS 架构和客户端预测。
## 范围
- **保留**:房间系统、地图加载/渲染、玩家移动WASD、客户端预测、WebSocket 通信
- **移除**:僵尸、武器/子弹、炮塔、火焰区域、掉落物、爆炸/特效、HUD、设置界面、地图编辑器、模板系统、对象池
---
## 阶段一:后端骨架
### 1.1 项目结构
```
zp2/backend/
├── pom.xml
└── src/main/java/com/zombie/game/
├── GameServerMain.java # 入口,仅启动 WebSocket 服务器
├── model/
│ ├── Constants.java # 精简为GRID_SIZE, TICK_RATE, PLAYER_SIZE, PLAYER_SPEED, MSG_*
│ ├── Room.java # 保持原样
│ ├── PlayerInfo.java # 保持原样
│ ├── GameMap.java # 精简:移除流向场,仅保留 isWall/isWalkable
│ ├── MapData.java # 保持,用于地图反序列化
│ └── StaticWall.java # 仅保留静态墙
├── server/
│ ├── GameWebSocketServer.java # 精简:房间操作 + 玩家输入 + 状态广播
│ ├── RoomManager.java # 保持原样
│ ├── GameService.java # 精简:仅注册 PlayerInputSystem + StateSyncSystem
│ ├── GameLoop.java # 保持原样30 TPS
│ ├── MessageUtils.java # 保持原样
│ └── MapStorage.java # 保留,用于加载地图文件
├── ecs/
│ ├── ECSWorld.java # 精简:仅管理 players移除僵尸/子弹/掉落物/炮塔/火焰区域
│ ├── System.java # 保持接口
│ └── components/
│ ├── Position.java # 保持
│ ├── PlayerInput.java # 精简:移除手雷相关字段
│ ├── Health.java # 精简
│ ├── Collision.java # 保持
│ └── RenderInfo.java # 精简
└── systems/
├── PlayerInputSystem.java # 保持:移动 + 碰撞 + 朝向
└── StateSyncSystem.java # 精简:仅广播玩家位置
```
### 1.2 关键变更
**Constants.java**
- 保留:`GRID_SIZE=32`, `TICK_RATE=30`, `PLAYER_SIZE=0.8f`, `PLAYER_SPEED=5.0f`, `MSG_*` 消息类型常量
- 移除:`ZOMBIE_SIZE`, `LOOT_*`, 武器常量
**GameMap.java**
- 保留:`loadFromJson()`, `isWall()`, `isWalkable()`, `getCells()`, `getSpawnPoints()`
- 移除:整个 `FlowField``updateFlowField()`, `getFlowDirection()`, `addNutWall()`, 坚果墙逻辑
- 仅保留 `StaticWall`(移除 `NutWall`, `TurretWall`
**ECSWorld.java**
- 保留:`entities`, `players`, `playerIdToEntity`, `map`, `systems`, `lock`
- 移除:`zombies`, `playerBullets`, `zombieBullets`, `loots`, `fireZones`, `turrets`, `wallEntities`
- 移除:所有对象池
- 移除:`createZombieEntity()`, `createBulletEntity()`, `createGrenadeEntity()`, `createMolotovEntity()`, `createLootEntity()`, `createFireZoneEntity()`, `createTurretEntity()`
- 保留:`createPlayerEntity()`(精简版,无 WeaponState/RespawnState
- 保留:`update(dt)` — 清理临时数据,遍历系统执行
**GameService.java**
- `startGame()`:仅注册 `PlayerInputSystem``StateSyncSystem`移除其他10个系统
- 保留:`processPlayerInput()`, `stopGame()`
**PlayerInputSystem.java**
- 保留移动dx/dy * speed、逐轴墙壁碰撞、朝向计算
- 移除:武器射击逻辑(如有)
**StateSyncSystem.java**
- 精简广播为:`{players: [{id, x, y, angle, health}], gameTime}`
- 移除zombies, bullets, zombieBullets, loots, explosions, removedBullets, waveNumber, score
**Room.java / RoomManager.java / GameLoop.java / MessageUtils.java**
- 保持原样(或微调清理)
**GameWebSocketServer.java**
- 保留:`onMessage()` 分发、房间 CRUD 处理、`handlePlayerInput()``broadcastGameState()`
- 可选移除:房间列表广播定时器(或保留用于大厅刷新)
### 1.3 地图数据
- 保留 `maps/d540209a.json` 格式walls + playerSpawns + zombieSpawns
- 复制一份到 `zp2/maps/`
---
## 阶段二:前端骨架
### 2.1 项目结构
```
zp2/frontend/
├── package.json # 同样依赖three, vite
├── vite.config.js # 同样代理配置
├── index.html # 同样最小化外壳
└── src/
├── main.js # 精简:大厅 → 游戏切换
├── style.css # 仅大厅 + 房间 + 画布样式
├── game/
│ ├── engine.js # 精简:仅玩家,无僵尸/子弹/战斗
│ └── scene.js # 精简:仅地图 + 玩家,无特效
├── network/
│ └── client.js # 保持原样
├── ui/
│ └── lobby.js # 保持原样(房间管理 UI
└── utils/
├── constants.js # 精简:仅地图 + 玩家 + 网络常量
├── grid.js # 精简:仅 Grid 类 + generateDefaultMap
└── input.js # 精简:仅移动 + 瞄准
```
### 2.2 关键变更
**constants.js**
- 保留:`GRID_SIZE`, `CELL_SIZE`, `PLAYER_SIZE`, `TICK_RATE`, `TICK_INTERVAL`
- 保留:`PLAYER_CONFIG`MAX_HEALTH, SPEED
- 保留:`MSG_TYPE`(所有房间 + 游戏消息类型)
- 移除:`WEAPONS`, `WEAPON_CONFIG`, `ZOMBIE_CONFIG`, `ZOMBIE_SIZE`
**grid.js**
- 保留:`Grid` 类的 `parseMap()`, `isWall()`, `worldToGrid()`, `gridToWorld()`, `isWalkable()`
- 保留:`generateDefaultMap()`(备用默认地图)
- 移除:`findPath()`, `getSpawnPoints()`, `isSpawnPoint()`
**input.js**
- 保留:`attach()`, `detach()`, `getMovement()`, `buildInputState()`(仅移动 + 瞄准)
- 移除:`getSelectedWeapon()`, 武器快捷键绑定
- 精简 `buildInputState()` 返回 `{seq, dx, dy, aimX, aimY}`(无 firing, weaponIndex, grenade 字段)
**engine.js约666→250行**
- 保留:`connect()`, `start()`, `stop()`, `_loop()`, `_tick()`
- 保留:`_applyLocalPrediction()` — 带碰撞的移动预测
- 保留:`_reconcileLocalPlayer()` — 服务器校正
- 保留:`_initPlayers()`, `_addPlayer()`, `_removePlayer()`
- 精简 `_processServerState()`:仅同步玩家位置,移除所有僵尸/子弹/掉落物/炮塔/特效处理
- 移除:`_handleGrenadeCharge()`, `_checkBulletHit()`, 武器状态, 手雷状态
- 移除:`zombies`, `bullets`, `zombieBullets`, `loots`, `turrets` 映射表
**scene.js约1331→250行**
- 保留:构造函数(场景、相机、渲染器、灯光、窗口自适应)
- 保留:`buildMap(mapData)` — 地板 + 墙壁
- 保留:`createPlayerModel()`, `addPlayer()`, `removePlayer()`, `updatePlayer()`
- 保留:`updateCamera()`, `getMouseGroundPos()`, `render()`
- 移除所有僵尸渲染约180行
- 移除所有子弹渲染约230行
- 移除所有特效约250行
- 移除炮塔、掉落物、坚果墙、手雷目标指示器约215行
- 移除:`updateEffects()`(或简化为空)
**lobby.js** — 保持原样
**main.js**
- 保留App 类、大厅绑定、游戏开始切换
- 移除HUD 初始化、设置界面初始化
- 移除:`_updateHUD()` 调用
**style.css**
- 保留:大厅样式、房间样式、画布容器样式
- 移除HUD 样式、武器面板、手雷充能、击杀信息、设置弹窗
---
## 阶段三:清理与验证
### 3.1 消息协议验证
确保以下消息端到端正常工作:
- `CREATE_ROOM` / `JOIN_ROOM` / `LEAVE_ROOM` / `READY` / `START_GAME`
- `ROOM_LIST` / `ROOM_STATE` / `GAME_STARTED` / `ERROR`
- `PLAYER_INPUT`仅移动dx, dy, aimX, aimY, seq
- `GAME_STATE`(仅玩家数据)
### 3.2 测试流程
1. 启动后端:`mvn package && java -jar target/*.jar`
2. 启动前端:`npm run dev`
3. 打开两个浏览器标签页
4. 标签页1创建房间 → 标签页2加入房间 → 标签页1开始游戏
5. 两个玩家应能在 32x32 地图上互相看到对方移动
6. WASD 移动响应流畅(客户端预测)
7. 玩家与墙壁碰撞正确
---
## 待创建文件清单共36个
### 后端23个文件
| 序号 | 文件 | 来源 |
|------|------|------|
| 1 | `backend/pom.xml` | 从 zp1 复制 |
| 2 | `GameServerMain.java` | 精简版 |
| 3 | `model/Constants.java` | 精简版 |
| 4 | `model/Room.java` | 基本保持 |
| 5 | `model/PlayerInfo.java` | 基本保持 |
| 6 | `model/GameMap.java` | 精简版(无流向场) |
| 7 | `model/MapData.java` | 基本保持 |
| 8 | `model/StaticWall.java` | 基本保持 |
| 9 | `ecs/System.java` | 保持 |
| 10 | `ecs/ECSWorld.java` | 精简版 |
| 11 | `ecs/components/Position.java` | 保持 |
| 12 | `ecs/components/PlayerInput.java` | 精简版 |
| 13 | `ecs/components/Health.java` | 精简版 |
| 14 | `ecs/components/Collision.java` | 保持 |
| 15 | `ecs/components/RenderInfo.java` | 精简版 |
| 16 | `systems/PlayerInputSystem.java` | 基本保持 |
| 17 | `systems/StateSyncSystem.java` | 精简版 |
| 18 | `server/GameWebSocketServer.java` | 精简版 |
| 19 | `server/RoomManager.java` | 基本保持 |
| 20 | `server/GameService.java` | 精简版 |
| 21 | `server/GameLoop.java` | 基本保持 |
| 22 | `server/MessageUtils.java` | 保持 |
| 23 | `server/MapStorage.java` | 基本保持 |
### 前端12个文件
| 序号 | 文件 | 来源 |
|------|------|------|
| 24 | `frontend/package.json` | 保持 |
| 25 | `frontend/vite.config.js` | 保持 |
| 26 | `frontend/index.html` | 保持 |
| 27 | `frontend/src/main.js` | 精简版 |
| 28 | `frontend/src/style.css` | 精简版 |
| 29 | `frontend/src/game/engine.js` | 精简版 |
| 30 | `frontend/src/game/scene.js` | 精简版 |
| 31 | `frontend/src/network/client.js` | 基本保持 |
| 32 | `frontend/src/ui/lobby.js` | 保持 |
| 33 | `frontend/src/utils/constants.js` | 精简版 |
| 34 | `frontend/src/utils/grid.js` | 精简版 |
| 35 | `frontend/src/utils/input.js` | 精简版 |
### 数据文件1个
| 序号 | 文件 | 来源 |
|------|------|------|
| 36 | `maps/d540209a.json` | 从 zp1 复制 |
---
## 关键设计决策
| 决策 | 选择 | 原因 |
|------|------|------|
| 技术栈 | Java 17 + Vite + Three.js | 与 zp1 一致,迁移成本最低 |
| ECS 架构 | 保留 | 结构清晰,便于后续扩展 |
| 客户端预测 | 保留 | 移动手感更好,代码量不大 |
| 流向场寻路 | 移除 | 仅僵尸 AI 需要 |
| 地图格式 | 保持 JSON | 兼容现有地图文件 |
| WebSocket 协议 | 保持相同消息类型 | 前后端保持同步 |
| 大厅 UI | 保持原样 | 房间管理是核心功能 |
| HUD/设置 | 移除 | 不在本次范围内 |
| 地图编辑器 | 移除 | 不在本次范围内 |
## 风险点
- **StateSyncSystem**:精简时需小心处理消息格式,确保前端能正确解析
- **ECSWorld**:移除组件映射表后,需检查所有引用这些组件的系统
- **客户端预测**engine.js 中的校正逻辑引用了武器状态,需验证精简版是否正常工作

143
docs/migration-plan.md Normal file
View File

@@ -0,0 +1,143 @@
# ZP1 Minimal Migration Plan
## Goal
Rewrite zp1 into zp2, keeping only: room management, map loading, player movement, and minimal frontend display. Same tech stack (Java + Vite/Three.js), ECS architecture, client prediction.
## Scope
- **Keep**: Room system, map loading/rendering, player movement (WASD), client prediction, WebSocket communication
- **Strip**: Zombies, weapons/bullets, turrets, fire zones, loots, explosions/effects, HUD, settings, map designer, template system, object pool
---
## Phase 1: Backend Skeleton
### 1.1 Project structure
```
zp2/backend/
├── pom.xml
└── src/main/java/com/zombie/game/
├── GameServerMain.java
├── model/
│ ├── Constants.java
│ ├── Room.java
│ ├── PlayerInfo.java
│ ├── GameMap.java
│ ├── MapData.java
│ └── StaticWall.java
├── server/
│ ├── GameWebSocketServer.java
│ ├── RoomManager.java
│ ├── GameService.java
│ ├── GameLoop.java
│ ├── MessageUtils.java
│ └── MapStorage.java
├── ecs/
│ ├── ECSWorld.java
│ ├── System.java
│ └── components/
│ ├── Position.java
│ ├── PlayerInput.java
│ ├── Health.java
│ ├── Collision.java
│ └── RenderInfo.java
└── systems/
├── PlayerInputSystem.java
└── StateSyncSystem.java
```
### 1.2 Key changes
**Constants.java** — Keep `GRID_SIZE`, `TICK_RATE`, `PLAYER_SIZE`, `PLAYER_SPEED`, `MSG_*`. Remove zombie/weapon/loot constants.
**GameMap.java** — Keep `loadFromJson()`, `isWall()`, `isWalkable()`, `getCells()`, `getSpawnPoints()`. Remove `FlowField` entirely.
**ECSWorld.java** — Keep `entities`, `players`, `playerIdToEntity`, `map`, `systems`, `lock`. Remove zombies/bullets/loots/turrets/fireZones. Remove object pools. Keep `createPlayerEntity()` only.
**GameService.java** — Register only `PlayerInputSystem` + `StateSyncSystem`.
**StateSyncSystem.java** — Broadcast `{players: [{id, x, y, angle, health}], gameTime}` only.
**GameWebSocketServer.java** — Keep room CRUD + player input + state broadcast.
### 1.3 Map data
- Copy `maps/d540209a.json` (walls + playerSpawns + zombieSpawns)
---
## Phase 2: Frontend Skeleton
### 2.1 Project structure
```
zp2/frontend/
├── package.json
├── vite.config.js
├── index.html
└── src/
├── main.js
├── style.css
├── game/
│ ├── engine.js
│ └── scene.js
├── network/
│ └── client.js
├── ui/
│ └── lobby.js
└── utils/
├── constants.js
├── grid.js
└── input.js
```
### 2.2 Key changes
**constants.js** — Keep `GRID_SIZE`, `PLAYER_SIZE`, `TICK_RATE`, `TICK_INTERVAL`, `PLAYER_CONFIG`, `MSG_TYPE`. Remove `WEAPONS`, `WEAPON_CONFIG`, `ZOMBIE_CONFIG`.
**grid.js** — Keep `Grid` class (`parseMap`, `isWall`, `worldToGrid`, `gridToWorld`, `isWalkable`). Remove `findPath`, `getSpawnPoints`.
**input.js** — Keep `attach/detach`, `getMovement`, `buildInputState` (movement + aim only). Remove weapon bindings.
**engine.js (~666→~250 lines)** — Keep connect, loop, tick, local prediction, reconciliation. Strip zombie/bullet/loot/turret processing.
**scene.js (~1331→~250 lines)** — Keep constructor, lighting, `buildMap`, player rendering, camera, `getMouseGroundPos`. Remove zombie/bullet/effect/turret/loot rendering.
**lobby.js** — Keep as-is.
**main.js** — Keep lobby wiring + game transition. Remove HUD/settings.
**style.css** — Keep lobby + room + canvas styles. Remove HUD/weapon/kill-feed styles.
---
## Phase 3: Cleanup & Verification
### 3.1 Message protocol
- `CREATE_ROOM` / `JOIN_ROOM` / `LEAVE_ROOM` / `READY` / `START_GAME`
- `ROOM_LIST` / `ROOM_STATE` / `GAME_STARTED` / `ERROR`
- `PLAYER_INPUT` (dx, dy, aimX, aimY, seq)
- `GAME_STATE` (players only)
### 3.2 Test flow
1. `mvn package && java -jar target/*.jar`
2. `npm run dev`
3. Open two tabs → Create room → Join room → Start game
4. Both players see each other move, WASD responsive, wall collision works
---
## Files to Create (36 total)
| # | File | Source |
|---|------|--------|
| 1-23 | Backend Java files (see 1.1) | Simplified from zp1 |
| 24-35 | Frontend JS/HTML/CSS (see 2.1) | Simplified from zp1 |
| 36 | `maps/d540209a.json` | Copy from zp1 |
## Key Decisions
| Decision | Choice | Reason |
|----------|--------|--------|
| Tech stack | Java 17 + Vite + Three.js | Same as zp1 |
| ECS | Keep | Structure preserved |
| Client prediction | Keep | Better movement feel |
| Flow field | Remove | Zombie-only feature |
| HUD/Settings | Remove | Not in scope |