# 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)
```

- 每 33ms（1000/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` | 键盘/鼠标输入 |
