Files
zp2/docs/architecture.md
2026-07-26 14:41:36 +08:00

20 KiB
Raw Blame History

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 接口

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

构建游戏状态快照:

{
  "players": [
    {
      "id": "player-uuid",
      "x": 12.5,
      "y": 8.3,
      "angle": 1.57,
      "health": 100,
      "lastProcessedSeq": 42
    }
  ],
  "gameTime": 12.5
}

3.7 地图系统

GameMap 从 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

核心数据结构

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

消息格式:

{
  "type": "player_input",
  "data": {"dx": 1, "dy": 0, "aimX": 10, "aimY": 5, "seq": 42}
}

4.5 输入管理 — input.js

键盘输入:

  • WASD / 方向键 → 移动向量
  • 对角线归一化×0.7071

鼠标输入:

  • 位置追踪 (clientX, clientY)
  • 左右键状态

buildInputState

{
  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} 信封格式:

{"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 键盘/鼠标输入