flutter3d
Showcase Changelog 38 packages API reference

One key, read by both engines: the source

since 0.7.0 The Flame bridge

Regions the guide quotes: live (line 93), walk (line 136), shared (line 175), press (line 186), reactions (line 197).

1/// `FlameInputBridge` translating a Flame keyboard event into the same
2/// `Bindings`/`InputState` pair `flutter3d_game`'s own desktop input writes
3/// into.
4///
5/// Quoted by `flame_input_bridge.md` and shown whole in the Source tab.
6library;
7
8import 'package:flame/components.dart';
9import 'package:flame/events.dart' show HasKeyboardHandlerComponents;
10import 'package:flame_flutter3d/flame_flutter3d.dart';
11import 'package:flutter/services.dart';
12import 'package:flutter/widgets.dart';
13import 'package:flutter3d/flutter3d.dart';
14import 'package:flutter3d_game/flutter3d_game.dart';
15import 'package:flutter3d_showcase/src/demo/demo.dart';
16import 'package:flutter3d_showcase/src/demo/flame_layer.dart';
17import 'package:flutter3d_sim/flutter3d_sim.dart';
19/// A Flame game that hands keyboard events to the components that ask.
20final class _InputGame extends TransparentFlameGame
21 with HasKeyboardHandlerComponents {}
23final class FlameInputBridgeDemo extends ShowcaseDemo {
24 late final String _report;
25 late final bool _consumed;
26 late final bool _heldByTheSharedState;
27 late final double _actorX;
29 /// Holds D for anyone without a keyboard to hand.
30 bool holdD = false;
32 late final DemoContext _context;
33 late final Scene _scene;
34 late final MeshNode _walker;
35 late final _InputGame _game;
36 late final FlameInputBridge _bridge;
37 late final InputState _input;
38 late final Widget _body = flameOrbit(
39 _context,
40 Flutter3dFlameWidget(
41 game: _game,
42 camera: _context.camera,
43 existing: (device: _context.device, renderer: _context.renderer),
44 buildScene: (GraphicsDevice device) => _scene,
45 ),
46 );
47 bool _syntheticDown = false;
49 static const double _speed = 3.0;
51 @override
52 void configureView(DemoContext context) {
53 context.orbit
54 ..distance = 8.0
55 ..pitch = 0.7
56 ..yaw = 0.0;
57 context.orbit.target.setValues(0.0, 0.0, 0.0);
58 }
60 @override
61 Scene build(DemoContext context) {
62 _context = context;
63 final (String report, bool consumed, bool held, double actorX) = _run();
64 _report = report;
65 _consumed = consumed;
66 _heldByTheSharedState = held;
67 _actorX = actorX;
69 _walker = MeshNode(
70 DeviceMesh.upload(context.device, SphereShape(segments: 24).build()),
71 Material(name: 'walker', baseColor: Vector4(0.5, 0.7, 0.9, 1.0)),
72 name: 'walker',
73 )..setPosition(0.0, 0.5, 0.0);
74 _scene = Scene()
75 ..ambientColor = Vector3(0.5, 0.55, 0.65)
76 ..ambientIntensity = 0.3
77 ..add(
78 MeshNode(
79 DeviceMesh.upload(
80 context.device,
81 CuboidShape(size: Vector3(8.0, 0.1, 8.0)).build(),
82 ),
83 Material(name: 'floor', baseColor: Vector4(0.36, 0.4, 0.38, 1.0)),
84 name: 'floor',
85 )..setPosition(0.0, -0.05, 0.0),
86 )
87 ..add(_walker)
88 ..add(
89 LightNode(name: 'sun', intensity: 2.5)
90 ..setLocalForward(Vector3(-0.3, -0.6, -0.4)),
91 );
93 // Four keys in the one table both engines read. The Flame side is a
94 // component that hands each key event to the bridge; it draws four
95 // keycaps that light while their action is held. The flutter3d side is
96 // `update` below, which reads the same state and walks the sphere.
97 final Bindings bindings = Bindings()
98 ..bind(
99 InputSource.key(LogicalKeyboardKey.keyW.keyId),
100 GameAction.moveForward,
101 )
102 ..bind(
103 InputSource.key(LogicalKeyboardKey.keyA.keyId),
104 GameAction.moveLeft,
105 )
106 ..bind(
107 InputSource.key(LogicalKeyboardKey.keyS.keyId),
108 GameAction.moveBack,
109 )
110 ..bind(
111 InputSource.key(LogicalKeyboardKey.keyD.keyId),
112 GameAction.moveRight,
113 );
114 _input = InputState();
115 _bridge = FlameInputBridge(bindings: bindings, inputState: _input);
116 _game = _InputGame()
117 ..add(_Keys(_bridge))
118 ..add(_KeyCap('W', GameAction.moveForward, _input, Vector2(56.0, 72.0)))
119 ..add(_KeyCap('A', GameAction.moveLeft, _input, Vector2(16.0, 112.0)))
120 ..add(_KeyCap('S', GameAction.moveBack, _input, Vector2(56.0, 112.0)))
121 ..add(_KeyCap('D', GameAction.moveRight, _input, Vector2(96.0, 112.0)));
122 _game
123 ..add(flameCaption('click the scene, then press W A S D'))
124 ..add(flameCaption(_report, at: Vector2(16.0, 40.0)));
125 return _scene;
126 }
128 @override
129 void update(DemoContext context, double dt) {
130 context.orbit.syncProjectionDepth(context.camera);
131 // For anyone without a keyboard: the same event a real D press makes.
132 if (holdD != _syntheticDown) {
133 _syntheticDown = holdD;
134 _sendD(holdD);
135 }
136 // The flutter3d side of the same key: it reads the state Flame wrote.
137 final Vector2 axis = _input.moveAxis;
138 final Vector3 at = _walker.readPosition();
139 _walker.setPosition(
140 (at.x + axis.x * _speed * dt).clamp(-3.5, 3.5),
141 at.y,
142 (at.z - axis.y * _speed * dt).clamp(-3.5, 3.5),
143 );
144 }
146 void _sendD(bool down) {
147 const PhysicalKeyboardKey physical = PhysicalKeyboardKey.keyD;
148 const LogicalKeyboardKey logical = LogicalKeyboardKey.keyD;
149 _bridge.onKeyEvent(
150 down
151 ? const KeyDownEvent(
152 physicalKey: physical,
153 logicalKey: logical,
154 timeStamp: Duration.zero,
155 )
156 : const KeyUpEvent(
157 physicalKey: physical,
158 logicalKey: logical,
159 timeStamp: Duration.zero,
160 ),
161 const <LogicalKeyboardKey>{},
162 );
163 }
165 @override
166 List<DemoControl> controls(DemoContext context) => <DemoControl>[
167 ToggleControl(
168 'Hold D for me',
169 value: () => holdD,
170 onChanged: (bool v) => holdD = v,
171 ),
172 ];
174 static (String, bool, bool, double) _run() {
175 // The same table and the same state a rebind screen edits and
176 // `DesktopInput` reads natively — a bridged game and a native one share
177 // one rebinding UI and one saved binding file, not two.
178 final bindings = Bindings()
179 ..bind(
180 InputSource.key(LogicalKeyboardKey.keyD.keyId),
181 GameAction.moveRight,
182 );
183 final inputState = InputState();
184 final bridge = FlameInputBridge(bindings: bindings, inputState: inputState);
186 // `onKeyEvent` returns false when it consumed the key, matching
187 // `KeyboardHandler`'s own polarity.
188 final bool consumed = !bridge.onKeyEvent(
189 const KeyDownEvent(
190 physicalKey: PhysicalKeyboardKey.keyD,
191 logicalKey: LogicalKeyboardKey.keyD,
192 timeStamp: Duration.zero,
193 ),
194 const <LogicalKeyboardKey>{},
195 );
197 // The Flame side already knows it consumed the key; the flutter3d side
198 // reads the very same `InputState` the press just wrote into, the way an
199 // actor's own movement would.
200 final bool held = inputState.held(GameAction.moveRight);
201 final double actorX = held ? 1.5 : 0.0;
203 return (
204 'the Flame key handler consumed the press: $consumed; the flutter3d '
205 'side reads the same action held: $held',
206 consumed,
207 held,
208 actorX,
209 );
210 }
212 /// The Flame game the page runs, for a test that steps it without a window.
213 @visibleForTesting
214 TransparentFlameGame get game => _game;
216 @override
217 Widget? customBody(BuildContext buildContext, DemoContext context) => _body;
219 @override
220 void verify(Scene scene, FrameResult frame) {
221 if (frame.drawCalls < 1) {
222 throw StateError('the walker marker was not drawn');
223 }
224 // Compared as booleans, not read back out of `_report`: see
225 // `sim_audio_xr/actors.dart` for why.
226 if (!_consumed) {
227 throw StateError('a bound key should be consumed, not passed through');
228 }
229 if (!_heldByTheSharedState) {
230 throw StateError(
231 'the flutter3d side should read the same InputState the Flame '
232 'bridge just pressed',
233 );
234 }
235 if (_actorX != 1.5) {
236 throw StateError('the flutter3d-side reaction should have moved');
237 }
238 }
241/// Hands every key event Flame sees to the bridge, which answers false when it
242/// consumed the key.
243final class _Keys extends Component with KeyboardHandler {
244 _Keys(this._bridge);
246 final FlameInputBridge _bridge;
248 @override
249 bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) =>
250 _bridge.onKeyEvent(event, keysPressed);
253/// A key drawn on Flame's side, lit while the shared state holds its action.
254final class _KeyCap extends PositionComponent {
255 _KeyCap(this._label, this._action, this._state, Vector2 at)
256 : super(position: at, size: Vector2.all(36.0));
258 final String _label;
259 final GameAction _action;
260 final InputState _state;
262 @override
263 void render(Canvas canvas) {
264 final bool held = _state.held(_action);
265 canvas.drawRRect(
266 RRect.fromRectAndRadius(size.toRect(), const Radius.circular(6.0)),
267 Paint()..color = held ? const Color(0xFFE8A33D) : const Color(0xAA14161A),
268 );
269 TextPaint(
270 style: TextStyle(
271 color: held ? const Color(0xFF14161A) : const Color(0xFFE8E8EC),
272 fontSize: 18.0,
273 fontWeight: FontWeight.bold,
274 ),
275 ).render(canvas, _label, Vector2(11.0, 8.0));
276 }