区域网格化
在使用A星算法和物体布局的过程中,常常会使用的网格的概念,即建立在网格的基础上,会使得游戏的相关编程变得简单的多。
格子的代码:
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Node { public Vector3 _worldPos;//格子中心点的位置 public int _gridX, _gridY;//在网格列表的下标 public Node(Vector3 Position, int x, int y) { _worldPos = Position; _gridX = x; _gridY = y; } }
网格代码:
using System.Collections; using System.Collections.Generic; using UnityEngine; //网格,网格的起点是在左下角,终点是右上角 public class Grid : MonoBehaviour { public static Grid instance; private Node[,] grid;//网格 public Vector2 gridSize;//网格横纵大小 public float nodeRadius;//格子的半径 private float nodeDiameter;//格子的直径 public int gridCntX, gridCntY;//两个方向上的网格数量 //Test public Transform tarTrans;//目标 public Node tar; public float dir;//射程 //目标区域 public Node zoneLeftDown;//网格的左下角 public Node zoneRightUp;//网格的右上角 Vector3 pos = new Vector3(); // Start is called before the first frame update void Awake() { instance = this; nodeDiameter = nodeRadius * 2; gridCntX = Mathf.RoundToInt(gridSize.x / nodeDiameter); gridCntY = Mathf.RoundToInt(gridSize.y / nodeDiameter); grid = new Node[gridCntX, gridCntY]; CreateGrid(); }//创建网格,起始点在左下角 private void CreateGrid() { //获得网格的左下角的坐标 Vector3 startPoint = transform.position - gridSize.x / 2 * Vector3.right - gridSize.y / 2* Vector3.up; for (int i = 0; i < gridCntX; i++) { for (int j = 0; j < gridCntY; j++) { Vector3 worldPoint = startPoint + Vector3.right * (i * nodeDiameter + nodeRadius) + Vector3.up * (j * nodeDiameter + nodeRadius); grid[i, j] = new Node(worldPoint, i, j); } } } //获取某个坐标处的格子 public Node GetFromPosition(Vector3 position) { //首先获得该坐标相对于网格的宽高的百分比 float percentX = (position.x + gridSize.x / 2) / gridSize.x; float percentY = (position.y + gridSize.y / 2) / gridSize.y; //保证百分比值在0到1之间 percentX = Mathf.Clamp01(percentX); percentY = Mathf.Clamp01(percentY); int x = Mathf.RoundToInt((gridCntX - 1) * percentX); int y = Mathf.RoundToInt((gridCntY - 1) * percentY); return grid[x, y]; } //获取一个正方形区域中随机点,length为区域的边长 public Vector3 GetZoneRandomPos(Vector3 center,float length) { //射程一定要大于等于0 //float len = Mathf.Abs(length) / 2; //获取射程网格区域 zoneLeftDown = GetFromPosition(center - new Vector3(length, length)); zoneRightUp = GetFromPosition(center + new Vector3(length, length)); //获取并返回射程网格区域中的一个随机点 int i = Random.Range(zoneLeftDown._gridX, zoneRightUp._gridX); int j = Random.Range(zoneLeftDown._gridY, zoneRightUp._gridY); return grid[i, j]._worldPos; } //获取整个区域中的一个随机点 public Vector3 GetZoneRandomPos() { int i = Random.Range(0, gridCntX); int j = Random.Range(0, gridCntY); return grid[i, j]._worldPos; } private void OnDrawGizmos() { //绘制网格边界线 Gizmos.DrawWireCube(transform.position, new Vector3(gridSize.x, gridSize.y, 1)); if (grid == null) return; Gizmos.color = new Color(1, 1, 1, 0.2f); //绘制网格 foreach (var node in grid) { Gizmos.DrawCube(node._worldPos+Vector3.forward, Vector3.one * (nodeDiameter - .1f*nodeDiameter)); } } }
运行结果:
一般A星算法实例一,具体算法原理略
这里修改原来的网格代码,从而符合A星算法的需求
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Node { public bool _walkable;//是否可走 public Vector3 _worldPos;//格子中心点的位置 public int _gridX, _gridY;//在网格列表的下标 public int gCost;//到起始点的曼哈顿距离 public int hCost;//到目标点的曼哈顿距离 public Node parent;//父亲网格点 public int fCost {//曼哈顿距离综合 get { return gCost +hCost; } } public Node(bool walkable,Vector3 Position, int x, int y) { _walkable = walkable; _worldPos = Position; _gridX = x; _gridY = y; } } public class Grid : MonoBehaviour { public static Grid instance; private Node[,] grid;//网格 public Vector2 gridSize;//网格横纵大小 public float nodeRadius;//格子的半径 private float nodeDiameter;//格子的直径 public int gridCntX, gridCntY;//两个方向上的网格数量 public Vector3 startPoint;//网格的最右下角的坐标 public LayerMask layer; //Test public Transform tarTrans;//目标 public Node tar; public float dir;//射程 public List<Node> path = new List<Node>(); //目标区域 public Node zoneLeftDown;//网格的左下角 public Node zoneRightUp;//网格的右上角 Vector3 pos = new Vector3(); // Start is called before the first frame update void Awake() { instance = this; nodeDiameter = nodeRadius * 2; gridCntX = Mathf.RoundToInt(gridSize.x / nodeDiameter); gridCntY = Mathf.RoundToInt(gridSize.y / nodeDiameter); grid = new Node[gridCntX, gridCntY]; CreateGrid(); } //创建网格,起始点在左下角 private void CreateGrid() { //获得网格的左下角的坐标 startPoint = transform.position - gridSize.x / 2 * Vector3.right - gridSize.y / 2 * Vector3.up; for (int i = 0; i < gridCntX; i++) { for (int j = 0; j < gridCntY; j++) { Vector3 worldPoint = startPoint + Vector3.right * (i * nodeDiameter + nodeRadius) + Vector3.up * (j * nodeDiameter + nodeRadius); bool walkable = !Physics2D.OverlapCircle(worldPoint, nodeRadius, layer); grid[i, j] = new Node(walkable,worldPoint, i, j); } } } //获取某个坐标处的格子,利用百分比,那么在网格区域外的一个点的网格点就是离它最近的那个网格点 public Node GetFromPosition(Vector3 position) { //首先获得该坐标相对于网格的宽高的百分比 float percentX = (position.x + gridSize.x / 2) / gridSize.x; float percentY = (position.y + gridSize.y / 2) / gridSize.y; //保证百分比值在0到1之间 percentX = Mathf.Clamp01(percentX); percentY = Mathf.Clamp01(percentY); int x = Mathf.RoundToInt((gridCntX - 1) * percentX); int y = Mathf.RoundToInt((gridCntY - 1) * percentY); return grid[x, y]; } //获取网格范围内一个正方形区域中随机点,length为区域的边长 public Vector3 GetZoneRandomPos(Vector3 center, float length) { //射程一定要大于等于0 //float len = Mathf.Abs(length) / 2; //获取射程网格区域 zoneLeftDown = GetFromPosition(center - new Vector3(length, length)); zoneRightUp = GetFromPosition(center + new Vector3(length, length)); //获取并返回射程网格区域中的一个随机点 int i = Random.Range(zoneLeftDown._gridX, zoneRightUp._gridX); int j = Random.Range(zoneLeftDown._gridY, zoneRightUp._gridY); return grid[i, j]._worldPos; } //获取整个区域中的一个随机点 public Vector3 GetZoneRandomPos() { int i = Random.Range(0, gridCntX); int j = Random.Range(0, gridCntY); return grid[i, j]._worldPos; } //获取某格子周围除了自身外的另外所有可走格子 public List<Node> GetNeibourhood(Node node) { List<Node> neibourhood = new List<Node>(); for(int i = -1; i <= 1; i++) { for(int j = -1; j<= 1; j++) { if (i == 0 && j == 0) continue; int tempX = node._gridX + i; int tempY = node._gridY + j; if (tempX > 0 && tempX < gridCntX && tempY > 0 && tempY < gridCntY) { neibourhood.Add(grid[tempX,tempY]); } } } return neibourhood; } private void OnDrawGizmos() { //绘制网格边界线 Gizmos.DrawWireCube(transform.position, new Vector3(gridSize.x, gridSize.y, 1)); if (grid == null) return; //Gizmos.color = new Color(1, 1, 1, 0.2f); //绘制网格 foreach (var node in grid) { Gizmos.color = node._walkable ? Color.white : Color.red; Gizmos.DrawCube(node._worldPos + Vector3.forward, Vector3.one * (nodeDiameter - .1f * nodeDiameter)); } if (path != null) { foreach(var node in path) { Gizmos.color = Color.black; Gizmos.DrawCube(node._worldPos + Vector3.forward, Vector3.one * (nodeDiameter - .1f * nodeDiameter)); } } Node tarNode = GetFromPosition(tarTrans.position); if (tarNode != null && tarNode._walkable) { Gizmos.color = Color.cyan; Gizmos.DrawCube(tarNode._worldPos, Vector3.one * (nodeDiameter - .1f * nodeDiameter)); } } }
下面是A星寻路算法,这里使用了两种不能的路径处理结果
using System.Collections; using System.Collections.Generic; using UnityEngine; //FindingPath为经典的A星寻路算法,获得网格路径。 //FindingPathForFlay是在寻路结束之后,将网格路径中不需要的网格删除调,从而使得运动方向更加平滑,剪除的过程在GeneratePathForFly中进行, //后期优化可以放在寻路过程中进行 public class FindPath : MonoBehaviour { public Transform hunter, tar; Grid _grid; // Start is called before the first frame update void Start() { _grid = GetComponent<Grid>(); } // Update is called once per frame void Update() { //FindingPath(hunter.position, tar.position); FindingPathForFly(hunter.position, tar.position); } //经典A*算法到目标点的精确路径查找 void FindingPath(Vector3 startPos,Vector3 endPos) { Node startNode = _grid.GetFromPosition(startPos); Node endNode = _grid.GetFromPosition(endPos); List<Node> openSet = new List<Node>(); HashSet<Node> closeSet = new HashSet<Node>(); openSet.Add(startNode); while (openSet.Count > 0) { //根据曼哈顿距离寻找新结点 Node currentNode = openSet[0]; for (int i = 0; i < openSet.Count; i++) { if (openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost) { currentNode = openSet[i]; } } openSet.Remove(currentNode); closeSet.Add(currentNode); //如果当前结点是最后一个结点,那么寻找结束 if (currentNode == endNode) { GeneratePath(startNode,endNode); return; } //添加周围的可用结点到openSet foreach (var node in _grid.GetNeibourhood(currentNode)) { if (!node._walkable || closeSet.Contains(node)) continue; int newCost = currentNode.gCost + GetDistanceNodes(currentNode, node); if (newCost < node.gCost || !openSet.Contains(node)) { node.gCost = newCost; node.hCost = GetDistanceNodes(node, endNode); node.parent = currentNode; if (!openSet.Contains(node)) { openSet.Add(node); } } } } } //经典A*算法下的飞行游戏路径查找 void FindingPathForFly(Vector3 startPos, Vector3 endPos) { Node startNode = _grid.GetFromPosition(startPos); Node endNode = _grid.GetFromPosition(endPos); List<Node> openSet = new List<Node>(); HashSet<Node> closeSet = new HashSet<Node>(); openSet.Add(startNode); while (openSet.Count > 0) { //根据曼哈顿距离寻找新结点 Node currentNode = openSet[0]; for (int i = 0; i < openSet.Count; i++) { if (openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost) { currentNode = openSet[i]; } } openSet.Remove(currentNode); closeSet.Add(currentNode); //如果当前结点是最后一个结点,那么寻找结束 if (currentNode == endNode) { GeneratePathForFly(startNode, endNode); return; } //添加周围的可用结点到openSet foreach (var node in _grid.GetNeibourhood(currentNode)) { if (!node._walkable || closeSet.Contains(node)) continue; int newCost = currentNode.gCost + GetDistanceNodes(currentNode, node); if (newCost < node.gCost || !openSet.Contains(node)) { node.gCost = newCost; node.hCost = GetDistanceNodes(node, endNode); node.parent = currentNode; if (!openSet.Contains(node)) { openSet.Add(node); } } } } } //经典A*算法下根据父子关系回溯获得路径 void GeneratePath(Node startNode,Node endNode) { List<Node> path = new List<Node>(); Node temp = endNode; while (temp != startNode)//回溯,将所有路径结点结合成路径列表 { path.Add(temp); temp = temp.parent; } path.Reverse();//将列表反转 _grid.path = path;//将列表交予Grid; } //经典A*算法下根据父子关系回溯获得路径,并且回溯。如果两个网格点之间没有障碍,可以直达,那么剪除两个网格点之间剪除不要的网格点 void GeneratePathForFly(Node startNode, Node endNode) { List<Node> path = new List<Node>(); Node temp = endNode; while (temp != startNode)//回溯,将所有路径网格结合成路径列表 { if(path.Count==0) path.Add(temp); RaycastHit2D hit; //如果前一个网格点与当前网格点的父亲网格之间有障碍,那么表明需要当前网格点,将其添加到路径表中 if(hit=Physics2D.Raycast(path[path.Count-1]._worldPos, temp.parent._worldPos - path[path.Count - 1]._worldPos,Vector3.Distance(temp.parent._worldPos, path[path.Count - 1]._worldPos))) { path.Add(temp); } temp = temp.parent; } path.Reverse();//将列表反转 _grid.path = path;//将列表交予Grid; } //获取两个点之间的曼哈顿距离 int GetDistanceNodes(Node a,Node b) { int cntX = Mathf.Abs(a._gridX - b._gridX); int cntY = Mathf.Abs(a._gridY - b._gridY); if (cntX > cntY) { return 14 * cntY + 10 * (cntX - cntY); } else { return 14 * cntX + 10 * (cntY - cntX); } } }
结果
剪除多余的网格点之后
对于不同体积的角色,避免碰撞的处理方案有两种:
第一种:将网格分为不同的层,每层的格子的大小不同。
第二种:寻路时,网格可以走的条件为,walkable为真,且在设定半径范围内没有其他碰撞体存在。
2D飞行游戏最简单AI设计:
1.如果自己与目标之间没有障碍,那么直接飞向目标
2.飞向目标的方式:转向目标方向,如果当前的方向与到目标方向的角度小于设定的某个角度,那么开启喷射引擎。
3.逃离目标:这里以圆形的障碍目标为例,如果检测到障碍,那么设定障碍到自身的方向为目标方向,转向该目标方向,从而逃离目标。由于这个过程中自身的位置 不断变化,目标方向也是变化的,但是获得的结果也更加真实。