ずっと気になってたFlutterのゲームエンジン「Flame」
とりあえず、ドキュメントを読みつつ、
いろいろ整理したときの備忘録(*´ω`*)
Flameとは | Getting Started
Flutter用のゲームエンジン。
シンプルで効果的なゲームループとゲームに必要な機能を提供。
入力、スプライト、アニメーション、衝突判定など。
Flame Component System(FCS)と呼ばれるコンポーネントシステムもある。
また、Bridge Packagesと呼ばれる、他のライブラリとの連携パッケージも提供。
AudioPlayers / Lottie / Riverpodなどなど
チュートリアルやサンプルなども用意されている。
Flameにはネットワーク機能はないので、複数人のオンラインゲーム等の場合は、
以下のパッケージの利用がおすすめされている。
- Nakama: Nakama is an open-source server designed to power modern games and apps.
- Firebase: Provides dozens of services that can be used to write simpler multiplayer experiences.
- Supabase: A cheaper alternative to Firebase, based on Postgres.
フォルダ構成 | File Structure
デフォルトのフォルダ構成は、こんな感じを期待。
.
└── assets
├── audio
│ ├── explosion.mp3
├── images
│ ├── enemy.png
│ └── player.png
└── tiles
└── level.tmx
既定の場所に配置されていると、以下の感じでファイルを読み込める。
void main() { FlameAudio.play('explosion.mp3'); Flame.images.load('player.png'); Flame.images.load('enemy.png'); TiledComponent.load('level.tmx', tileSize); }
もちろん、pubspec.yamlにも設定は必要。
flutter: assets: - assets/audio/explosion.mp3 - assets/images/player.png - assets/images/enemy.png - assets/tiles/level.tmx
audioは、以下の2つに分けてもOK
music... BGMなどの音楽sfx... 効果音
.
└── assets
├─── music
│ └── bgm.mp3
└─── sfx
└── button.mp3
登場人物
主要な登場人物はこんな感じ
- Gameインスタンス
- Flameでつくるゲームの起点、エンジン部分っぽい
- 低レベルのgame APIへのアクセスを提供するAbstractクラス
- Gameインスタンスに描画したいコンポーネントを追加したりする
- 通常は完全な実装のFlameGameを利用する
- FlameGame — Flame
- Game Widget
- Gameインスタンスを描画するFlutter Widget
- Game Widget — Flame
- Component
- Flame Component System(FCS)で扱えるコンポネントのAbstractクラス
- 用意されているUI/Sprite/Effect/Timerなどの親クラス
- ゲームを構成する要素や部品/パーツの意味だとしっくりくる
- Components — Flame
- World
- 描画する全コンポーネントのルートコンポーネント
- Camera component — Flame
ほかにもRouter/Collision/Effect/Cameraなどいろいろあるけど、
まずはこのあたりが基本。
階層的にはこんな感じ。
Flutter Widget Tree → Game Widget → Gameインスタンス → Worldコンポーネント → 各Component
Game Widget
GameインスタンスをFlutterのWidget treeに描画するためのクラス。
こんな形で利用する感じ。runApp()直下でなくてもOK。
void main() { runApp( GameWidget(game: MyGame()), ); }
GameWidgetはStatefulWidgetを継承。
class GameWidget<T extends Game> extends StatefulWidget
Gameの描画だけでなく、以下も提供している。
- loadingBuilder to display something while the game is loading;
- errorBuilder which will be shows if the game throws an error;
- backgroundBuilder to draw some decoration behind the game;
- overlayBuilderMap to draw one or more widgets on top of the game.
GameWidgetはサイズを超えて描画されるのではみ出ることがあるらしい。
必要に応じて、cameraを調整したり、FlutterのClipRectを使うと良い。
コンストラクタは2つあり、Container配下など、
他のWidgetの中に描画する場合は、GameWidget.controlledを使うといいらしい。
class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(20), child: GameWidget.controlled( gameFactory: MyGame.new, ), ); } }
Gameインスタンス / Game Loop
GameLoopはゲームループの概念をシンプルに抽象化したモジュール。
renderメソッドで、現在の状態を描画するためにキャンバスを取得updateメソッドで、前回の更新から経過した時間を受け取って、次の状態に変更
ここでいう状態というのは、
スコアであったり、キャラの位置だったり、などなど
Componentに用意されているupdate()などの関数をオーバーライドでき、
Gameは追加されたComponentのupdate()の呼び出しを繰り返す。
update()に「呼ばれるたびに右へ1移動する」
のような処理を書けば、自動で移動するコンポーネントができる。
update()を含めたライフサイクルはこんな感じ。

onLoad()で、スプライトや画像の読み込みをおこなったり、
/// A component that renders the crate sprite, with a 16 x 16 size. class MyCrate extends SpriteComponent { MyCrate() : super(size: Vector2.all(16)); @override Future<void> onLoad() async { sprite = await Sprite.load('crate.png'); } }
onRemove()で、子コンポーネントやキャッシュの削除したりにつかう。
@override void onRemove() { // Optional based on your game needs. removeAll(children); processLifecycleEvents(); Flame.images.clearCache(); Flame.assets.clearCache(); // Any other code that you want to run when the game is removed. }
SingleGameInstance mixin
Gameインスタンスはいくつも作れるが、基本は単一。
SingleGameInstance mixinを使うと、
特定の状況でパフォーマンスが上がるらしい。
class MyGame extends FlameGame with SingleGameInstance { // ... }
Pause/Resuming/Stepping game execution
一時停止や再開などの機能も提供されている。
方法は2つあり
pauseEngine()やresumeEngine()メソッドを使うpaused属性を変更する
一時停止すると、GameLoopが停止して、update()などが呼ばれなくなる。
また、開発向けの機能として、一時停止中にstepEngine()を呼ぶと、
1フレーム進めることができる。
Components
全コンポーネントが継承するComponent。
Flame Component System(FCS)のベース。

UIも画像もEffectも全部がこのComponentを継承していて、
Componentからライフサイクルのメソッドが呼ばれる感じ。
Componentには親子関係があり、子コンポーネントを追加したりできる。
void main() { final component1 = Component(children: [Component(), Component()]); final component2 = Component(); component2.add(Component()); component2.addAll([Component(), Component()]); }
Gameインスタンスと同様にライフサイクルがあり、
それぞれを継承して、各コンポーネントを実装していく。

Priority(z-index)
コンポーネントには優先度をつけることができ、
どれを上に描画するかを決めることができる。
priorityの値が大きい順に上に描画される。
class MyGame extends FlameGame { @override void onLoad() { final myComponent = PositionComponent(priority: 5); add(myComponent); } } class MyComponent extends PositionComponent with TapCallbacks { MyComponent() : super(priority: 1); @override void onTapDown(TapDownEvent event) { priority = 2; } }
Visibility of components
もっともよいのはadd、removeでコンポーネント自体を削除すること。
ただ一時的な非表示の場合は、HasVisibility mixinをつかうっぽい。
/// Example that implements HasVisibility class MyComponent extends PositionComponent with HasVisibility {} /// Usage of the isVisible property final myComponent = MyComponent(); add(myComponent); myComponent.isVisible = false;
Effect
いろんなエフェクトが用意されていて、
エフェクトを適用したいコンポーネントに追加すればOK
final effect = MoveEffect.by( Vector2(30, 30), EffectController(duration: 1.0), ); final component1 = MyComponent(); component1.add(effect);
Collision
衝突判定(Collision Detection)は、
HasCollisionDetection mixinやCollisionCallbacks mixinを使って実装する。
Camera / World
横スクロールゲームのように、プレイヤーが移動すると背景が切り替わるような場合、
ステージ全体(=描画するすべての要素)がWorldで、
プレイヤーを中心に見えている範囲がCameraな感じ。
Router
FlutterのNavigatorのようなルーティング機能をもつComponentのサブクラス。
Routeの名前被りのため、hideが必要。
import 'package:flutter/material.dart' hide Route; class MyGame extends FlameGame { late final RouterComponent router; @override void onLoad() { add( router = RouterComponent( routes: { 'home': Route(HomePage.new), 'level-selector': Route(LevelSelectorPage.new), 'settings': Route(SettingsPage.new, transparent: true), 'pause': PauseRoute(), 'confirm-dialog': OverlayRoute.existing(), }, initialRoute: 'home', ), ); } } class PauseRoute extends Route { ... }
通常の画面はRouteを継承して実装する形。
ほかにも、OverlayRouteとValueRouteもある。
OverlayRoute- 一時停止中のダイアログなど、今の画面に重ねて表示したい画面
ValueRoute- 結果画面など、スコアなどの値を受け取って表示したい画面
Tap/Drag/Gesture Eventなど
それぞれmixinが用意されていて、
それを使うことで各コールバックをオーバーライドできる。
class MyComponent extends PositionComponent with TapCallbacks { MyComponent() : super(size: Vector2(80, 60)); @override void onTapUp(TapUpEvent event) { // Do something in response to a tap event } }
Overlays
一時停止画面やメニューなど画面の上に重ねるUIのための機能
こんな感じで、overlayBuilderMapにkeyとbuilderを設定すると、
// On the widget declaration final game = MyGame(); Widget build(BuildContext context) { return GameWidget( game: game, overlayBuilderMap: { 'PauseMenu': (BuildContext context, MyGame game) { return Text('A pause menu'); }, }, ); }
こんな感じで、keyを使って、overlays.addなどで表示させることができる。
// Inside your game: final pauseOverlayIdentifier = 'PauseMenu'; // Marks 'PauseMenu' to be rendered. overlays.add(pauseOverlayIdentifier); // Marks 'PauseMenu' to not be rendered. overlays.remove(pauseOverlayIdentifier);
Bridge Packages
ほかのライブラリを橋渡しするためのパッケージ群。
ゲーム関連のものがいくつも用意されているっぽい。
- 音声/Audio
- 状態管理 ...
- アニメーション
- flame_lottie(Lottie) Adobe After Effects アニメーション
- flame_rive(Rive) インタラクティブアニメーション
- flame_spine(Spine) ゲーム用2Dアニメーション
- 2Dタイルマップ
- テクスチャアトラス ... 複数の画像を一つの画像にまとめたもの
- 物理演算(Box2D)
- その他
- flame_isolate ... 重い処理を別スレッドで実行
- flame_network_assets ... ネットワーク上のアセットの取得
- flame_svg ... SVG画像の描画
- flame_bloc ... Blocでの状態管理
- flame_oxygen ... FCSではない、軽量コンポーネントシステム(Oxygen)への置き換え
- jenny ... Yarn Spinnerを使った会話文/シナリオの表示など
flame_riverpod
flame_riverpodは気になるので、もう少し。
Riverpodは変更があるとをWidgetを再描画してくれるが、
flameではWidgetではないComponentを利用している。
そのため、Componentでも変更に応じて呼び出されるようにする、
WidgetやMixinが提供される
RiverpodAwareGameWidget...GameWidgetの代わりに使うRiverpodGameMixin...FlameGameにmixinするRiverpodComponentMixin...Componentにmixinする
/// An excerpt from the Example. Check it out! class RefExampleGame extends FlameGame with RiverpodGameMixin { @override Future<void> onLoad() async { await super.onLoad(); add(TextComponent(text: 'Flame')); add(RiverpodAwareTextComponent()); } } class RiverpodAwareTextComponent extends PositionComponent with RiverpodComponentMixin { late TextComponent textComponent; int currentValue = 0; @override void onMount() { // build関数に処理を追加 addToGameWidgetBuild(() { ref.listen(countingStreamProvider, (p0, p1) { if (p1.hasValue) { currentValue = p1.value!; textComponent.text = '$currentValue'; } }); }); // super.onMount()の前にaddToGameWidgetBuildを呼ぶ super.onMount(); add(textComponent = TextComponent(position: position + Vector2(0, 27))); } }
Casual Games Toolkit
Flutter公式ドキュメントにも、カジュアルゲームをつくるために便利な
パッケージをまとめたCasual Games Toolkitというページがある
FlameやBridge Packagesで紹介されているのも載っているが、
app_reviewなど、他のもあるので、
目を通しておくとより捗る気がする。
以上!! 思った以上にいろいろできる。。すごい。。(*´ω`*)