flutter3d
Showcase Changelog 38 packages API reference

Bridging a flutter3d_sim actor

since 0.7.0 The Flame bridge

ActorComponent wraps one flutter3d_sim Actor the same way Object3dComponent wraps any other bridged transform, plus the one extra hop an actor needs: its body's simulated position lives on a CharacterController, stepped by ActorSystem, not on the SceneNode itself. ActorSystemComponent is the one place a bridged game steps that shared system, once a frame.

Step 1: A brain that decides #

A game's own brain extends Brain and overrides only the hooks it cares about. This one always walks towards +x.

/// Walks straight towards +x, every step, for as long as it lives.
final class _WalkBrain extends Brain {
  @override
  void act(Mind it) => it.steer(Vector3(1.0, 0.0, 0.0));
}

Step 2: An actor with a body #

ActorSystem.spawn gives an entity a CharacterController body and a brain, and hands back a thin Actor handle onto both.

final world = CollisionWorld();
world.addBox(Vector3(0.0, -0.5, 0.0), Vector3(20.0, 1.0, 20.0));
final system = ActorSystem(world: world, random: GameRandom(1));
final controller = CharacterController(
  world: world,
  position: Vector3(0.0, 3.0, 0.0),
);
final goblin = system.spawn(
  body: controller,
  brain: _WalkBrain(),
  name: 'goblin',
);

Step 3: Bridge it, then step it #

ActorComponent bridges the actor's body to a SceneNode; ActorSystemComponent is the only thing that calls ActorSystem.beginStep/step, once a frame — never the actor component itself, which would double-step a shared system.

final scene = Scene();
final node = MeshNode(
  DeviceMesh.upload(device, SphereShape(segments: 16).build()),
  Material(name: 'bridged-goblin', baseColor: Vector4(0.5, 0.7, 0.3, 1.0)),
);
scene.add(node);
final actorComponent = ActorComponent(
  actor: goblin,
  node: node,
  scene: scene,
  plane: BridgePlane.ground(),
);
final systemComponent = ActorSystemComponent(
  system: system,
  focus: Vector3.zero,
);
// The system decides where the goblin's body goes; the actor component
// copies that body's position onto the node, then the plane it shares
// carries it onto the Flame side — the same seam `Object3dComponent`
// gives every other bridged transform.
for (var i = 0; i < 90; i++) {
  systemComponent.update(1 / 60);
  actorComponent.update(1 / 60);
}

Ninety steps of the shared system moved the goblin's real physics body forward, and the bridged Flame position read back exactly the body's own x — the mesh followed the actor, through the bridge, with nothing hand-copied in between.

Step 4: Three goblins on the map #

The same pieces, three times over. Every goblin is an actor of one shared system; each has a bridge component that copies its body onto its mesh and, through the plane, onto a Flame position, and the dots on the map are those Flame positions. One ActorSystemComponent steps the whole system once a frame. A goblin that walks off the far edge is put back on the near one, so the walk goes on.

// Three goblins in one shared system, each with a bridge component that
// copies its body onto its node and, through the plane, onto a Flame
// position. One `ActorSystemComponent` steps the system, once a frame; the
// map's dots are those Flame positions.
final CollisionWorld world = CollisionWorld()
  ..addBox(Vector3(0.0, -0.5, 0.0), Vector3(20.0, 1.0, 20.0));
final ActorSystem system = ActorSystem(world: world, random: GameRandom(1));
final FlameMinimap map = FlameMinimap();
final List<Component> bridges = <Component>[];
final List<CharacterController> bodies = <CharacterController>[];
for (var i = 0; i < 3; i++) {
  final CharacterController body = CharacterController(
    world: world,
    position: Vector3(-_edge + i * 1.5, 1.0, (i - 1) * 2.0),
  );
  bodies.add(body);
  final Actor goblin = system.spawn(
    body: body,
    brain: _WalkBrain(),
    name: 'goblin $i',
  );
  final MeshNode node = MeshNode(
    DeviceMesh.upload(
      context.device,
      SphereShape(segments: 24, radius: 0.4).build(),
    ),
    Material(
      name: 'goblin $i',
      baseColor: Vector4(_colors[i].r, _colors[i].g, _colors[i].b, 1.0),
    ),
    name: 'goblin $i',
  );
  _scene.add(node);
  bridges.add(
    ActorComponent(
      actor: goblin,
      node: node,
      scene: _scene,
      plane: BridgePlane.ground(),
    )..add(flameDot(_colors[i])),
  );
}
map.world.addAll(bridges);
_game = TransparentFlameGame()
  ..add(ActorSystemComponent(system: system, focus: Vector3.zero))
  // After the system, so the bodies have moved by the time they are read.
  ..add(_Wrap(bodies))
  ..add(map);