Ошибка алгоритма поиска пути для гексов - PullRequest
1 голос
/ 21 февраля 2020

Я пытаюсь реализовать алгоритм поиска пути A * для двумерной гексагональной карты тайлов. У меня есть следующий класс HexCell, который действует как контейнер данных и искатель смежности, используя целое число шестнадцатеричного индекса:

namespace Expedition
{
    using System;
    using System.Collections.Generic;
    using UnityEngine;

    public class HexCell
    {
        public List<HexCell> AdjacencyList = new List<HexCell>();
        public Vector3 Position;
        public HexCell Parent;
        public float f = 0;
        public float g = 0;
        public float h = 0;
        private int index;
        private enum HexDirection { Northeast, East, Southeast, Southwest, West, Northwest };

        public HexCell(int index, Vector3 position)
        {
            this.index = index;
            Position = position;
        }

        // Reset references to valid adjacent hexes relative to this hex cell
        public void RefreshAdjacencyList()
        {
            AdjacencyList.Clear();
            var directions = Enum.GetValues(typeof(HexDirection));
            HexCell adjacentCell;
            int gridMaxX = ExpeditionController.gridMaxX;
            int gridMaxY = ExpeditionController.gridMaxY;
            int targetIndex;

            foreach (HexDirection dir in directions)
            {
                targetIndex = -1;

                switch (dir)
                {
                    case HexDirection.Northeast:
                        // Not on right and not on top
                        if (index - ((index / gridMaxX) * gridMaxX) != gridMaxX - 1 && index / gridMaxX != gridMaxY - 1)
                        {
                            targetIndex = index + gridMaxX;
                        }
                        break;
                    case HexDirection.East:
                        // Not on right
                        if (index - ((index / gridMaxX) * gridMaxX) != gridMaxX - 1)
                        {
                            targetIndex = index + 1;
                        }
                        break;
                    case HexDirection.Southeast:
                        // Not on right and not on bottom
                        if (index - ((index / gridMaxX) * gridMaxX) != gridMaxX - 1 && index / gridMaxX != 0)
                        {
                            targetIndex = index - gridMaxX + 1;
                        }
                        break;
                    case HexDirection.Southwest:
                        // Not on left and not on bottom
                        if (index % (gridMaxX) != 0 && index / gridMaxX != 0)
                        {
                            targetIndex = index - gridMaxX - 1;
                        }
                        break;
                    case HexDirection.West:
                        // Not on left
                        if (index % (gridMaxX) != 0)
                        {
                            targetIndex = index - 1;
                        }
                        break;
                    case HexDirection.Northwest:
                        // Not on left and not on top
                        if (index % (gridMaxX) != 0 && index / gridMaxX != gridMaxY - 1)
                        {
                            targetIndex = index + gridMaxX - 1;
                        }
                        break;
                }

                if (targetIndex != -1)
                {
                    adjacentCell = ExpeditionController.Instance.Cells[targetIndex];
                    AdjacencyList.Add(adjacentCell);
                }
            }
        }
    }
}

Затем у меня есть класс ExpeditionController, который выполняет фактическое нахождение пути между шестнадцатеричными ячейками, используя их соответствующие списки соседей шестнадцатеричных ячеек. Я использую OnDrawGizmos для рисования сфер для каждого узла в пути:


namespace Expedition
{
    using Common;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Tilemaps;

    public class ExpeditionController : StateMachine
    {
        public static ExpeditionController Instance;
        public GameObject HexCellHighlightPrefab;
        public Dictionary<int, HexCell> Cells { get; private set; } = new Dictionary<int, HexCell>();
        [HideInInspector] public static int gridMaxX = 13;
        [HideInInspector] public static int gridMaxY = 21;
        public TileBase tilebaseTest;
        private const float worldmapVerticalOffset = 100f;
        private Dictionary<Vector3Int, GameObject> cellHighlightObjects = new Dictionary<Vector3Int, GameObject>();
        private HexCell selectedHex;
        private Stack<HexCell> path = new Stack<HexCell>();

        // These will change based on player position later, for now they're static at the origin:
        private float playerF = 0;
        private float playerG = 0;
        private float playerH = 0;
        [SerializeField] private GameObject worldMap;
        [SerializeField] private Camera mainCamera;
        [SerializeField] private GameObject minimapParent;
        [SerializeField] private RenderTexture minimapRawImageRenderTexture;
        private Camera worldMinimapCamera;
        private Grid grid;
        private Tilemap tilemap;
        private const float hexDimension = 0.8660254f;

        private void Awake()
        {
            Instance = this;
        }        

        private void Start()
        {
            var bottomPanel = Instantiate(GameManager.Instance.BottomPanelPrefab);
            bottomPanel.GetComponent<RectTransform>().SetParent(GameManager.Instance.Canvas.transform, false);
            worldMap = Instantiate(GameManager.Instance.WorldMapPrefab);
            worldMinimapCamera = worldMap.GetComponentInChildren<Camera>();
            grid = worldMap.GetComponent<Grid>();
            tilemap = grid.GetComponentInChildren<Tilemap>();
            BuildMap();
            foreach (var hexCell in Cells.Values)
            {
                hexCell.RefreshAdjacencyList();
            }
            ChangeState<TravelState>();
        }

        public void ToggleWorldMapVisibility()
        {
            if (mainCamera.gameObject.activeInHierarchy)
            {
                // Hide world map and show travel view
                minimapParent.SetActive(false);
                worldMinimapCamera.targetTexture = null;
                mainCamera.gameObject.SetActive(false);
            }
            else
            {
                minimapParent.SetActive(true);
                worldMinimapCamera.targetTexture = minimapRawImageRenderTexture;
                mainCamera.gameObject.SetActive(true);
            }
        }

        public void RegisterCellHighlightObject(Vector3 position, GameObject go)
        {
            var intPos = Vector3Int.RoundToInt(position);
            if (!cellHighlightObjects.ContainsKey(intPos))
            {
                cellHighlightObjects.Add(intPos, go);
            }
        }

        public void OnClick(Vector3 clickPosition)
        {
            if (worldMinimapCamera != null && grid != null)
            {
                Vector3 mouseWorldPos = worldMinimapCamera.ScreenToWorldPoint(clickPosition);
                Vector3Int coordinate = grid.WorldToCell(mouseWorldPos);
                var index = ConvertPositionToHexCellIndex(coordinate);
                // Debug.Log("Index: " + index);

                tilemap.SetTile(coordinate, tilebaseTest);

                // Activate the corresponding highlight object for the tile
                cellHighlightObjects[coordinate].SetActive(!cellHighlightObjects[coordinate].activeInHierarchy);
                if (Cells.ContainsKey(index))
                {
                    selectedHex = Cells[index];
                    if (selectedHex != null)
                    {
                        // Debug.Log("selected hex at " + selectedHex.Position);
                        DeterminePathToHexCell(selectedHex);
                    }
                }
            }
        }

        private void BuildMap()
        {
            float xCalc;
            int index = 0;
            for (float y = 0; y < gridMaxY; y++)
            {
                for (float x = 0; x < gridMaxX; x++)
                {
                    if ((int) y % 2 == 0)
                    {
                        xCalc = x * hexDimension;
                    }
                    else
                    {
                        xCalc = (x * hexDimension) + (hexDimension / 2);
                    }

                    var hexCoordinate = new Vector3(xCalc, y * 0.75f, 0);

                    // Debug.Log("creating tile at index " + index);
                    Cells.Add(index, new HexCell(index, hexCoordinate));
                    index++;
                    tilemap.SetTile(new Vector3Int((int)x, (int)y, 0), tilebaseTest);
                }
            }
        }

        private HexCell FindLowestF(List<HexCell> list)
        {
            HexCell lowest = list[0];
            foreach (HexCell t in list)
            {
                if (t.f < lowest.f)
                {
                    lowest = t;
                }
            }

            list.Remove(lowest);
            return lowest;
        }

        private void DeterminePathToHexCell(HexCell target)
        {
            var openList = new List<HexCell>();
            var closedList = new List<HexCell>(); // When the target tile is added to the closed list, we are done

            openList.Add(Cells[0]);

            var currentCellPos = Cells[0].Position;
            var targetCellPos = selectedHex.Position;
            playerH = Vector3.Distance(currentCellPos, targetCellPos);
            playerF = playerH;

            // If openList count is ever 0, that means we've not found the shortest path
            while (openList.Count > 0)
            {
                HexCell lowestFCell = FindLowestF(openList);
                closedList.Add(lowestFCell);

                if (lowestFCell == target)
                {
                    // Path found
                    BuildPathAndStartMoving(lowestFCell);
                    return;
                }

                foreach (HexCell cell in lowestFCell.AdjacencyList)
                {
                    var tilePos = cell.Position;
                    var cPos = lowestFCell.Position;

                    // Add this later:
                    //if (cell.unit != unit && cell != target && cell.unit != null)
                    //{
                    //    // Don't allow movement through occupied tiles (unless its the moving unit or the target unit)
                    //    closedList.Add(cell);
                    //}

                    if (closedList.Contains(cell))
                    {
                        // Do nothing, already processed
                    }
                    else if (openList.Contains(cell))
                    {
                        float tempG = lowestFCell.g + Vector3.Distance(tilePos, cPos);
                        if (tempG < cell.g)
                        {
                            cell.Parent = lowestFCell;
                            cell.g = tempG;
                            cell.f = cell.g + cell.h;
                        }
                    }
                    else
                    {
                        cell.Parent = lowestFCell;
                        cell.g = lowestFCell.g + Vector3.Distance(tilePos, cPos);
                        cell.h = Vector3.Distance(tilePos, targetCellPos);
                        cell.f = cell.g + cell.h;
                        openList.Add(cell);
                    }
                }
            }

            Debug.Log("Path not found");
        }

        private void BuildPathAndStartMoving(HexCell cell)
        {
            path.Clear();

            // Start at the end tile
            HexCell next = cell;
            while (next != null)
            {
                path.Push(next);
                next = next.Parent;
            }

            // StartMoving();
        }

        private int ConvertPositionToHexCellIndex(Vector3 position)
        {
            int xIndex = (int)position.x;
            int yIndex = (int)position.y * gridMaxX;
            var index = xIndex + yIndex;

            if (index < 0 || index > gridMaxY * gridMaxX)
            {
                return -1;
            }

            return index;
        }


        private void OnDrawGizmos()
        {
            // Mark all hex centers for easier coordinate validation
            Gizmos.color = Color.white;
            foreach (var hc in Cells.Values)
            {
                // The map is currently 100 units above everything else in the scene and has a z value of 8, change this later
                Gizmos.DrawSphere(hc.Position + new Vector3(0, 100, 8), 0.1f);
            }

            // Mark all nodes in the path
            Gizmos.color = Color.blue;
            foreach (var hc in path)
            {
                //Debug.Log("path" + hc.Position);
                Gizmos.DrawSphere(hc.Position + new Vector3(0, 100, 8), 0.1f);
            }

            // Mark the selected (clicked) hex
            if (selectedHex != null)
            {
                Gizmos.color = Color.red;
                Gizmos.DrawSphere(selectedHex.Position + new Vector3(0, 100f, 8), 0.1f);
            }
        }
    }
}

На данный момент путь всегда будет исходить из источника мозаичного изображения (в нижнем левом углу) и заканчиваться там, где пользователь щелкает. В некоторых простых случаях путь выглядит правильным: enter image description here

Тем не менее, выбор более диагонального гексагона открывает субоптимальный путь: enter image description here

Я попытался полностью поменять алгоритм поиска пути на другой и получил те же самые результаты, так что в этот момент я думаю, что все еще есть ошибка в том, как определяются смежности для каждого гекса. Любое понимание того, почему был создан неправильный путь, будет оценено.

Редактировать: я реорганизовал практически весь этот код, чтобы найти смежность на основе индекса с момента первой публикации, и теперь путь улучшен, так как он не больше пропускает гексы, но все равно длиннее оптимального и поэтому неверно. По крайней мере, теперь я вполне уверен, что нет логики c, основанной на квадрате, которая может мешать.

1 Ответ

0 голосов
/ 23 февраля 2020

Нашел решение моей проблемы. Логика смежности c в каждом гексе была не совсем правильной, как я и подозревал. Для нахождения правильных смежных индексов требуется смещение в каждой строке из-за расположения шестиугольной сетки.

...