using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; namespace BulletHellTemplate { /// /// Provides A* pathfinding on a 2D grid, using a Tilemap to mark blocked cells. /// public class Pathfinding2D : MonoBehaviour { [Tooltip("Tilemap whose tiles represent obstacles (blocked cells)")] public Tilemap obstacleTilemap; private Node[,] grid; private int gridWidth; private int gridHeight; private Vector3Int origin; private void Awake() { InitializeGrid(); } /// /// Builds the grid of nodes based on the obstacleTilemap bounds. /// private void InitializeGrid() { BoundsInt bounds = obstacleTilemap.cellBounds; origin = bounds.min; gridWidth = bounds.size.x; gridHeight = bounds.size.y; grid = new Node[gridWidth, gridHeight]; for (int x = 0; x < gridWidth; x++) { for (int y = 0; y < gridHeight; y++) { Vector3Int cell = new Vector3Int(origin.x + x, origin.y + y, origin.z); bool walkable = !obstacleTilemap.HasTile(cell); Vector3 worldPos = obstacleTilemap.CellToWorld(cell) + obstacleTilemap.cellSize * 0.5f; grid[x, y] = new Node(walkable, worldPos, x, y); } } } /// /// Finds a path from start to end world positions. Returns a list of world-space waypoints. /// public List FindPath(Vector3 startWorld, Vector3 endWorld) { Node startNode = NodeFromWorldPoint(startWorld); Node targetNode = NodeFromWorldPoint(endWorld); var openSet = new List(); var closedSet = new HashSet(); openSet.Add(startNode); while (openSet.Count > 0) { Node current = openSet[0]; for (int i = 1; i < openSet.Count; i++) { if (openSet[i].FCost < current.FCost || (openSet[i].FCost == current.FCost && openSet[i].HCost < current.HCost)) current = openSet[i]; } openSet.Remove(current); closedSet.Add(current); if (current == targetNode) return RetracePath(startNode, targetNode); foreach (Node neighbor in GetNeighbors(current)) { if (!neighbor.walkable || closedSet.Contains(neighbor)) continue; int newCost = current.GCost + GetDistance(current, neighbor); if (newCost < neighbor.GCost || !openSet.Contains(neighbor)) { neighbor.GCost = newCost; neighbor.HCost = GetDistance(neighbor, targetNode); neighbor.parent = current; if (!openSet.Contains(neighbor)) openSet.Add(neighbor); } } } return null; // no path found } private List RetracePath(Node start, Node end) { var path = new List(); Node current = end; while (current != start) { path.Add(current.worldPosition); current = current.parent; } path.Reverse(); return path; } private Node NodeFromWorldPoint(Vector3 worldPos) { Vector3Int cell = obstacleTilemap.WorldToCell(worldPos); int x = cell.x - origin.x; int y = cell.y - origin.y; x = Mathf.Clamp(x, 0, gridWidth - 1); y = Mathf.Clamp(y, 0, gridHeight - 1); return grid[x, y]; } private List GetNeighbors(Node node) { var neighbors = new List(); for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { if (dx == 0 && dy == 0) continue; // allow 4-way only if (dx != 0 && dy != 0) continue; int checkX = node.gridX + dx; int checkY = node.gridY + dy; if (checkX >= 0 && checkX < gridWidth && checkY >= 0 && checkY < gridHeight) neighbors.Add(grid[checkX, checkY]); } } return neighbors; } private int GetDistance(Node a, Node b) { int dstX = Mathf.Abs(a.gridX - b.gridX); int dstY = Mathf.Abs(a.gridY - b.gridY); return dstX + dstY; } private class Node { public bool walkable; public Vector3 worldPosition; public int GCost, HCost; public Node parent; public int gridX, gridY; public int FCost => GCost + HCost; public Node(bool walkable, Vector3 worldPos, int x, int y) { this.walkable = walkable; worldPosition = worldPos; gridX = x; gridY = y; } } } }