ずっと気になってた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など、他のもあるので、
目を通しておくとより捗る気がする。
以上!! 思った以上にいろいろできる。。すごい。。(*´ω`*)