Skip to main content

gizmo_ai/
pathfinding.rs

1use gizmo_math::Vec3;
2use std::cmp::Ordering;
3use std::collections::{BinaryHeap, HashMap, HashSet};
4
5#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
6pub struct GridPos {
7    pub x: i32,
8    pub y: i32,
9    pub z: i32, // Katman veya yükseklik
10}
11
12impl GridPos {
13    pub fn new(x: i32, y: i32, z: i32) -> Self {
14        Self { x, y, z }
15    }
16}
17
18pub struct NavGrid {
19    pub cell_size: f32,
20    pub width: i32,
21    pub height: i32,
22    pub obstacles: HashSet<GridPos>,
23    pub needs_rebuild: bool,
24}
25
26impl NavGrid {
27    pub fn new(cell_size: f32, width: i32, height: i32) -> Self {
28        Self {
29            cell_size,
30            width,
31            height,
32            obstacles: HashSet::new(),
33            needs_rebuild: true,
34        }
35    }
36
37    pub fn add_obstacle_world(&mut self, world_pos: Vec3) {
38        let gp = self.world_to_grid(world_pos);
39        self.obstacles.insert(gp);
40    }
41
42    pub fn remove_obstacle_world(&mut self, world_pos: Vec3) {
43        let gp = self.world_to_grid(world_pos);
44        self.obstacles.remove(&gp);
45    }
46
47    pub fn world_to_grid(&self, pos: Vec3) -> GridPos {
48        GridPos {
49            x: (pos.x / self.cell_size).floor() as i32,
50            y: (pos.y / self.cell_size).floor() as i32,
51            z: (pos.z / self.cell_size).floor() as i32, // Genelde yer zemindir ama 3D de olabilir
52        }
53    }
54
55    pub fn grid_to_world(&self, gp: GridPos) -> Vec3 {
56        Vec3::new(
57            (gp.x as f32 + 0.5) * self.cell_size,
58            (gp.y as f32 + 0.5) * self.cell_size, // eğer y = Z ekseni yukarıysa buna göre güncelleyebiliriz
59            (gp.z as f32 + 0.5) * self.cell_size,
60        )
61    }
62
63    pub fn is_walkable(&self, pos: GridPos) -> bool {
64        if pos.x < 0 || pos.x >= self.width || pos.z < 0 || pos.z >= self.height {
65            return false;
66        }
67        !self.obstacles.contains(&pos) // Engel yoksa yürünebilir
68    }
69
70    // Yalnızca X,Z düzleminde Dört yön hareket algılayan komşuluk.
71    pub fn get_neighbors(&self, pos: GridPos) -> Vec<GridPos> {
72        let mut neighbors = Vec::with_capacity(8);
73        let dirs = [(1, 0), (-1, 0), (0, 1), (0, -1)];
74        let diagonals = [(1, 1), (-1, -1), (-1, 1), (1, -1)];
75
76        // 1. Düz yönler
77        for (dx, dz) in dirs.iter() {
78            let n = GridPos::new(pos.x + dx, pos.y, pos.z + dz);
79            if self.is_walkable(n) {
80                neighbors.push(n);
81            }
82        }
83
84        // 2. Çapraz yönler (Köşeden geçerken her iki kenarın da açık olması şart! Yoksa çarpar)
85        for (dx, dz) in diagonals.iter() {
86            let n = GridPos::new(pos.x + dx, pos.y, pos.z + dz);
87            let side1 = GridPos::new(pos.x + dx, pos.y, pos.z);
88            let side2 = GridPos::new(pos.x, pos.y, pos.z + dz);
89
90            if self.is_walkable(n) && self.is_walkable(side1) && self.is_walkable(side2) {
91                neighbors.push(n);
92            }
93        }
94        neighbors
95    }
96
97    /// Fizik dünyasındaki statik nesneleri tarayıp navigasyon engel ızgarasını (NavMesh) günceller
98    pub fn update_from_physics_world(&mut self, physics: &gizmo_physics_rigid::world::PhysicsWorld) {
99        let cell_size = self.cell_size;
100
101        let world_to_grid_fn = |pos: Vec3| -> GridPos {
102            GridPos {
103                x: (pos.x / cell_size).floor() as i32,
104                y: (pos.y / cell_size).floor() as i32,
105                z: (pos.z / cell_size).floor() as i32,
106            }
107        };
108
109        let chunk_size = (physics.entities.len() / 8).max(1);
110
111        self.obstacles = std::thread::scope(|s| {
112            let mut handles = Vec::new();
113
114            let mut start = 0;
115            while start < physics.entities.len() {
116                let end = (start + chunk_size).min(physics.entities.len());
117                handles.push(s.spawn(move || {
118                    let mut local_obs = HashSet::new();
119                    for i in start..end {
120                        let rb = &physics.rigid_bodies[i];
121                        if rb.body_type == gizmo_physics_rigid::components::BodyType::Dynamic {
122                            continue;
123                        }
124                        let transform = &physics.transforms[i];
125                        let collider = &physics.colliders[i];
126
127                        let aabb = collider.compute_aabb(transform.position, transform.rotation);
128                        let min_grid = world_to_grid_fn(aabb.min.into());
129                        let max_grid = world_to_grid_fn(aabb.max.into());
130
131                        for x in min_grid.x..=max_grid.x {
132                            for y in min_grid.y..=max_grid.y {
133                                for z in min_grid.z..=max_grid.z {
134                                    local_obs.insert(GridPos::new(x, y, z));
135                                }
136                            }
137                        }
138                    }
139                    local_obs
140                }));
141                start = end;
142            }
143
144            let mut combined = HashSet::new();
145            for handle in handles {
146                combined.extend(handle.join().unwrap());
147            }
148            combined
149        });
150
151        self.needs_rebuild = false;
152    }
153}
154
155#[derive(Copy, Clone, PartialEq)]
156struct AStarNode {
157    pos: GridPos,
158    cost: u32, // f_cost
159}
160
161impl Eq for AStarNode {}
162
163// BinaryHeap büyükten küçüğe sıralar, küçük cost için tersine çalışması lazım.
164impl Ord for AStarNode {
165    fn cmp(&self, other: &Self) -> Ordering {
166        other
167            .cost
168            .cmp(&self.cost) // Ters çevirildi
169            .then_with(|| self.pos.x.cmp(&other.pos.x))
170            .then_with(|| self.pos.y.cmp(&other.pos.y))
171            .then_with(|| self.pos.z.cmp(&other.pos.z))
172    }
173}
174
175impl PartialOrd for AStarNode {
176    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
177        Some(self.cmp(other))
178    }
179}
180
181/// Octile mesafe tahmini (Çapraz harekete uygun)
182fn heuristic(a: GridPos, b: GridPos) -> u32 {
183    let dx = (a.x - b.x).unsigned_abs();
184    let dz = (a.z - b.z).unsigned_abs();
185    let (mn, mx) = if dx < dz { (dx, dz) } else { (dz, dx) };
186    14 * mn + 10 * (mx - mn)
187}
188
189impl NavGrid {
190    /// A* Pathfinding Fonksiyonu
191    pub fn find_path(&self, start_world: Vec3, end_world: Vec3) -> Option<Vec<Vec3>> {
192        let start = self.world_to_grid(start_world);
193        let end = self.world_to_grid(end_world);
194
195        if !self.is_walkable(end) || !self.is_walkable(start) {
196            return None; // Hedef duvar içinde
197        }
198
199        let mut open_set = BinaryHeap::new();
200        let mut came_from: HashMap<GridPos, GridPos> = HashMap::new();
201        let mut g_score: HashMap<GridPos, u32> = HashMap::new();
202        let mut closed_set: HashSet<GridPos> = HashSet::new();
203
204        open_set.push(AStarNode {
205            pos: start,
206            cost: 0,
207        });
208        g_score.insert(start, 0);
209
210        let max_iterations = 25_000usize;
211
212        let mut iterations = 0usize;
213        while let Some(current_node) = open_set.pop() {
214            iterations += 1;
215            if iterations > max_iterations {
216                tracing::error!(
217                    "[AI] Pathfinding limit aşıldı ({}/{}). Ulaşılamaz rota?",
218                    iterations, max_iterations
219                );
220                break;
221            }
222
223            let current = current_node.pos;
224
225            if closed_set.contains(&current) {
226                continue;
227            }
228            closed_set.insert(current);
229
230            if current == end {
231                // Yolu Geri İzle
232                let mut path = Vec::new();
233                let mut curr = end;
234                while curr != start {
235                    path.push(self.grid_to_world(curr));
236                    curr = match came_from.get(&curr) {
237                        Some(p) => *p,
238                        None => break,
239                    };
240                }
241                path.reverse();
242                return Some(path);
243            }
244
245            let curr_g = *g_score.get(&current).unwrap_or(&u32::MAX);
246
247            for neighbor in self.get_neighbors(current) {
248                // Çaprazlar 14, düzler 10 birim maliyet.
249                let move_cost = if neighbor.x != current.x && neighbor.z != current.z {
250                    14
251                } else {
252                    10
253                };
254                let tentative_g = curr_g + move_cost;
255
256                if tentative_g < *g_score.get(&neighbor).unwrap_or(&u32::MAX) {
257                    came_from.insert(neighbor, current);
258                    g_score.insert(neighbor, tentative_g);
259
260                    let f_score = tentative_g + heuristic(neighbor, end);
261                    open_set.push(AStarNode {
262                        pos: neighbor,
263                        cost: f_score,
264                    });
265                }
266            }
267        }
268
269        None // Yol bulunamadı
270    }
271}