flutter3d
Showcase Changelog 38 packages API reference

The level format: the source

since 0.4.2 Simulation, audio and XR

Regions the guide quotes: live (line 75), level (line 127), write (line 138), validate (line 141).

1/// The document a level is: brushes, entities and lights, and the validator
2/// that reports what is wrong with one before it is ever played.
3///
4/// Quoted by `level_format.md` and shown whole in the Source tab.
5library;
6
7import 'package:flutter3d/flutter3d.dart';
8import 'package:flutter3d_showcase/src/demo/demo.dart';
9import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
10import 'package:flutter3d_sim/flutter3d_sim.dart';
11import 'package:vector_math/vector_math.dart';
13final class LevelFormatDemo extends ShowcaseDemo {
14 late final String _report;
16 double gap = -1.0;
17 bool withSpawn = true;
19 bool _dirty = true;
20 late final MeshNode _first;
21 late final MeshNode _second;
22 late final MeshNode _spawn;
23 late final List<MeshNode> _issues;
25 static const int _lamps = 6;
27 @override
28 void configureView(DemoContext context) {
29 context.orbit
30 ..distance = 13.0
31 ..pitch = 0.9
32 ..yaw = 0.0;
33 context.orbit.target.setValues(1.5, 0.0, 1.0);
34 }
36 @override
37 Scene build(DemoContext context) {
38 _report = _run();
39 _first = blockNode(
40 context,
41 'brush 1',
42 Vector3(4.0, 2.0, 4.0),
43 Vector4(0.55, 0.65, 0.8, 1.0),
44 );
45 _second = blockNode(
46 context,
47 'brush 2',
48 Vector3(4.0, 1.9, 4.0),
49 Vector4(0.8, 0.65, 0.5, 1.0),
50 );
51 _spawn = ballNode(context, 'spawn', 0.35, Vector4(0.4, 0.9, 0.5, 1.0));
52 _issues = <MeshNode>[
53 for (var i = 0; i < _lamps; i++)
54 ballNode(
55 context,
56 'issue $i',
57 0.25,
58 Vector4(0.9, 0.3, 0.3, 1.0),
59 at: Vector3(-2.5 + i * 0.8, 0.3, 4.5),
60 ),
61 ];
62 return sceneOf(<SceneNode>[
63 floorNode(context, width: 16.0, depth: 12.0),
64 _first,
65 _second,
66 _spawn,
67 ..._issues,
68 ]);
69 }
71 @override
72 void update(DemoContext context, double dt) {
73 if (!_dirty) return;
74 _dirty = false;
75 // The level of the first step, with the second brush slid along by the
76 // slider (a negative gap is an overlap) and the spawn optional, written to
77 // JSON, read back and validated.
78 final Level level = Level(
79 name: 'sample',
80 brushes: <Brush>[
81 Brush(centre: Vector3(0, 0, 0), size: Vector3(4, 2, 4)),
82 Brush(centre: Vector3(4.0 + gap, 0, 0), size: Vector3(4, 2, 4)),
83 ],
84 entities: <EntityDef>[
85 if (withSpawn) EntityDef(type: 'spawn', name: 'start'),
86 ],
87 );
88 final Level reread = Level.fromJson(level.toJson());
89 final List<LevelIssue> issues = LevelValidator(
90 registry: EntityRegistry(const <EntityKind>[]),
91 ).validate(reread);
92 _first.setPosition(0.0, 1.0, 0.0);
93 _second.setPosition(4.0 + gap, 0.95, 0.0);
94 _spawn.visible = withSpawn;
95 _spawn.setPosition(0.0, 2.35, 0.0);
96 for (var i = 0; i < _lamps; i++) {
97 _issues[i].visible = i < issues.length;
98 }
99 }
101 @override
102 List<DemoControl> controls(DemoContext context) => <DemoControl>[
103 SliderControl(
104 'Gap between the brushes',
105 min: -2.0,
106 max: 2.0,
107 value: () => gap,
108 onChanged: (double v) {
109 gap = v;
110 _dirty = true;
111 },
112 format: (double v) => v < 0
113 ? 'overlap ${(-v).toStringAsFixed(1)} m'
114 : '${v.toStringAsFixed(1)} m',
115 ),
116 ToggleControl(
117 'A spawn point',
118 value: () => withSpawn,
119 onChanged: (bool v) {
120 withSpawn = v;
121 _dirty = true;
122 },
123 ),
124 ];
126 static String _run() {
127 // Two brushes that overlap by a metre, which nothing in this format
128 // forbids outright but which the validator flags as worth a look.
129 final level = Level(
130 name: 'sample',
131 brushes: <Brush>[
132 Brush(centre: Vector3(0, 0, 0), size: Vector3(4, 2, 4)),
133 Brush(centre: Vector3(3, 0, 0), size: Vector3(4, 2, 4)),
134 ],
135 entities: <EntityDef>[EntityDef(type: 'spawn', name: 'start')],
136 );
138 final json = level.toJson();
139 final reread = Level.fromJson(json);
141 final validator = LevelValidator(
142 registry: EntityRegistry(const <EntityKind>[]),
143 );
144 final issues = validator.validate(reread);
146 return '${reread.brushes.length} brushes round-tripped through JSON\n'
147 '${issues.length} issue(s): ${issues.map((LevelIssue i) => i.message).join(' | ')}';
148 }
150 @override
151 void verify(Scene scene, FrameResult frame) {
152 if (frame.drawCalls < 1) {
153 throw StateError('the level marker was not drawn');
154 }
155 if (!_report.contains('2 brushes round-tripped')) {
156 throw StateError('every brush should survive a trip through JSON');
157 }
158 if (_report.contains('0 issue(s)')) {
159 throw StateError(
160 'two overlapping brushes should be reported by the validator',
161 );
162 }
163 }