网络的部分搞定后,主要就是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); // 默认情况,没有方向
- }
- }
- }
https://blog.csdn.net/u011926026/article/details/63682788#:~:text=%E7%9B%B8%E4%BF%A1%E5%A4%A7%E5%AE%B6%E7%94%A8unity3d%E8%87%AA%E5%B8%A6navmeshagent%E5%AF%BB%E8%B7%AF%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E4%B8%80%E5%AE%9A%E4%BC%9A%E7%A2%B0%E5%88%B0%E5%A4%9A%E4%BA%BA%E5%AF%BB%E8%B7%AF%E7%9B%B8%E4%BA%92%E6%8C%A4%E5%8E%8B%E7%9A%84%E9%97%AE%E9%A2%98%E3%80%82,%E8%BF%99%E4%B8%AA%E6%88%91%E5%B7%B2%E7%BB%8F%E8%A7%A3%E5%86%B3%E4%BA%86%EF%BC%8C%E8%80%8C%E4%B8%94%E5%B7%B2%E7%BB%8F%E5%BA%94%E7%94%A8%E5%88%B0%E6%88%91%E4%BB%AC%E7%9A%84%E9%A1%B9%E7%9B%AE%E4%B8%AD%EF%BC%8C%E6%84%9F%E8%A7%89%E8%BF%98%E4%B8%8D%E9%94%99%E3%80%82%20%E9%A6%96%E5%85%88%E6%8F%90%E4%B8%8B%E9%97%AE%E9%A2%98%E7%9A%84%E5%8E%9F%E5%9B%A0%EF%BC%9Anavmeshagent%E5%9C%A8%E5%AF%BB%E8%B7%AF%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E5%B9%B6%E4%B8%8D%E4%BC%9A%E6%8A%8A%E5%88%AB%E4%BA%BA%E5%BD%93%E6%88%90%E9%9A%9C%E7%A2%8D%E7%89%A9%EF%BC%8C%E8%80%8C%E6%98%AF%E5%9C%A8%E5%8D%B3%E5%B0%86%E5%92%8C%E5%85%B6%E4%BB%96agent%E7%A2%B0%E6%92%9E%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E9%80%9A%E8%BF%87%E5%87%8F%E9%80%9F%EF%BC%8C%E7%A2%B0%E6%92%9E%E8%BD%AC%E5%BC%AF%EF%BC%8C%E5%AE%9E%E7%8E%B0%E9%81%BF%E5%BC%80%E5%85%B6%E4%BB%96agent.
评论记录:
回复评论: