flutter3d
Showcase Changelog 62 packages API reference

Demo: the shooter in a browser

Dungeon, running on flutter3d_webgl. The crypt, its monsters, four weapons and the weapon held in the hands, through the same HAL the desktop build uses, with one file swapped.

WebGL2 · 1280×720 internal, scaled by CSS Open full screen ↗

Click the frame first; the keyboard goes to whatever was clicked last. This one takes appreciably longer to start than the platformer: the crypt is a bigger level with more textures, and the counters in the corner report the catch-up while it settles.

Controls #

W A S D
Move
Mouse
Aim. Click once and the browser hands the pointer over; Escape gives it back
Click
Fire, once the pointer is captured
1 2 3 4
Fists, pistol, shotgun, rocket launcher
E or F
Use: doors, lifts, buttons, notes
Space
Jump
Shift
Sprint
M
The automap: what you have walked, with the fight running underneath
R
Restart, once the run is over
G
Toggles the fog, which is also a before-and-after measurement: the far wall the fog exists to hide appears and disappears in place
C, or the right stick's click
Crouch. The body has been able to since the package was written (it shrinks, it walks slower, it refuses to stand under something), and for a long time nothing on any device asked it to

What else is in the crypt #

Fire and look are the same two buttons they are on a desktop #

They were not, and the reason is worth keeping. A browser was treated as a platform with no pointer to capture, so a drag stood in for the mouse; and a drag that also had to not fire would have made aiming and shooting mutually exclusive, so they were one gesture. That is a reasonable answer to the wrong question. Browsers have had requestPointerLock for a decade; flutter3d_pointer_lock simply had no browser backend.

It has one now, so this build behaves like the desktop one: the pointer is captured on the first click, the mouse aims, the button fires, Escape releases. On a phone the on-screen stick appears instead, which it never used to, because the guard that hid pointer capture hid the touch controls as well. Flutter reports a mobile browser as android or iOS, so nothing had to be detected for that.

Playing asks the three questions separately now: can the pointer be captured, where does the camera come from, does the player have fingers. kIsWeb was one answer to all three, and wrong about two.

What the browser costs #

Sound, pointer capture and saved settings were all listed here as missing. All three work now: flutter_soloud ships a WebAssembly build, flutter3d_pointer_lock grew a browser backend over document.requestPointerLock, and settings and saves live in localStorage instead of files.

Frame rate The game's own counter read 53 fps with no dropped frames on the WebAssembly build, against 60 on Impeller, and 15 to 30 before --wasm and before the cube atlas stopped being sized from the cascade's resolution. That was read once, off one machine, from the on-screen counter, and the machine, the browser and the date were not written down, so take it as the shape of the change and not a figure anybody can reproduce. The published demo is a dart2js build again, because the dart2wasm build of the WebGL backend throws on its first frame (site/tool/demos.sh has the details), and it has not been timed since. The crypt is the heavier of the two demos: more lights, more shadow casters, particles on every torch, a second pass for the view model
Fixed resolution 1280×720 internally, stretched by CSS. A WebGlDevice owns its canvas and a WebGL canvas resets its drawing buffer when resized
Download About 55 MB on a first load, read off the deployed build the same way and with the same caveat

Read the frame rate as it is. Nothing here is optimised for this backend: the render list, the pass order and the shadow atlas are all sized for a discrete GPU, and the composite path blits a 720p frame to a canvas the browser then composites again. What the demo shows is that the seam holds, meaning an engine written against a HAL runs on a backend it was not written for. It makes no claim that WebGL2 is where this engine is fastest.

What changed in the application #

The same three files as the platformer, plus one decision about the pointer.

// lib/src/backend.dart
export 'backend_native.dart' if (dart.library.js_interop) 'backend_web.dart';
// A first-person camera reads the look delta inside the step, so the loop is
// the right place to drain it — unlike the platformer, whose follow camera
// reads it during the frame. Which *source* it drains is `Playing`'s answer
// (`final _playing = Playing.ofPlatform()`, held by the game), not this file's.
void _drainLook(Vector2 out) {
  if (_playing.dragLook) {
    _dragLook.drainInto(out);
  } else {
    _devices.drainLook(out);
  }
  _pad.drainLook(out);
}

There is no kIsWeb anywhere in this application. The drag layer above the frame is mounted if (_playing.dragLook), the capture is asked for if (_playing.capturesPointer), and the on-screen stick appears if (_playing.touch). A desktop browser takes all three branches a desktop takes.

Nothing in flutter3d, flutter3d_game, flutter3d_physics or flutter3d_game_shooter changed to make this run.

Three things that had to be fixed #

Each was a real defect instead of a web caveat, and each is fixed in the repository.

  1. The generated GLSL was stale. flutter3d_webgl/lib/engine_shaders.dart had not been regenerated since cascaded shadows added shadow_matrix_far, so the backend could not draw one sphere. The detail is on the platformer's demo page.
  2. The canvas took the pointer. A WebGL canvas is a display surface, not a control, and left interactive it swallowed every event the widgets above it needed. It now carries pointer-events: none.
  3. flutter3d_pointer_lock threw where it promised not to. PointerLock documents that it no-ops on a platform without an implementation, and it does. Its constructor, though, subscribed to an event channel, and that is a MissingPluginException on the first listener. It hands back an empty stream now, which is what "this platform never changes capture state" comes to in code.

Building it yourself #

(cd packages/flutter3d_webgl && dart run tool/generate_shaders.dart)
(cd apps/flutter3d_demo_dungeon && flutter build web --release)
python3 -m http.server 8000 --directory apps/flutter3d_demo_dungeon/build/web

Next #