using System.Collections.Generic; using UnityEngine; using System.IO; using Newtonsoft.Json; /// /// 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. /// public class GridMap : MonoBehaviour { // Ukuran grid pada sumbu X [SerializeField] int numX; // Ukuran grid pada sumbu Y [SerializeField] int numY; // Prefab untuk visualisasi node grid [SerializeField] GameObject gridNodeViewPrefab; // Mengizinkan atau melarang pergerakan diagonal pada pathfinding [SerializeField] bool allowDiagonalMovement = false; // Properti publik untuk mengakses allowDiagonalMovement dari luar public bool AllowDiagonalMovement { get { return allowDiagonalMovement; } set { allowDiagonalMovement = value; } } // Warna-warna untuk representasi visual berbagai status node public Color COLOR_WALKABLE = new Color(0.4f, 0.4f, 0.8f, 1.0f); // Warna untuk node yang dapat dilalui public Color COLOR_NONWALKABLE = Color.black; // Warna untuk node yang tidak dapat dilalui public Color COLOR_CURRENT_NODE = Color.cyan; // Warna untuk node yang sedang diproses public Color COLOR_ADD_TO_OPENLIST = Color.green; // Warna untuk node yang ditambahkan ke open list public Color COLOR_ADD_TO_CLOSEDLIST = Color.grey; // Warna untuk node yang ditambahkan ke closed list public Color COLOR_PATH = Color.blue; // Warna untuk node yang menjadi bagian dari jalur final // Getter untuk ukuran grid public int NumX { get { return numX; } } public int NumY { get { return numY; } } // Referensi ke NPC yang akan menggunakan path [SerializeField] NPC npc; // Referensi ke Transform tujuan untuk visualisasi [SerializeField] Transform destination; public Transform Destination { get { return destination; } } // Ukuran fisik setiap node dalam grid float gridNodeWidth = 1.0f; float gridNodeHeight = 1.0f; // Getter untuk ukuran node public float GridNodeWidth { get { return gridNodeWidth; } } public float GridNodeHeight { get { return gridNodeHeight; } } // Array 2D untuk menyimpan semua GridNodeView private GridNodeView[,] gridNodeViews = null; // Posisi mouse terakhir untuk tracking perubahan private Vector2 lastMousePosition; // Node terakhir yang statusnya diubah private GridNodeView lastToggledNode = null; /// /// Inisialisasi grid pada saat permainan dimulai /// void Start() { // Membuat array untuk menyimpan semua node view gridNodeViews = new GridNodeView[NumX, NumY]; for (int i = 0; i < NumX; i++) { for (int j = 0; j < NumY; j++) { // Membuat instance dari prefab node grid di posisi yang sesuai GameObject obj = Instantiate( gridNodeViewPrefab, new Vector3( i * GridNodeWidth, j * GridNodeHeight, 0.0f), Quaternion.identity); // Memberi nama pada objek grid node untuk identifikasi obj.name = "GridNode_" + i.ToString() + "_" + j.ToString(); GridNodeView gnv = obj.GetComponent(); gridNodeViews[i, j] = gnv; gnv.Node = new GridNode(new Vector2Int(i, j), this); // Menjadikan node sebagai child dari GridMap obj.transform.SetParent(transform); } } // Mengatur posisi kamera agar dapat melihat seluruh grid SetCameraPosition(); // Menetapkan referensi grid map pada NPC npc.Map = this; // Menetapkan posisi awal NPC pada node (0,0) npc.SetStartNode(gridNodeViews[0, 0].Node); } /// /// Mengatur posisi kamera agar dapat menampilkan seluruh grid /// void SetCameraPosition() { // Calculate the center of the grid float gridCenterX = ((numX - 1) * GridNodeWidth) / 2; float gridCenterY = ((numY - 1) * GridNodeHeight) / 2; // Calculate dynamic offset based on grid size float gridWidth = (numX - 1) * GridNodeWidth; float gridHeight = (numY - 1) * GridNodeHeight; // For 20x20 grid: x=5.6, y=10.7, orthoSize=12.4 float baseX = 5.6f; float baseY = 10.7f; float baseOrthoSize = 12.4f; float baseGridSize = 20f; // Scale position and ortho size based on current grid size relative to 20x20 float scaleFactor = Mathf.Max(numX, numY) / baseGridSize; float xPos = baseX * scaleFactor; float yPos = baseY * scaleFactor; float orthoSize = baseOrthoSize * scaleFactor; // Position camera Camera.main.transform.position = new Vector3( xPos, yPos, -100.0f); // Set orthographic size Camera.main.orthographicSize = orthoSize; } // Tabel offset arah untuk pathfinding 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 }; /// /// Mendapatkan daftar node tetangga yang dapat dilalui dari node yang diberikan /// /// Node saat ini /// Daftar node tetangga yang dapat dilalui public List> GetNeighbours(PathFinding.Node loc) { // Pre-alokasi dengan kapasitas maksimum untuk menghindari resize List> neighbours = new List>(8); int x = loc.Value.x; int y = loc.Value.y; // Periksa arah kardinal terlebih dahulu (lebih cepat dan selalu ada) for (int dir = 0; dir < 4; dir++) { int nx = x + directions[dir, 0]; int ny = y + directions[dir, 1]; // Periksa bounds dan walkability dalam satu kondisi if (nx >= 0 && nx < numX && ny >= 0 && ny < numY && gridNodeViews[nx, ny].Node.IsWalkable) { neighbours.Add(gridNodeViews[nx, ny].Node); } } // Jika diagonal movement diizinkan, periksa 4 arah diagonal if (allowDiagonalMovement) { for (int dir = 4; dir < 8; dir++) { int nx = x + directions[dir, 0]; int ny = y + directions[dir, 1]; // Periksa bounds dan walkability dalam satu kondisi if (nx >= 0 && nx < numX && ny >= 0 && ny < numY && gridNodeViews[nx, ny].Node.IsWalkable) { neighbours.Add(gridNodeViews[nx, ny].Node); } } } return neighbours; } /// /// Mendeteksi node pada posisi klik mouse dan mengubah status walkable-nya /// public void RayCastAndToggleWalkable() { // Konversi posisi mouse ke koordinat dunia Vector2 rayPos = new Vector2( Camera.main.ScreenToWorldPoint(Input.mousePosition).x, Camera.main.ScreenToWorldPoint(Input.mousePosition).y); // Melakukan raycast untuk mendeteksi objek pada posisi mouse RaycastHit2D hit = Physics2D.Raycast(rayPos, Vector2.zero, 0f); if (hit) { // Mendapatkan objek yang terkena raycast GameObject obj = hit.transform.gameObject; GridNodeView gnv = obj.GetComponent(); // Memastikan node adalah valid dan belum diubah sebelumnya if (gnv != null && gnv != lastToggledNode) { //Debug.Log($"Toggling walkable state for node at position: {gnv.Node.Value}"); ToggleWalkable(gnv); lastToggledNode = gnv; } } else { lastToggledNode = null; } } /// /// Mendeteksi node pada posisi klik mouse dan menetapkannya sebagai tujuan /// public void RayCastAndSetDestination() { // Don't allow changing destination during pathfinding, visualization, or movement if (npc != null && (npc.pathFinder?.Status == PathFinding.PathFinderStatus.RUNNING || npc.IsVisualizingPath || npc.IsMoving)) { // Pathfinding, visualization or movement is in progress - ignore position change return; } // Konversi posisi mouse ke koordinat dunia Vector2 rayPos = new Vector2( Camera.main.ScreenToWorldPoint(Input.mousePosition).x, Camera.main.ScreenToWorldPoint(Input.mousePosition).y); // Melakukan raycast untuk mendeteksi objek pada posisi mouse RaycastHit2D hit = Physics2D.Raycast(rayPos, Vector2.zero, 0f); if (hit) { // Mendapatkan objek yang terkena raycast GameObject obj = hit.transform.gameObject; GridNodeView gnv = obj.GetComponent(); // Memindahkan objek destination ke posisi node yang dipilih Vector3 pos = destination.position; pos.x = gnv.Node.Value.x * gridNodeWidth; pos.y = gnv.Node.Value.y * gridNodeHeight; destination.position = pos; } } /// /// Mengubah status walkable dari suatu node dan memperbarui warnanya /// /// GridNodeView yang akan diubah status walkable-nya public void ToggleWalkable(GridNodeView gnv) { if (gnv == null) return; int x = gnv.Node.Value.x; int y = gnv.Node.Value.y; // Membalik status walkable node gnv.Node.IsWalkable = !gnv.Node.IsWalkable; // Memperbarui warna node berdasarkan status walkable-nya if (gnv.Node.IsWalkable) { gnv.SetInnerColor(COLOR_WALKABLE); } else { gnv.SetInnerColor(COLOR_NONWALKABLE); } } /// /// Mendeteksi node pada posisi klik mouse dan menetapkannya sebagai posisi NPC /// public void RayCastAndSetNPCPosition() { // Don't allow changing NPC position during pathfinding, visualization, or movement if (npc != null && (npc.pathFinder?.Status == PathFinding.PathFinderStatus.RUNNING || npc.IsVisualizingPath || npc.IsMoving)) { // Pathfinding, visualization or movement is in progress - ignore position change return; } // Konversi posisi mouse ke koordinat dunia Vector2 rayPos = new Vector2( Camera.main.ScreenToWorldPoint(Input.mousePosition).x, Camera.main.ScreenToWorldPoint(Input.mousePosition).y); // Melakukan raycast untuk mendeteksi objek pada posisi mouse RaycastHit2D hit = Physics2D.Raycast(rayPos, Vector2.zero, 0f); if (hit) { // Mendapatkan objek yang terkena raycast GameObject obj = hit.transform.gameObject; GridNodeView gnv = obj.GetComponent(); if (gnv != null && gnv.Node.IsWalkable) { // Set posisi NPC ke node yang dipilih npc.SetStartNode(gnv.Node); //Debug.Log($"Setting NPC position to: {gnv.Node.Value}"); } } } /// /// Update dipanggil setiap frame untuk menangani input pengguna /// void Update() { // Check if pathfinding, visualization, or movement is active bool isPathfindingActive = npc != null && (npc.pathFinder?.Status == PathFinding.PathFinderStatus.RUNNING || npc.IsVisualizingPath || npc.IsMoving); // Handle camera panning with middle mouse button if (Input.GetMouseButton(2)) // Middle mouse button { 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; } // Mengubah status walkable node saat Shift + tombol kiri mouse ditekan if (Input.GetMouseButton(0)) { Vector2 currentMousePosition = new Vector2( Camera.main.ScreenToWorldPoint(Input.mousePosition).x, Camera.main.ScreenToWorldPoint(Input.mousePosition).y); if (currentMousePosition != lastMousePosition) { // If pathfinding is active, only allow camera movement, no interaction with grid if (isPathfindingActive) { lastMousePosition = currentMousePosition; return; } // Menggambar dinding dengan Shift+Left Click if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) { RayCastAndToggleWalkable(); } // Set NPC position dengan Left Click biasa else { RayCastAndSetNPCPosition(); } lastMousePosition = currentMousePosition; } } else { lastToggledNode = null; } // Menetapkan tujuan baru saat tombol kanan mouse ditekan if (Input.GetMouseButtonDown(1)) { if (!isPathfindingActive) { RayCastAndSetDestination(); } } // Menyesuaikan ukuran kamera dengan scroll wheel float scroll = Input.GetAxis("Mouse ScrollWheel"); if (scroll != 0.0f) { // Increase max zoom out to 200 and keep min zoom in at 1 Camera.main.orthographicSize = Mathf.Clamp(Camera.main.orthographicSize - scroll * Camera.main.orthographicSize, 1.0f, 200.0f); } } /// /// Mendapatkan GridNode pada koordinat (x,y) /// /// Koordinat X /// Koordinat Y /// GridNode pada koordinat tersebut atau null jika tidak valid public GridNode GetGridNode(int x, int y) { if (x >= 0 && x < numX && y >= 0 && y < numY) { return gridNodeViews[x, y].Node; } return null; } /// /// Mendapatkan GridNodeView pada koordinat (x,y) /// /// Koordinat X /// Koordinat Y /// GridNodeView pada koordinat tersebut atau null jika tidak valid public GridNodeView GetGridNodeView(int x, int y) { if (x >= 0 && x < numX && y >= 0 && y < numY) { return gridNodeViews[x, y]; } return null; } // Berbagai fungsi penghitungan jarak untuk algoritma pathfinding /// /// Menghitung jarak Manhattan (jarak grid) antara dua titik /// public static float GetManhattanCost( Vector2Int a, Vector2Int b) { return Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y); } /// /// Menghitung jarak Euclidean (jarak garis lurus) antara dua titik /// 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) ); } /// /// Alias untuk GetCostBetweenTwoCells (jarak Euclidean) /// public static float GetEuclideanCost( Vector2Int a, Vector2Int b) { return GetCostBetweenTwoCells(a, b); } /// /// Callback yang dipanggil saat algoritma pathfinding mengubah current node /// public void OnChangeCurrentNode(PathFinding.PathFinder.PathFinderNode node) { int x = node.Location.Value.x; int y = node.Location.Value.y; GridNodeView gnv = gridNodeViews[x, y]; gnv.SetInnerColor(COLOR_CURRENT_NODE); } /// /// Callback yang dipanggil saat node ditambahkan ke open list dalam algoritma pathfinding /// public void OnAddToOpenList(PathFinding.PathFinder.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); } /// /// Callback yang dipanggil saat node ditambahkan ke closed list dalam algoritma pathfinding /// public void OnAddToClosedList(PathFinding.PathFinder.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); } /// /// Mengatur ulang warna semua node grid ke warna default berdasarkan status walkable-nya /// 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); } } } } /// /// Simple class to store grid positions for serialization /// [System.Serializable] public class SerializablePosition { public float x; public float y; public SerializablePosition(float x, float y) { this.x = x; this.y = y; } } /// /// Kelas untuk menyimpan status grid untuk keperluan save/load /// [System.Serializable] public class GridState { public bool[,] walkableStates; public SerializablePosition npcPosition; // Position of NPC public SerializablePosition destinationPosition; // Position of destination } /// /// Menyimpan status walkable dari semua node grid ke file /// /// Path file untuk menyimpan data public void SaveGridState(string filePath) { try { // Pastikan direktori ada 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]; // Menyimpan status walkable dari setiap node for (int i = 0; i < numX; i++) { for (int j = 0; j < numY; j++) { gridState.walkableStates[i, j] = gridNodeViews[i, j].Node.IsWalkable; } } // Save NPC position using our serializable class gridState.npcPosition = new SerializablePosition( npc.transform.position.x / GridNodeWidth, npc.transform.position.y / GridNodeHeight ); // Save destination position using our serializable class gridState.destinationPosition = new SerializablePosition( destination.position.x / GridNodeWidth, destination.position.y / GridNodeHeight ); // Configure serializer settings to ignore Unity-specific circular references JsonSerializerSettings settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; // Mengkonversi ke JSON dan menyimpan ke file 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}"); } } /// /// Memuat status walkable dari semua node grid dari file /// /// Path file untuk memuat data public void LoadGridState(string filePath) { try { if (!File.Exists(filePath)) { Debug.LogError($"Save file not found: {filePath}"); return; } // Membaca dan mengkonversi data dari file JSON string json = File.ReadAllText(filePath); // Configure deserializer settings to handle missing properties (compatibility) JsonSerializerSettings settings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }; GridState gridState = JsonConvert.DeserializeObject(json, settings); // Periksa apakah ukuran grid dalam file sesuai dengan grid saat ini 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)); } // Menerapkan status walkable ke setiap node 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); } } // Check if this is an older save file version (backward compatibility) bool hasPositionData = gridState.npcPosition != null && gridState.destinationPosition != null; // Load NPC position if saved if (npc != null) { if (hasPositionData) { // Find the closest valid grid node position int npcX = Mathf.Clamp(Mathf.RoundToInt(gridState.npcPosition.x), 0, numX - 1); int npcY = Mathf.Clamp(Mathf.RoundToInt(gridState.npcPosition.y), 0, numY - 1); // Make sure the position is walkable GridNode npcNode = GetGridNode(npcX, npcY); if (npcNode != null && npcNode.IsWalkable) { // Set NPC position npc.SetStartNode(npcNode); } else { // Find a walkable node if the saved position isn't walkable FindWalkableNodeAndSetNPC(); } } else { // For older save files, find a walkable position FindWalkableNodeAndSetNPC(); } } // Load destination position if saved if (destination != null) { if (hasPositionData) { // Find the closest valid grid node position int destX = Mathf.Clamp(Mathf.RoundToInt(gridState.destinationPosition.x), 0, numX - 1); int destY = Mathf.Clamp(Mathf.RoundToInt(gridState.destinationPosition.y), 0, numY - 1); // Set destination position SetDestination(destX, destY); } else { // For older save files, set destination to the opposite corner or a walkable node FindWalkableNodeAndSetDestination(); } } Debug.Log($"Grid state loaded from {filePath}"); } catch (System.Exception e) { Debug.LogError($"Error loading grid state: {e.Message}"); } } /// /// Find a walkable node and set the NPC position to it /// private void FindWalkableNodeAndSetNPC() { // Find first walkable node 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; } } } } /// /// Resizes the grid to the specified dimensions /// public bool ResizeGrid(int newSizeX, int newSizeY) { // Enforce grid size limits 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; } // Clean up existing grid 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); } } } } // Update dimensions numX = newSizeX; numY = newSizeY; // Create new grid 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(); gridNodeViews[i, j] = gnv; gnv.Node = new GridNode(new Vector2Int(i, j), this); obj.transform.SetParent(transform); } } // Update camera position SetCameraPosition(); // Reset NPC position if needed if (npc != null) { npc.SetStartNode(gridNodeViews[0, 0].Node); } return true; } /// /// Sets the destination position for pathfinding /// 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); } } /// /// Membuat maze dengan algoritma Recursive Backtracking /// /// Kepadatan dinding dalam persen (0-100), mempengaruhi rasio jalur terhadap ruang terbuka public void GenerateRandomMaze(float density = 35f) { // Handle special cases for 0% and 100% density if (density <= 0f) { // Make all cells walkable (no walls) 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) { // Make all cells non-walkable (all walls) 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); } } } } // No path creation - truly 100% blocked return; } // Use the recursive backtracking maze generation for normal density values GenerateRecursiveBacktrackingMaze(density); } /// /// Membuat maze menggunakan algoritma recursive backtracking dengan kontrol densitas dinding. /// /// Persentase dinding dalam maze (0-100) public void GenerateRecursiveBacktrackingMaze(float density = 30f) { // Inisialisasi semua tile sebagai dinding (bukan jalur) 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); } } } } // Hitung jumlah total tile dan target dinding int totalTiles = numX * numY; int targetWallCount = Mathf.RoundToInt((density / 100f) * totalTiles); // Buat sistem random 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 }; // Mulai dari posisi acak int startX = random.Next(0, numX); int startY = random.Next(0, numY); // Buat cell awal menjadi jalur MakeCellWalkable(startX, startY); // Stack untuk recursive backtracking Stack stack = new Stack(); stack.Push(new Vector2Int(startX, startY)); // Jalankan recursive backtracking while (stack.Count > 0) { // Ambil posisi saat ini Vector2Int current = stack.Peek(); // Daftar untuk menyimpan arah yang valid (belum dikunjungi) List directions = new List(); // Cek semua arah for (int i = 0; i < 4; i++) { // Periksa tetangga 2 langkah (untuk memastikan kita tidak membuat jalur yang bersebelahan) int nx = current.x + dx[i] * 2; int ny = current.y + dy[i] * 2; // Jika tetangga dalam batas grid dan belum dikunjungi if (nx >= 0 && nx < numX && ny >= 0 && ny < numY) { GridNode neighborNode = GetGridNode(nx, ny); if (neighborNode != null && !neighborNode.IsWalkable) { // Tambahkan arah ke daftar valid directions.Add(i); } } } // Jika ada arah yang valid if (directions.Count > 0) { // Pilih arah secara acak int direction = directions[random.Next(0, directions.Count)]; // Hitung posisi tetangga dan dinding di antaranya 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]; // Buat jalur di tetangga dan dinding di antaranya MakeCellWalkable(nx, ny); MakeCellWalkable(wallX, wallY); // Tambahkan tetangga ke stack stack.Push(new Vector2Int(nx, ny)); } else { // Tidak ada arah yang valid, backtrack stack.Pop(); } } // Hitung jumlah dinding saat ini int currentWallCount = CountNonWalkableCells(); // Jika kita memiliki terlalu banyak dinding (kepadatan terlalu tinggi) if (currentWallCount > targetWallCount) { // Hapus dinding secara acak hingga mencapai target RemoveRandomWalls(currentWallCount - targetWallCount); } // Jika kita memiliki terlalu sedikit dinding (kepadatan terlalu rendah) else if (currentWallCount < targetWallCount) { // Tambahkan dinding secara acak hingga mencapai target AddRandomWalls(targetWallCount - currentWallCount); } // Pastikan posisi NPC dan tujuan dapat dilalui EnsureNodeAndNeighborsWalkable((int)(npc.transform.position.x / gridNodeWidth), (int)(npc.transform.position.y / gridNodeHeight)); EnsureNodeAndNeighborsWalkable((int)(destination.position.x / gridNodeWidth), (int)(destination.position.y / gridNodeHeight)); } /// /// Menghitung jumlah sel yang tidak dapat dilalui (dinding) di grid /// 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; } /// /// Membuat sebuah sel menjadi dapat dilalui (jalur) /// 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); } } } /// /// Membuat sebuah sel menjadi tidak dapat dilalui (dinding) /// 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); } } } /// /// Menghapus dinding secara acak dari maze /// private void RemoveRandomWalls(int count) { System.Random random = new System.Random(); // Buat daftar semua dinding List walls = new List(); 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)); } } } // Acak daftarnya 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; } // Hapus dinding hingga mencapai target for (int i = 0; i < count && i < walls.Count; i++) { Vector2Int wall = walls[i]; MakeCellWalkable(wall.x, wall.y); } } /// /// Menambahkan dinding secara acak ke maze /// private void AddRandomWalls(int count) { System.Random random = new System.Random(); // Buat daftar semua jalur List paths = new List(); for (int x = 0; x < numX; x++) { for (int y = 0; y < numY; y++) { GridNode node = GetGridNode(x, y); if (node != null && node.IsWalkable) { // Jangan tambahkan dinding di posisi NPC atau tujuan 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)); } } } } // Acak daftarnya 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; } // Tambahkan dinding hingga mencapai target atau hingga daftar jalur habis int added = 0; for (int i = 0; i < paths.Count && added < count; i++) { Vector2Int path = paths[i]; // Pastikan menambahkan dinding tidak memotong jalur penting if (!IsPathCritical(path.x, path.y)) { MakeCellNonWalkable(path.x, path.y); added++; } } } /// /// Memeriksa apakah sebuah sel merupakan jalur kritis yang tidak boleh diblokir /// private bool IsPathCritical(int x, int y) { // Hindari memblokir jalur satu-satunya 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++; } } } // Jika hanya memiliki satu atau dua tetangga yang dapat dilalui, ini mungkin jalur penting return walkableNeighbors <= 2; } /// /// Memastikan bahwa node dan tetangganya dapat dilalui /// private void EnsureNodeAndNeighborsWalkable(int x, int y) { // Pastikan node utama dapat dilalui MakeCellWalkable(x, y); // Pastikan setidaknya satu tetangga dapat dilalui agar tidak terjebak int[] dx = { 0, 1, 0, -1 }; int[] dy = { 1, 0, -1, 0 }; bool hasWalkableNeighbor = false; // Cek jika sudah ada tetangga yang dapat dilalui 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; } } } // Jika tidak ada tetangga yang dapat dilalui, buat salah satu tetangga dapat dilalui 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 } } } } /// /// Find a walkable node and set the destination position to it /// private void FindWalkableNodeAndSetDestination() { // Try to place destination in the far corner from the NPC int npcX = (int)(npc.transform.position.x / GridNodeWidth); int npcY = (int)(npc.transform.position.y / GridNodeHeight); // Try opposite corner first int destX = numX - 1; int destY = numY - 1; GridNode destinationNode = GetGridNode(destX, destY); if (destinationNode != null && destinationNode.IsWalkable) { SetDestination(destX, destY); return; } // If the opposite corner isn't walkable, find any walkable node that's different from NPC position for (int i = numX - 1; i >= 0; i--) { for (int j = numY - 1; j >= 0; j--) { // Skip the node where NPC is if (i == npcX && j == npcY) continue; GridNode node = GetGridNode(i, j); if (node != null && node.IsWalkable) { SetDestination(i, j); return; } } } // If no suitable node found, use the NPC node as a fallback SetDestination(npcX, npcY); } }