TOP(About this memo)) > Flutter > 一覧(Flame) > GameWidget/Game/Component
GestureDetector(
onTap: () {
print("GestureDetector called");//こちらの処理は反応しない
},
child: GameWidget<_MyGame>(
//...
),
);
コンポーネントは、Flutter のウィジェットや Unity の GameObject に非常に似ています。
ゲーム内のあらゆるエンティティは、特にそのエンティティに何らかの視覚的外観がある場合や、時間の経過とともに変化する場合は、コンポーネントとして表すことができます。
たとえば、プレイヤー、敵、飛んでくる弾丸、空の雲、建物、岩など、すべてをコンポーネントとして表すことができます。
一部のコンポーネントは、エフェクト、動作、データ ストア、グループなど、より抽象的なエンティティを表すこともできます。
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
SafeArea(child: GameWidget(game: Example())),
);
}
class Example extends FlameGame with TapCallbacks {
Example() {
debugMode = true;
}
late Parent c;
@override
Future<void> onLoad() async {
add(c = Parent(size: size));
}
@override
void onTapUp(TapUpEvent event) {
remove(c);
}
}
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 = "test";
add(GrandChild());
}
@override
void onRemove() {
print("onRemove Child");
}
}
class GrandChild extends PositionComponent {
@override
void onRemove() {
print("onRemove GrandChild");
}
}
FutureOr
// ~/.pub-cache/hosted/pub.dev/flame-1.18.0/lib/src/components/core/component.dart
FutureOr<void> _addChild(Component child) {
//...
if (!child.isLoaded && !child.isLoading && (game?.hasLayout ?? false)) {// 1回のみ呼ばれる
return child._startLoading();
}
}
FutureOr<void> _startLoading() {
assert(_state == _initial);
assert(_parent != null);
assert(_parent!.findGame() != null);
assert(_parent!.findGame()!.hasLayout);
_setLoadingBit();
final onLoadFuture = onLoad();//ここで呼ばれている
if (onLoadFuture is Future) {
return onLoadFuture.then((dynamic _) => _finishLoading());
} else {
_finishLoading();
}
}
class Example extends FlameGame with DragCallbacks, ScaleDetector {
Example() {
// 動作する
add(TextComponent(text: "test", position: Vector2(30, 30)));
// sizeが決定していないため実行時エラーとなる
//add(TextComponent(position: Vector2(size.x/2, size.y/2)));
}
}
void onGameResize
// ~/.pub-cache/hosted/pub.dev/flame-1.18.0/lib/src/components/core/component.dart
@mustCallSuper
void onGameResize(Vector2 size) => handleResize(size);// onGameResizeでは、子のonGameResizeも呼び出す
//...
@mustCallSuper
@internal
void handleResize(Vector2 size) {
_children?.forEach((child) {
if (child.isLoading || child.isLoaded) {
child.onGameResize(size);// ここで呼ばれる
}
});
}
//...
void _mount() {
assert(_parent != null && _parent!.isMounted);
assert(isLoaded && !isLoading);
_setMountingBit();
onGameResize(_parent!.findGame()!.canvasSize);//ここで呼ばれる
//...
onMount();
//...
_parent!.children.add(this);
//...
_clearMountingBit();
//...
}
void onMount
void onRemove
// flame-1.19.0/lib/src/game/game_widget/game_widget.dart
Future<void> get loaderFuture => _loaderFuture ??= (() async {
//...
await game.load();
game.mount();
//...
})();
//...
@override
Widget build(BuildContext context) {
Widget? internalGameWidget = RenderGameWidget(
//...
);
// ...
return FutureBuilder(
future: loaderFuture,
//...
);
//...
}
void gameLoopCallback(double dt) {// これはattach時のGameLoop生成時にcallbackとして渡される
assert(attached);
if (!attached) {
return;
}
game.update(dt);//FlameGame.updateはchildrenのupdateTreeを呼び出す
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
context.canvas.save();
context.canvas.translate(offset.dx, offset.dy);
game.render(context.canvas);//FlameGame.renderはchildrenのrenderTreeを呼び出す
context.canvas.restore();
}
class GameLoop {
GameLoop(this.callback) {
_ticker = Ticker(_tick);
}
void Function(double dt) callback;
//...
void _tick(Duration timestamp) {
final durationDelta = timestamp - _previous;
final dt = durationDelta.inMicroseconds / Duration.microsecondsPerSecond;
_previous = timestamp;
callback(dt);// RenderGameWidget.gameLoopCallbackが実行される
}
//...
void start() {
if (!_ticker.isActive) {
_ticker.start();
}
}
//...
}
@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,
);
}
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");
}
}
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 ...
}
}
await /* ... */
if (!isMounted) return;
print(game);
// flame_forge2d-0.18.2/lib/body_component.dart
Body createBody() {
assert(
bodyDef != null,
'Ensure this.bodyDef is not null or override createBody',
);
final body = world.createBody(bodyDef!);
fixtureDefs?.forEach(body.createFixture);
return body;
}
@mustCallSuper
@override
Future<void> onLoad() async {
await super.onLoad();
body = createBody();
}
Forge2DWorld get world => game.world;