実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

Flame02:Flame公式ドキュメントを読み下していく2

はじめに

前回に引き続き、Flameの公式サイトを頭から読み下していくシリーズの第2弾です。

ここからは2Dゲームエンジンの主役中の主役であるキャラクタやその組み合わせ、そして画面そのものを扱うComponentの概念になります。

Componentが中心なだけに非常に長く、次回もComponentが続きます。

相変わらず初学者向けとはとても言えないドキュメントの内容になっていまして、ポンポン話が飛んでいるような構成になっています。ここでの引用がぶっ飛んでいるのではなく、公式がそうなっていますので念の為。

それでも肌感覚を身に着けていくため、ゴリゴリ読み進めていってみましょう。





コンポーネント

f:id:linkedsort:20211210144859p:plain

この図は少し威圧的に見えるかもしれませんが、見た目よりは複雑ではないので心配はいりません。

全てのコンポーネントは抽象クラスのComponentを継承しています。

抽象的な話をスキップしたければPositionComponentのところまで読み飛ばしてもOKです。

すべてのComponentはFlameGameクラスが利用するためのいくつかのメソッドを選択的に実装しています。FlameGameクラスを使っていないのであれば、ご自身のゲームループのためにこれらのメソッドを活用してもOKです。

f:id:linkedsort:20211210145022p:plain

onGameResizeメソッドは画面がリサイズされたときにいつでも呼ばれます。そしてコンポーネントがゲームに加わったときにもaddメソッドを通じて一度最初に呼ばれます。

属性値shouldRemoveはコンポーネントが次のアップデートループに入る前にFlameGameによって削除される状況でtrueにします。これをtrueにするともうrenderやupdateの適用範囲外です。game.remove(Component c)やcomponent.removeFromParent()も同様に親から排除するときに利用可能です。


属性値respectCameraはfalseに設定すると(デフォルトはtrue)、FlameGameのカメラ処理においてそのコンポーネントを無視します。これはルートのFlameGameに直接addされたコンポーネントにのみ適用されることに注意してください。(注:下にコメントしていますが現バージョンでこの属性値はComponentに存在していません)

onRemoveメソッドはコンポーネントがそのゲームから排除される直前で実行されます。このタイミングでの処理を追加したい場合はこのメソッドをオーバーライドします。

onLoadメソッドは非同期な初期化コードの実装を追加するためにオーバーライドできます。たとえばイメージのローディングなどに用いられます。このメソッドはコンポーネントの初期の準備の直後に実行されます。つまりは一番最初のonGameResizeがコールされた直後のタイミングであり、FlameGameのコンポーネントリストに含められる直前に実行されることになります。

onMountメソッドはコンポーネントの最初の準備の直後、onGameResizeとonLoadの前に呼ばれます。そしてコンポーネントのリストに加えられる直前です。

https://docs.flame-engine.org/1.0.0/components.html

最初の図が少し複雑ですが、これは多くのコンポーネントや、Flame上で作られる多くのゲームの土台であるFlameGameがComponentクラスを継承している、ということを表しています。つまりFlameGameもComponentです。

親から削除する、親から排除するという表現が多く出てきます。これはComponentは親子構造を持っていて、あるComponentに、別のComponentを子供として複数加えることができます。子供のComponentからすると、所属しているComponentは「親」ということになります。

FlameGameもComponentですので、FlameGameにComponentを加えていくというのと、SpriteなどのComponentをグループ化するために親子関係にすることは、同じ操作で可能です。

属性値のrespectCameraについて言及がありますが、現バージョン1のソースコードを見る限りComponentにこの名前の属性値が見当たりません。代わって下の方で解説しているpositionTypeが使われています。


通常自分でコンポーネントを作りたい場合はPositionComponentを継承します。ただもしポジショニングを違う形で扱いたい場合は、Componentを直接継承してもOKです。

例えばflame_forge2dのなかで、SpriteBodyComponent、PositionBodyComponent、BodyComponentはComponentを継承しています(PositionComponentではなく)。なぜならこれらのコンポーネントは画面に関連するポジションを持っているのではなくForge2Dの世界での座標系を持っているからです。

コンポーネントの組み合わせ
コンポーネントに別のコンポーネントを含めると便利な場合があります。例えばコンポーネントの見た目を階層的にグルーピングする場合などです。これはPositionComponentなどに子コンポーネントを加える操作で実現できます。コンポーネントが子コンポーネントをもつ場合、そのコンポーネントは全ての子コンポーネントを同一の条件のもとでupdateとrenderします。

下のコードは、1つのコンポーネントに2つの子コンポーネントを加えている例です:

class GameOverPanel extends PositionComponent with HasGameRef<MyGame> {
  bool visible = false;
  final Image spriteImage;

  GameOverPanel(this.spriteImage);

  @override
  Future<void> onLoad() async {
    final gameOverText = GameOverText(spriteImage); // GameOverText is a Component
    final gameOverButton = GameOverButton(spriteImage); // GameOverRestart is a SpriteComponent

    add(gameOverText);
    add(gameOverButton);
  }

  @override
  void render(Canvas canvas) {
    if (visible) {
    } // If not visible none of the children will be rendered
  }
}

https://docs.flame-engine.org/1.0.0/components.html

唐突にflutter_forge2dが出てきます。このForge2DというのはFlameの枠組みのなかでBox2Dという有名な物理エンジンを使えるようにポーティングしたものです。


pub.dev

ちなみにBox2Dはこのようなものです:

www.youtube.com

これは非常に面白いパッケージですのでぜひ活用したいですね。

上記のドキュメントの中では、PositionComponentを継承しないで直接Componentを継承して活用する実例を説明するために急にこのパッケージが引き合いに出されています。実際にソースコードを見てみると、その通りになっています。flutter_forge2dのgithubからご確認下さい。

一番下のサンプルコードでは、 Componentを継承しているGameOverPanelのonLoadメソッドにて、2つのコンポーネントを子に持たせていますね。そしてrenderメソッドのなかでvisibleが真であればレンダリングする、という形で2つのコンポーネントをひとまとめにして扱っています。


子コンポーネントに対するクエリ

コンポーネントに加えられた子コンポーネントはQueryableOrderedSetに格納されます。このSetのなかで特定のコンポーネントに対してクエリするには、まずそのクエリをそのSetで登録しておく必要があります。そうすると、クエリ関数が効率的にそのクエリを実行してくれます。通常この登録はonLoadで行われます。

例えば下記のようです:

Future<void> onLoad async {
  await super.onLoad();
  components.register<PositionComponent>();
}

上記ではPositionComponentを抽出するクエリが登録されています。下のコードはその登録されたクエリを実行する例です:

void update(double dt) {
  final allPositionComponents = components.query<PositionComponent>();
}

https://docs.flame-engine.org/1.0.0/components.html

唐突に出てくるQueryableOrderedSetは、Flameの中で使っている以下のパッケージの中のクラスです。
pub.dev


OrderedSetというのは、順序付けられた集合です。通常なる集合はイテレーションするときの要素の順序をケアしていません。しかしここで活用しているOrderedSetは下記の例のように自然な順序付けのイテレーションを提供したり(一番下のtoListの結果が必ず自然な順序の1,2になることを、通常の集合(Set)は保証しません):

  import 'package:ordered_set/ordered_set.dart';

  main() {
    final items = OrderedSet<int>();
    items.add(2);
    items.add(1);
    print(items.toList()); // [1, 2]
  }

あるいは柔軟な順序付けをサポートしています:

  // nameの長さで順序付け
  final people = OrderedSet<Person>(Comparing.on((p) => p.name.length));

  // nameの逆辞書順で順序付け
  final people = OrderedSet<Person>(Comparing.reverse(Comparing.on((p) => p.name)));

  // roleで順序付け、さらにそのなかでnameで順序付け
  final people = OrderedSet<Person>(Comparing.join([(p) => p.role, (p) => p.name]));

順序付けが非常に柔軟で、compareToなどをいちいち定義しなくてもその場の集合にユニークな順序付けを定義できる面白いパッケージですね。

そしてQueryableOrderedSetは上記の順序付け可能な集合に、さらに特定のクラスのものを効率的に抽出する機能がそなわっているものです。とりあえず現時点では子コンポーネントの集合のなかでSpriteComponentだとかPositionComponentだとかを限定して瞬時に取り出す仕組みがあるという理解でOKかと思います。


Positioning types

HUDだとか、ほかのなにかゲームの世界とは違うポジショニングのものを作りたい場合、コンポーネントのPosisionTypeを変更することができます。

デフォルトのPositionTypeはPositionType.gameです。これを必要に応じて変更します。

PositionType.game (デフォルト):カメラとビューポイントに準じた位置

PositionType.viewport:ビューポートのみに準じた位置(カメラは無視)

PositionType.widget:FlutterのGameWidget(すなわち生のCanvas)に準じた位置

殆どの場合はPositionType.gameがいいでしょう。ビューポートとカメラの両方に準じたポジショニングを活用する場合が多くあります。ただ、カメラが動いたとしても画面に常に配置されているボタンやテキストはカメラの位置に左右されないですよね。こういう場合はPositionType.viewportを使います。PositionType.widgetはいくつかのレアケースでカメラやビューポートによらないポジションを使う場合にはPositionType.widgetを使います。

https://docs.flame-engine.org/1.0.0/components.html

カメラとビューポートについては、例えば下記の動画が参考になるかもしれません(字幕で日本語をつけることができます):


www.youtube.com

カメラがマップのなかから一部の場面を切り出してくる役割をします。ビューポートはプレイヤーがみている画面を構成しているものです。カメラとビューポートは必ずしも同じ大きさではなく、ある倍率を持って縦横に伸縮されているかもしれません。なのでカメラで切り出した部分と、ビューポートで写っているものは縮尺がずれている可能性があることに注意です。


PositionComponent

このクラスは画面の中で位置を持っているものを表しています。コンポーネントのなかに子コンポーネントが含まれれば、それらのグループの位置を表すことになります。

PositionComponentの基盤は位置、サイズ、スケール、角度、そしてアンカーを持っていて、これらがコンポーネントがどうレンダリング(描画)されるかを決定する要素になります。

Position
位置はVector2で、コンポーネントのアンカーの位置を表します。親要素がFlameGameである場合、ビューポート上での位置と関連します。

Size
サイズはカメラのズームレベルが1.0のとき(デフォルトのズームしていない状態)の大きさを表します。サイズは親のコンポーネントとは関係がありません。

Scale
スケールはコンポーネントとその子コンポーネントの倍率を表します。Vector2で表現され、xとyを同量変えることで形を変えずに伸縮できますし、バラバラにxとyを変えれば縦横に伸縮します。

Angle
角度(アングル)はアンカーの周りの回転の度合いを表します。ラジアンで表すdouble型の値です。親の角度と相対的な関係になります。

Anchor
アンカーはそのコンポーネントの位置と、回転の軸を決めるものです。デフォルトは左上(Anchor.topLeft)です。Anchor.centerにすると、そのコンポーネントの位置はComponentの中央になるし、回転軸もComponentの中心んあります。Flameがそのコンポーネントを「掴んでいる」位置、という理解でOKです。

https://docs.flame-engine.org/1.0.0/components.html

Vector2はFlutter公式の2次元ベクトルを表すクラスです:


api.flutter.dev

double型のxとyのベクトル演算を広くサポートしていますので、2Dゲーム作りでは必須のクラスです。

ゲームやキャラクタの状況によって、キャラクタのどこの部分を中心に座標系や回転を与えたほうが都合が良いかがことなりますので、その事情に合わせてPositionComponentのアンカーの位置を決めます。アンカーが決まると、あとは角度を与えればアンカー位置を中心に回りますし、位置もアンカーを起点に指定することになります。


PositionComponentの子コンポーネント

PositionComponentに属する全ての子Componentは、親要素と連動して動きます。つまり位置、角度、スケール(倍率)は親の状態に対して相対的です。例えば親の中心の位置に対して50論理ピクセル上に子要素を持ってきたい場合は下のようになります:

final parent = PositionComponent(
  position: Vector2(100, 100),
  size: Vector2(100, 100),
  anchor: Anchor.center,
);
final child = PositionComponent(position: Vector2(0, -50));
parent.add(child);

ほとんどの画面上に描画されるコンポーネントはPositionComponentです。ここでのパターンはSpriteComponentやSpriteAnimationComponentでも使われます。

https://docs.flame-engine.org/1.0.0/components.html

ここでは、PositionComponentに、子として加えられるComponentの位置などの考え方を解説しています。子コンポーネントは全て親との相対位置のみの位置、角度、スケール指定だとしています。親の外のことは一切考えません。

なので例題では子の位置をVector2(0, -50)としてy軸で上に50ピクセルずらしている、としています。これは絶対的な座標系の(0, -50)ではなく、親の位置に対してY軸で50ピクセル上にずらしている、という意味です。親のコンポーネントが動けば、子コンポーネントの位置は(0, -50)という指定のままでも、同じ位置関係で移動しますし、親が回転すれば子も角度を独自に与えなくても回転するということです。


PositionComponentの描画

PositionComponentを継承してrenderメソッドを実装する場合、左上の角から描画することを覚えておいて下さい。renderメソッドはスクリーンのどこにレンダリングするかを扱うべきではありません。どこにレンダリングするかはposition、angle、anchorの属性値を設定するだけで、残りの作業はFlameが自動的にやってくれます。

もし画面のどこにコンポーネントがあるかを知りたければ、toRectメソッドを使ってスクリーンのどこにバウンディングボックスがあるかを知ることができます。

In the event that you want to change the direction of your components rendering, you can also use renderFlipX and renderFlipY to flip anything drawn to canvas during render(Canvas canvas). This is available on all PositionComponent objects, and is especially useful on SpriteComponent and SpriteAnimationComponent. For example set component.renderFlipX = true to mirror the horizontal rendering.
コンポーネントの描画の向きを変えたい場合、renderFlipXとrenderFlipYを使うことでキャンバスへの描画をフリップできます。これはすべてのPositionComponentで利用可能で、特にSpriteComponent、SpriteAnimationComponentにおいて有用です。例えばcomponent.renderFlipX = trueとすれば水平に反転させることができます。

https://docs.flame-engine.org/1.0.0/components.html

バウンディングボックスという言葉に馴染みがなければ、下記の動画などが参考になると思います:


www.youtube.com

ただPositionComponentを継承してレンダリングのメソッドを改造することを最初から考えなくて良いと思いますので、このあたりのことは一旦スキップしてまた大分後で戻ってきましょう。







おわりに

Componentの章が長すぎるので、一旦ここで区切って続きは次回に譲ります。

すでになんらかのゲームエンジンに慣れている人は確認のような内容だと思いますが、初学者にはまだまだストーリーが見えないところですね。

まるで500ページの小説の最初の50ページのような、何の話が進行しているのかよくわからないまま読み進めているような感覚になりますね。もう少し公式の最初の文書なんだから噛み砕いてほしいのですが…というかその話、今いるか?みたいな事が多いんですけどもね。

などと思いつつも素直に楽しんでいきましょうか。


f:id:linkedsort:20211213220314j:plain