using PathFinding; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using UnityEngine; public struct PathfindingMetrics { // Untuk Pengukuran Kinerja public float timeTaken; // in milliseconds public int pathLength; public int nodesExplored; // number of nodes in path public long memoryUsed; // memory used by pathfinding in bytes // Untuk Visualisasi public int maxOpenListSize; // maximum size of open list during pathfinding public int maxClosedListSize; // maximum size of closed list during pathfinding // Tambahan untuk cost metrics public float totalGCost; // Total biaya G untuk jalur (jarak sebenarnya) public float totalHCost; // Total biaya H untuk jalur (heuristik) public float totalFCost; // Total biaya F untuk jalur (G + H) } /// /// NPC adalah komponen utama yang mengelola pergerakan NPC dalam sistem pathfinding. /// Kelas ini bertanggung jawab untuk membuat, menampilkan, dan mengelola jalur untuk NPC. /// public class NPC : MonoBehaviour { public float speed = 2.0f; public Queue wayPoints = new Queue(); // Event that fires when pathfinding is complete with performance metrics public event Action OnPathfindingComplete; // Last measured memory usage (for accessing from outside) public long LastMeasuredMemoryUsage { get; private set; } = 0; /// /// Enumerasi yang merepresentasikan berbagai algoritma pathfinding yang tersedia. /// public enum PathFinderType { ASTAR, DIJKSTRA, GREEDY, BACKTRACKING, BFS, } [SerializeField] public PathFinderType pathFinderType = PathFinderType.ASTAR; public PathFinder pathFinder = null; public GridMap Map { get; set; } // List to store all steps for visualization playback private List visualizationSteps = new List(); private bool isVisualizingPath = false; private bool isMoving = false; // Public accessor for visualization state public bool IsVisualizingPath => isVisualizingPath; // Public accessor for movement state public bool IsMoving => isMoving; // Event that fires when visualization is complete public event Action OnVisualizationComplete; // Event that fires when movement is complete public event Action OnMovementComplete; // Properties to control visualization [SerializeField] // Visualization speed is time between visualization steps public float visualizationSpeed = 0.0f; // Default 0; set higher for slower visualization // Visualization batch is the number of steps to visualize at once public int visualizationBatch = 1; // Default 1; set higher value for faster visualization [SerializeField] public bool showVisualization = true; // Whether to show visualization at all // Struct to store each step of the pathfinding process for visualization private struct PathfindingVisualizationStep { public enum StepType { CurrentNode, OpenList, ClosedList, FinalPath } public StepType type; public Vector2Int position; public PathfindingVisualizationStep(StepType type, Vector2Int position) { this.type = type; this.position = position; } } private IEnumerator Coroutine_MoveOverSeconds(GameObject objectToMove, Vector3 end, float seconds) { float elaspedTime = 0.0f; Vector3 startingPos = objectToMove.transform.position; while (elaspedTime < seconds) { objectToMove.transform.position = Vector3.Lerp(startingPos, end, elaspedTime / seconds); elaspedTime += Time.deltaTime; yield return new WaitForEndOfFrame(); } objectToMove.transform.position = end; } IEnumerator Coroutine_MoveToPoint(Vector2 p, float speed) { Vector3 endP = new Vector3(p.x, p.y, transform.position.z); float duration = (transform.position - endP).magnitude / speed; yield return StartCoroutine( Coroutine_MoveOverSeconds( transform.gameObject, endP, duration)); } public IEnumerator Coroutine_MoveTo() { while (true) { while (wayPoints.Count > 0) { // Set the moving flag when starting to move if (!isMoving && wayPoints.Count > 0) { isMoving = true; UnityEngine.Debug.Log("NPC movement started"); } yield return StartCoroutine( Coroutine_MoveToPoint( wayPoints.Dequeue(), speed)); } // If we were moving but now have no more waypoints, signal movement completion if (isMoving && wayPoints.Count == 0) { isMoving = false; UnityEngine.Debug.Log("NPC movement complete, invoking OnMovementComplete event"); OnMovementComplete?.Invoke(); } yield return null; } } private void AddWayPoint(GridNode node) { wayPoints.Enqueue(new Vector2( node.Value.x * Map.GridNodeWidth, node.Value.y * Map.GridNodeHeight)); // We set a color to show the path. GridNodeView gnv = Map.GetGridNodeView(node.Value.x, node.Value.y); gnv.SetInnerColor(Map.COLOR_PATH); } public void SetStartNode(GridNode node) { wayPoints.Clear(); transform.position = new Vector3( node.Value.x * Map.GridNodeWidth, node.Value.y * Map.GridNodeHeight, transform.position.z); } private void Start() { // Initialize pathfinder based on type InitializePathFinder(); // Start the movement coroutine StartCoroutine(Coroutine_MoveTo()); } private void InitializePathFinder() { // Hitung perkiraan jumlah node dalam grid int estimatedNodeCount = 0; if (Map != null) { estimatedNodeCount = Map.NumX * Map.NumY; } // Log informasi ukuran grid dan strategi optimisasi bool isLargeGrid = estimatedNodeCount > 2500; // Create new pathfinder instance switch (pathFinderType) { case PathFinderType.ASTAR: pathFinder = new AStarPathFinder(estimatedNodeCount); break; case PathFinderType.DIJKSTRA: pathFinder = new DijkstraPathFinder(estimatedNodeCount); break; case PathFinderType.GREEDY: pathFinder = new GreedyPathFinder(); break; case PathFinderType.BACKTRACKING: pathFinder = new BacktrackingPathFinder(); break; case PathFinderType.BFS: pathFinder = new BFSPathFinder(); break; } // Set up callbacks pathFinder.onSuccess = OnSuccessPathFinding; pathFinder.onFailure = OnFailurePathFinding; // Gunakan setting asli pathFinder.HeuristicCost = GridMap.GetManhattanCost; pathFinder.NodeTraversalCost = GridMap.GetEuclideanCost; } public void MoveTo(GridNode destination, bool silentMode = false) { // inialisaasi pathfinder jika belum ada if (pathFinder == null) { InitializePathFinder(); } if (pathFinder.Status == PathFinderStatus.RUNNING) { return; } GridNode start = Map.GetGridNode( (int)(transform.position.x / Map.GridNodeWidth), (int)(transform.position.y / Map.GridNodeHeight)); if (start == null || destination == null) { return; } SetStartNode(start); // Reset grid colors if (!silentMode) { Map.ResetGridNodeColours(); } visualizationSteps.Clear(); isVisualizingPath = false; // jika gagal menginisialisasi pathfinder, tidak perlu melanjutkan if (!pathFinder.Initialise(start, destination)) { return; } StartCoroutine(Coroutine_FindPathStep(silentMode)); } IEnumerator Coroutine_FindPathStep(bool silentMode = false) { yield return StartCoroutine(MeasurePerformance(silentMode)); // Start visualization after calculation is complete regardless of success or failure // This allows visualization of the explored area even when no path is found if (showVisualization && !silentMode && (pathFinder.Status == PathFinderStatus.SUCCESS || pathFinder.Status == PathFinderStatus.FAILURE)) { yield return StartCoroutine(VisualizePathfinding()); } } IEnumerator MeasurePerformance(bool silentMode = false) { // Memory tracking for pathfinding structures - tetap untuk visualisasi int maxOpenListSize = 0; int currentOpenListSize = 0; int maxClosedListSize = 0; int currentClosedListSize = 0; // Pre-allocate visualizationSteps with estimated capacity to avoid reallocations visualizationSteps = new List(4); //GC.Collect(2, GCCollectionMode.Forced, true, true); //GC.WaitForPendingFinalizers(); // Tunggu semua finalizers selesai // ===== MEMORY MEASUREMENT START: Ukur memory sebelum algoritma ===== long memoryBefore = System.GC.GetTotalMemory(false); // Setup callbacks before running algorithm SetupCallbacks(silentMode, ref maxOpenListSize, ref currentOpenListSize, ref maxClosedListSize, ref currentClosedListSize); // ===== STOPWATCH START: Pengukuran waktu algoritma ===== Stopwatch algorithmTimer = Stopwatch.StartNew(); // Counter untuk jumlah step yang dilakukan algoritma int stepCount = 0; // Execute the pathfinding algorithm synchronously in a single frame without visualization while (pathFinder.Status == PathFinderStatus.RUNNING) { stepCount++; pathFinder.Step(); } // ===== STOPWATCH STOP: Akhir pengukuran waktu algoritma ===== algorithmTimer.Stop(); // ===== MEMORY MEASUREMENT END: Ukur memory setelah algoritma ===== long memoryAfter = System.GC.GetTotalMemory(false); long memoryUsed = memoryAfter - memoryBefore; // Store the memory usage for external access LastMeasuredMemoryUsage = memoryUsed > 0 ? memoryUsed : 1024; float milliseconds = (algorithmTimer.ElapsedTicks * 1000.0f) / Stopwatch.Frequency; // Calculate path length once and reuse int pathLength = 0; int nodesExplored = 0; float totalGCost = 0; float totalHCost = 0; float totalFCost = 0; // Add memory for path reconstruction (final path) if (pathFinder.Status == PathFinderStatus.SUCCESS) { pathLength = CalculatePathLength(); nodesExplored = pathFinder.ClosedListCount; // Hitung total G, H, dan F cost CalculatePathCosts(out totalGCost, out totalHCost, out totalFCost); } // Create and send metrics - waktu pengukuran algoritma yang tepat PathfindingMetrics metrics = new PathfindingMetrics { timeTaken = milliseconds, // Waktu algoritma yang diukur dengan stopwatch pathLength = pathLength, nodesExplored = nodesExplored, memoryUsed = memoryUsed, maxOpenListSize = maxOpenListSize, maxClosedListSize = maxClosedListSize, totalGCost = totalGCost, totalHCost = totalHCost, totalFCost = totalFCost, }; // *** IMPORTANT FIX: Always invoke the event, regardless of silent mode *** // Report metrics before visualization OnPathfindingComplete?.Invoke(metrics); // Path visualization and handling HandlePathFindingResult(silentMode, pathLength); // Pastikan untuk mengembalikan nilai di akhir coroutine yield return null; } /// /// Setup callbacks for tracking nodes in open/closed lists and visualization /// private void SetupCallbacks(bool silentMode, ref int maxOpenListSize, ref int currentOpenListSize, ref int maxClosedListSize, ref int currentClosedListSize) { // Buat variabel lokal untuk menghindari masalah dengan ref parameter dalam lambda int localCurrentOpenListSize = currentOpenListSize; int localMaxOpenListSize = maxOpenListSize; int localCurrentClosedListSize = currentClosedListSize; int localMaxClosedListSize = maxClosedListSize; if (silentMode) { // In silent mode, just set minimal callbacks for metrics pathFinder.onAddToOpenList = (node) => { localCurrentOpenListSize++; if (localCurrentOpenListSize > localMaxOpenListSize) localMaxOpenListSize = localCurrentOpenListSize; }; pathFinder.onAddToClosedList = (node) => { localCurrentClosedListSize++; if (localCurrentClosedListSize > localMaxClosedListSize) localMaxClosedListSize = localCurrentClosedListSize; localCurrentOpenListSize--; // When a node is moved from open to closed list }; } else { // In regular mode, track and prepare for visualization pathFinder.onAddToOpenList = (node) => { visualizationSteps.Add(new PathfindingVisualizationStep( PathfindingVisualizationStep.StepType.OpenList, node.Location.Value)); localCurrentOpenListSize++; if (localCurrentOpenListSize > localMaxOpenListSize) localMaxOpenListSize = localCurrentOpenListSize; }; pathFinder.onAddToClosedList = (node) => { visualizationSteps.Add(new PathfindingVisualizationStep( PathfindingVisualizationStep.StepType.ClosedList, node.Location.Value)); localCurrentClosedListSize++; if (localCurrentClosedListSize > localMaxClosedListSize) localMaxClosedListSize = localCurrentClosedListSize; localCurrentOpenListSize--; // When a node is moved from open to closed list }; pathFinder.onChangeCurrentNode = (node) => { visualizationSteps.Add(new PathfindingVisualizationStep( PathfindingVisualizationStep.StepType.CurrentNode, node.Location.Value)); }; } // Setelah lambda selesai dijalankan, perbarui variabel ref maxOpenListSize = localMaxOpenListSize; currentOpenListSize = localCurrentOpenListSize; maxClosedListSize = localMaxClosedListSize; currentClosedListSize = localCurrentClosedListSize; } /// /// Handle path finding result (success or failure) /// private void HandlePathFindingResult(bool silentMode, int pathLength) { if (pathFinder.Status == PathFinderStatus.SUCCESS) { OnSuccessPathFinding(); // In non-silent mode, prepare visualization data for the path if (!silentMode && showVisualization) { // Add the path nodes for visualization in efficient batched way PathFinder.PathFinderNode node = pathFinder.CurrentNode; List pathPositions = new List(pathLength); // Pre-allocate with known size // Build path in reverse order while (node != null) { pathPositions.Add(node.Location.Value); node = node.Parent; } // Process path in correct order for (int i = pathPositions.Count - 1; i >= 0; i--) { visualizationSteps.Add(new PathfindingVisualizationStep( PathfindingVisualizationStep.StepType.FinalPath, pathPositions[i])); } } } else if (pathFinder.Status == PathFinderStatus.FAILURE) { OnFailurePathFinding(); // For failure case, we don't add any final path visualization steps // The exploration steps (open/closed lists) are already added during the search // and will be visualized to show what nodes were explored before failure UnityEngine.Debug.Log($"Pathfinding failed - visualization will show {visualizationSteps.Count} exploration steps"); } } /// /// Memformat ukuran byte menjadi string yang lebih mudah dibaca /// private string FormatBytes(long bytes) { string[] sizes = { "B", "KB", "MB", "GB" }; int order = 0; double size = bytes; while (size >= 1024 && order < sizes.Length - 1) { order++; size = size / 1024; } return $"{size:0.##} {sizes[order]}"; } void OnSuccessPathFinding() { float totalGCost = 0; float totalHCost = 0; float totalFCost = 0; // Hitung biaya-biaya path menggunakan metode yang sudah ada CalculatePathCosts(out totalGCost, out totalHCost, out totalFCost); // Informasi dasar int pathLength = CalculatePathLength(); } void OnFailurePathFinding() { UnityEngine.Debug.Log("Pathfinding failed"); } /// /// Changes the pathfinding algorithm at runtime /// public void ChangeAlgorithm(PathFinderType newType) { // Don't change if pathfinding is in progress if (pathFinder != null && pathFinder.Status == PathFinderStatus.RUNNING) { UnityEngine.Debug.Log("Cannot change algorithm while pathfinding is running"); return; } pathFinderType = newType; // Hitung perkiraan jumlah node dalam grid int estimatedNodeCount = 0; if (Map != null) { estimatedNodeCount = Map.NumX * Map.NumY; } // Create new pathfinder instance switch (pathFinderType) { case PathFinderType.ASTAR: pathFinder = new AStarPathFinder(estimatedNodeCount); break; case PathFinderType.DIJKSTRA: pathFinder = new DijkstraPathFinder(estimatedNodeCount); break; case PathFinderType.GREEDY: pathFinder = new GreedyPathFinder(); break; case PathFinderType.BACKTRACKING: pathFinder = new BacktrackingPathFinder(); break; case PathFinderType.BFS: pathFinder = new BFSPathFinder(); break; } // Set up callbacks pathFinder.onSuccess = OnSuccessPathFinding; pathFinder.onFailure = OnFailurePathFinding; // Gunakan setting asli pathFinder.HeuristicCost = GridMap.GetManhattanCost; pathFinder.NodeTraversalCost = GridMap.GetEuclideanCost; } private int CalculatePathLength() { int pathLength = 0; PathFinder.PathFinderNode node = pathFinder.CurrentNode; while (node != null) { pathLength++; node = node.Parent; } return pathLength; } IEnumerator VisualizePathfinding() { if (!showVisualization) yield break; UnityEngine.Debug.Log("Path visualization starting"); isVisualizingPath = true; // First, ensure grid is reset Map.ResetGridNodeColours(); // Visualize each step with a delay - use batch processing for efficiency int stepCount = visualizationSteps.Count; int batchSize = Mathf.Min(visualizationBatch, stepCount); // set higher value for faster visualization // Detect if pathfinding failed - we'll need to know this when processing steps bool pathfindingFailed = pathFinder.Status == PathFinderStatus.FAILURE; if (pathfindingFailed) { UnityEngine.Debug.Log($"Visualizing failed pathfinding attempt with {stepCount} steps"); } for (int i = 0; i < stepCount; i += batchSize) { int end = Mathf.Min(i + batchSize, stepCount); // Process a batch of steps for (int j = i; j < end; j++) { var step = visualizationSteps[j]; GridNodeView gnv = Map.GetGridNodeView(step.position.x, step.position.y); if (gnv != null) { switch (step.type) { case PathfindingVisualizationStep.StepType.CurrentNode: gnv.SetInnerColor(Map.COLOR_CURRENT_NODE); break; case PathfindingVisualizationStep.StepType.OpenList: gnv.SetInnerColor(Map.COLOR_ADD_TO_OPENLIST); break; case PathfindingVisualizationStep.StepType.ClosedList: gnv.SetInnerColor(Map.COLOR_ADD_TO_CLOSEDLIST); break; case PathfindingVisualizationStep.StepType.FinalPath: gnv.SetInnerColor(Map.COLOR_PATH); // Only add waypoints for successful pathfinding if (!pathfindingFailed) { GridNode pathNode = Map.GetGridNode(step.position.x, step.position.y); AddWayPoint(pathNode); } break; } } } // Yield after each batch to prevent frame drops yield return new WaitForSeconds(visualizationSpeed); } isVisualizingPath = false; UnityEngine.Debug.Log("Path visualization complete, invoking OnVisualizationComplete event"); // Notify any listeners that visualization is complete OnVisualizationComplete?.Invoke(); } /// /// Menghitung biaya G, H, dan F untuk jalur /// private void CalculatePathCosts(out float totalGCost, out float totalHCost, out float totalFCost) { // Inisialisasi nilai awal totalGCost = 0; totalHCost = 0; totalFCost = 0; // Jika tidak ada path yang ditemukan, return nilai 0 if (pathFinder.CurrentNode == null) return; // Untuk algoritma yang menggunakan heuristik bool usesHeuristic = pathFinderType == PathFinderType.ASTAR || pathFinderType == PathFinderType.GREEDY; // Node final berisi total cost jalur PathFinder.PathFinderNode finalNode = pathFinder.CurrentNode; // G cost adalah biaya sebenarnya dari start ke goal, sudah terakumulasi di node akhir totalGCost = finalNode.GCost; // H cost di node final idealnya 0 (sudah di tujuan), // tapi untuk info lengkap, kita dapat path's H cost dari node awal if (usesHeuristic) { // H cost dari node awal ke tujuan (untuk referensi) totalHCost = finalNode.HCost; // F cost adalah G + H di node akhir totalFCost = finalNode.FCost; } else { // Algoritma tanpa heuristik (seperti Dijkstra) totalFCost = totalGCost; } //// Hitung rata-rata biaya per langkah untuk analisis //int pathLength = CalculatePathLength(); //float avgCostPerStep = pathLength > 0 ? totalGCost / pathLength : 0; } }