mirror of
https://github.com/BobbyRafael31/Unity-MazeRunner-Pathfinding-Visualizer.git
synced 2025-08-13 08:52:21 +00:00
1010 lines
29 KiB
C#
1010 lines
29 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using System.IO;
|
|
using Newtonsoft.Json;
|
|
|
|
/// <summary>
|
|
/// GridMap adalah komponen utama yang mengelola grid untuk algoritma pathfinding.
|
|
/// Kelas ini bertanggung jawab untuk membuat, menampilkan, dan mengelola node-node grid
|
|
/// serta memberikan fungsionalitas untuk pathfinding.
|
|
/// </summary>
|
|
|
|
public class GridMap : MonoBehaviour
|
|
{
|
|
[SerializeField]
|
|
int numX;
|
|
|
|
[SerializeField]
|
|
int numY;
|
|
|
|
[SerializeField]
|
|
GameObject gridNodeViewPrefab;
|
|
|
|
[SerializeField]
|
|
bool allowDiagonalMovement = false;
|
|
|
|
public bool AllowDiagonalMovement
|
|
{
|
|
get { return allowDiagonalMovement; }
|
|
set { allowDiagonalMovement = value; }
|
|
}
|
|
|
|
public Color COLOR_WALKABLE = Color.white;
|
|
public Color COLOR_NONWALKABLE = Color.black;
|
|
public Color COLOR_CURRENT_NODE = Color.cyan;
|
|
public Color COLOR_ADD_TO_OPENLIST = Color.green;
|
|
public Color COLOR_ADD_TO_CLOSEDLIST = Color.grey;
|
|
public Color COLOR_PATH = Color.blue;
|
|
|
|
public int NumX { get { return numX; } }
|
|
public int NumY { get { return numY; } }
|
|
|
|
[SerializeField]
|
|
NPC npc;
|
|
|
|
[SerializeField]
|
|
Transform destination;
|
|
|
|
public Transform Destination { get { return destination; } }
|
|
|
|
float gridNodeWidth = 1.0f;
|
|
float gridNodeHeight = 1.0f;
|
|
|
|
public float GridNodeWidth { get { return gridNodeWidth; } }
|
|
public float GridNodeHeight { get { return gridNodeHeight; } }
|
|
|
|
private GridNodeView[,] gridNodeViews = null;
|
|
private GridNodeView lastToggledNode = null;
|
|
|
|
private Vector2 lastMousePosition;
|
|
|
|
|
|
void Start()
|
|
{
|
|
gridNodeViews = new GridNodeView[NumX, NumY];
|
|
for (int i = 0; i < NumX; i++)
|
|
{
|
|
for (int j = 0; j < NumY; j++)
|
|
{
|
|
GameObject obj = Instantiate(
|
|
gridNodeViewPrefab,
|
|
new Vector3(
|
|
i * GridNodeWidth,
|
|
j * GridNodeHeight,
|
|
0.0f),
|
|
Quaternion.identity);
|
|
|
|
obj.name = "GridNode_" + i.ToString() + "_" + j.ToString();
|
|
GridNodeView gnv = obj.GetComponent<GridNodeView>();
|
|
gridNodeViews[i, j] = gnv;
|
|
gnv.Node = new GridNode(new Vector2Int(i, j), this);
|
|
|
|
obj.transform.SetParent(transform);
|
|
}
|
|
}
|
|
|
|
SetCameraPosition();
|
|
|
|
npc.Map = this;
|
|
npc.SetStartNode(gridNodeViews[0, 0].Node);
|
|
}
|
|
|
|
void SetCameraPosition()
|
|
{
|
|
float gridCenterX = ((numX - 1) * GridNodeWidth) / 2;
|
|
float gridCenterY = ((numY - 1) * GridNodeHeight) / 2;
|
|
|
|
float gridWidth = (numX - 1) * GridNodeWidth;
|
|
float gridHeight = (numY - 1) * GridNodeHeight;
|
|
|
|
float baseX = 5.6f;
|
|
float baseY = 10.7f;
|
|
float baseOrthoSize = 12.4f;
|
|
float baseGridSize = 20f;
|
|
|
|
float scaleFactor = Mathf.Max(numX, numY) / baseGridSize;
|
|
float xPos = baseX * scaleFactor;
|
|
float yPos = baseY * scaleFactor;
|
|
float orthoSize = baseOrthoSize * scaleFactor;
|
|
|
|
Camera.main.transform.position = new Vector3(
|
|
xPos,
|
|
yPos,
|
|
-100.0f);
|
|
|
|
Camera.main.orthographicSize = orthoSize;
|
|
}
|
|
|
|
private static readonly int[,] directions = new int[,] {
|
|
{ 0, 1 }, // Atas
|
|
{ 1, 0 }, // Kanan
|
|
{ 0, -1 }, // Bawah
|
|
{ -1, 0 }, // Kiri
|
|
{ 1, 1 }, // Kanan Atas
|
|
{ 1, -1 }, // Kanan Bawah
|
|
{ -1, -1 }, // Kiri Bawah
|
|
{ -1, 1 } // Kiri Atas
|
|
};
|
|
|
|
public List<PathFinding.Node<Vector2Int>> GetNeighbours(PathFinding.Node<Vector2Int> loc)
|
|
{
|
|
List<PathFinding.Node<Vector2Int>> neighbours = new List<PathFinding.Node<Vector2Int>>(8);
|
|
|
|
int x = loc.Value.x;
|
|
int y = loc.Value.y;
|
|
|
|
for (int dir = 0; dir < 4; dir++)
|
|
{
|
|
int nx = x + directions[dir, 0];
|
|
int ny = y + directions[dir, 1];
|
|
|
|
if (nx >= 0 && nx < numX && ny >= 0 && ny < numY && gridNodeViews[nx, ny].Node.IsWalkable)
|
|
{
|
|
neighbours.Add(gridNodeViews[nx, ny].Node);
|
|
}
|
|
}
|
|
|
|
if (allowDiagonalMovement)
|
|
{
|
|
for (int dir = 4; dir < 8; dir++)
|
|
{
|
|
int nx = x + directions[dir, 0];
|
|
int ny = y + directions[dir, 1];
|
|
|
|
if (nx >= 0 && nx < numX && ny >= 0 && ny < numY && gridNodeViews[nx, ny].Node.IsWalkable)
|
|
neighbours.Add(gridNodeViews[nx, ny].Node);
|
|
}
|
|
}
|
|
|
|
return neighbours;
|
|
}
|
|
|
|
public void RayCastAndToggleWalkable()
|
|
{
|
|
Vector2 rayPos = new Vector2(
|
|
Camera.main.ScreenToWorldPoint(Input.mousePosition).x,
|
|
Camera.main.ScreenToWorldPoint(Input.mousePosition).y);
|
|
|
|
RaycastHit2D hit = Physics2D.Raycast(rayPos, Vector2.zero, 0f);
|
|
|
|
if (hit)
|
|
{
|
|
GameObject obj = hit.transform.gameObject;
|
|
GridNodeView gnv = obj.GetComponent<GridNodeView>();
|
|
|
|
if (gnv != null && gnv != lastToggledNode)
|
|
{
|
|
ToggleWalkable(gnv);
|
|
lastToggledNode = gnv;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
lastToggledNode = null;
|
|
}
|
|
}
|
|
|
|
public void RayCastAndSetDestination()
|
|
{
|
|
if (npc != null && (npc.pathFinder?.Status == PathFinding.PathFinderStatus.RUNNING ||
|
|
npc.IsVisualizingPath ||
|
|
npc.IsMoving))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector2 rayPos = new Vector2(
|
|
Camera.main.ScreenToWorldPoint(Input.mousePosition).x,
|
|
Camera.main.ScreenToWorldPoint(Input.mousePosition).y);
|
|
|
|
RaycastHit2D hit = Physics2D.Raycast(rayPos, Vector2.zero, 0f);
|
|
|
|
if (hit)
|
|
{
|
|
GameObject obj = hit.transform.gameObject;
|
|
GridNodeView gnv = obj.GetComponent<GridNodeView>();
|
|
|
|
Vector3 pos = destination.position;
|
|
pos.x = gnv.Node.Value.x * gridNodeWidth;
|
|
pos.y = gnv.Node.Value.y * gridNodeHeight;
|
|
destination.position = pos;
|
|
}
|
|
}
|
|
|
|
public void ToggleWalkable(GridNodeView gnv)
|
|
{
|
|
if (gnv == null)
|
|
return;
|
|
|
|
int x = gnv.Node.Value.x;
|
|
int y = gnv.Node.Value.y;
|
|
|
|
gnv.Node.IsWalkable = !gnv.Node.IsWalkable;
|
|
|
|
if (gnv.Node.IsWalkable)
|
|
{
|
|
gnv.SetInnerColor(COLOR_WALKABLE);
|
|
}
|
|
else
|
|
{
|
|
gnv.SetInnerColor(COLOR_NONWALKABLE);
|
|
}
|
|
}
|
|
|
|
public void RayCastAndSetNPCPosition()
|
|
{
|
|
if (npc != null && (npc.pathFinder?.Status == PathFinding.PathFinderStatus.RUNNING ||
|
|
npc.IsVisualizingPath ||
|
|
npc.IsMoving))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector2 rayPos = new Vector2(
|
|
Camera.main.ScreenToWorldPoint(Input.mousePosition).x,
|
|
Camera.main.ScreenToWorldPoint(Input.mousePosition).y);
|
|
|
|
RaycastHit2D hit = Physics2D.Raycast(rayPos, Vector2.zero, 0f);
|
|
|
|
if (hit)
|
|
{
|
|
|
|
GameObject obj = hit.transform.gameObject;
|
|
GridNodeView gnv = obj.GetComponent<GridNodeView>();
|
|
|
|
if (gnv != null && gnv.Node.IsWalkable)
|
|
{
|
|
npc.SetStartNode(gnv.Node);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
bool isPathfindingActive = npc != null && (npc.pathFinder?.Status == PathFinding.PathFinderStatus.RUNNING ||
|
|
npc.IsVisualizingPath ||
|
|
npc.IsMoving);
|
|
// Middle mouse button
|
|
if (Input.GetMouseButton(2))
|
|
{
|
|
float mouseX = Input.GetAxis("Mouse X");
|
|
float mouseY = Input.GetAxis("Mouse Y");
|
|
Vector3 moveDirection = new Vector3(-mouseX, -mouseY, 0) * Camera.main.orthographicSize * 0.15f;
|
|
Camera.main.transform.position += moveDirection;
|
|
}
|
|
|
|
// Left mouse button
|
|
if (Input.GetMouseButton(0))
|
|
{
|
|
Vector2 currentMousePosition = new Vector2(
|
|
Camera.main.ScreenToWorldPoint(Input.mousePosition).x,
|
|
Camera.main.ScreenToWorldPoint(Input.mousePosition).y);
|
|
|
|
if (currentMousePosition != lastMousePosition)
|
|
{
|
|
if (isPathfindingActive)
|
|
{
|
|
lastMousePosition = currentMousePosition;
|
|
return;
|
|
}
|
|
|
|
// Shift + Left Click
|
|
if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
|
|
{
|
|
RayCastAndToggleWalkable();
|
|
}
|
|
else
|
|
{
|
|
RayCastAndSetNPCPosition();
|
|
}
|
|
|
|
lastMousePosition = currentMousePosition;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
lastToggledNode = null;
|
|
}
|
|
|
|
if (Input.GetMouseButtonDown(1))
|
|
{
|
|
if (!isPathfindingActive)
|
|
{
|
|
RayCastAndSetDestination();
|
|
}
|
|
}
|
|
|
|
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
|
if (scroll != 0.0f)
|
|
{
|
|
|
|
Camera.main.orthographicSize = Mathf.Clamp(
|
|
Camera.main.orthographicSize - scroll * Camera.main.orthographicSize,
|
|
1.0f,
|
|
200.0f);
|
|
}
|
|
}
|
|
|
|
public GridNode GetGridNode(int x, int y)
|
|
{
|
|
if (x >= 0 && x < numX && y >= 0 && y < numY)
|
|
return gridNodeViews[x, y].Node;
|
|
|
|
return null;
|
|
}
|
|
|
|
public GridNodeView GetGridNodeView(int x, int y)
|
|
{
|
|
if (x >= 0 && x < numX && y >= 0 && y < numY)
|
|
return gridNodeViews[x, y];
|
|
|
|
return null;
|
|
}
|
|
|
|
public static float GetManhattanCost(Vector2Int a, Vector2Int b)
|
|
{
|
|
return Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y);
|
|
}
|
|
|
|
public static float GetCostBetweenTwoCells(Vector2Int a, Vector2Int b)
|
|
{
|
|
return Mathf.Sqrt(
|
|
(a.x - b.x) * (a.x - b.x) +
|
|
(a.y - b.y) * (a.y - b.y)
|
|
);
|
|
}
|
|
|
|
public static float GetEuclideanCost(Vector2Int a, Vector2Int b)
|
|
{
|
|
return GetCostBetweenTwoCells(a, b);
|
|
}
|
|
|
|
public void OnChangeCurrentNode(PathFinding.PathFinder<Vector2Int>.PathFinderNode node)
|
|
{
|
|
int x = node.Location.Value.x;
|
|
int y = node.Location.Value.y;
|
|
GridNodeView gnv = gridNodeViews[x, y];
|
|
gnv.SetInnerColor(COLOR_CURRENT_NODE);
|
|
}
|
|
|
|
public void OnAddToOpenList(PathFinding.PathFinder<Vector2Int>.PathFinderNode node)
|
|
{
|
|
int x = node.Location.Value.x;
|
|
int y = node.Location.Value.y;
|
|
GridNodeView gnv = gridNodeViews[x, y];
|
|
gnv.SetInnerColor(COLOR_ADD_TO_OPENLIST);
|
|
}
|
|
|
|
public void OnAddToClosedList(PathFinding.PathFinder<Vector2Int>.PathFinderNode node)
|
|
{
|
|
int x = node.Location.Value.x;
|
|
int y = node.Location.Value.y;
|
|
GridNodeView gnv = gridNodeViews[x, y];
|
|
gnv.SetInnerColor(COLOR_ADD_TO_CLOSEDLIST);
|
|
}
|
|
|
|
public void ResetGridNodeColours()
|
|
{
|
|
for (int i = 0; i < numX; ++i)
|
|
{
|
|
for (int j = 0; j < numY; ++j)
|
|
{
|
|
GridNodeView gnv = gridNodeViews[i, j];
|
|
if (gnv.Node.IsWalkable)
|
|
{
|
|
gnv.SetInnerColor(COLOR_WALKABLE);
|
|
}
|
|
else
|
|
{
|
|
gnv.SetInnerColor(COLOR_NONWALKABLE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class SerializablePosition
|
|
{
|
|
public float x;
|
|
public float y;
|
|
|
|
public SerializablePosition(float x, float y)
|
|
{
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class GridState
|
|
{
|
|
public bool[,] walkableStates;
|
|
public SerializablePosition npcPosition;
|
|
public SerializablePosition destinationPosition;
|
|
}
|
|
|
|
public void SaveGridState(string filePath)
|
|
{
|
|
try
|
|
{
|
|
string directory = Path.GetDirectoryName(filePath);
|
|
if (!Directory.Exists(directory) && !string.IsNullOrEmpty(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
GridState gridState = new GridState();
|
|
gridState.walkableStates = new bool[numX, numY];
|
|
|
|
for (int i = 0; i < numX; i++)
|
|
{
|
|
for (int j = 0; j < numY; j++)
|
|
{
|
|
gridState.walkableStates[i, j] = gridNodeViews[i, j].Node.IsWalkable;
|
|
}
|
|
}
|
|
|
|
gridState.npcPosition = new SerializablePosition(
|
|
npc.transform.position.x / GridNodeWidth,
|
|
npc.transform.position.y / GridNodeHeight
|
|
);
|
|
|
|
gridState.destinationPosition = new SerializablePosition(
|
|
destination.position.x / GridNodeWidth,
|
|
destination.position.y / GridNodeHeight
|
|
);
|
|
|
|
JsonSerializerSettings settings = new JsonSerializerSettings
|
|
{
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
|
};
|
|
|
|
string json = JsonConvert.SerializeObject(gridState, settings);
|
|
File.WriteAllText(filePath, json);
|
|
Debug.Log($"Grid state saved to {filePath}");
|
|
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
Debug.LogError($"Error saving grid state: {e.Message}");
|
|
}
|
|
}
|
|
|
|
public void LoadGridState(string filePath)
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(filePath))
|
|
{
|
|
Debug.LogError($"Save file not found: {filePath}");
|
|
return;
|
|
}
|
|
|
|
string json = File.ReadAllText(filePath);
|
|
|
|
JsonSerializerSettings settings = new JsonSerializerSettings
|
|
{
|
|
MissingMemberHandling = MissingMemberHandling.Ignore
|
|
};
|
|
|
|
GridState gridState = JsonConvert.DeserializeObject<GridState>(json, settings);
|
|
|
|
if (gridState.walkableStates.GetLength(0) != numX ||
|
|
gridState.walkableStates.GetLength(1) != numY)
|
|
{
|
|
Debug.LogWarning($"Grid size mismatch. File: {gridState.walkableStates.GetLength(0)}x{gridState.walkableStates.GetLength(1)}, Current: {numX}x{numY}. Resizing grid...");
|
|
ResizeGrid(gridState.walkableStates.GetLength(0), gridState.walkableStates.GetLength(1));
|
|
}
|
|
|
|
for (int i = 0; i < numX; i++)
|
|
{
|
|
for (int j = 0; j < numY; j++)
|
|
{
|
|
gridNodeViews[i, j].Node.IsWalkable = gridState.walkableStates[i, j];
|
|
gridNodeViews[i, j].SetInnerColor(gridState.walkableStates[i, j] ? COLOR_WALKABLE : COLOR_NONWALKABLE);
|
|
}
|
|
}
|
|
|
|
bool hasPositionData = gridState.npcPosition != null && gridState.destinationPosition != null;
|
|
|
|
if (npc != null)
|
|
{
|
|
if (hasPositionData)
|
|
{
|
|
int npcX = Mathf.Clamp(Mathf.RoundToInt(gridState.npcPosition.x), 0, numX - 1);
|
|
int npcY = Mathf.Clamp(Mathf.RoundToInt(gridState.npcPosition.y), 0, numY - 1);
|
|
|
|
GridNode npcNode = GetGridNode(npcX, npcY);
|
|
if (npcNode != null && npcNode.IsWalkable)
|
|
{
|
|
npc.SetStartNode(npcNode);
|
|
}
|
|
else
|
|
{
|
|
FindWalkableNodeAndSetNPC();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FindWalkableNodeAndSetNPC();
|
|
}
|
|
}
|
|
|
|
if (destination != null)
|
|
{
|
|
if (hasPositionData)
|
|
{
|
|
int destX = Mathf.Clamp(Mathf.RoundToInt(gridState.destinationPosition.x), 0, numX - 1);
|
|
int destY = Mathf.Clamp(Mathf.RoundToInt(gridState.destinationPosition.y), 0, numY - 1);
|
|
|
|
SetDestination(destX, destY);
|
|
}
|
|
else
|
|
{
|
|
FindWalkableNodeAndSetDestination();
|
|
}
|
|
}
|
|
|
|
Debug.Log($"Grid state loaded from {filePath}");
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
Debug.LogError($"Error loading grid state: {e.Message}");
|
|
}
|
|
}
|
|
|
|
private void FindWalkableNodeAndSetNPC()
|
|
{
|
|
for (int i = 0; i < numX; i++)
|
|
{
|
|
for (int j = 0; j < numY; j++)
|
|
{
|
|
GridNode node = GetGridNode(i, j);
|
|
if (node != null && node.IsWalkable)
|
|
{
|
|
npc.SetStartNode(node);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool ResizeGrid(int newSizeX, int newSizeY)
|
|
{
|
|
const int MAX_GRID_SIZE = 200;
|
|
const int MIN_GRID_SIZE = 2;
|
|
|
|
if (newSizeX > MAX_GRID_SIZE || newSizeY > MAX_GRID_SIZE)
|
|
{
|
|
Debug.LogWarning($"Attempted to resize grid beyond maximum size of {MAX_GRID_SIZE}x{MAX_GRID_SIZE}. Operation cancelled.");
|
|
return false;
|
|
}
|
|
|
|
if (newSizeX < MIN_GRID_SIZE || newSizeY < MIN_GRID_SIZE)
|
|
{
|
|
Debug.LogWarning($"Attempted to resize grid below minimum size of {MIN_GRID_SIZE}x{MIN_GRID_SIZE}. Operation cancelled.");
|
|
return false;
|
|
}
|
|
|
|
if (gridNodeViews != null)
|
|
{
|
|
for (int i = 0; i < numX; i++)
|
|
{
|
|
for (int j = 0; j < numY; j++)
|
|
{
|
|
if (gridNodeViews[i, j] != null)
|
|
{
|
|
Destroy(gridNodeViews[i, j].gameObject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
numX = newSizeX;
|
|
numY = newSizeY;
|
|
|
|
gridNodeViews = new GridNodeView[NumX, NumY];
|
|
for (int i = 0; i < NumX; i++)
|
|
{
|
|
for (int j = 0; j < NumY; j++)
|
|
{
|
|
GameObject obj = Instantiate(
|
|
gridNodeViewPrefab,
|
|
new Vector3(
|
|
i * GridNodeWidth,
|
|
j * GridNodeHeight,
|
|
0.0f),
|
|
Quaternion.identity);
|
|
|
|
obj.name = "GridNode_" + i.ToString() + "_" + j.ToString();
|
|
GridNodeView gnv = obj.GetComponent<GridNodeView>();
|
|
gridNodeViews[i, j] = gnv;
|
|
gnv.Node = new GridNode(new Vector2Int(i, j), this);
|
|
obj.transform.SetParent(transform);
|
|
}
|
|
}
|
|
|
|
SetCameraPosition();
|
|
|
|
if (npc != null)
|
|
{
|
|
npc.SetStartNode(gridNodeViews[0, 0].Node);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void SetDestination(int x, int y)
|
|
{
|
|
if (x >= 0 && x < numX && y >= 0 && y < numY)
|
|
{
|
|
destination.position = new Vector3(
|
|
x * GridNodeWidth,
|
|
y * GridNodeHeight,
|
|
destination.position.z);
|
|
}
|
|
}
|
|
|
|
public void GenerateRandomMaze(float density = 35f)
|
|
{
|
|
if (density <= 0f)
|
|
{
|
|
for (int x = 0; x < numX; x++)
|
|
{
|
|
for (int y = 0; y < numY; y++)
|
|
{
|
|
GridNode node = GetGridNode(x, y);
|
|
if (node != null)
|
|
{
|
|
node.IsWalkable = true;
|
|
GridNodeView gnv = GetGridNodeView(x, y);
|
|
if (gnv != null)
|
|
{
|
|
gnv.SetInnerColor(COLOR_WALKABLE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
else if (density >= 100f)
|
|
{
|
|
for (int x = 0; x < numX; x++)
|
|
{
|
|
for (int y = 0; y < numY; y++)
|
|
{
|
|
GridNode node = GetGridNode(x, y);
|
|
if (node != null)
|
|
{
|
|
node.IsWalkable = false;
|
|
GridNodeView gnv = GetGridNodeView(x, y);
|
|
if (gnv != null)
|
|
{
|
|
gnv.SetInnerColor(COLOR_NONWALKABLE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
GenerateRecursiveBacktrackingMaze(density);
|
|
}
|
|
|
|
public void GenerateRecursiveBacktrackingMaze(float density = 30f)
|
|
{
|
|
for (int x = 0; x < numX; x++)
|
|
{
|
|
for (int y = 0; y < numY; y++)
|
|
{
|
|
GridNode node = GetGridNode(x, y);
|
|
if (node != null)
|
|
{
|
|
node.IsWalkable = false;
|
|
GridNodeView gnv = GetGridNodeView(x, y);
|
|
if (gnv != null)
|
|
{
|
|
gnv.SetInnerColor(COLOR_NONWALKABLE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int totalTiles = numX * numY;
|
|
int targetWallCount = Mathf.RoundToInt((density / 100f) * totalTiles);
|
|
|
|
System.Random random = new System.Random();
|
|
|
|
// Arah: Atas (0), Kanan (1), Bawah (2), Kiri (3)
|
|
int[] dx = { 0, 1, 0, -1 };
|
|
int[] dy = { 1, 0, -1, 0 };
|
|
|
|
int startX = random.Next(0, numX);
|
|
int startY = random.Next(0, numY);
|
|
|
|
MakeCellWalkable(startX, startY);
|
|
|
|
Stack<Vector2Int> stack = new Stack<Vector2Int>();
|
|
stack.Push(new Vector2Int(startX, startY));
|
|
|
|
// Jalankan recursive backtracking
|
|
while (stack.Count > 0)
|
|
{
|
|
Vector2Int current = stack.Peek();
|
|
|
|
List<int> directions = new List<int>();
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
int nx = current.x + dx[i] * 2;
|
|
int ny = current.y + dy[i] * 2;
|
|
|
|
if (nx >= 0 && nx < numX && ny >= 0 && ny < numY)
|
|
{
|
|
GridNode neighborNode = GetGridNode(nx, ny);
|
|
if (neighborNode != null && !neighborNode.IsWalkable)
|
|
{
|
|
directions.Add(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (directions.Count > 0)
|
|
{
|
|
int direction = directions[random.Next(0, directions.Count)];
|
|
|
|
int nx = current.x + dx[direction] * 2;
|
|
int ny = current.y + dy[direction] * 2;
|
|
int wallX = current.x + dx[direction];
|
|
int wallY = current.y + dy[direction];
|
|
|
|
MakeCellWalkable(nx, ny);
|
|
MakeCellWalkable(wallX, wallY);
|
|
|
|
stack.Push(new Vector2Int(nx, ny));
|
|
}
|
|
else
|
|
{
|
|
stack.Pop();
|
|
}
|
|
}
|
|
|
|
int currentWallCount = CountNonWalkableCells();
|
|
|
|
if (currentWallCount > targetWallCount)
|
|
{
|
|
RemoveRandomWalls(currentWallCount - targetWallCount);
|
|
}
|
|
else if (currentWallCount < targetWallCount)
|
|
{
|
|
AddRandomWalls(targetWallCount - currentWallCount);
|
|
}
|
|
|
|
EnsureNodeAndNeighborsWalkable((int)(npc.transform.position.x / gridNodeWidth),
|
|
(int)(npc.transform.position.y / gridNodeHeight));
|
|
|
|
EnsureNodeAndNeighborsWalkable((int)(destination.position.x / gridNodeWidth),
|
|
(int)(destination.position.y / gridNodeHeight));
|
|
}
|
|
|
|
private int CountNonWalkableCells()
|
|
{
|
|
int count = 0;
|
|
for (int x = 0; x < numX; x++)
|
|
{
|
|
for (int y = 0; y < numY; y++)
|
|
{
|
|
GridNode node = GetGridNode(x, y);
|
|
if (node != null && !node.IsWalkable)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
private void MakeCellWalkable(int x, int y)
|
|
{
|
|
GridNode node = GetGridNode(x, y);
|
|
if (node != null)
|
|
{
|
|
node.IsWalkable = true;
|
|
GridNodeView gnv = GetGridNodeView(x, y);
|
|
if (gnv != null)
|
|
{
|
|
gnv.SetInnerColor(COLOR_WALKABLE);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void MakeCellNonWalkable(int x, int y)
|
|
{
|
|
GridNode node = GetGridNode(x, y);
|
|
if (node != null)
|
|
{
|
|
node.IsWalkable = false;
|
|
GridNodeView gnv = GetGridNodeView(x, y);
|
|
if (gnv != null)
|
|
{
|
|
gnv.SetInnerColor(COLOR_NONWALKABLE);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RemoveRandomWalls(int count)
|
|
{
|
|
System.Random random = new System.Random();
|
|
|
|
List<Vector2Int> walls = new List<Vector2Int>();
|
|
for (int x = 0; x < numX; x++)
|
|
{
|
|
for (int y = 0; y < numY; y++)
|
|
{
|
|
GridNode node = GetGridNode(x, y);
|
|
if (node != null && !node.IsWalkable)
|
|
{
|
|
walls.Add(new Vector2Int(x, y));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < walls.Count; i++)
|
|
{
|
|
int j = random.Next(i, walls.Count);
|
|
Vector2Int temp = walls[i];
|
|
walls[i] = walls[j];
|
|
walls[j] = temp;
|
|
}
|
|
|
|
for (int i = 0; i < count && i < walls.Count; i++)
|
|
{
|
|
Vector2Int wall = walls[i];
|
|
MakeCellWalkable(wall.x, wall.y);
|
|
}
|
|
}
|
|
|
|
private void AddRandomWalls(int count)
|
|
{
|
|
System.Random random = new System.Random();
|
|
|
|
List<Vector2Int> paths = new List<Vector2Int>();
|
|
for (int x = 0; x < numX; x++)
|
|
{
|
|
for (int y = 0; y < numY; y++)
|
|
{
|
|
GridNode node = GetGridNode(x, y);
|
|
if (node != null && node.IsWalkable)
|
|
{
|
|
if ((x != (int)(npc.transform.position.x / gridNodeWidth) ||
|
|
y != (int)(npc.transform.position.y / gridNodeHeight)) &&
|
|
(x != (int)(destination.position.x / gridNodeWidth) ||
|
|
y != (int)(destination.position.y / gridNodeHeight)))
|
|
{
|
|
paths.Add(new Vector2Int(x, y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < paths.Count; i++)
|
|
{
|
|
int j = random.Next(i, paths.Count);
|
|
Vector2Int temp = paths[i];
|
|
paths[i] = paths[j];
|
|
paths[j] = temp;
|
|
}
|
|
|
|
int added = 0;
|
|
for (int i = 0; i < paths.Count && added < count; i++)
|
|
{
|
|
Vector2Int path = paths[i];
|
|
|
|
if (!IsPathCritical(path.x, path.y))
|
|
{
|
|
MakeCellNonWalkable(path.x, path.y);
|
|
added++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsPathCritical(int x, int y)
|
|
{
|
|
int walkableNeighbors = 0;
|
|
|
|
// Arah: Atas, Kanan, Bawah, Kiri
|
|
int[] dx = { 0, 1, 0, -1 };
|
|
int[] dy = { 1, 0, -1, 0 };
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
int nx = x + dx[i];
|
|
int ny = y + dy[i];
|
|
|
|
if (nx >= 0 && nx < numX && ny >= 0 && ny < numY)
|
|
{
|
|
GridNode neighbor = GetGridNode(nx, ny);
|
|
if (neighbor != null && neighbor.IsWalkable)
|
|
{
|
|
walkableNeighbors++;
|
|
}
|
|
}
|
|
}
|
|
return walkableNeighbors <= 2;
|
|
}
|
|
|
|
private void EnsureNodeAndNeighborsWalkable(int x, int y)
|
|
{
|
|
MakeCellWalkable(x, y);
|
|
|
|
int[] dx = { 0, 1, 0, -1 };
|
|
int[] dy = { 1, 0, -1, 0 };
|
|
|
|
bool hasWalkableNeighbor = false;
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
int nx = x + dx[i];
|
|
int ny = y + dy[i];
|
|
|
|
if (nx >= 0 && nx < numX && ny >= 0 && ny < numY)
|
|
{
|
|
GridNode neighbor = GetGridNode(nx, ny);
|
|
if (neighbor != null && neighbor.IsWalkable)
|
|
{
|
|
hasWalkableNeighbor = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasWalkableNeighbor)
|
|
{
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
int nx = x + dx[i];
|
|
int ny = y + dy[i];
|
|
|
|
if (nx >= 0 && nx < numX && ny >= 0 && ny < numY)
|
|
{
|
|
MakeCellWalkable(nx, ny);
|
|
break; // Cukup buat satu tetangga
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void FindWalkableNodeAndSetDestination()
|
|
{
|
|
int npcX = (int)(npc.transform.position.x / GridNodeWidth);
|
|
int npcY = (int)(npc.transform.position.y / GridNodeHeight);
|
|
|
|
int destX = numX - 1;
|
|
int destY = numY - 1;
|
|
|
|
GridNode destinationNode = GetGridNode(destX, destY);
|
|
if (destinationNode != null && destinationNode.IsWalkable)
|
|
{
|
|
SetDestination(destX, destY);
|
|
return;
|
|
}
|
|
|
|
for (int i = numX - 1; i >= 0; i--)
|
|
{
|
|
for (int j = numY - 1; j >= 0; j--)
|
|
{
|
|
if (i == npcX && j == npcY)
|
|
continue;
|
|
|
|
GridNode node = GetGridNode(i, j);
|
|
if (node != null && node.IsWalkable)
|
|
{
|
|
SetDestination(i, j);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
SetDestination(npcX, npcY);
|
|
}
|
|
} |