Unity寻路大集结(一) 流场寻路

330 阅读4分钟

Unity寻路大集结(一) 流场寻路

流场寻路 FlowField:

流场寻路适用于大量寻路单位的情况。流场也叫向量场。我们可以确定一个2D的平面地图将地图网格栅格化,利用格子节点存储到目标点的向量。向量用于给寻路单位体施加推力,每个格子上计算出来的推力使单位体抵达目标点。

流场寻路主要分成3个部分:

1.生成热度图:计算网格中所有格子节点与目标点的距离。

2.生成向量场: 通过热度图计算向量场,向量场中每个格子的向量方向指向目标点。

3.单位体的行为操纵:为所有单位体设置同一目标点,通过格子上向量设定的力将单位体推动到目标点。

具体实现步骤:

1.栅格化地图 比如100100的地图区域按单位11划分成10000个格子。设置格子的世界坐标 格子索引位置 以及格子的代价。这里的代价我们用数字表示,为了方便这里用byte。并且默认初始化为1,而障碍物默认设置为最大字节数。

关键代码:

public class Grid { //格子的世界坐标 public Vector3 worldPos; // public Vector2Int gridIndex; //流场相关 最小代价 public ushort bestCost; //代价 public byte cost;

//初始化方向 public Vector2Int bestDirection; public Grid() { } ///

/// 初始化格子 /// /// /// public Grid(Vector3 pos,Vector2Int gridIndex) { worldPos = pos; this.bestCost = 255; this.cost = 1; this.gridIndex = gridIndex; //初始方向 bestDirection = new Vector2Int(0, 0); } /// /// 增量代价 /// /// public void IncreaseCost(int amount) { //障碍物设置代价为最大 255 if (this.cost == 255) { return; } else if (this.cost+amount<255) { this.cost +=(byte)amount; } else { this.cost = byte.MaxValue; } } }

2.设置障碍物层级 障碍物列表。通过相交球计算所有格子与障碍物的碰撞区域 设置代价。

int obstaclesMask = LayerMask.GetMask("Obstacle"); foreach (Grid currentGrid in grid) {

var temp = Vector3.one

//相交球计算 Collider[] obstacles = Physics.OverlapBox(currentGrid.worldPos, temp, Quaternion.identity, obstaclesMask); foreach (Collider c in obstacles) {

//注意这里我设置的是9 为障碍层 if (c.gameObject.layer == 9) { currentGrid.IncreaseCost(255); } } }

//栅格化地图 mapSize为地图大小比如 100x100。GridSize为格子大小1x1

public void CreateGrid() { grid = new Grid[mapSize.x, mapSize.y]; for (int x = 0; x < mapSize.x; x++) { for (int y = 0; y < mapSize.y; y++) { Vector3 worldPos = new Vector3(GridSize * x + GridSize / 2, 0f, GridSize * y + GridSize / 2); grid[x, y] = new Grid(worldPos, new Vector2Int(x, y)); } } }

3.计算邻近格子 为每个格子的邻近格子计算力场方向 代价。

//计算周围的格子 private List GetNeighbors(Vector2Int nodeIndex) { List neighbors = new List(); foreach (Vector2Int currentDirection in AllDirections) { Grid currentNeighbor = GridRelativePosition(nodeIndex, currentDirection); if (currentNeighbor != null) neighbors.Add(currentNeighbor); } return neighbors; }

初始化方向 这里只考虑上下左右四个方向所以numNeighbors =2

public void InitDirectionsList (int numNeighbors =2) { if (AllDirections.Capacity == 0) { for (int i = -numNeighbors; i <= numNeighbors; i++) { for (int j = -numNeighbors; j <= numNeighbors; j++) { if (i == 0 && j == 0) { continue; } else { AllDirections.Add(new Vector2Int(i, j)); } } } AllDirections.Sort(new Vector2IntCompare()); } }

//计算到目标格子 所需要的消耗

public void ComputerTargetField(Grid _destination) { destination = _destination; destination.cost = 0; destination.bestCost = 0; Queue GridsToCheck = new Queue(); GridsToCheck.Enqueue(destination); while (GridsToCheck.Count > 0) { Grid currentGrid = GridsToCheck.Dequeue(); List currentNeighbors = GetNeighbors(currentGrid.gridIndex); foreach (Grid currentNeighbor in currentNeighbors) { if (currentNeighbor.cost != byte.MaxValue && currentNeighbor.cost + currentGrid.bestCost < currentNeighbor.bestCost) { currentNeighbor.bestCost = (ushort)(currentNeighbor.cost + currentGrid.bestCost); GridsToCheck.Enqueue(currentNeighbor); } } } }

//计算每个格子移动的力场 bestDirection

public void CreateFlowField() { foreach (Grid currentGrid in grid) { List currentNeighbors = GetNeighbors(currentGrid.gridIndex); int bestCost = currentGrid.bestCost; foreach (Grid currentNeighbor in currentNeighbors) { if (currentNeighbor.bestCost < bestCost) { bestCost = currentNeighbor.bestCost; currentGrid.bestDirection = currentNeighbor.gridIndex - currentGrid.gridIndex; } } } }

4.可移动单位的控制器 计算每个可移动单位的所处的格子位置。为可移动单位的刚体添加一个速度(当前所处格子的X,Z坐标)。

可移动单位控制器:

public void Move(Vector3 direction) { Rigidbody unitRB = GetComponent(); //为可移动单位的刚体设置速度 unitRB.velocity = direction.normalized * speed;

//播放移动动画 PlayAnimation("combat_run_aim");

//可移动单位的方向计算 if (unitRB.velocity.x > 0.1 ||unitRB.velocity.x < -0.1|| unitRB.velocity.z > 0.1||unitRB.velocity.z < -0.1) { transform.LookAt(transform.position + unitRB.velocity); } }

可移动单位单例管理器:

在可移动单位 update 添加鼠标点击事件的处理。(这里方便理解先用最简易的MVC模式把实现细节表达出来。后期更新会使用 Unity Dots 实现OOD来提升性能。)

if (Input.GetMouseButtonDown(1)) { Debug.LogError("按下了右键000000000000点击"); Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hitInfo; if (Physics.Raycast(ray, out hitInfo)) { if (hitInfo.collider.name == "Ground") { target = hitInfo.point; Debug.LogError("点击Ground"); if (!startSearch) { startSearch = true; }

//这里提供了多种寻路模式 后续再介绍。这里使用的SEARCHMODE.FLOWFIELD if (searchmode == SEARCHMODE.ORCA) { //ORCA 动态避障寻路 SetGoals(target); }

// else if (searchmode == SEARCHMODE.FLOWFIELD) { //流场寻路 FlowFieldSearch(target); } } } }

private void FlowFieldSearch(Vector3 worldPos) { //初始化流场

// 格子大小 地图大小 flowField = new FlowField(1, 100); flowField.CreateGrid(); flowField.CreateCostField();

//鼠标点击的世界坐标点转化为目标点格子 Grid destination = flowField.GetGridPosWorldToGrid(worldPos); //计算目标点的力场 flowField.ComputerTargetField(destination);

//计算每个格子之间移动的消耗 flowField.CreateFlowField(); }

在fixedUpdate 设置可移动单位的速度。

foreach (SolderUnit unit in solderUnits) { Grid nodeBelow = flowField.GetGridPosWorldToGrid(unit.transform.position); Vector3 direction = new Vector3(nodeBelow.bestDirection.x, 0f, nodeBelow.bestDirection.y); unit.Move(direction); }

效果展示:

视频封面

上传视频封面

写在最后:目前的只是一个基本的流场寻路。还存在一些细节问题 比如在仅占一个格子的小建筑单位的边缘 流场寻路速度会卡住 并不流场。另外后续笔者会把整个工程按照ECS的模式 用unity DOTS 实现。敬请期待