using PathFinding;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
///
/// Metrik yang digunakan untuk mengukur performa pathfinding.
/// Dibuat dalam bentuk struct untuk efisiensi memori dan kemudahan penggunaan.
///
public struct PathfindingMetrics
{
public float timeTaken; // miliseconds
public int pathLength;
public int nodesExplored;
public long memoryUsed; // bytes
public int maxOpenListSize;
public int maxClosedListSize;
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();
public event Action OnPathfindingComplete;
public long LastMeasuredMemoryUsage { get; private set; } = 0;
public enum PathFinderType
{
ASTAR,
DIJKSTRA,
GREEDY,
BACKTRACKING,
BFS,
}
[SerializeField]
public PathFinderType pathFinderType = PathFinderType.ASTAR;
public PathFinder pathFinder = null;
public GridMap Map { get; set; }
private List visualizationSteps = new List();
private bool isVisualizingPath = false;
private bool isMoving = false;
public bool IsVisualizingPath => isVisualizingPath;
public bool IsMoving => isMoving;
public event Action OnVisualizationComplete;
public event Action OnMovementComplete;
public float visualizationSpeed = 0.0f;
public int visualizationBatch = 1;
public bool showVisualization = true;
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)
{
if (!isMoving && wayPoints.Count > 0)
{
isMoving = true;
UnityEngine.Debug.Log("NPC movement started");
}
yield return StartCoroutine(
Coroutine_MoveToPoint(
wayPoints.Dequeue(),
speed));
}
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));
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()
{
InitializePathFinder();
StartCoroutine(Coroutine_MoveTo());
}
private void InitializePathFinder()
{
int estimatedNodeCount = 0;
if (Map != null)
{
estimatedNodeCount = Map.NumX * Map.NumY;
}
bool isLargeGrid = estimatedNodeCount > 2500;
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;
}
pathFinder.onSuccess = OnSuccessPathFinding;
pathFinder.onFailure = OnFailurePathFinding;
pathFinder.HeuristicCost = GridMap.GetManhattanCost;
pathFinder.NodeTraversalCost = GridMap.GetEuclideanCost;
}
public void MoveTo(GridNode destination, bool silentMode = false)
{
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);
if (!silentMode)
{
Map.ResetGridNodeColours();
}
visualizationSteps.Clear();
isVisualizingPath = false;
if (!pathFinder.Initialise(start, destination))
{
return;
}
StartCoroutine(Coroutine_FindPathStep(silentMode));
}
IEnumerator Coroutine_FindPathStep(bool silentMode = false)
{
yield return StartCoroutine(MeasurePerformance(silentMode));
if (showVisualization && !silentMode &&
(pathFinder.Status == PathFinderStatus.SUCCESS || pathFinder.Status == PathFinderStatus.FAILURE))
{
yield return StartCoroutine(VisualizePathfinding());
}
}
IEnumerator MeasurePerformance(bool silentMode = false)
{
int maxOpenListSize = 0;
int currentOpenListSize = 0;
int maxClosedListSize = 0;
int currentClosedListSize = 0;
visualizationSteps = new List(4);
long memoryBefore = System.GC.GetTotalMemory(false);
SetupCallbacks(silentMode, ref maxOpenListSize, ref currentOpenListSize,
ref maxClosedListSize, ref currentClosedListSize);
Stopwatch algorithmTimer = Stopwatch.StartNew();
int stepCount = 0;
while (pathFinder.Status == PathFinderStatus.RUNNING)
{
stepCount++;
pathFinder.Step();
}
algorithmTimer.Stop();
long memoryAfter = System.GC.GetTotalMemory(false);
long memoryUsed = memoryAfter - memoryBefore;
LastMeasuredMemoryUsage = memoryUsed > 0 ? memoryUsed : 1024;
float milliseconds = (algorithmTimer.ElapsedTicks * 1000.0f) / Stopwatch.Frequency;
int pathLength = 0;
int nodesExplored = 0;
float totalGCost = 0;
float totalHCost = 0;
float totalFCost = 0;
if (pathFinder.Status == PathFinderStatus.SUCCESS)
{
pathLength = CalculatePathLength();
nodesExplored = pathFinder.ClosedListCount;
CalculatePathCosts(out totalGCost, out totalHCost, out totalFCost);
}
PathfindingMetrics metrics = new PathfindingMetrics
{
timeTaken = milliseconds,
pathLength = pathLength,
nodesExplored = nodesExplored,
memoryUsed = memoryUsed,
maxOpenListSize = maxOpenListSize,
maxClosedListSize = maxClosedListSize,
totalGCost = totalGCost,
totalHCost = totalHCost,
totalFCost = totalFCost,
};
OnPathfindingComplete?.Invoke(metrics);
HandlePathFindingResult(silentMode, pathLength);
yield return null;
}
private void SetupCallbacks(
bool silentMode,
ref int maxOpenListSize,
ref int currentOpenListSize,
ref int maxClosedListSize,
ref int currentClosedListSize)
{
int localCurrentOpenListSize = currentOpenListSize;
int localMaxOpenListSize = maxOpenListSize;
int localCurrentClosedListSize = currentClosedListSize;
int localMaxClosedListSize = maxClosedListSize;
if (silentMode)
{
pathFinder.onAddToOpenList = (node) =>
{
localCurrentOpenListSize++;
if (localCurrentOpenListSize > localMaxOpenListSize)
localMaxOpenListSize = localCurrentOpenListSize;
};
pathFinder.onAddToClosedList = (node) =>
{
localCurrentClosedListSize++;
if (localCurrentClosedListSize > localMaxClosedListSize)
localMaxClosedListSize = localCurrentClosedListSize;
localCurrentOpenListSize--;
};
}
else
{
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--;
};
pathFinder.onChangeCurrentNode = (node) =>
{
visualizationSteps.Add(new PathfindingVisualizationStep(
PathfindingVisualizationStep.StepType.CurrentNode,
node.Location.Value));
};
}
maxOpenListSize = localMaxOpenListSize;
currentOpenListSize = localCurrentOpenListSize;
maxClosedListSize = localMaxClosedListSize;
currentClosedListSize = localCurrentClosedListSize;
}
private void HandlePathFindingResult(bool silentMode, int pathLength)
{
if (pathFinder.Status == PathFinderStatus.SUCCESS)
{
OnSuccessPathFinding();
if (!silentMode && showVisualization)
{
PathFinder.PathFinderNode node = pathFinder.CurrentNode;
List pathPositions = new List(pathLength);
while (node != null)
{
pathPositions.Add(node.Location.Value);
node = node.Parent;
}
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();
UnityEngine.Debug.Log($"Pathfinding failed - visualization will show {visualizationSteps.Count} exploration steps");
}
}
void OnSuccessPathFinding()
{
float totalGCost = 0;
float totalHCost = 0;
float totalFCost = 0;
CalculatePathCosts(out totalGCost, out totalHCost, out totalFCost);
int pathLength = CalculatePathLength();
}
void OnFailurePathFinding()
{
UnityEngine.Debug.Log("Pathfinding failed");
}
public void ChangeAlgorithm(PathFinderType newType)
{
if (pathFinder != null && pathFinder.Status == PathFinderStatus.RUNNING)
{
UnityEngine.Debug.Log("Cannot change algorithm while pathfinding is running");
return;
}
pathFinderType = newType;
int estimatedNodeCount = 0;
if (Map != null)
{
estimatedNodeCount = Map.NumX * Map.NumY;
}
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;
}
pathFinder.onSuccess = OnSuccessPathFinding;
pathFinder.onFailure = OnFailurePathFinding;
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;
Map.ResetGridNodeColours();
int stepCount = visualizationSteps.Count;
int batchSize = Mathf.Min(visualizationBatch, stepCount);
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);
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);
if (!pathfindingFailed)
{
GridNode pathNode = Map.GetGridNode(step.position.x, step.position.y);
AddWayPoint(pathNode);
}
break;
}
}
}
yield return new WaitForSeconds(visualizationSpeed);
}
isVisualizingPath = false;
UnityEngine.Debug.Log("Path visualization complete, invoking OnVisualizationComplete event");
OnVisualizationComplete?.Invoke();
}
private void CalculatePathCosts(out float totalGCost, out float totalHCost, out float totalFCost)
{
totalGCost = 0;
totalHCost = 0;
totalFCost = 0;
if (pathFinder.CurrentNode == null)
return;
bool usesHeuristic = pathFinderType == PathFinderType.ASTAR ||
pathFinderType == PathFinderType.GREEDY;
PathFinder.PathFinderNode finalNode = pathFinder.CurrentNode;
totalGCost = finalNode.GCost;
if (usesHeuristic)
{
totalHCost = finalNode.HCost;
totalFCost = finalNode.FCost;
}
else
{
totalFCost = totalGCost;
}
}
}