This commit is contained in:
wfz
2026-04-25 22:09:27 +08:00
parent 9be6b17593
commit 7bffe41d41
33 changed files with 3461 additions and 581 deletions

View File

@@ -0,0 +1,288 @@
# 僵尸流向场寻路系统技术文档
## 1. 概述
本系统将僵尸的寻路算法从 A* 个体寻路改为基于距离场/流向场的群体寻路算法,显著提升了多僵尸同时寻路的效率和自然性。
### 1.1 核心优势
| 特性 | A* 算法 | 流向场算法 |
|------|---------|------------|
| 计算方式 | 每个僵尸独立计算 | 共享流向场,统一计算 |
| 多目标支持 | 需要为每个目标重新计算 | 自动支持多个玩家目标 |
| 性能 | O(n) 每僵尸 | O(m) 每帧 (m=地图格子数) |
| 动态适应 | 需要重新寻路 | 自动适应玩家移动 |
## 2. 系统架构
### 2.1 核心组件
```
GameWorld
├── GameMap
│ ├── distanceField[][] (距离场)
│ ├── flowFieldX[][] (流向场X分量)
│ ├── flowFieldY[][] (流向场Y分量)
│ └── flowFieldValid (流向场有效标志)
└── Zombie[]
├── x, y (当前位置)
├── targetX, targetY (目标网格中心)
├── hasTarget (是否有目标)
├── reservedGridX/Y (预留格子)
└── hasReservation (是否有预留)
```
### 2.2 数据流
```
每帧更新流程:
1. GameWorld.updateFlowField()
└── 收集所有存活玩家位置
└── 调用 GameMap.updateFlowField()
2. GameMap.updateFlowField()
└── BFS 扩散计算距离场
└── 基于距离场计算流向场
3. Zombie.move()
└── 查询流向场获取移动方向
└── 检查格子占用/预留
└── 移动到网格中心
└── 碰撞分离处理
```
## 3. 距离场与流向场
### 3.1 距离场 (Distance Field)
使用 BFS (广度优先搜索) 算法计算每个格子到最近玩家的距离。
```java
// 初始化所有玩家位置距离为0加入队列
for (float[] pos : playerPositions) {
int px = (int) Math.floor(pos[0]);
int py = (int) Math.floor(pos[1]);
distanceField[py][px] = 0;
queue.add(new int[]{px, py});
}
// BFS 扩散
while (!queue.isEmpty()) {
int[] current = queue.poll();
for (int[] dir : dirs) {
int nx = current[0] + dir[0];
int ny = current[1] + dir[1];
// 检查墙壁和角落阻挡
if (dir[0] != 0 && dir[1] != 0) {
if (isWall(cx + dir[0], cy) || isWall(cx, cy + dir[1])) {
continue;
}
}
float moveCost = (dir[0] != 0 && dir[1] != 0) ? 1.414f : 1.0f;
float newDist = currentDist + moveCost;
if (newDist < distanceField[ny][nx]) {
distanceField[ny][nx] = newDist;
queue.add(new int[]{nx, ny});
}
}
}
```
**特性**
- 支持 8 方向移动(对角线代价为 √2
- 正确处理角落碰撞(斜向移动时检查相邻两格是否都为墙)
### 3.2 流向场 (Flow Field)
基于距离场计算每个格子的最优流向方向。
```java
// 对于每个非墙格子,找到距离场梯度下降方向
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (isWall(x, y)) continue;
float bestDist = distanceField[y][x];
float bestDirX = 0, bestDirY = 0;
// 检查8个方向邻居
for (int[] dir : dirs) {
int nx = x + dir[0];
int ny = y + dir[1];
if (distanceField[ny][nx] < bestDist) {
bestDist = distanceField[ny][nx];
// 单位化方向向量
float len = (float) Math.sqrt(dir[0]*dir[0] + dir[1]*dir[1]);
bestDirX = dir[0] / len;
bestDirY = dir[1] / len;
}
}
flowFieldX[y][x] = bestDirX;
flowFieldY[y][x] = bestDirY;
}
}
```
## 4. 僵尸移动逻辑
### 4.1 网格中心对齐
与 A* 寻路类似,僵尸朝向网格中心点 (`gridX + 0.5f, gridY + 0.5f`) 移动,避免飘忽不定。
```java
// 选择下一个目标网格
if (!hasTarget || centerDist < 0.15f) {
float[] flowDir = map.getFlowDirection(x, y);
int nextGridX = currentGridX + (int) Math.round(dirX);
int nextGridY = currentGridY + (int) Math.round(dirY);
// 目标 = 网格中心
targetX = nextGridX + 0.5f;
targetY = nextGridY + 0.5f;
hasTarget = true;
}
```
### 4.2 格子预留机制
防止多个僵尸同时选择同一格子导致死锁。
```java
// 预留目标格子
reservedGridX = nextGridX;
reservedGridY = nextGridY;
hasReservation = true;
// 检查格子是否被占用或预留
private boolean isGridOccupiedOrReserved(int gridX, int gridY, Collection<Zombie> others) {
for (Zombie other : others) {
// 检查当前占用
if (otherGridX == gridX && otherGridY == gridY) return true;
// 检查预留
if (other.hasReservation() &&
other.getReservedGridX() == gridX &&
other.getReservedGridY() == gridY) {
return true;
}
}
return false;
}
```
### 4.3 替代路径选择
当首选格子被占用时,寻找最接近目标方向的替代路径。
```java
private int[] findAlternativeDirection(...) {
// 收集所有可行方向
for (int[] dir : allDirs) {
if (isGridOccupiedOrReserved(nx, ny, others)) continue;
float dotProduct = dir[0] * dirX + dir[1] * dirY;
candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)});
}
// 按点积排序(最接近原始方向优先)
candidates.sort((a, b) -> b[2] - a[2]);
return candidates.isEmpty() ? null : candidates.get(0);
}
```
### 4.4 碰撞分离
移动后处理僵尸之间的物理分离,避免重叠。
```java
float minSeparationDist = Constants.ZOMBIE_SIZE; // 0.8f
for (Zombie other : others) {
float sepDist = distanceTo(other);
if (sepDist < minSeparationDist && sepDist > 0.01f) {
float overlap = minSeparationDist - sepDist;
float pushX = (sepDx / sepDist) * overlap * 0.5f;
float pushY = (sepDy / sepDist) * overlap * 0.5f;
// 尝试推开
if (map.isWalkable(x + pushX, y + pushY, ZOMBIE_SIZE)) {
x += pushX;
y += pushY;
}
}
}
```
### 4.5 斜向移动防卡墙
处理僵尸斜向移动时碰到墙角卡住的问题。
```java
// 检测是否斜向穿过墙角
if (checkX != checkCurrentX && checkY != checkCurrentY) {
boolean wallInX = map.isWall(checkX, checkCurrentY);
boolean wallInY = map.isWall(checkCurrentX, checkY);
if (wallInX || wallInY) {
// 尝试沿单轴滑动
if (!wallInX && canMoveX) {
x = newX;
} else if (!wallInY && canMoveY) {
y = newY;
}
}
}
```
## 5. 移动顺序
僵尸按 ID 顺序移动,确保低 ID 僵尸优先选择路径。
```java
// GameWorld.updateZombies()
List<Zombie> sortedZombies = new ArrayList<>(zombies.values());
sortedZombies.sort((a, b) -> Integer.compare(a.getId(), b.getId()));
for (Zombie z : sortedZombies) {
z.move(map, dt, zombies.values());
}
```
## 6. 关键常量
| 常量 | 值 | 说明 |
|------|-----|------|
| `GRID_SIZE` | 32 | 地图尺寸 32×32 格 |
| `ZOMBIE_SIZE` | 0.8f | 僵尸碰撞半径 |
| 网格中心阈值 | 0.15f | 到达网格中心后重新选择方向 |
| 分离距离 | ZOMBIE_SIZE | 僵尸开始分离的距离 |
| 分离推力 | 0.5f | 每次分离移动的比例 |
## 7. 潜在问题与解决方案
### 7.1 僵尸聚集在门口
**原因**: 流向场在门口处汇聚。
**解决方案**: 可在距离场中加入惩罚项,让僵尸分散。
### 7.2 低帧率下移动不稳定
**原因**: 流向场每帧更新,帧率低时变化剧烈。
**解决方案**: 考虑增加流向场更新间隔或使用插值平滑。
## 8. 性能考虑
- 流向场计算: O(width × height × 8) ≈ O(8000) 每帧
- 单僵尸移动: O(1) 查询流向场
- 总复杂度: O(m) + O(n),其中 m=格子数n=僵尸数
相比 A* 每帧 O(n × 路径长度),大幅提升性能。