网络的部分搞定后,主要就是RTS的几大难点.
这里先复盘一下遇到的最大的坑:寻路系统
方案的选择:
1.基于路点寻路+A*算法的Unity Navigation
优点:优化非常好,上手很简单,使用了地图烘焙
缺点:
1.多角色之间的碰撞和挤压很难看且难以避免
如果是人型角色这倒没什么,谁没在CS里被挤过,但是一堆坦克挤来挤去就很酸爽了.这点亲身体会,另外可以参考这个帖子.
总之一点,这个东西对于单人或者对挤压没那么敏感的游戏可以,否则得深入底层改一些东西.
2.动态添加障碍物比如RTS这种能够随时随地建建筑,会产生一定bug
3.不适用于帧同步(浮点\碰撞)
2.现成的A*框架
性能开销过大,适用于单人或者少量角色.
*3.流场寻路算法(采用)
思路参考:
优点:大量单位公用同一份寻路结果节省了大量时间
PS.寻路远非线路规划这么简单,因为没有单位能够完全按照规划路线行进,每个单位之间会相互影响,这个影响逻辑怎么写是最麻烦的,这不在本文复盘内容之内.
一.建立网格系统
首先无论是A*还是流场寻路,都必须建立一个网格系统,网格系统听着复杂,其实就是写一些方法,将当前的Unity 转化成抽象的网格上的坐标.
比如我有一个位置Vector3(5,10.6,8),我想知道他在我抽象的网格世界的网格坐标是什么
就有了了Vector3Int currentGrid = Grid.FromWorldToCell(Vector3(5,10.6,8));
我这里的代码是用LStepLock(一个开源定点库)的LVector3,原理一样.
- using Lockstep.Math;
- using UnityEngine;
-
- public class LGrid : MonoBehaviour
- {
- [SerializeField] private LFloat cellSize;
-
- ///
- /// 获得当前真实三维坐标对应的网格坐标
- ///
- ///
- public LVector3 LWorldToCell(LVector3 world)
- {
- return new LVector3(
- LFloat.Divide(world.x, cellSize),
- LFloat.Divide(world.y, cellSize),
- LFloat.Divide(world.z, cellSize)
- );
- }
- ///
- /// 获得当前网格(左下角)的真实三位坐标中
- ///
- ///
- //默认网格初始位置0,0,0
- public LVector3 LCellToWorld(LVector3Int cell)
- {
- return new LVector3(
- (cell.x -1) * cellSize,
- (cell.y) * cellSize,
- (cell.z -1) * cellSize);
- }
- ///
- /// 获得当前网格(中心点)的真实三位坐标
- ///
- ///
- public LVector3 LGetCellCenterWorld(LVector3 cell)
- {
- return new LVector3(
- (cell.x-1)*cellSize+cellSize/2,
- (cell.y)*cellSize,
- (cell.z-1)*cellSize+cellSize/2
- );
- }
- ///
- /// 获得当前网格(中心点)的真实三位坐标*1000000L的坐标的值
- ///
- ///
- ///
- public LVector3 LGetCellCenterWorld_s(LVector3 cell)
- {
- return new LVector3(
- ((cell._x-1)*cellSize+cellSize/2),
- (cell._y)*cellSize,
- (cell._z-1)*cellSize+cellSize/2
- );
- }
- }
二.初始化导航地图
这个步骤相当复杂,但是简单来说就是建立一个bool[,]的二维地图,尺寸比如500x500,值为false表示障碍物,true表示可通行.
对于这次的复盘的任务而言就是传入一张这样的地图,起始点,终点,怎样找到一条从起点到终点的二维地图中的坐标数组
流场算法
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- using System.Collections;
- using System.Collections.Generic;
- using System.Threading;
- using Lockstep.Math;
- using UnityEngine;
-
- ///
- /// 流场算法
- ///
- public class FlowFieldPath
- {
- private bool[,] grid;
- public int[,] heatMap;
- private int targetX, targetY;
- private int[,] flowField;
- public bool ifGenerateFineshed = false;
-
- ///
- /// 初始化流场寻路:传入导航地图
- ///
- ///
- public FlowFieldPath(bool[,] grid)
- {
- this.grid = grid;
- heatMap = new int[grid.GetLength(0), grid.GetLength(1)];
- for (int i = 0; i < heatMap.GetLength(0); i++)
- {
- for (int j = 0; j < heatMap.GetLength(1); j++)
- {
- heatMap[i, j] = int.MaxValue;
- }
- }
- }
-
- ///
- /// 传入终点,根据重点生成导航热力图
- /// 1.创建新的线程
- ///
- /// 终点
- public void GenerateHeatMap(Vector2Int target)
- {
- new Thread(() => { GenerateHeatMap_T(target); }).Start();
- }
-
- ///
- /// 传入终点,根据重点生成导航热力图
- /// 2.正式开始生成热力图,生成的热力图
- ///
- ///
- ///
- public int[,] GenerateHeatMap_T(Vector2Int target)
- {
- heatMap = new int[grid.GetLength(0), grid.GetLength(1)];
- for (int i = 0; i < heatMap.GetLength(0); i++)
- {
- for (int j = 0; j < heatMap.GetLength(1); j++)
- {
- heatMap[i, j] = int.MaxValue;
- }
- }
-
- this.targetX = target.x;
- this.targetY = target.y;
- Queue
queue = new Queue(); - queue.Enqueue(new Vector2Int(targetX, targetY));
- heatMap[targetX, targetY] = 0;
-
- while (queue.Count > 0)
- {
- Vector2Int current = queue.Dequeue();
- List
neighbors = GetNeighbors(current); - foreach (Vector2Int neighbor in neighbors)
- {
- if (grid[neighbor.x, neighbor.y])
- {
- int newHeat = heatMap[current.x, current.y] +
- ((neighbor.x == current.x || neighbor.y == current.y) ? 1 : 2);
- if (newHeat < heatMap[neighbor.x, neighbor.y])
- {
- heatMap[neighbor.x, neighbor.y] = newHeat;
- queue.Enqueue(neighbor);
- }
- }
- }
- }
-
- ifGenerateFineshed = true;
- return heatMap;
- }
-
- ///
- /// 获取热力图某点的热力值
- ///
- ///
- ///
- public int GetHeatMapPoint(LVector2Int loc)
- {
- return heatMap[loc.x, loc.y];
- }
-
- ///
- /// 根据热力图生成流场图
- ///
- ///
- ///
- public int[,] GenerateFlowField(Vector2Int target)
- {
- Debug.Log("确定流场目标:" + target.x + "," + target.y);
- GenerateHeatMap(target);
- flowField = new int[grid.GetLength(0), grid.GetLength(1)];
- for (int i = 0; i < flowField.GetLength(0); i++)
- {
- for (int j = 0; j < flowField.GetLength(1); j++)
- {
- List
neighbors = GetNeighbors(new Vector2Int(i, j)); - int minHeat = int.MaxValue;
- foreach (Vector2Int neighbor in neighbors)
- {
- if (grid[neighbor.x, neighbor.y] && heatMap[neighbor.x, neighbor.y] < minHeat)
- {
- minHeat = heatMap[neighbor.x, neighbor.y];
- flowField[i, j] = DirectionToIndex(new Vector2Int(i, j), neighbor);
- }
- }
- }
- }
-
- return flowField;
- }
-
- public int CheckDirection(LVector2Int loc)
- {
- return flowField[loc.x, loc.y];
- }
-
- private List
GetNeighbors(Vector2Int point) - {
- List
neighbors = new List(); - for (int dx = -1; dx <= 1; dx++)
- {
- for (int dy = -1; dy <= 1; dy++)
- {
- if (dx == 0 && dy == 0) continue; // 跳过自身
- int nx = point.x + dx;
- int ny = point.y + dy;
- if (nx >= 0 && ny >= 0 && nx < grid.GetLength(0) && ny < grid.GetLength(1))
- {
- neighbors.Add(new Vector2Int(nx, ny));
- }
- }
- }
-
- return neighbors;
- }
-
-
- private int DirectionToIndex(Vector2Int current, Vector2Int target)
- {
- Vector2Int direction = target - current;
- if (direction.x == 0 && direction.y == -1) return 0; // 上
- if (direction.x == 1 && direction.y == -1) return 1; // 右上
- if (direction.x == 1 && direction.y == 0) return 2; // 右
- if (direction.x == 1 && direction.y == 1) return 3; // 右下
- if (direction.x == 0 && direction.y == 1) return 4; // 下
- if (direction.x == -1 && direction.y == 1) return 5; // 左下
- if (direction.x == -1 && direction.y == 0) return 6; // 左
- if (direction.x == -1 && direction.y == -1) return 7; // 左上
-
- return -1;
- }
-
- public LVector2Int GetFlowFieldDirection(LVector2Int point)
- {
- int directionIndex = flowField[point.x, point.y];
- LVector2Int direction = IndexToDirection(directionIndex);
- return direction;
- }
-
- private LVector2Int IndexToDirection(int index)
- {
- switch (index)
- {
- case 0: return new LVector2Int(0, 1); // 上
- case 1: return new LVector2Int(1, 1); // 右上
- case 2: return new LVector2Int(1, 0); // 右
- case 3: return new LVector2Int(1, -1); // 右下
- case 4: return new LVector2Int(0, -1); // 下
- case 5: return new LVector2Int(-1, -1); // 左下
- case 6: return new LVector2Int(-1, 0); // 左
- case 7: return new LVector2Int(-1, 1); // 左上
- default: return new LVector2Int(0, 0); // 默认情况,没有方向
- }
- }
- }
评论记录:
回复评论: