Sitelet https://github.com/pleiondev/flutter3d/pull/42
Skip to content

flutter3d 0.8.3 and 0.8.4: River Sortie, and what building it asked of the bridge - #42

Open
dzolotov wants to merge 76 commits into
fixes-0.8.2from
river-0.8.3
Open

dzolotov wants to merge 76 commits into
fixes-0.8.2from
river-0.8.3

Conversation

@dzolotov

@dzolotov dzolotov commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #41 (0.8.2), which is still open.

River Sortie (apps/flutter3d_demo_river): the second game on flame_flutter3d, after River Raid. Flame decides every hit with its own hitboxes and draws the panel; flutter3d draws the river. Five levels with a task each, targets that go down the way their kind would, generated sound, a stick and a fire button on Android and iOS. Demo page, sidebar entry and front-page card on the site.

0.8.3 of three packages, each for something the game had to work around:

  • flutter3d_core: vertex colour documented as linear, linearFromSrgb, the sRGB curve exported, MeshData.withColor.
  • flutter3d: ModelAsset.instantiateFitted, which Meteor Yard now uses in place of its own copy.
  • flame_flutter3d: Object3dComponent takes size and anchor.

0.8.4, published to pub.dev and tagged v0.8.4 on a796eca:

  • flame_flutter3d 0.8.4: an actor system with several foci, a horde drawn as slots of one instanced batch (InstancedActorComponent, InstancedPoseComponent), bodies drawn between their steps, PlayerSeats and keys read from the keyboard rather than the focus, replaceScene3d, ViewCamera, FixtureVisualsComponent, and UpdatesAtRoot, which puts the camera sync back after Flame's camera wherever it is added.
  • flame_flutter3d_audio 0.8.4: the audio scene hears from where the camera is this frame.
  • flutter3d_sim 0.8.1: several foci, damage counted per focus, a restore that builds again what was born during play. focusBody can still be set, deprecated.
  • flutter3d_game_shooter 0.8.1: a claw that catches the player beside the one it swung at has caught them.
  • flame_multiplayer 0.1.0 (new, no dependencies): a room with a handshake and channels, rollback play in step, turns with a replay for the one watching, a feed for a ghost.
  • flame_multiplayer_dashwire 0.1.0 (new): a dashwire connection as its wire.
  • flutter3d_net 0.8.1: NetTransportWire, and NetSession running on the shared rollback with the same wire format.

Three showcase pages for the bridge's 0.8.4 (seats, the horde, a level as a scene), and two River Sortie pictures for the article on the bridge.

Checked: tool/structure.dart 35/35; dart pub publish --dry-run with no warnings on all seven packages; the suites of every changed package and of what depends on them: flame_multiplayer (9), flame_multiplayer_dashwire (3, including a room through the relay over dashwire's WebSocket), flutter3d_net (15), flutter3d_sim (606), flutter3d_game (254), the four genre packages, flame_flutter3d (213), flame_flutter3d_audio (11), River Sortie (53), Meteor Yard (20), racing (193), dungeon (133), showcase (502). Published shooter 0.8.0 analyzes clean against flutter3d_sim 0.8.1.

flutter3d_core 0.8.3: VertexLayout.color and MeshBuilder.addVertex say
what the shader always did, that a vertex colour is linear while a
material's baseColor is sRGB. linearFromSrgb converts a colour picked
on screen, and the sRGB curve the format readers kept to themselves is
exported with it. MeshData.withColor paints a whole mesh, so shapes in
several colours merge into one mesh drawn with one white material.

flutter3d 0.8.3: ModelAsset.instantiateFitted scales a model uniformly
to a length along one axis of its bounds and centres it on its parent,
or stands it there with onGround. Free models come in any unit; Meteor
Yard did this by hand and now calls it.

flame_flutter3d 0.8.3: Object3dComponent takes size and anchor, which a
hitbox and the point written into the scene both depend on.
The sixth demo game and the second on flame_flutter3d, after River Raid:
a jet up a river that never ends, laid out a stretch at a time by a
seeded generator. Flame owns the game, with its own hitboxes and
collision callbacks deciding every hit and its canvas drawing the
panel; each component is an Object3dComponent and the scene is only
told where things are.

Five levels, each a few bridges long with a task, whose last bridge is
shielded until the task is done, and an open river after them. Tankers
sink, helicopters spin into the water, a depot takes its neighbours
with it, a bridge breaks in two, and gunner helicopters fire back. The
sound is generated square waves and shift-register noise. Free models
for the craft, credited in assets/models/LICENSES.md; the valley, the
bridges and the effects are built in code. A stick and a fire button
on Android and iOS. 43 tests.

The site gets its demo page, its place in the sidebar and on the front
page, and the test counts move to 10615.
flutter_soloud 5 ships one loader, init_soloud.js, where 4.x had the
plugin script and init_module.dart.js. The shooter, the platformer and
the racer still asked for the 4.x pair, and the second of them is a 404
against 5.x, so their web builds opened no speakers.
Object3dComponent: flowing Flame to the scene it writes again after its
children, so an effect reaches the scene in the frame it moves; it
places a nested component where Flame draws it, hides a component the
moment it is let go, lifts it off the plane by an elevation, scales it,
follows Flame's visibility, and offers a visual node under the bridged
one that the bridge never turns. Flutter3dFlameWidget hands a game its
renderer through onRendererReady.

The engine gets Renderer.releaseMeshAfterFrame, which lets go of a mesh
once no frame in flight can draw it, and ModelWardrobe, which loads
models by key and puts them on every node waiting for them.

River Sortie drops its second plane, its pivot nodes, its model maps and
its own one-frame release queue; Meteor Yard its three craft maps.
Test counts move to 10623.
River Sortie is the proof for the next part of the bridge. A chase camera
follows the jet in perspective through the shared CameraRig, so a crash
shakes it. A projector puts "+30" in Flame's viewport over a target that
went down. The river's stretches come and go through ChunkStreamer, the
shots are slots of one instanced batch, and fire and sparks go through a
particle system on Flame's clock instead of a scene node per shard. The
phone's stick and fire button go through the input bridge.

In the engine, InstancedMeshNode hands out slots by handle and fills a
released slot with the last one, so owners of instances that leave in any
order keep finding their own.
A mesh particle could only add light, so River Sortie drew its dark smoke
as a scene node per shard. MeshParticleContributor now takes a blend, and
its darkening blend takes the particle's colour out of what is behind it.
Multiplication commutes as addition does, so the pool is still one
unsorted draw, and no shader changed: the fragment stage already handed
back colour times alpha.

Particles3dComponent passes the blend through. River Sortie's fire, sparks
and spray share one pool and its smoke has another; BurstComponent is
gone. flutter3d_particles goes to 0.8.1.
A near cascade reaches back to the furthest caster towards the sun, and
its bias, kept in metres, shrank in stored depth by as much. The shadow
map stores depth as a half float, and in River Sortie's valley the near
cascade's bias came to a seventh of the step between two stored depths:
a lit floor shadowed itself in diagonal bands on Metal, and not on the
software backend, which keeps full floats in any format.

The bias of each cascade no longer goes below the step the map stores
depth with, unless the settings ask for less than that.
Every source had a low-pass open at 16 kHz. SoLoud runs a voice's filters
at the voice's own rate, and 16 kHz is past the Nyquist frequency of a
22.05 kHz file, where the biquad is unstable and feeds itself until it
overflows. River Sortie's engine came out as a scream and every shot,
blast and refuel as silence, though each voice played to its end.

The backend reads each file's sample rate from its WAV header and keeps
each voice's cutoff below its Nyquist frequency for the speed it plays at.
flutter3d_audio goes to 0.8.1.
Smoke puffs are soft balls that take a third of the light at their
thickest and thin as they rise, so a burning tanker's column builds to a
grey-brown rather than black. The engine is a low hum under everything
instead of a loud square wave, and the shot, the blasts, the crash and
the refuel tones were made again to cut through it.
Flame does not draw the children of a component it hides, and a child's
scene node is not under its parent's, so a frog went on being drawn on a
log that blinked. A bridged component now shows its node only while every
ancestor that can be hidden is shown, and scales it by Flame's absolute
scale rather than its own, which its place already carried.

ActorComponent turns with its actor's yaw, and it and RigidBodyComponent
take a size, an anchor and an elevation like Object3dComponent. The host
widget's two tests, skipped as hanging, run in seconds and are back on.
HasFlutter3d gives a FlameGame its scene, device, camera, renderer and
projector, with a clear colour and render settings the frame is drawn
with. The game builds its world in onOpen3d and uses the renderer in
onRenderer3d, each run once after the game has loaded, whatever order the
widget or a test opens things in; handed the renderer at once, River
Sortie met it with nothing built yet.

Flutter3dFlameWidget takes such a game alone. River Sortie's main.dart no
longer builds the scene, the lens, the haze, the projector or the chase
camera; the game does.
PhysicsStepComponent and ActorSystemComponent handed Flame's frame time
straight to the solver: a body fell a different distance at 30 and at 144
frames a second, and a stalled frame let it step through a wall. Both now
cut the frame's time into steps of one size, at most five after a stall,
and dispatch contacts after each step. A rigid body or an actor handed its
stepper is drawn between its last two steps instead of in jumps.
Under a perspective 3D camera, Flame's taps and debug hitboxes work on the
plane the game plays on, which is not where a craft is drawn. A bridged
component with Tap3dCallbacks now hears a tap that falls on the screen
rectangle its node covers, and a Taps3dComponent hands each tap to the
nearest such component. HasFlutter3d.debugHitboxes3d draws every bridged
hitbox in the scene, round its craft, red while it collides.

For that the renderer takes lines of the application's own through
Renderer.debugLines and draws them with its debug overlay. River Sortie
shows its hitboxes with RIVER_HITBOXES.
flame_flutter3d_audio, a package of its own so the bridge carries no
native audio library: AudioSceneComponent is the game's audio scene,
silent until the player's first input opens the speakers and heard from
the game's 3D camera; SoundEmitterComponent is a loop held open by the
state it stands for, sounding from its bridged parent and moving onto the
speakers when they open.

River Sortie's hand-kept scene, ears and three loops became those, and
its main.dart no longer opens or closes a device. The game says what its
loops want before its children update, so the mix hears it the same frame.
MeshNode.tint multiplies its material's colour for that node's draw alone,
and its alpha fades it through the transparent pass. No shader changed:
the tint goes into the base colour each draw already sends, so every
backend has it, and an untinted node draws exactly as before.

On the bridge, Object3dComponent is an OpacityProvider and has a tint, so
Flame's OpacityEffect fades a craft in 3D. River Sortie's wrecks go down
charred and a fallen bridge fades rather than blinking out.
A node's setters mark it changed whatever they are given, and the engine
redraws its shadow cascades and repacks its bounds when anything changed.
Every bridged component rewrote its transform every frame, so one still
tanker had every shadow redrawn every frame. Object3dComponent and
InstancedObject3dComponent now write only when Flame's transform moved,
and without allocating to do it.

BridgePriority names the order a bridged frame runs in and is the default
of the bridge's components. ColliderRegistry is the collider-to-component
map the arcade kept by hand, forgetting a component when it leaves the
game. Flutter3dFlameWidget draws with a camera or clear colour handed in
by a rebuild, and says in a debug build when an opaque game background
hides the 3D layer.
The README described the bridge as it was a week ago. It now covers a game
that owns its world, what crosses between the layers, the camera, taps,
hitboxes, fixed steps, streaming and particles, and points at the sound
package.

Two showcase pages show the newest of it: a HasFlutter3d game whose crates
hear a tap on what the perspective camera shows, and eighty Flame sparks
drawn as one instanced batch beside fire and darkening smoke. The showcase
moves to 0.8.3+1 with them.
An analysis of six Atari games against the Flame bridge, and the state
after the work it led to: what is in the bridge now, what the engine
gained on the way, and what is left.
RigidBodyComponent and ActorComponent wrote their body's place onto the
node every frame, so a sleeping body redrew every shadow the way a still
prop did. They now leave a node alone where it already is.

FlameInputBridge gains stepEnd, which closes the input step once the frame
has read it, pointer, an aim and a tap as an action, and swipes as presses.
River Sortie no longer calls endStep by hand.
HasFixedStep spends each frame in steps of one size and calls fixedUpdate
on the game and on every FixedStepUpdate component in each, before
Flame's frame update. River Sortie flew a different distance at 30 and at
120 frames a second; its run, targets, bridges and shots now step, and a
second of flight covers the same river at any frame rate.

The input step stays open through a frame with no step in it, so a press
is read before it is closed.
CollisionBridge relays to any component with Flame's collision callbacks,
so a bot's body can tell its ActorComponent what it touched; before, only
a rigid body's could. InstancedObject3dComponent has a tint and an
opacity, written into its slot's colour, for a hit flash on one of many.
Object3dComponent takes the meshes it owns and gives them back when it is
removed, after the frames in flight, so a game no longer keeps its own
list of what to release and when. River Sortie's bridges own their span
and shield.
ProjectedViewfinder maps a screen point to the point of the game's plane
under it through the 3D camera, and back. Under a perspective camera the
viewfinder's affine transform put a tap metres from the craft it was on,
so a world component's TapCallbacks, Flame's hit test and a game's own
globalToLocal all missed. The sky meets no plane and hits nothing.
ModelAnimationComponent advances a model's AnimationPlayer from Flame's
update, so a paused game's characters stop, and changes clip by name with
a crossfade. MeshFlipbookComponent swaps a node's mesh through a few
frames, for animation that is a handful of shapes.
…through

CameraSyncController takes the viewport's height and maps Flame's zoom to
pixels per world unit, and can keep Flame's viewfinder angle and the
camera's roll the same. Flutter3dFlameWidget hands Flame's overlays, its
initial overlays and its focus node to the GameWidget.
A tanker going up, a bridge falling, sparks off a shield and a
helicopter's burst now play at their place on the river: quieter far up
it and from the side of the screen they are on, heard from the chasing
camera. The jet's own sounds stay flat, as on the cartridge.
WrapSpace wraps its children round a rectangle, draws a ghost of each one
near an edge on the other side, and gives it ghost hitboxes one world
across, so a rock half over the edge is seen on both sides and a shot
coming in on the left hits it on the right, through Flame's own collision
detection.

buildPolyline joins a closed line's ends with an elbow.
OpenPath measures a path in metres along it, with a heading that turns
through corners, and ribbon lays a strip of road on it between two
distances. BridgeSpace says where a Flame point goes in the scene;
BridgePlane is the flat one and CurvilinearSpace lays Flame's world along
a path, so a racing game keeps its cars side by side in Flame while the
road winds under them.
The engine gains Atmosphere, the sky, fog, sun and ambient light blended
as one, with AtmosphereCycle for a day of them and LightGroup to dim
lamps together; Material.fogged for a horizon that stands beyond the fog;
CellGrid, cells worn away and drawn as merged blocks; and LineStripNode,
a line that grows in place and bounds itself by its points.

The bridge runs a day on Flame's clock with AtmosphereComponent, wears a
CellGridComponent away where a shot lands, and lays a TrailComponent
behind a bridged missile.
CharacterBodyComponent carries a bare CharacterController across the
bridge and steps it with a drive, in the game's fixed steps when it has
them, so the platformer's runner, which already climbs ladders and ropes,
can be a Flame game's player. The bridge's README and roadmap say what
the bridge now covers and what is left.
pad_input reads up to four controllers, each in a player's slot: GamepadPlatform.readPad and Gamepad(index:) pick the slot. On macOS and iOS the plugin reads every GameController pad and lights its player index; in the browser slots follow the standard pads' order; on Android every device is reported and its buttons are forwarded natively with their device, so two pads pressing A are no longer one A and a key-sending d-pad is the pad's own.
River Sortie draws pixel-art sprites in code and stands them in the scene as Flame sprite billboards: reeds and bushes along each stretch's banks, placed the same every time the stretch is built, and a one-shot flash in every blast. BillboardAtlas lets billboards share one texture, material and card set per image, and SpriteBillboardComponent gains removeOnFinish.
A billboard can now carry lettering. BillboardAtlas.spriteOfText writes a
string with Flame's TextPaint into a sprite, and SpriteBillboardComponent
gains smooth, for linear sampling, and a sprite setter that uploads the
new picture before the card switches to it.

Each fuel depot in River Sortie stands a FUEL sign on its near side, a
child of the depot so it goes up with it. Drawing the sprites now happens
once however often it is asked: the game and a caller both asking dressed
the banks twice, and every reed stood doubled.
Renderer.renderPost queued bloom's pooled targets for release but never
drained a slot of the ring or moved the frame counter; only render did.
A host calling renderPost alone allocated a fresh chain on every call and
the pool grew without bound, which on WebGL is a leak.

The slot retirement render did at its top is now _retireFrameSlot, and
renderPost calls it first and advances the counter when it is done, in a
finally that also closes its timeline block.
The modeller's MCP server was bound once, to the history the window opened
with. Opening a file, starting a new project or recovering an autosave
installed a new ModelHistory, and every tool call after that landed on the
old one: it reported success, and nothing was drawn or saved.

ModelSession.rebind points a session at a new history, attaching the
recovery journal and forgetting the export cache, since ids start again.
Every opening path goes through _installOpened, which now calls
rebindMcpHistory. The session is held before the socket is bound, so an
open during start-up rebinds too, and a port already taken lets a later
start try again.
The Play screen's frame is drawn inside the viewport's build, and the
route is opaque, so the editor's ticker beneath it is muted. Nothing asked
the screen to rebuild, and the game moved only when something else did: a
held W walked nowhere. The screen now keeps a ticker of its own.
_onTick called setState on the modeller's screen every tick, rebuilding
the panels, the outliner and every field sixty times a second to move a
camera, and kept the main viewport rendering under dialogs that render a
viewport of their own.

The tick now bumps a notifier. ModelerViewport takes it as frames and
redraws itself; the main viewport is configured inside a builder on it, so
what follows the camera or the pointer stays current. The orientation
dial, the retopology overlay and the LOD panel's reading follow the camera
the same way, and the status line's frame time is a listenable written
once a tick rather than during layout. What used to become visible only
through the per-frame rebuild now asks for one: undo and redo when a
stroke closes its transaction, the playhead in animation mode, a
measurement report.
The tablet shell laid its actions out unbounded, and at the width a
desktop window opens at they overflowed by 172 pixels, which the modeller
reports as a crash on launch. They are now capped and scroll, as the
desktop bar's already are.
- The host cleared the game's redrawer3d by comparing it with a fresh
  tear-off of _redraw, which is never identical, so a game that outlived
  its widget kept the disposed host. The tear-off is now taken once.
- A host adds its clock only once it is showing the game. Added from the
  first build, a host still opening or one that failed to start ticked a
  game another host was showing, and every update called onTick twice.
- New overlay builders under the same names reach the screen through the
  current config without a new GameWidget, which updated the game again
  from layout on every rebuild of a parent writing its map inline.
- BillboardAtlas keys materials by sampling as well as image, makes no
  texture for an upload that finishes after dispose, and gives textures
  back after the frames in flight through Renderer.releaseTextureAfterFrame,
  which is new.
- CharacterBodyComponent and KinematicBodyComponent take removeFrom, as
  RigidBodyComponent does, so a despawned character or lift leaves the
  collision world.

The documents' test counts move with the tests of this and the last
commits.
What the engine already has for a top-down co-op crawler, what it lacks,
and in which order to build it: the horde measured first, then several
foci swept by one multi-source flow field, heroes and a shared camera,
generators, the rest of the rules, and an app.
flutter3d_core, flutter3d and flame_flutter3d say what this branch fixed
in them since the entries already written, and flutter3d_model_mcp goes to
0.8.1 for ModelSession.rebind.
The page was built from the showcase's catalog alone, and flame_flutter3d
has no showcase pages, so none of its releases appeared. Each release now
also lists the bridge's changes: the bold claim every CHANGELOG entry opens
with, linked to the full record on pub.dev, folded when a release has more
than eight. A release that changed only the bridge gets a section of its
own.
tool/horde_cost.dart steps fifty to four hundred awake chasers through a
thirty-six-room maze behind a focus that never stops crossing cells. Two
hundred cost 2.1 ms a step, an eighth of a frame; four hundred cost 7.8 ms,
and the growth past two hundred comes from bodies packing round the focus,
not from the field, which is one sweep whatever the count.
ActorSystem.step takes a list of foci beside the single focus it always
took. The flow field is swept from all of them at once and records which
one each cell's route ends at, so several players still cost one sweep per
class of body, and each actor attends to the nearest by walking: a monster
behind a wall goes round to the player it can reach, not through the stone
to the one it is closest to. Without navigation it is the nearest in a
straight line.

Damage to the foci is counted per focus through hurtFocus, and ChaseBrain
uses it, so a claw that catches the player beside the one it swung at
counts against the one it caught. Velocities are measured per focus and a
save carries every one; a single focus saves under the key it always had.
flutter3d_game_crawler is a new genre package: one to four heroes, each a
class with its own speed and armour, whose health drains a point a second
until food puts it back. Loot is taken by walking into it; a key opens any
one locked door and is spent. Monsters are stepped with every living hero
as a focus, so each goes for the hero it can reach first and its blows
land through that hero's armour.

The heroes share one view and none of them can walk out of it. The edge
is read off CrawlFraming, which is worked out from where the heroes stand
and numbers the game fixes, so it gives the same answer after a load and
on every machine; CrawlCamera only eases towards it through CameraRig.

flutter3d_sim goes to 0.8.1 for the several foci the crawl stands on.
A snapshot fills in a world that already exists, so an actor spawned after
the level loaded had nothing to be filled in when the save was loaded
afresh, and a rollback past its birth left one too many.
ActorSystem.spawn(entity:) builds an actor under an entity a restore put
back, and EcsWorld.vacant says whether a slot is one: restore the
allocation, build each recorded actor under its own entity, restore again
for the numbers. Same index and order, so the restored run thinks on the
same beat as the one that was saved.

Takeable.restore removed the trigger of a taken thing and never put it
back, so a rollback to before the taking left it drawn and untakeable.
It adds the trigger back now.
A Generator makes a MonsterKind every period until its cap is alive and
stops when it is broken. The Horde keeps every monster, removes the dead,
and records each one's kind, generator and entity, so a save loaded into a
fresh world or rolled back past a birth continues as the run did; both are
checked against a run that was never interrupted.

Held fire shoots along the way a hero last walked, through other heroes
and into the first wall, monster or generator. Walking into a monster is
attacking it at the class's melee rate, a potion reaches everything in
view scaled by the class's magic, and kills score for whoever made them.
Death drains fast, cannot be shot or fought, and leaves once it has dealt
its fill; only a potion reaches it. A thief harms nobody: it takes a
potion, or a key, from the hero it reaches and runs, and is gone with it
after four seconds unless somebody kills it, which gives the thing back.

crawlerRegistry reads the format's spawns, doors and exits with food,
keys, potions, treasure, generators and placed monsters, and stageCrawl
turns a level into a crawl with a hero on each spawn by its slot. Loot and
generators an author left unnamed are saved under a name made from where
they stand, so food eaten before a save stays eaten after the load.
ActorSystemComponent steps several foci and opens the system's step at the
start of each game step, so a death caused by the game's own logic is still
there to read. StepClock lets anything that steps - the game itself among
them - tell ActorComponent, CharacterBodyComponent and the physics bodies to
keep their places before it moves them, so a body the game moves is drawn
between its steps rather than where it already is.

An ActorComponent goes when its actor does, and can take the actor with it.
InstancedActorComponent and InstancedPoseComponent draw a horde and its
shots as slots of one batch. Keys can come from HardwareKeyboard instead of
the game's focus, PlayerSeats lets players join by pressing, a game moves to
its next level's scene with replaceScene3d, FixtureVisualsComponent keeps a
level's furniture drawn and released, and ViewCamera frames whatever a
function says.
apps/flutter3d_demo_crawler plays flutter3d_game_crawler on a screen: a
select screen where players join by pressing fire, three levels written by
flutter3d_editor_core/tool/levels/crawler.dart, the horde drawn one batch per
kind, and the party carried from level to level. It runs its own loop, input
and renderer for now; the next change moves it onto Flame and the bridge.

The levels are rooms on a grid with no ceilings and one wall per grid edge,
so the view from above sees into them and no two walls overlap. A bot walks
the first one to its exit in a test. Shadows are drawn with one cascade: on
Metal the near cascades left a black disc on the floor under the camera.
CrawlerGame is a FlameGame with HasFlutter3d and HasFixedStep: the crawl's
own step runs in its fixedUpdate, and the game is the StepClock the
heroes and the horde are drawn between steps by. Players join through
PlayerSeats over two keyboard layouts and four controller slots, read from
the keyboard itself. Heroes are CharacterBodyComponents, monsters
InstancedActorComponents over one batch a kind, shots InstancedPoseComponents,
the level's furniture a FixtureVisualsComponent, and the view a ViewCamera
turned by the crawl's framing; the next level is replaceScene3d.

The app's own loop, renderer, input and visuals are gone. The frame test
loads the game itself with no widget, steps it and draws a frame. Grunts
are purple now: brown, they were the colour of the pillars.
HasFixedStep keeps its list of stepping components and walks the tree only
when one comes, goes or changes priority. A pad is read against this
frame's time through HasFixedStep.frameSeconds. An upright billboard turns
about its own plane's normal and writes its rotation only when it changed.

The crawl's view starts at fifteen metres and opens to twenty-six: at ten a
party standing together saw one room, and monsters arrived from off screen.
0.8.3 ordered a CameraSyncComponent after Flame's CameraComponent by its
priority, but a priority orders siblings only: added to the world, where a
game adds its components, it ran inside the world's update, before the
camera, and the 3D camera trailed camera.follow() by a frame again.

UpdatesAtRoot does a component's frame work from a driver at the game's
root, at the component's own priority, when it is mounted anywhere else.
The camera sync has it, and so do the two others that assumed the same:
the input step's end, which closed a press before a button in the
viewport had read it, and AudioSceneComponent, whose listener stood where
the camera had been a frame before.
The crawler gets an online mode over the flutter3d_net relay: one machine
makes a room, the other joins, and each drives its own hero. A handshake
trades classes so both put the party in the room's order, each level runs
its own rollback session on its own channel of one wire, and a level ends
only on a step both machines have agreed on. The next level starts from
that agreed state on both.

net_crawl_test holds two crawls against each other and against a run that
never guessed, over a late and lossy loopback; online_game_test does the
same through two whole CrawlerGames, handshake included.
flame_multiplayer holds what the crawler's co-op had grown on its own:
a room with a handshake and separate channels, rollback play in step with
an ending both machines agree on, turns where one machine plays and the
other replays it, and a feed for a ghost. It depends on nothing. A wire is
an adapter: NetTransportWire in flutter3d_net for its relay and WebRTC,
and flame_multiplayer_dashwire for a dashwire connection, whose binary
WebSocket passes through the same relay.

The crawler's online co-op now runs on RollbackPlay. River Sortie gets two
players: turns on one machine, turns across two with the other watching a
replay, and a race against a ghost on the same river.
flutter3d_net's NetSession wraps flame_multiplayer's RollbackSession over
a Snapshot, keeping its constructor, defaults and wire format, so the
repository has one rollback rather than two. droppedCorrections becomes a
getter.
The co-op crawler, its genre package and its level generator leave this
branch, and River Sortie goes back to one player; both live on the
multiplayer branch until that work is ready to be shown. The package
count, the test counts and the publishing order follow.

The article on the Flame bridge gets two pictures of River Sortie: a
stretch of river in play and a bridge coming down.
flame_flutter3d and flame_flutter3d_audio go to 0.8.4, the audio package
asking for the bridge that has UpdatesAtRoot. flutter3d_game_shooter goes
to 0.8.1: its chase brain credits a hit through hurtFocus, which is
flutter3d_sim 0.8.1's, so it asks for that release.

flutter3d_sim 0.8.1 and flutter3d_net 0.8.1 keep what 0.8.0 code wrote
to: ActorSystem.focusBody can still be set, deprecated since step
overwrites it, and NetSession.droppedCorrections still writes through to
the shared session. Code built against 0.8.0 compiles against both.
Three pages for flame_flutter3d 0.8.4. Two keyboard layouts claim seats by
pressing, keys read from the keyboard rather than the focus. Ninety-six
simulated monsters take slots in one instanced batch and each chases the
nearer of two players, drawn between their steps. A game moves between two
level scenes with replaceScene3d while a view camera keeps the whole party
in frame.

Each page has a guide and a test that fails when its feature is taken away.
The showcase moves to 0.8.4+1 so its pages may name that release.
@dzolotov dzolotov changed the title flutter3d 0.8.3: River Sortie, and what building it asked of the engine flutter3d 0.8.3 and 0.8.4: River Sortie, and what building it asked of the bridge Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant