docs

TOP(About this memo)) > Flutter > 一覧(Flame) > GameWidget/Game/Component

GameWidget

Game

Component

(参考)GameWidgetState内部の実装

  @override
  @mustCallSuper
  void render(Canvas canvas) {
    if (parent == null) {
      renderTree(canvas);
    }
  }

  @override
  void renderTree(Canvas canvas) {
    if (parent != null) {
      render(canvas);
    }
    for (final component in children) {
      component.renderTree(canvas);// Component.renderTreeはrenderを呼び、子のrenderTreeを呼ぶ
    }
  }

  @override
  @mustCallSuper
  void update(double dt) {
    if (parent == null) {
      updateTree(dt);
    }
  }

  @override
  void updateTree(double dt) {
    processLifecycleEvents();
    if (parent != null) {
      update(dt);
    }
    for (final component in children) {
      component.updateTree(dt);// Component.updateTreeはupdateを呼び、子のupdateTreeを呼ぶ
    }
    processRebalanceEvents();
  }
  void processLifecycleEvents() {
    //...
      for (final event in _queue) {
        //...
        final status = switch (event.kind) {
          final child = event.child!;
          final parent = event.parent!;
          _LifecycleEventKind.add => child.handleLifecycleEventAdd(parent),
          _LifecycleEventKind.remove =>
            child.handleLifecycleEventRemove(parent),
          //...
        };
        //...
      }
    //...
  }
    // 親がマウントされていて、かつロードが完了している場合はマウントする。
    // 親がマウントされていて、かつロードを開始していない場合はロードを開始する。
    // --> ただし、_addChildの最後に_startLoading()を実行しているため、addを実行したタイミングで基本的にはローディングは即時開始されていると考えられる。
    //     そのため、この分岐がどういったケースで通るのかは理解できていない。
    // 親がマウントされていない場合 または ローディングが完了していない場合は、マウントを見送る
    //   ※ この場合はキューから削除されないため、次回のゲームループの際に再度処理される。(ロードが終わるまではキューに残り続ける)
  @internal
  LifecycleEventStatus handleLifecycleEventAdd(Component parent) {
    assert(!isMounted);
    if (parent.isMounted && isLoaded) {
      _parent ??= parent;
      _mount();
      return LifecycleEventStatus.done;
    } else {
      if (parent.isMounted && !isLoading) {
        _startLoading();
      }
      return LifecycleEventStatus.block;
    }
  }

  @internal
  LifecycleEventStatus handleLifecycleEventRemove(Component parent) {
    if (_parent == null) {
      parent._children?.remove(this);
    } else {
      _remove();
      assert(_parent == null);
    }
    return LifecycleEventStatus.done;
  }

  // 親のchildrenから自身を削除して、
  // 自身と子孫についてすべて親をnull、onRemoveを呼ぶ、removedフラグを立てる、等の処理をしている。(再帰的に_removedを実行するわけではない。) 
  void _remove() {
    //...
    _parent!.children.remove(this);
    propagateToChildren(
      (Component component) {
        component
          ..onRemove()
          .._unregisterKey()
          .._clearMountedBit()
          .._clearRemovingBit()
          .._setRemovedBit()
          .._removeCompleter?.complete()
          .._removeCompleter = null
          .._parent!.onChildrenChanged(component, ChildrenChangeType.removed)
          .._parent = null;
        return true;
      },
      includeSelf: true,
    );
  }

参考: FlameGameのonRemove処理

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      home: MyWidget(),
    ),
  );
}

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  bool useGame = true;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: useGame ? GameWidget(game: MyGame()) : null,
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          useGame = !useGame;
          setState(
            () {},
          );
        },
        child: Text(useGame ? "detach game" : "attach game"),
      ),
    );
  }
}

class MyGame extends FlameGame with TapCallbacks {
  MyGame() {
    debugMode = true;
  }

  late Parent p;

  @override
  Future<void> onLoad() async {
    add(p = Parent(size: size));
  }

  @override
  void onTapUp(TapUpEvent event) {
    // remove処理はゲームループにおいて、FlameGame.updateTreeによって
    // キューによって処理される。
    // この処理の際は全ての子孫のonRemoveも実行される。
    remove(p);
  }

  @override
  void onAttach() {
    // attach(GameWidgetがFlutterのツリーにアタッチされた時)した際に呼ばれる
    // ※ GameRenderBox.attachから呼ばれる
    print("onAttach MyGame");
  }

  @override
  void onDetach() {
    // detach(GameWidgetがFlutterのツリーからデタッチされる時)した際に呼ばれる
    // ※ GameRenderBox.detachから呼ばれる
    // onDetachは(なぜか)アタッチの前にも呼ばれる
    print("onDetach MyGame");
  }

  @override
  void onDispose() {
    // GameWidget.disposeの際に呼ばれる。
    print("onDispose MyGame");
  }

  @override
  void onRemove() {
    // 通常のコンポーネントのonRemoveとは異なり、この関数はゲームループ内の
    // キュー処理で呼ばれるものではない点に注意。
    // Game.onRemoveはGameWidget.disposeやGameWidget.didUpdateWidgetから呼ばれる。
    //  ※ didUpdateWidgetの場合は新旧のオブジェクトが異なる時のみ。
    print("onRemove MyGame");

    // 上記の事由により、onRemove内でremoveを子に対して呼ぶのみでは、
    // 実際の削除・クリーンアップ処理は(キュー処理によって)実行されないため注意。
    //  ※ 下記のprocessLifecycleEvents以降をコメントアウトして実行すると
    //     子孫のonRemove内のprintが出力されないことからも分かる。
    // これは、GameWidgetがdisposeされると、ゲームループ自体が終了するため。
    // 配下のchildrenのクリーンアップも全て実行したいのであれば
    // 下記のように実行する。
    // https://docs.flame-engine.org/latest/flame/game.html#lifecycle
    removeAll(children); //キューにいれる
    processLifecycleEvents();//キューの処理関数を直接実行する。
    Flame.images.clearCache();
    Flame.assets.clearCache();
  }
}

class Parent extends PositionComponent {
  Parent({super.size});
  late Child ember;

  @override
  Future<void> onLoad() async {
    add(ember = Child(position: Vector2(size.x / 2, size.y / 2)));
  }

  @override
  void onRemove() {
    print("onRemove Parent");
  }
}

class Child extends TextComponent {
  Child({super.position, super.key})
      : super(
          size: Vector2.all(100),
          anchor: Anchor.center,
        );

  @override
  Future<void> onLoad() async {
    text = "child";
    add(GrandChild());
  }

  @override
  void onRemove() {
    print("onRemove Child");
  }
}

class GrandChild extends PositionComponent {
  @override
  void onRemove() {
    print("onRemove GrandChild");
  }
}

非同期処理をまたがるgameの参照の注意

void main() {
  runApp(
    SafeArea(child: GameWidget(game: Example())),
  );
}

class Example extends FlameGame {
  @override
  Future<void> onLoad() async {
    late Component component;
    add(
      component = MyComponent(),
    );
    remove(component);
  }

  // 上記の例では、一つの関数内で同じコンポーネントをadd/removeしているが、
  // 例えば下記のように別のコンポーネントのremoveとaddを行っているケースも注意が必要である。
  // この関数が、次回のゲームループまでに2回以上呼ばれると、同一の処理内(イベントループ内)で同一コンポーネントが
  // add/removeされるため、同じ問題が発生する。
  // void resetChildren() {
  //   removeAll(children);
  //   add(
  //     MyComponent(),
  //   );
  // }
}

class MyComponent extends Component with HasGameRef {
  MyComponent();

  @override
  Future<void> onLoad() async {
    await super.onLoad();// awaitが完了する頃には、removeの処理まで完了している。
    print(game);// assert error: Could not find Game instance ...
  }
}

flame_forge2dのBodyComponent