289 lines
7.9 KiB
Markdown
289 lines
7.9 KiB
Markdown
# 僵尸流向场寻路系统技术文档
|
||
|
||
## 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 × 路径长度),大幅提升性能。
|