mirror of
https://github.com/BobbyRafael31/Unity-MazeRunner-Pathfinding-Visualizer.git
synced 2025-08-13 08:52:21 +00:00
510 lines
18 KiB
C#
510 lines
18 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
|
|
public class PathfindingTester : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
public GridMap gridMap;
|
|
public NPC npc;
|
|
public Button startTestButton;
|
|
public TMP_Text statusText;
|
|
public Slider progressBar;
|
|
|
|
[Header("Test Configuration")]
|
|
[Tooltip("Number of times to run each test combination")]
|
|
public int testsPerCombination = 3;
|
|
|
|
[Tooltip("Delay between tests in seconds")]
|
|
public float delayBetweenTests = 0.5f;
|
|
|
|
[Tooltip("Whether to save results to a CSV file")]
|
|
public bool saveResultsToFile = true;
|
|
|
|
[Tooltip("File name for test results (CSV)")]
|
|
public string resultsFileName = "pathfinding_tests.csv";
|
|
|
|
// Test matrix parameters
|
|
private NPC.PathFinderType[] algorithmsToTest = new NPC.PathFinderType[] {
|
|
NPC.PathFinderType.ASTAR,
|
|
NPC.PathFinderType.DIJKSTRA,
|
|
NPC.PathFinderType.GREEDY,
|
|
NPC.PathFinderType.BACKTRACKING,
|
|
NPC.PathFinderType.BFS
|
|
};
|
|
|
|
private Vector2Int[] gridSizesToTest = new Vector2Int[] {
|
|
new Vector2Int(20, 20),
|
|
new Vector2Int(35, 35),
|
|
new Vector2Int(50, 50),
|
|
new Vector2Int(40, 25) // Another non-square grid
|
|
};
|
|
|
|
private float[] mazeDensitiesToTest = new float[] {
|
|
0f, // Empty (no walls)
|
|
10f, // Very low
|
|
30f, // Medium
|
|
50f, // High
|
|
100f // Fully blocked (all walls)
|
|
};
|
|
|
|
private bool[] diagonalMovementOptions = new bool[] {
|
|
true,
|
|
false
|
|
};
|
|
|
|
// Test state tracking
|
|
private bool isTestingRunning = false;
|
|
private int totalTests;
|
|
private int completedTests;
|
|
private List<PathfindingTestResult> testResults = new List<PathfindingTestResult>();
|
|
|
|
// Current test parameters
|
|
private NPC.PathFinderType currentTestAlgorithm;
|
|
private Vector2Int currentTestGridSize;
|
|
private float currentTestDensity;
|
|
private bool currentTestDiagonal;
|
|
private int currentTestIndex;
|
|
|
|
// Structure to store test results
|
|
private struct PathfindingTestResult
|
|
{
|
|
public NPC.PathFinderType algorithm;
|
|
public Vector2Int gridSize;
|
|
public float mazeDensity;
|
|
public bool diagonalMovement;
|
|
public float timeTaken;
|
|
public int pathLength;
|
|
public int nodesExplored;
|
|
public long memoryUsed;
|
|
public bool pathFound;
|
|
public int testIndex;
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
// Subscribe to NPC's pathfinding completion event
|
|
npc.OnPathfindingComplete += OnPathfindingComplete;
|
|
|
|
// Set up the button listener
|
|
startTestButton.onClick.AddListener(StartTesting);
|
|
|
|
// Initial status message
|
|
UpdateStatus("Ready to start testing. Click the Start Tests button.");
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
// Unsubscribe from events
|
|
if (npc != null)
|
|
{
|
|
npc.OnPathfindingComplete -= OnPathfindingComplete;
|
|
}
|
|
}
|
|
|
|
private void UpdateStatus(string message)
|
|
{
|
|
if (statusText != null)
|
|
{
|
|
statusText.text = message;
|
|
}
|
|
Debug.Log(message);
|
|
|
|
// Update progress bar if available
|
|
if (progressBar != null && totalTests > 0)
|
|
{
|
|
progressBar.value = (float)completedTests / totalTests;
|
|
}
|
|
}
|
|
|
|
public void StartTesting()
|
|
{
|
|
if (isTestingRunning)
|
|
{
|
|
Debug.LogWarning("Tests are already running.");
|
|
return;
|
|
}
|
|
|
|
// Clear previous results
|
|
testResults.Clear();
|
|
|
|
// Calculate total number of tests
|
|
totalTests = algorithmsToTest.Length *
|
|
gridSizesToTest.Length *
|
|
mazeDensitiesToTest.Length *
|
|
diagonalMovementOptions.Length *
|
|
testsPerCombination;
|
|
|
|
completedTests = 0;
|
|
isTestingRunning = true;
|
|
|
|
UpdateStatus($"Starting {totalTests} tests...");
|
|
|
|
// Disable button during testing
|
|
startTestButton.interactable = false;
|
|
|
|
// Start the test coroutine
|
|
StartCoroutine(RunTestMatrix());
|
|
}
|
|
|
|
private IEnumerator RunTestMatrix()
|
|
{
|
|
// Iterate through all test combinations
|
|
foreach (var algorithm in algorithmsToTest)
|
|
{
|
|
foreach (var gridSize in gridSizesToTest)
|
|
{
|
|
foreach (var density in mazeDensitiesToTest)
|
|
{
|
|
foreach (var useDiagonals in diagonalMovementOptions)
|
|
{
|
|
for (int testIndex = 0; testIndex < testsPerCombination; testIndex++)
|
|
{
|
|
yield return StartCoroutine(RunSingleTest(algorithm, gridSize, density, useDiagonals, testIndex));
|
|
|
|
// Wait between tests
|
|
yield return new WaitForSeconds(delayBetweenTests);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// All tests completed
|
|
isTestingRunning = false;
|
|
startTestButton.interactable = true;
|
|
|
|
// Save results if enabled
|
|
if (saveResultsToFile)
|
|
{
|
|
SaveResultsToCSV();
|
|
}
|
|
|
|
UpdateStatus($"Testing complete! {completedTests} tests run. Results saved to {resultsFileName}");
|
|
}
|
|
|
|
private IEnumerator RunSingleTest(NPC.PathFinderType algorithm, Vector2Int gridSize, float density, bool useDiagonals, int testIndex)
|
|
{
|
|
// Update status with current test info
|
|
UpdateStatus($"Test {completedTests+1}/{totalTests}: {algorithm} - Grid: {gridSize.x}x{gridSize.y} - Density: {density}% - Diagonals: {useDiagonals}");
|
|
|
|
// Configure the test environment
|
|
yield return StartCoroutine(SetupTestEnvironment(algorithm, gridSize, density, useDiagonals));
|
|
|
|
// Generate start and destination points
|
|
GridNode startNode = FindValidStartNode();
|
|
GridNode destNode = FindValidDestinationNode(startNode);
|
|
|
|
if (startNode == null || destNode == null)
|
|
{
|
|
Debug.LogWarning($"Failed to find valid start/destination nodes for test - density: {density}%");
|
|
|
|
// Record the test as "impossible" with a failed path
|
|
PathfindingTestResult result = new PathfindingTestResult
|
|
{
|
|
algorithm = algorithm,
|
|
gridSize = gridSize,
|
|
mazeDensity = density,
|
|
diagonalMovement = useDiagonals,
|
|
timeTaken = 0f,
|
|
pathLength = 0,
|
|
nodesExplored = 0,
|
|
memoryUsed = 0,
|
|
pathFound = false,
|
|
testIndex = testIndex
|
|
};
|
|
|
|
testResults.Add(result);
|
|
completedTests++;
|
|
yield break;
|
|
}
|
|
|
|
// Position NPC at start node
|
|
npc.SetStartNode(startNode);
|
|
|
|
// Position destination
|
|
gridMap.SetDestination(destNode.Value.x, destNode.Value.y);
|
|
|
|
// Wait a frame to ensure everything is set up
|
|
yield return null;
|
|
|
|
// Store the current test parameters
|
|
currentTestAlgorithm = algorithm;
|
|
currentTestGridSize = gridSize;
|
|
currentTestDensity = density;
|
|
currentTestDiagonal = useDiagonals;
|
|
currentTestIndex = testIndex;
|
|
|
|
// Flag to track if callback was triggered
|
|
bool callbackTriggered = false;
|
|
|
|
// Setup a temporary callback listener to detect if the event fires
|
|
System.Action<PathfindingMetrics> tempCallback = (metrics) => { callbackTriggered = true; };
|
|
npc.OnPathfindingComplete += tempCallback;
|
|
|
|
// Run pathfinding in silent mode - now the event will still fire with our NPC fix
|
|
npc.MoveTo(destNode, true);
|
|
|
|
// Wait for pathfinding to complete
|
|
float timeout = 10.0f;
|
|
float elapsed = 0f;
|
|
|
|
while (npc.pathFinder != null && npc.pathFinder.Status == PathFinding.PathFinderStatus.RUNNING && elapsed < timeout)
|
|
{
|
|
yield return null;
|
|
elapsed += Time.deltaTime;
|
|
}
|
|
|
|
// Wait a bit more to ensure completion
|
|
yield return new WaitForSeconds(0.1f);
|
|
|
|
// Remove the temporary callback
|
|
npc.OnPathfindingComplete -= tempCallback;
|
|
|
|
// If callback wasn't triggered but pathfinding is complete, manually record the result
|
|
if (!callbackTriggered && npc.pathFinder != null && npc.pathFinder.Status != PathFinding.PathFinderStatus.RUNNING)
|
|
{
|
|
Debug.LogWarning($"Callback wasn't triggered for test {completedTests+1}. Recording results manually.");
|
|
|
|
// Create a metrics object with available data
|
|
PathfindingMetrics metrics = new PathfindingMetrics
|
|
{
|
|
timeTaken = elapsed * 1000f, // convert to ms
|
|
pathLength = CalculatePathLength(npc.pathFinder),
|
|
nodesExplored = npc.pathFinder.ClosedListCount,
|
|
memoryUsed = npc.LastMeasuredMemoryUsage // Get the last memory measurement from NPC
|
|
};
|
|
|
|
// Create a test result entry directly
|
|
PathfindingTestResult result = new PathfindingTestResult
|
|
{
|
|
algorithm = currentTestAlgorithm,
|
|
gridSize = currentTestGridSize,
|
|
mazeDensity = currentTestDensity,
|
|
diagonalMovement = currentTestDiagonal,
|
|
timeTaken = metrics.timeTaken,
|
|
pathLength = metrics.pathLength,
|
|
nodesExplored = metrics.nodesExplored,
|
|
memoryUsed = metrics.memoryUsed,
|
|
pathFound = npc.pathFinder.Status == PathFinding.PathFinderStatus.SUCCESS,
|
|
testIndex = currentTestIndex
|
|
};
|
|
|
|
// Add to results directly
|
|
testResults.Add(result);
|
|
|
|
// Log result
|
|
Debug.Log($"Test {completedTests} (manual): {GetAlgorithmName(result.algorithm)} - {result.timeTaken:F2}ms - Path: {result.pathLength} - Nodes: {result.nodesExplored}");
|
|
}
|
|
|
|
// Increment test counter
|
|
completedTests++;
|
|
}
|
|
|
|
private int CalculatePathLength(PathFinding.PathFinder<Vector2Int> pathFinder)
|
|
{
|
|
if (pathFinder == null || pathFinder.CurrentNode == null)
|
|
return 0;
|
|
|
|
int length = 0;
|
|
var node = pathFinder.CurrentNode;
|
|
while (node != null)
|
|
{
|
|
length++;
|
|
node = node.Parent;
|
|
}
|
|
return length;
|
|
}
|
|
|
|
private IEnumerator SetupTestEnvironment(NPC.PathFinderType algorithm, Vector2Int gridSize, float density, bool useDiagonals)
|
|
{
|
|
Debug.Log($"Setting up test environment: Algorithm={algorithm}, Grid={gridSize.x}x{gridSize.y}, Density={density}, Diagonals={useDiagonals}");
|
|
|
|
// Resize grid
|
|
gridMap.ResizeGrid(gridSize.x, gridSize.y);
|
|
yield return null;
|
|
|
|
// Set algorithm
|
|
npc.ChangeAlgorithm(algorithm);
|
|
yield return null;
|
|
|
|
// Set diagonal movement
|
|
gridMap.AllowDiagonalMovement = useDiagonals;
|
|
yield return null;
|
|
|
|
// Generate maze with specified density
|
|
gridMap.GenerateRandomMaze(density);
|
|
yield return null;
|
|
|
|
// Note: We no longer change visualization settings here
|
|
// This is handled in the RunSingleTest method
|
|
|
|
yield return null;
|
|
}
|
|
|
|
private GridNode FindValidStartNode()
|
|
{
|
|
// Find a walkable node for start position, starting from the top left
|
|
for (int x = 0; x < gridMap.NumX; x++)
|
|
{
|
|
for (int y = 0; y < gridMap.NumY; y++)
|
|
{
|
|
GridNode node = gridMap.GetGridNode(x, y);
|
|
if (node != null && node.IsWalkable)
|
|
{
|
|
return node;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private GridNode FindValidDestinationNode(GridNode startNode)
|
|
{
|
|
if (startNode == null)
|
|
return null;
|
|
|
|
// Find a walkable node far from the start position, starting from the bottom right
|
|
int maxDistance = 0;
|
|
GridNode bestNode = null;
|
|
|
|
// First try to find a node with good distance
|
|
for (int x = gridMap.NumX - 1; x >= 0; x--)
|
|
{
|
|
for (int y = gridMap.NumY - 1; y >= 0; y--)
|
|
{
|
|
GridNode node = gridMap.GetGridNode(x, y);
|
|
if (node != null && node.IsWalkable && node != startNode)
|
|
{
|
|
int distance = Mathf.Abs(x - startNode.Value.x) + Mathf.Abs(y - startNode.Value.y);
|
|
if (distance > maxDistance)
|
|
{
|
|
maxDistance = distance;
|
|
bestNode = node;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found a node and the distance is decent, use it
|
|
if (bestNode != null && maxDistance > gridMap.NumX / 4)
|
|
{
|
|
return bestNode;
|
|
}
|
|
|
|
// If no good node was found or distance is too small, try harder to find any walkable node
|
|
// different from start node (this helps with very high density mazes)
|
|
for (int x = 0; x < gridMap.NumX; x++)
|
|
{
|
|
for (int y = 0; y < gridMap.NumY; y++)
|
|
{
|
|
GridNode node = gridMap.GetGridNode(x, y);
|
|
if (node != null && node.IsWalkable && node != startNode)
|
|
{
|
|
return node; // Return the first walkable node that isn't the start
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we get here and bestNode is null, there's only one walkable node in the entire grid
|
|
// In this case, we have to return null and the test should be skipped
|
|
return bestNode;
|
|
}
|
|
|
|
private void OnPathfindingComplete(PathfindingMetrics metrics)
|
|
{
|
|
if (!isTestingRunning)
|
|
{
|
|
Debug.Log("OnPathfindingComplete called but test is not running - ignoring");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"OnPathfindingComplete called for test {completedTests+1} with algorithm {currentTestAlgorithm}");
|
|
|
|
// Create a test result entry
|
|
PathfindingTestResult result = new PathfindingTestResult
|
|
{
|
|
algorithm = currentTestAlgorithm,
|
|
gridSize = currentTestGridSize,
|
|
mazeDensity = currentTestDensity,
|
|
diagonalMovement = currentTestDiagonal,
|
|
timeTaken = metrics.timeTaken,
|
|
pathLength = metrics.pathLength,
|
|
nodesExplored = metrics.nodesExplored,
|
|
memoryUsed = metrics.memoryUsed,
|
|
pathFound = npc.pathFinder.Status == PathFinding.PathFinderStatus.SUCCESS,
|
|
testIndex = currentTestIndex
|
|
};
|
|
|
|
// Add to results
|
|
testResults.Add(result);
|
|
|
|
// Log result
|
|
Debug.Log($"Test {completedTests+1}: {GetAlgorithmName(result.algorithm)} - {result.timeTaken:F2}ms - Path: {result.pathLength} - Nodes: {result.nodesExplored} - Success: {result.pathFound} - Results count: {testResults.Count}");
|
|
}
|
|
|
|
private void SaveResultsToCSV()
|
|
{
|
|
try
|
|
{
|
|
Debug.Log($"Saving {testResults.Count} test results to CSV...");
|
|
|
|
if (testResults.Count == 0)
|
|
{
|
|
Debug.LogWarning("No test results to save!");
|
|
return;
|
|
}
|
|
|
|
// Create directory if it doesn't exist
|
|
string directory = Path.Combine(Application.persistentDataPath, "TestResults");
|
|
if (!Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
string filePath = Path.Combine(directory, resultsFileName);
|
|
|
|
StringBuilder csv = new StringBuilder();
|
|
|
|
// Write header
|
|
csv.AppendLine("Algorithm,GridSizeX,GridSizeY,Density,DiagonalMovement,TimeTaken,PathLength,NodesExplored,MemoryUsed,PathFound,TestIndex");
|
|
|
|
// Write each result
|
|
foreach (var result in testResults)
|
|
{
|
|
csv.AppendLine($"{result.algorithm},{result.gridSize.x},{result.gridSize.y},{result.mazeDensity},{result.diagonalMovement}," +
|
|
$"{result.timeTaken},{result.pathLength},{result.nodesExplored},{result.memoryUsed},{result.pathFound},{result.testIndex}");
|
|
}
|
|
|
|
// Write file
|
|
File.WriteAllText(filePath, csv.ToString());
|
|
|
|
Debug.Log($"Test results saved to {filePath}");
|
|
|
|
// Make it easier to find in Windows Explorer
|
|
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\"");
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
Debug.LogError($"Error saving test results: {e.Message}");
|
|
}
|
|
}
|
|
|
|
// Utility method to get algorithm name as a string
|
|
private string GetAlgorithmName(NPC.PathFinderType algorithm)
|
|
{
|
|
switch (algorithm)
|
|
{
|
|
case NPC.PathFinderType.ASTAR: return "A*";
|
|
case NPC.PathFinderType.DIJKSTRA: return "Dijkstra";
|
|
case NPC.PathFinderType.GREEDY: return "Greedy";
|
|
case NPC.PathFinderType.BACKTRACKING: return "Backtracking";
|
|
case NPC.PathFinderType.BFS: return "BFS";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
} |