Keeping a Flame position and a flutter3d node in step
Open the live demo · Read the source · View on GitHub
Object3dComponent is the one place a Flame PositionComponent and a
flutter3d SceneNode are kept at the same place, on one BridgePlane. A
SyncDirection decides which side is authoritative each frame; nothing here
infers a direction from which value changed most recently.
Step 1: The scene is authoritative #
The default direction, SyncDirection.sceneToFlame, is what every existing
flutter3d system — a rigid body, an actor — already wants: it decides where
things are, and the Flame component only ever reads that. Moving the node and
calling update carries the move onto the Flame side.
// The flutter3d side moves; `SyncDirection.sceneToFlame` is the default,
// so `Object3dComponent` reads the node and writes the Flame component.
final node = MeshNode(
DeviceMesh.upload(
device,
CuboidShape(size: Vector3(0.4, 0.4, 0.4)).build(),
),
Material(
name: 'scene-authoritative',
baseColor: Vector4(0.5, 0.7, 0.9, 1.0),
),
);
scene.add(node);
final sceneToFlame = Object3dComponent(
node: node,
scene: scene,
plane: plane,
);
node.setPosition(3.0, 0.0, 2.0);
sceneToFlame.update(1 / 60);
final Vector2 flamePosition = sceneToFlame.position;
Step 2: Flame is authoritative #
SyncDirection.flameToScene reverses it: the Flame component decides, and
update writes that position onto the node instead — a Flame-driven prop
that should also draw as a 3D object, say.
// The Flame side moves instead; `SyncDirection.flameToScene` makes
// `Object3dComponent` read the Flame component and write the node.
final otherNode = MeshNode(
DeviceMesh.upload(
device,
CuboidShape(size: Vector3(0.4, 0.4, 0.4)).build(),
),
Material(
name: 'flame-authoritative',
baseColor: Vector4(0.9, 0.6, 0.3, 1.0),
),
);
scene.add(otherNode);
final flameToScene = Object3dComponent(
node: otherNode,
scene: scene,
plane: plane,
direction: SyncDirection.flameToScene,
);
flameToScene.position = Vector2(4.0, -1.0);
flameToScene.update(1 / 60);
final Vector3 scenePosition = otherNode.readPosition();
Step 3: One BridgePlane, both directions #
Both components above share the same ground plane, so a Flame (x, y) and a
flutter3d (x, height, y) mean the same point everywhere in this scene.
// One ground plane, shared by both components below, so a Flame (x, y)
// and a flutter3d (x, height, y) mean the same point everywhere in this
// scene.
final plane = BridgePlane.ground();
A node moved to (3, 0, 2) reads back as Flame (3, 2); a Flame position set
to (4, -1) reads back as scene x=4, z=-1.
The node that moved carried its position onto the Flame side, and the Flame position that moved carried its onto the node — both readings came back exactly where the bridge's own plane said they would.
Step 4: Watch them keep step #
On the page the blue cube is the scene-authoritative one: flutter3d moves it round in a circle, and the blue dot on Flame's map follows. The orange cube is the reverse: Flame slides its component along a figure of eight, and the cube in the scene follows. Drag to turn the view and compare the floor with the map.
// The same two components as above, now in a running game: each frame
// Flame updates them and each copies one way. The map in the corner is
// Flame's own view of the ground plane, a metre to 24 pixels.
final BridgePlane plane = BridgePlane.ground();
final Object3dComponent reads = Object3dComponent(
node: _leader,
scene: _scene,
plane: plane,
)..add(flameDot(const Color(0xFF80B3E6)));
final Object3dComponent writes = Object3dComponent(
node: follower,
scene: _scene,
plane: plane,
direction: SyncDirection.flameToScene,
)..add(flameDot(const Color(0xFFE6994D)));
final FlameMinimap map = FlameMinimap()
..world.addAll(<Component>[reads, writes]);
_game = TransparentFlameGame();
_game
// Before `writes`, so the position is set before it is copied.
..add(_Drift(writes))
..add(map)
..add(flameCaption('blue: the node moves, Flame reads it'))
..add(
flameCaption(
'orange: Flame moves, the node reads it',
at: Vector2(16.0, 40.0),
),
);