Navigation¶
grid A* pathfinding with a click-to-move character.
▶ Run in browserTags: 2d
NavGrid2D is the engine’s grid pathfinder. Build one with a size in cells, mark the walls
with set_solid(x, y), and find_path(from_cell, to_cell) returns the shortest walkable
route as a list of (x, y) cells, both ends included. It returns an empty list when either
end is off the grid or solid, or when no route exists.
corners decides whether a route may step diagonally past the corner of a wall. "never",
used here, keeps every step orthogonal. "strict" (the default) allows a diagonal only when
both cells it passes between are free, "relaxed" when either is, and "always" allows
every diagonal.
The scene works in cell units: cell (x, y) covers x..x+1 by y..y+1, so its centre is
(x + 0.5, y + 0.5). Each frame the node’s scale and position fit the grid to the
window, which scales everything on_draw draws, and to_local maps a press back into cell
units. The character walks the route waypoint by waypoint at a steady speed with
Vec2.move_toward, and the green cells are the part of the route still ahead. A new press
re-plans from the cell the character is walking to, so it never cuts through a wall. A press
on a wall or an unreachable cell is ignored.
Click or tap a free cell to walk there.
Source¶
1"""Navigation: grid A* pathfinding with a click-to-move character.
2
3`NavGrid2D` is the engine's grid pathfinder. Build one with a size in cells, mark the walls
4with `set_solid(x, y)`, and `find_path(from_cell, to_cell)` returns the shortest walkable
5route as a list of `(x, y)` cells, both ends included. It returns an empty list when either
6end is off the grid or solid, or when no route exists.
7
8`corners` decides whether a route may step diagonally past the corner of a wall. `"never"`,
9used here, keeps every step orthogonal. `"strict"` (the default) allows a diagonal only when
10both cells it passes between are free, `"relaxed"` when either is, and `"always"` allows
11every diagonal.
12
13The scene works in cell units: cell `(x, y)` covers `x..x+1` by `y..y+1`, so its centre is
14`(x + 0.5, y + 0.5)`. Each frame the node's `scale` and `position` fit the grid to the
15window, which scales everything `on_draw` draws, and `to_local` maps a press back into cell
16units. The character walks the route waypoint by waypoint at a steady speed with
17`Vec2.move_toward`, and the green cells are the part of the route still ahead. A new press
18re-plans from the cell the character is walking to, so it never cuts through a wall. A press
19on a wall or an unreachable cell is ignored.
20
21Click or tap a free cell to walk there.
22"""
23
24import math
25
26from simvx.core import Anchor, CanvasLayer, Label, NavGrid2D, Node2D, Pointer, Vec2, on_input
27from simvx.graphics import App
28
29COLS, ROWS = 20, 15
30SPEED = 5.0 # cells per second
31
32
33class NavigationDemo(Node2D):
34 dynamic = True # the character moves every frame
35
36 def on_ready(self):
37 self.grid = NavGrid2D(COLS, ROWS, corners="never")
38 walls = [(x, 5) for x in range(5, 15)] + [(8, y) for y in range(2, 10)] + [(x, 10) for x in range(3, 8)]
39 for x, y in walls:
40 self.grid.set_solid(x, y)
41 self.player = Vec2(1.5, 1.5)
42 self.route: list[tuple[int, int]] = [] # cells still to walk, the next one first
43
44 # Example help overlay: not part of the technique shown.
45 hud = self.add_child(CanvasLayer())
46 hud.add_child(Label("Click or tap a free cell to walk there", anchor=Anchor.BOTTOM_LEFT, inset=10))
47
48 @on_input(Pointer.PRIMARY)
49 def walk_to(self, event):
50 x, y = self.to_local(event.position)
51 start = self.route[0] if self.route else (math.floor(self.player.x), math.floor(self.player.y))
52 # An empty path (a wall, off the grid, or unreachable) keeps the current walk.
53 self.route = self.grid.find_path(start, (math.floor(x), math.floor(y))) or self.route
54
55 def on_update(self, dt):
56 width, height = self.viewport_size
57 cell = min(width / COLS, height / ROWS)
58 self.scale = (cell, cell)
59 self.position = ((width - COLS * cell) / 2, (height - ROWS * cell) / 2)
60
61 if self.route:
62 x, y = self.route[0]
63 self.player = self.player.move_toward((x + 0.5, y + 0.5), SPEED * dt)
64 if self.player == (x + 0.5, y + 0.5):
65 self.route.pop(0)
66
67 def on_draw(self, renderer):
68 for x in range(COLS):
69 for y in range(ROWS):
70 colour = (0.75, 0.22, 0.22) if self.grid.is_solid(x, y) else (0.12, 0.12, 0.16)
71 renderer.rect((x, y), (0.97, 0.97), fill=colour)
72 for x, y in self.route:
73 renderer.rect((x + 0.1, y + 0.1), (0.77, 0.77), fill=(0.24, 0.68, 0.28))
74 renderer.circle(self.player, 0.33, fill=(0.24, 0.71, 1.0))
75
76
77if __name__ == "__main__":
78 App(title="Navigation", width=800, height=600).run(NavigationDemo())