第七章 寻路与地图对象(Pathfinding and Map Object)
这一章主要进行寻路与地图对象的部分工作。
五 搜索移动范围与路径(Search Move Range and Path)
就像我们之前说的,移动范围与攻击范围的搜索算法十分的类似,只需要修改少部分内容:
计算消耗G;
在判断是否能加入开放集时,需要判断格子是否有效(是否可移动);
对外调用的函数,加入移动消耗。
1 修改寻路(Extend Pathfinding)
这个方法,我们在PathFinding
类中添加。
首先,修改对外调用函数:
// 修改后寻找移动范围 public bool SearchMoveRange(IHowToFind howToFind, CellData start, float movePoint, MoveConsumption consumption) { if (howToFind == null || start == null || movePoint < 0) { return false; } Reset(); m_HowToFind = howToFind; m_MoveConsumption = consumption; m_StartCell = start; m_StartCell.ResetAStar(); m_Range.y = movePoint; m_Reachable.Add(m_StartCell); return SearchRangeInternal(); } // 修改后搜寻路径 public bool SearchPath(IHowToFind howToFind, CellData start, CellData end, MoveConsumption consumption) { if (howToFind == null || start == null || end == null) { return false; } Reset(); m_HowToFind = howToFind; m_MoveConsumption = consumption; m_StartCell = start; m_StartCell.ResetAStar(); m_EndCell = end; m_EndCell.ResetAStar(); m_Reachable.Add(m_StartCell); m_StartCell.h = m_HowToFind.CalcH(this, m_StartCell); return SearchRangeInternal(); } /// 修改后寻找攻击范围 /// 这里添加的参数 `useEndCell` , /// 是在 `移动范围` 与 `攻击范围` 一起显示时, /// 不破坏 `起始节点`。 public bool SearchAttackRange(IHowToFind howToFind, CellData start, int minRange, int maxRange, bool useEndCell = false) { if (howToFind == null || start == null || minRange < 1 || maxRange < minRange) { return false; } Reset(); m_HowToFind = howToFind; m_Range = new Vector2(minRange, maxRange); // 在重置时,不重置 `父亲节点` , // 其一:没有用到 // 其二:二次查找时不破坏路径,否则路径将被破坏 if (useEndCell) { m_EndCell = start; m_EndCell.g = 0f; m_EndCell.h = 0f; m_Reachable.Add(m_EndCell); } else { m_StartCell = start; m_StartCell.g = 0f; m_StartCell.h = 0f; m_Reachable.Add(m_StartCell); } return SearchRangeInternal(); }
当然,你还要记得修改
MapGraph
里的对应函数。其次,修改
Reset
函数:在寻路过程中,我们会不断的重新计算A星的G和H,还有父亲节点。
在
Reset
时,我们不必每个都重置,只需要之前做的修改那样,将关键点重置(例如起始结束节点,而攻击范围没有用到父亲节点,所以只重置G和H)。/// <summary> /// 重置 /// </summary> public void Reset() { m_Reachable.Clear(); m_Explored.Clear(); m_Result.Clear(); m_Range = Vector2.zero; m_StartCell = null; m_EndCell = null; m_CurrentCell = null; m_Finished = false; m_HowToFind = null; m_MoveConsumption = null; m_SearchCount = 0; }
再次,添加获取消耗方法:
/// <summary> /// 获取移动消耗 /// </summary> /// <param name="terrainType"></param> /// <returns></returns> public float GetMoveConsumption(TerrainType terrainType) { if (m_MoveConsumption == null) { return 1f; } return m_MoveConsumption[terrainType]; }
最后,添加建立路径方法:
/// <summary> /// 建立路径List /// </summary> /// <param name="endCell"></param> /// <param name="useResult"></param> /// <returns></returns> public List<CellData> BuildPath(CellData endCell, bool useResult) { if (endCell == null) { Debug.LogError("PathFinding -> Argument named `endCell` is null."); return null; } List<CellData> path = useResult ? m_Result : new List<CellData>(); CellData current = endCell; path.Add(current); while (current.previous != null) { current = current.previous; path.Insert(0, current); } return path; } /// <summary> /// 建立路径Stack /// </summary> /// <param name="endCell"></param> /// <returns></returns> public Stack<CellData> BuildPath(CellData endCell) { if (endCell == null) { Debug.LogError("PathFinding -> Argument named `endCell` is null."); return null; } Stack<CellData> path = new Stack<CellData>(); CellData current = endCell; path.Push(current); while (current.previous != null) { current = current.previous; path.Push(current); } return path; }
2 搜索移动范围(Search Move Range)
我们先新建类:
using UnityEngine;
namespace DR.Book.SRPG_Dev.Maps.FindPath
{
[CreateAssetMenu(fileName = "FindMoveRange.asset", menuName = "SRPG/How to find move range")]
public class FindMoveRange : FindRange
{
public override CellData ChoseCell(PathFinding search)
{
// TODO
return base.ChoseCell(search);
}
public override float CalcGPerCell(PathFinding search, CellData adjacent)
{
// TODO
return base.CalcGPerCell(search, adjacent);
}
public override bool CanAddAdjacentToReachable(PathFinding search, CellData adjacent)
{
// TODO
return base.CanAddAdjacentToReachable(search, adjacent);
}
}
}
首先,选择节点,根据A星需要选择F最小的节点:
public override CellData ChoseCell(PathFinding search) { if (search.reachable.Count == 0) { return null; } /// 取得f最小的节点(因为我们没有计算h,这里就是g) /// 当你在寻找路径有卡顿时,请一定使用更好的查找方式, /// 例如可以改用二叉树的方式, /// 也可将PathFinding里面reachable.Add(adjacent)的方法改成边排序边加入的方法 search.reachable.Sort((cell1, cell2) => -cell1.f.CompareTo(cell2.f)); int index = search.reachable.Count - 1; CellData chose = search.reachable[index]; search.reachable.RemoveAt(index); return chose; }
其次, 是判断格子是否可移动,还需要判断是否更新新的损失与父亲节点:
public override bool CanAddAdjacentToReachable(PathFinding search, CellData adjacent) { // 没有Tile if (!adjacent.hasTile) { return false; } // 已经有对象了 if (adjacent.hasMapObject) { return false; } // 如果已经在关闭集 if (search.IsCellInExpored(adjacent)) { return false; } // 计算消耗 = 当前cell的消耗 + 邻居cell的消耗 float g = search.currentCell.g + CalcGPerCell(search, adjacent); // 已经加入过开放集 if (search.IsCellInReachable(adjacent)) { // 如果新消耗更低 if (g < adjacent.g) { adjacent.g = g; adjacent.previous = search.currentCell; } return false; } // 不在范围内 if (g < 0f || g > search.range.y) { return false; } adjacent.g = g; adjacent.previous = search.currentCell; return true; }
最后,我们填充计算G的函数:
public override float CalcGPerCell(PathFinding search, CellData adjacent) { // 获取邻居的Tile SrpgTile tile = search.map.GetTile(adjacent.position); // 返回本格子的消耗 return search.GetMoveConsumption(tile.terrainType); }
3 搜索路径(Search Path)
直接搜索路径可以说是最标准的A星了,以前我们已经写了很多方法,所以不用再重新写了。
由于多了目标节点,所以在结束搜索的判断上要进行更改。
我们需要做的工作如下:
更改结束搜索的判断;
计算H值;
更改加入是否开放集的判断;
更改建立结果。
我们一个一个来解决。
第一,还是新建一个类:
using System.Linq; using UnityEngine; namespace DR.Book.SRPG_Dev.Maps.FindPath { [CreateAssetMenu(fileName = "FindPathDirect.asset", menuName = "SRPG/How to find path")] public class FindPathDirect : FindMoveRange { public override bool IsFinishedOnChose(PathFinding search) { // TODO return true; } public override float CalcH(PathFinding search, CellData adjacent) { // TODO return 0f; } public override bool CanAddAdjacentToReachable(PathFinding search, CellData adjacent) { // TODO return true; } public override void BuildResult(PathFinding search) { // TODO } } }
第二,结束搜索:
要注意结束搜索的条件,当开放集中没有节点时说明没有找到到达目标点的路径,这就需要我们寻找离目标点最近的节点作为路径目标。
public override bool IsFinishedOnChose(PathFinding search) { // 如果开放集中已经空了,则说明没有达到目标点 if (search.currentCell == null) { // 使用h最小值建立结果 CellData minHCell = search.explored.First(cell => cell.h == search.explored.Min(c => c.h)); search.BuildPath(minHCell, true); return true; } // 找到了目标点 if (search.currentCell == search.endCell) { return true; } if (!search.IsCellInExpored(search.currentCell)) { search.explored.Add(search.currentCell); } return false; }
第三,判断加入开放集的条件:
这里和移动范围相差不多,只是不用再判断范围了,还需要计算H值。
public override bool CanAddAdjacentToReachable(PathFinding search, CellData adjacent) { // 没有Tile if (!adjacent.hasTile) { return false; } // 已经有对象了 if (adjacent.hasMapObject) { return false; } // 如果已经在关闭集 if (search.IsCellInExpored(adjacent)) { return false; } // 计算消耗 = 当前cell的消耗 + 邻居cell的消耗 float g = search.currentCell.g + CalcGPerCell(search, adjacent); // 已经加入过开放集 if (search.IsCellInReachable(adjacent)) { // 如果新消耗更低 if (g < adjacent.g) { adjacent.g = g; adjacent.previous = search.currentCell; } return false; } adjacent.g = g; adjacent.h = CalcH(search, adjacent); adjacent.previous = search.currentCell; return true; }
第四,计算H值:
H值是预计消耗,我们规定这里每个格子的预计消耗为1,即到达目标点的预计消耗为距离。
public override float CalcH(PathFinding search, CellData adjacent) { Vector2 hVec; hVec.x = Mathf.Abs(adjacent.position.x - search.endCell.position.x); hVec.y = Mathf.Abs(adjacent.position.y - search.endCell.position.y); return hVec.x + hVec.y; }
第五,建立结果:
public override void BuildResult(PathFinding search) { // 当没有达到目标点时,已经建立过结果 if (search.result.Count > 0) { return; } search.BuildPath(search.endCell, true); }
4 测试寻路与移动(Test Pathfinding and Moving)
要测试寻路,遵循以下步骤(修改后的测试代码见下一节):
1 建立新的寻路资源:点击菜单
Assets/Create/SRPG/How to find move range
和Assets/Create/SRPG/How to find path
;2 将资源拖入到
MapGraph
;3 修改
EditorTestPathFinding.cs
测试代码,加入移动范围和直接寻路的代码;4 在地图中查看测试效果;
5 在Console面板查看输出。
看起来还不错。
5 测试完整代码(Testing Full Code)
#region ---------- File Info ----------
/// **********************************************************************
/// Copyright (C) 2018 DarkRabbit(ZhangHan)
///
/// File Name: EditorTestPathFinding.cs
/// Author: DarkRabbit
/// Create Time: Sun, 02 Sep 2018 01:49:51 GMT
/// Modifier:
/// Module Description:
/// Version: V1.0.0
/// **********************************************************************
#endregion ---------- File Info ----------
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DR.Book.SRPG_Dev.Maps.Testing
{
using DR.Book.SRPG_Dev.Maps.FindPath;
using DR.Book.SRPG_Dev.Models;
public class EditorTestPathFinding : MonoBehaviour
{
[Serializable]
public enum TestPathfindingType
{
Attack,
Move,
MoveAndAttack,
Path
}
public MapGraph m_Map;
public MapClass m_TestClassPrefab;
public GameObject m_TestCursorPrefab;
/// <summary>
/// 选择测试的寻路类型
/// </summary>
public TestPathfindingType m_PathfindingType;
/// <summary>
/// 移动点数
/// </summary>
public float m_MovePoint = 9f;
/// <summary>
/// 攻击范围
/// </summary>
public Vector2Int m_AttackRange = new Vector2Int(2, 3);
/// <summary>
/// 是否打印不关键的信息
/// </summary>
public bool m_DebugInfo = true;
/// <summary>
/// 是否打印寻路的每一步
/// </summary>
public bool m_DebugStep = false;
private List<GameObject> m_TestCursors;
private MapClass m_TestClass;
private MoveConsumption m_MoveConsumption;
private List<CellData> m_CursorCells;
private CellData m_StartCell;
private CellData m_EndCell;
#if UNITY_EDITOR
private void Awake()
{
if (m_Map == null)
{
m_Map = GameObject.FindObjectOfType<MapGraph>();
}
if (m_Map == null)
{
Debug.LogError("EditorTestPathFinding -> Map was not found.");
return;
}
m_Map.InitMap();
m_Map.searchPath.onStep += MapPathfinding_OnStep;
if (m_TestCursorPrefab == null)
{
Debug.LogError("EditorTestPathFinding -> Cursor Prefab is null.");
return;
}
m_TestCursors = new List<GameObject>();
if (m_TestClassPrefab == null)
{
Debug.LogError("EditorTestPathFinding -> Class Prefab is null.");
return;
}
m_TestClass = GameObject.Instantiate<MapClass>(
m_TestClassPrefab,
m_Map.transform.Find("MapObject"),
false);
m_TestClass.map = m_Map;
m_TestClass.UpdatePosition(new Vector3Int(-1, -1, 0));
m_TestClass.onMovingEnd += M_TestClass_onMovingEnd;
RecreateMoveConsumption();
m_CursorCells = new List<CellData>();
}
private void M_TestClass_onMovingEnd(CellData endCell)
{
m_TestClass.animatorController.StopMove();
}
private void MapPathfinding_OnStep(PathFinding searchPath)
{
if (m_DebugInfo && m_DebugStep)
{
Debug.LogFormat("{0}: The G value of Cell {1} is {2}.",
searchPath.searchCount,
searchPath.currentCell.position.ToString(),
searchPath.currentCell.g.ToString());
}
}
private void Update()
{
if (m_Map == null || m_TestCursorPrefab == null)
{
return;
}
if (Input.anyKeyDown && m_TestClass.moving)
{
Debug.Log("Wait for moving.");
return;
}
// 左键建立范围
if (Input.GetMouseButtonDown(0))
{
if (m_AttackRange.x < 1 || m_AttackRange.y < m_AttackRange.x)
{
Debug.LogError("EditorTestPathFinding -> Check the attack range.");
return;
}
Vector3 mousePos = Input.mousePosition;
Vector3 world = Camera.main.ScreenToWorldPoint(mousePos);
Vector3Int cellPosition = m_Map.grid.WorldToCell(world);
CellData selectedCell = m_Map.GetCellData(cellPosition);
if (selectedCell != null)
{
ClearTestCursors();
switch (m_PathfindingType)
{
case TestPathfindingType.Move:
case TestPathfindingType.MoveAndAttack:
ShowMoveRangeCells(selectedCell);
break;
case TestPathfindingType.Path:
if (!ShowPathCells(selectedCell))
{
return;
}
break;
default:
ShowAttackRangeCells(selectedCell);
break;
}
}
}
// 右键删除建立的cursor
if (Input.GetMouseButtonDown(1))
{
ClearTestCursors();
ClearMoveRangeAndPath();
}
// 中建重新生成移动消耗
if (Input.GetMouseButtonDown(2))
{
RecreateMoveConsumption();
}
}
private void OnDestroy()
{
if (m_Map != null)
{
m_Map.searchPath.onStep -= MapPathfinding_OnStep;
m_Map = null;
}
}
/// <summary>
/// 生成Cursors
/// </summary>
/// <param name="cells"></param>
/// <param name="atk"></param>
public void CreateTestCursors(List<CellData> cells, bool atk)
{
if (cells != null && cells.Count > 0)
{
Color color = Color.clear;
if (atk)
{
color = Color.red;
color.a = 0.5f;
}
foreach (var cell in cells)
{
CreateTestCursor(cell, color);
}
}
}
/// <summary>
/// 生成单个Cursor
/// </summary>
/// <param name="cell"></param>
/// <param name="color"></param>
public void CreateTestCursor(CellData cell, Color color)
{
Vector3 pos = m_Map.GetCellPosition(cell.position);
GameObject find = GameObject.Instantiate(m_TestCursorPrefab, transform, false);
find.name = cell.position.ToString();
find.transform.position = pos;
if (color != Color.clear)
{
find.GetComponent<SpriteRenderer>().color = color;
find.name += " atk";
}
m_TestCursors.Add(find);
}
public void ClearTestCursors()
{
if (m_TestCursors != null && m_TestCursors.Count > 0)
{
foreach (var find in m_TestCursors)
{
GameObject.Destroy(find);
}
m_TestCursors.Clear();
}
}
public void ClearMoveRangeAndPath()
{
m_StartCell = null;
m_EndCell = null;
m_TestClass.UpdatePosition(new Vector3Int(-1, -1, 0));
m_CursorCells.Clear();
}
/// <summary>
/// 重新生成移动消耗
/// </summary>
public void RecreateMoveConsumption()
{
// TODO 在构造函数内部用的Random初始化,
// 有了数据后,这个地方将进行修改
m_MoveConsumption = new MoveConsumption(ClassType.Knight1);
Debug.LogFormat("{0}={1}, {2}={3}",
TerrainType.Plain.ToString(),
m_MoveConsumption[TerrainType.Plain].ToString(),
TerrainType.Road.ToString(),
m_MoveConsumption[TerrainType.Road].ToString());
}
/// <summary>
/// 当左键按下时,Move类型的活动
/// </summary>
/// <param name="selectedCell"></param>
/// <returns></returns>
public List<CellData> ShowMoveRangeCells(CellData selectedCell)
{
List<CellData> cells;
if (m_CursorCells.Count == 0 || !m_CursorCells.Contains(selectedCell))
{
m_CursorCells.Clear();
if (m_DebugInfo)
{
Debug.LogFormat("MoveRange: start position is {0}, move point is {1}",
selectedCell.position.ToString(),
m_MovePoint.ToString());
}
m_TestClass.UpdatePosition(selectedCell.position);
cells = new List<CellData>(m_Map.SearchMoveRange(selectedCell, m_MovePoint, m_MoveConsumption));
m_CursorCells.AddRange(cells);
if (m_PathfindingType == TestPathfindingType.MoveAndAttack)
{
// 移动范围后,进行查找攻击范围
List<CellData> attackCells = new List<CellData>();
foreach (var cell in m_CursorCells)
{
List<CellData> atks = m_Map.SearchAttackRange(cell, m_AttackRange.x, m_AttackRange.y, true);
foreach (var c in atks)
{
if (!cells.Contains(c) && !attackCells.Contains(c))
{
attackCells.Add(c);
}
}
}
CreateTestCursors(attackCells, true);
}
}
else
{
if (m_DebugInfo)
{
Debug.LogFormat("Selected end position {0}", selectedCell.position);
}
m_CursorCells.Clear();
Stack<CellData> pathCells = m_Map.searchPath.BuildPath(selectedCell);
cells = new List<CellData>(pathCells);
m_TestClass.animatorController.PlayMove();
m_TestClass.StartMove(pathCells);
}
CreateTestCursors(cells, false);
return cells;
}
/// <summary>
/// 当左键按下时,Attack类型的活动
/// </summary>
/// <param name="selectedCell"></param>
/// <returns></returns>
public List<CellData> ShowAttackRangeCells(CellData selectedCell)
{
if (m_DebugInfo)
{
Debug.LogFormat("AttackRange: start position is {0}, range is {1}",
selectedCell.position.ToString(),
m_AttackRange.ToString());
}
List<CellData> cells = m_Map.SearchAttackRange(selectedCell, m_AttackRange.x, m_AttackRange.y);
CreateTestCursors(cells, true);
return cells;
}
/// <summary>
/// 当左键按下时,Path类型的活动
/// </summary>
/// <param name="selectedCell"></param>
/// <returns></returns>
public bool ShowPathCells(CellData selectedCell)
{
if (m_StartCell == null)
{
m_StartCell = selectedCell;
if (m_DebugInfo)
{
Debug.LogFormat("Selected start position {0}", m_StartCell.position);
}
m_TestClass.UpdatePosition(selectedCell.position);
return false;
}
m_EndCell = selectedCell;
if (m_DebugInfo)
{
Debug.LogFormat("Selected end position {0}", m_EndCell.position);
}
List<CellData> cells = m_Map.SearchPath(m_StartCell, m_EndCell, m_MoveConsumption);
m_TestClass.animatorController.PlayMove();
m_TestClass.StartMove(new Stack<CellData>(cells));
m_StartCell = null;
m_EndCell = null;
CreateTestCursors(cells, false);
return true;
}
#endif
}
}