A flow field: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: live (line 49), aim (line 116), follow (line 123), grid (line 158), field (line 162), descend (line 165).
1/// One sweep from a goal, read by every agent in the level as the direction
2/// to walk from wherever it stands.
3///
4/// Quoted by `flow_field.md` and shown whole in the Source tab.
5library;
7import 'dart:math' as math;
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
12import 'package:flutter3d_sim/flutter3d_sim.dart';
13import 'package:vector_math/vector_math.dart';
15final class FlowFieldDemo extends ShowcaseDemo {
16 late final String _report;
18 int goal = 0;
19 bool cycling = true;
21 late final NavGrid _grid;
22 late final FlowField _field;
23 late final MeshNode _beacon;
24 final List<MeshNode> _agents = <MeshNode>[];
25 final List<Vector3> _starts = <Vector3>[];
26 double _clock = 0.0;
27 int _shown = -1;
29 static const List<(double, double)> _goals = <(double, double)>[
30 (18.0, 17.0),
31 (18.0, 3.0),
32 (3.0, 17.0),
33 (3.0, 3.0),
34 ];
37 void configureView(DemoContext context) {
38 context.orbit
39 ..distance = 26.0
40 ..pitch = 1.0
41 ..yaw = 0.0;
42 context.orbit.target.setValues(10.0, 0.0, 10.0);
43 }
46 Scene build(DemoContext context) {
47 _report = _run();
49 // A floor with two walls across it, each leaving a gap at one end: an
50 // agent has to snake round them, and the field knows how.
51 _grid = NavGrid.bake(<Brush>[
52 Brush(centre: Vector3(10, 0, 10), size: Vector3(20, 1, 20)),
53 Brush(centre: Vector3(7, 2, 6), size: Vector3(1, 4, 12)),
54 Brush(centre: Vector3(13, 2, 14), size: Vector3(1, 4, 12)),
55 ], cellSize: 0.5);
56 _field = FlowField(_grid);
58 final math.Random random = math.Random(4);
59 for (var i = 0; i < 16; i++) {
60 final Vector3 start = Vector3(
61 1.0 + 4.0 * random.nextDouble(),
62 0.5,
63 1.0 + 18.0 * random.nextDouble(),
64 );
65 _starts.add(start);
66 _agents.add(
67 ballNode(
68 context,
69 'agent $i',
70 0.25,
71 Vector4(0.9, 0.6, 0.3, 1.0),
72 at: start + Vector3(0.0, 0.25, 0.0),
73 ),
74 );
75 }
76 _beacon = blockNode(
77 context,
78 'goal',
79 Vector3(0.5, 2.0, 0.5),
80 Vector4(0.95, 0.85, 0.3, 1.0),
81 );
82 return sceneOf(<SceneNode>[
83 blockNode(
84 context,
85 'floor',
86 Vector3(20.0, 0.5, 20.0),
87 Vector4(0.34, 0.38, 0.36, 1.0),
88 at: Vector3(10.0, 0.25, 10.0),
89 ),
90 blockNode(
91 context,
92 'wall 1',
93 Vector3(1.0, 1.5, 12.0),
94 Vector4(0.55, 0.5, 0.5, 1.0),
95 at: Vector3(7.0, 1.25, 6.0),
96 ),
97 blockNode(
98 context,
99 'wall 2',
100 Vector3(1.0, 1.5, 12.0),
101 Vector4(0.55, 0.5, 0.5, 1.0),
102 at: Vector3(13.0, 1.25, 14.0),
103 ),
104 _beacon,
105 ..._agents,
106 ]);
107 }
110 void update(DemoContext context, double dt) {
111 _clock += dt;
112 if (cycling) goal = (_clock ~/ 12.0) % _goals.length;
113 final (double gx, double gz) = _goals[goal];
114 if (goal != _shown) {
115 _shown = goal;
116 // A new goal is one rebuild; every agent then follows the same table.
117 _field.rebuild(Vector3(gx, 0.5, gz));
118 _beacon.setPosition(gx, 1.5, gz);
119 }
120 final Vector3 direction = Vector3.zero();
121 for (var i = 0; i < _agents.length; i++) {
122 final Vector3 at = _agents[i].readPosition()..y = 0.5;
123 // One step down the table from wherever the agent stands.
124 if (_field.descend(at, direction)) {
125 // A long frame (a tab left in the background) must not fling an agent
126 // out of the level in one step.
127 at.addScaled(direction, 3.0 * math.min(dt, 0.05));
128 } else {
129 // Not on the floor any more: back to where it started.
130 at.setFrom(_starts[i]);
131 }
132 if ((at.x - gx).abs() + (at.z - gz).abs() < 1.8) {
133 at.setFrom(_starts[i]);
134 }
135 _agents[i].setPosition(at.x, 0.75, at.z);
136 }
137 }
140 List<DemoControl> controls(DemoContext context) => <DemoControl>[
141 ChoiceControl(
142 'Goal',
143 options: const <String>['far corner', 'near corner', 'far side', 'start'],
144 index: () => goal,
145 onChanged: (int i) {
146 cycling = false;
147 goal = i;
148 },
149 ),
150 ToggleControl(
151 'Move the goal',
152 value: () => cycling,
153 onChanged: (bool v) => cycling = v,
154 ),
155 ];
157 static String _run() {
158 final grid = NavGrid.bake(<Brush>[
159 Brush(centre: Vector3(5, 0, 5), size: Vector3(10, 1, 10)),
160 ], cellSize: 0.5);
162 final field = FlowField(grid);
163 field.rebuild(Vector3(9.0, 0.0, 9.0));
165 final from = Vector3(1.0, 0.0, 1.0);
166 final direction = Vector3.zero();
167 final found = field.descend(from, direction);
168 final distance = field.walkingDistanceTo(from);
170 return 'a step from (1, 1) points towards '
171 '(${direction.x.toStringAsFixed(2)}, ${direction.z.toStringAsFixed(2)}), '
172 'found: $found\n'
173 'walking distance to the goal: ${distance?.toStringAsFixed(2)} m';
174 }
177 void verify(Scene scene, FrameResult frame) {
178 if (frame.drawCalls < 1) {
179 throw StateError('the agent marker was not drawn');
180 }
181 if (!_report.contains('found: true')) {
182 throw StateError(
183 'an open floor should always have a direction to the goal',
184 );
185 }
186 // The direction from (1, 1) towards (9, 9) points diagonally: both
187 // components positive.
188 if (_report.contains('(-') || _report.contains(', -')) {
189 throw StateError(
190 'the direction towards a goal up and to the right '
191 'should point up and to the right',
192 );
193 }
194 }
195}