Path-finding: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: brushes (line 43), bake (line 51), live (line 76), query (line 135).
1/// Where an agent can stand, baked once from the level's architecture into a
2/// lattice a step can query in one array lookup.
3///
4/// Quoted by `nav_grid.md` and shown whole in the Source tab.
5library;
7import 'dart:math' as math;
9import 'package:flutter/widgets.dart';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:flutter3d_sim/flutter3d_sim.dart';
13import 'package:vector_math/vector_math.dart';
15final class NavGridDemo extends ShowcaseDemo {
16 late final NavGrid _grid;
18 bool sweeping = true;
19 double _clock = 0.0;
21 /// Where the probe is, in metres.
22 final Vector2 _probe = Vector2(2.0, 2.0);
25 Scene build(DemoContext context) {
26 _grid = _bake();
27 final material = Material(
28 name: 'floor',
29 baseColor: Vector4(0.5, 0.55, 0.6, 1.0),
30 );
31 final node = MeshNode(
32 DeviceMesh.upload(context.device, SphereShape(segments: 16).build()),
33 material,
34 );
35 return Scene()
36 ..add(node)
37 ..add(
38 LightNode(name: 'sun', intensity: 3.0)
39 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
40 );
41 }
43 /// Two floor slabs joined by a narrow bridge, so most of the gap between
44 /// them is not walkable but the bridge is.
45 static List<Brush> _brushes() => <Brush>[
46 Brush(centre: Vector3(2, 0, 2), size: Vector3(4, 1, 4)),
47 Brush(centre: Vector3(5.5, 0, 3.5), size: Vector3(3, 1, 1)),
48 Brush(centre: Vector3(9, 0, 2), size: Vector3(4, 1, 4)),
49 ];
51 static NavGrid _bake() => NavGrid.bake(_brushes(), cellSize: 0.5);
54 void update(DemoContext context, double dt) {
55 _clock += dt;
56 if (sweeping) {
57 // Across both slabs and the gap between, and over the edge.
58 _probe.setValues(
59 5.5 + 5.5 * math.sin(_clock * 0.5),
60 2.4 + 2.2 * math.sin(_clock * 0.83 + 1.0),
61 );
62 }
63 }
66 List<DemoControl> controls(DemoContext context) => <DemoControl>[
67 ToggleControl(
68 'Sweep the probe',
69 value: () => sweeping,
70 onChanged: (bool v) => sweeping = v,
71 ),
72 ];
75 Widget? customBody(BuildContext buildContext, DemoContext context) {
76 // Asking the baked grid about one point: which cell it falls in, and
77 // whether an agent could stand there.
78 final int cell = _grid.cellAtPoint(_probe.x, _probe.y);
79 final bool walkable = _grid.isWalkable(cell);
80 return ColoredBox(
81 color: const Color(0xFF14161A),
82 child: Padding(
83 padding: const EdgeInsets.all(24),
84 child: Column(
85 crossAxisAlignment: CrossAxisAlignment.start,
86 children: <Widget>[
87 Text(
88 'probe at (${_probe.x.toStringAsFixed(1)}, '
89 '${_probe.y.toStringAsFixed(1)}): '
90 '${walkable ? 'walkable' : 'not walkable'}. Tap the map to '
91 'put it somewhere.',
92 style: const TextStyle(color: Color(0xFFE8E8EC), fontSize: 15),
93 ),
94 const SizedBox(height: 16),
95 Expanded(
96 child: Center(
97 child: AspectRatio(
98 aspectRatio: _grid.columns / _grid.rows,
99 child: LayoutBuilder(
100 builder: (BuildContext context, BoxConstraints box) =>
101 GestureDetector(
102 onTapDown: (TapDownDetails d) {
103 sweeping = false;
104 _probe.setValues(
105 d.localPosition.dx /
106 box.maxWidth *
107 _grid.columns *
108 _grid.cellSize,
109 d.localPosition.dy /
110 box.maxHeight *
111 _grid.rows *
112 _grid.cellSize,
113 );
114 },
115 child: CustomPaint(
116 painter: _GridPainter(_grid, _probe, walkable),
117 child: const SizedBox.expand(),
118 ),
119 ),
120 ),
121 ),
122 ),
123 ),
124 ],
125 ),
126 ),
127 );
128 }
131 void verify(Scene scene, FrameResult frame) {
132 if (frame.drawCalls < 1) {
133 throw StateError('the floor marker was not drawn');
134 }
135 final onFirstSlab = _grid.isWalkable(_grid.cellAtPoint(2.0, 2.0));
136 final inTheGap = _grid.isWalkable(_grid.cellAtPoint(4.5, 0.5));
137 final onBridge = _grid.isWalkable(_grid.cellAtPoint(5.0, 3.5));
138 if (!onFirstSlab || inTheGap || !onBridge) {
139 throw StateError(
140 'the slabs and the bridge should be walkable and the open gap '
141 'should not',
142 );
143 }
144 }
145}
147/// Draws the grid from above: light for a walkable cell, dark for not.
148final class _GridPainter extends CustomPainter {
149 const _GridPainter(this.grid, this.probe, this.walkable);
151 final NavGrid grid;
152 final Vector2 probe;
153 final bool walkable;
156 void paint(Canvas canvas, Size size) {
157 final cellW = size.width / grid.columns;
158 final cellH = size.height / grid.rows;
159 final paint = Paint();
160 for (var index = 0; index < grid.cellCount; index++) {
161 final cx = grid.cellX(index);
162 final cz = grid.cellZ(index);
163 paint.color = grid.isWalkable(index)
164 ? const Color(0xFF7FB0FF)
165 : const Color(0xFF2A2C33);
166 canvas.drawRect(
167 Rect.fromLTWH(cx * cellW, cz * cellH, cellW, cellH),
168 paint,
169 );
170 }
171 final Offset at = Offset(
172 probe.x / grid.cellSize * cellW,
173 probe.y / grid.cellSize * cellH,
174 );
175 canvas.drawCircle(
176 at,
177 cellW * 1.6,
178 Paint()
179 ..color = walkable ? const Color(0xFF6CE07C) : const Color(0xFFE5665A),
180 );
181 }
184 bool shouldRepaint(covariant _GridPainter oldDelegate) => true;
185}