A menu over the game
This is the example that is boring in Orblit and hard everywhere else.
A pause menu over a 3D view means, in most engines, a second UI toolkit that
exists only inside that engine, with its own layout rules and no way to test it
without launching the game. Here the scene is a widget, so the menu is a
Stack.
import 'package:flutter/material.dart';import 'package:flutter/scheduler.dart';import 'package:orblit_filament/orblit_filament.dart';import 'package:orblit_ui/orblit_ui.dart';import 'package:vector_math/vector_math_64.dart' hide Colors;
void main() => runApp(const PauseApp());
class PauseApp extends StatefulWidget { const PauseApp({super.key});
@override State<PauseApp> createState() => _PauseAppState();}
class _PauseAppState extends State<PauseApp> with SingleTickerProviderStateMixin { late final Ticker _clock = createTicker((elapsed) { if (_paused) return; setState(() => _seconds = elapsed.inMicroseconds / 1e6); })..start();
double _seconds = 0; bool _paused = false;
@override void dispose() { _clock.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Stack( fit: StackFit.expand, children: [ // The 3D view is the bottom of an ordinary stack. It is not a // platform view in a window of its own, which is why anything at // all can be drawn over it. OrblitView(scene: _scene()),
if (_paused) // A real Flutter blur over real 3D content. This is the thing a // platform view cannot do. Positioned.fill( child: ColoredBox( color: const Color(0xAA000000), child: Center(child: _menu()), ), ),
Positioned( right: 16, top: 16, child: IconButton( icon: Icon(_paused ? Icons.play_arrow : Icons.pause), color: Colors.white, onPressed: () => setState(() => _paused = !_paused), ), ), ], ), ), ); }
/// The menu described as a document rather than as widgets. /// /// This is `orblit_ui`: a tree of nodes with a utility class list on each, /// built into real Flutter widgets. Worth it when the interface comes from /// somewhere else, such as a file the editor wrote, a script or a server, /// and overkill when it doesn't. A plain `Column` makes a perfectly good /// pause menu. Widget _menu() { return UiSurface( width: 320, description: const UiNode( type: 'column', classes: 'p-6 gap-4 items-center bg-slate-900 rounded-xl', children: [ UiNode( type: 'text', text: 'Paused', classes: 'text-2xl text-slate-100', ), UiNode( type: 'button', text: 'Resume', classes: 'px-4 py-2 bg-orange-600 rounded-lg text-slate-50', props: {'onPressed': 'resume'}, ), UiNode( type: 'button', text: 'Quit', classes: 'px-4 py-2 bg-slate-700 rounded-lg text-slate-50', props: {'onPressed': 'quit'}, ), ], ), // Handlers are named in the document and resolved here, so the // description carries no closures and can come from a file. onEvent: (handler, payload) { switch (handler) { case 'resume': setState(() => _paused = false); case 'quit': debugPrint('quit'); } }, ); }
OrblitScene _scene() { return OrblitScene( camera: OrblitCamera( position: Vector3(4, 3, 6), target: Vector3(0, 0.5, 0), ), objects: [ OrblitObject( key: 1, transform: Matrix4.rotationY(_seconds) ..setTranslation(Vector3(0, 0.5, 0)), colour: Vector3(0.85, 0.42, 0.16), ), OrblitObject( key: 2, transform: Matrix4.identity() ..setTranslation(Vector3(0, -0.5, 0)) ..scaleByDouble(12, 1, 12, 1), colour: Vector3(0.18, 0.19, 0.21), ), ], lights: [ OrblitLight( key: 1, kind: OrblitLightKind.directional, direction: Vector3(-0.4, -1, -0.6)..normalize(), intensity: 100000, ), ], ); }}Pausing is not the engine’s business
Section titled “Pausing is not the engine’s business”if (_paused) return; in the ticker, and that is the entire pause
implementation. The scene stops being rebuilt, so it stops changing.
There is no engine.pause(), because there is nothing accumulating that would
need to be told. The scene is a function of _seconds, so stop advancing
_seconds and the picture holds. The same property is why scrubbing a
cutscene backwards works. See sampled, not stepped.
When to use orblit_ui and when to use a Column
Section titled “When to use orblit_ui and when to use a Column”The menu above could have been four ordinary widgets, and for a menu written in Dart and never changed, it should be.
orblit_ui earns its place when the description comes from somewhere that
isn’t Dart: a canvas the editor saved, a TypeScript script, a layout served
over the wire. The tree carries no closures, because handlers are named
strings that onEvent resolves, and that’s exactly what lets it be data.
What you get either way is real widgets at the end: laid out by Flutter, hit-tested by Flutter, drawn by Impeller. It has a widget test. It is not a second widget system pretending to be one.
Why the overlay works at all
Section titled “Why the overlay works at all”On macOS, where this example runs, the renderer draws into an IOSurface-backed pixel buffer that Flutter’s texture registry adopts directly, with no readback and no copy through the CPU. From the compositor’s point of view, the 3D view is just a texture in the layer tree.
So it takes part in layout. It can be clipped to a rounded rectangle, animated,
put inside a PageView, have a panel overlap it, or be laid out beside
something that resizes it. An engine that embeds through a platform view puts
its content in a window on top of the application, which is why those engines
generally cannot let anything overlap the viewport.
Building an interface covers the styling vocabulary, theming and responsiveness.
