SRPG游戏开发(二十七)第七章 寻路与地图对象 - 五 搜索移动范围与路径(Search Move Range and Path)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/darkrabbit/article/details/82532736

返回总目录

第七章 寻路与地图对象(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 rangeAssets/Create/SRPG/How to find path

    How to find assets

  • 2 将资源拖入到MapGraph

    MapGraph

  • 3 修改EditorTestPathFinding.cs测试代码,加入移动范围和直接寻路的代码;

    EditorTestPathFinding

  • 4 在地图中查看测试效果;

    Test Result 1

    Test Result 2

  • 5 在Console面板查看输出。

    Test 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

    }
}

猜你喜欢

转载自blog.csdn.net/darkrabbit/article/details/82532736