実践Flutter

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

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

はじめに

今回はFlame公式ドキュメントを読み下していく第3弾、Componentの続き編です。

序盤でいきなりボスラッシュのように様々なComponentが脈絡もなく紹介され続けますが、今回で一段落しますのでじっくり見ていきましょう。

相変わらず顔見せをチラチラしていくスタイルです。ここまで長いですが、全然丁寧には説明されていません。登場人物がとにかく多いんです。

初学者に不親切極まりない内容だと思いますが、ちゃんと調べながらじゃないと読めない内容をわざと提示しているということでしょうかね。わざとやっているならすごいです。

などと思いつつ、ともかくまずざっと見ていきましょう。あとで主要なところは詳しく掘り下げていく予定です。






SpriteComponent

PositionComponentの実装のなかで最も使われるのがSpriteComponentです。これはスプライトを伴って作られます:

import 'package:flame/components/component.dart';

class MyGame extends FlameGame {
  late final SpriteComponent player;

  @override
  Future<void> onLoad() async {
    final sprite = await Sprite.load('player.png');
    final size = Vector2.all(128.0);
    final player = SpriteComponent(size: size, sprite: sprite);

    // screen coordinates
    player.position = ... // Vector2(0.0, 0.0) by default, can also be set in the constructor
    player.angle = ... // 0 by default, can also be set in the constructor
    add(player); // Adds the component
  }
}

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

サンプルコードのonLoadのところで、まずSpriteを作って、これをSpriteComponentのコンストラクタに与えていますね。

基本的にこのSpriteComponentでマリオ的なゲームのキャラクタを作ることができるわけですね。

ちなみに英語の「sprite」は妖精という意味になります。ファミコンやMSXなどの時代から、ゲーム内を動き回るキャラクタを表示する仕組みとして、ゲームの基盤中の基盤になっています。


SpriteAnimationComponent

このクラスはスプライトの循環的なアニメーションをさせることができるComponentです。

次のコードは3つの異なるイメージからなるアニメーションの例です:

final sprites = [0, 1, 2].map((i) => Sprite.load('player_$i.png'));
final animation = SpriteAnimation.spriteList(
  await Future.wait(sprites),
  stepTime: 0.01,
);
this.player = SpriteAnimationComponent(
  animation: animation,
  size: Vector2.all(64.0),
);

もしスプライトシートがあるなら、下のコードのようなsequencedという名前付きコンストラクタが使えます:

final size = Vector2.all(64.0);
final data = SpriteAnimationData.sequenced(
  textureSize: size,
  amount: 2,
  stepTime: 0.1,
);
this.player = SpriteAnimationComponent.fromFrameData(
  await images.load('player.png'),
  data,
);

FlameGameを使わないのであれば、このコンポーネントがupdateを必要とすることを覚えておいて下さい。アニメーションオブジェクトはフレームを動かすためにtickさせる必要があります。

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

スプライトシートというのは、アニメのコマが並べられていて一枚の画像になっているものです。これを決められたサイズで切り抜いてアニメの一コマにします。例えば下記を参照して下さい:


helpx.adobe.com

なのでスプライトシートの方のアニメーション設定は画像1枚で行っています。

基本的にスプライトアニメーションの初期化は直感的でわかりやすいと思います。


SpriteAnimationGroup

SpriteAnimationGroupComponentはSpriteAnimationComponentのシンプルなラッパーで、コンポーネントに複数のアニメーションをもたせて切り替えることができるようにするものです。

使い方はSpriteAnimationComponentと非常に似ていて、一つのアニメーションによる初期化の代わりにジェネリック型TをキーとしてSpriteAnimationを値とするMapによって初期化します。

enum RobotState {
  idle,
  running,
}

final running = await loadSpriteAnimation(/* omitted */);
final idle = await loadSpriteAnimation(/* omitted */);

final robot = SpriteAnimationGroupComponent<RobotState>(
  animations: {
    RobotState.running: running,
    RobotState.idle: idle,
  },
  current: RobotState.idle,
);

// Changes current animation to "running"
robot.current = RobotState.running;

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

ジェネリック型Tのキーというのは、要するにどんな型でもキーにすることができるということですね。Stringのキーであれば上記のSpriteAnimationGroupComponentのコンストラクタは例えば:

final robot = SpriteAnimationGroupComponent<String>(
  animations: {
    'running' : running,
    'idle': idle,
  },
  current: 'idle',
);

となる感じですね。もちろん数字でもOKです。元のサンプルのように列挙型で状態の名前を表して支障がなければそれが一番よいかもしれません。

ちなみに文中の「ラッパー」は「wrapper」で包み込むもの、です。サランラップでお馴染みのラップです。R指定や呂布カルマの方じゃありません。上の文で言っているのは、本質的なパーツはSpriteAnimationComponentだけれども、それを複数組み合わせて(包み込んで)便利な機能を実現したのがSpriteAnimationGroupだよ、ということです。


SpriteGroup

SpriteGroupComponentはアニメ版と非常に似ていて、そのスプライト版というわけです。

Example:

class ButtonComponent extends SpriteGroupComponent<ButtonState>
    with HasGameRef<SpriteGroupExample>, Tappable {
  @override
  Future<void>? onLoad() async {
    final pressedSprite = await gameRef.loadSprite(/* omitted */);
    final unpressedSprite = await gameRef.loadSprite(/* omitted /*);

    sprites = {
      ButtonState.pressed: pressedSprite,
      ButtonState.unpressed: unpressedSprite,
    };

    current = ButtonState.unpressed;
  }

  // tap methods handler omitted...
}

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

前節がSpriteAnimationGroupComponentで、これとほとんど同じなのがSpriteGroupComponentというわけです。Animationが抜けているバージョンですね。

状態によって表示するSpriteが切り替わるものです。前節は更にその高機能版でSpriteのAnimationが切り替わっていて、それを単純な1枚絵の組み合わせにしたバージョンというわけです。



SvgComponent

Note: To use SVG with Flame, use the flame_svg package.
SVGを使うにはflame_svgパッケージを使う必要があります。

This component uses an instance of Svg class to represent a Component that has a svg that is rendered in the game:
このコンポーネントはSvgクラスのインスタンスを使います。これによってSvgをゲームの中でレンダリングできます。

final svg = await Svg.load('android.svg');
final android = SvgComponent.fromSvg(
  svg,
  position: Vector2.all(100),
  size: Vector2.all(100),
);


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

一瞬FlutterのSVGを扱うための超人気パッケージの「flutter_svg」と空目してしまいますが、「flame_svg」のパッケージと言っています:


pub.dev

このパッケージでサンプルコードにあるSvgクラス、SvgComponentクラスが与えられています。

Svgのデータをスプライトのように扱うためにはこのflame_svgパッケージをインストールします。

ちなみにflame_svgのなかでflutter_svgを利用していますのでSVGの処理自体は定番のflutter_svgと同じです。



FlareActorComponent

以前のバージョンのFlare統合API(Flare integration API)は現在非推奨になっているので注意です。

フレアをFlameで使うにはflame_flareパッケージを使います。

これはFlameの中でフレアアニメーションを使用するインターフェイスです。FlareActorComponentはFlareActorウィジェットとほぼ同じAPIです。

import 'package:flame_flare/flame_flare.dart';

class YourFlareController extends FlareControls {

  late ActorNode rightHandNode;

  void initialize(FlutterActorArtboard artboard) {
    super.initialize(artboard);

    // get flare node
    rightHand = artboard.getNode('right_hand');
  }
}

final fileName = 'assets/george_washington.flr';
final size = Vector2(1776, 1804);
final controller = YourFlareController();

FlareActorComponent flareAnimation = FlareActorComponent(
  fileName,
  controller: controller,
  width: 306,
  height: 228,
);

flareAnimation.x = 50;
flareAnimation.y = 240;
add(flareAnimation);

// to play an animation
controller.play('rise_up');

// you can add another animation to play at the same time
controller.play('close_door_way_out');

// also, you can get a flare node and modify it
controller.rightHandNode.rotation = math.pi;

You can also change the current playing animation by using the updateAnimation method.

For a working example, check the example in the flame_flare repository.

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

Flareは非常に強力なアニメーションフレームワークです。下記の動画シリーズでそのパワフルな動きを見てみてください:


www.youtube.com

Flareを編集するには下記のようなツールを使います。


rive.app

Flare自体は非常にヘビーなトピックですので、ここではFlareというアニメをFlameでも取り込める、ということを覚えておく程度で一旦次に進みましょう。


ParallaxComponent

このComponentはいくつかの透過性のあるイメージを重ね合わせて、深さを感じさせるような背景の描画で使うことができます。ここでそれぞれの画像やアニメーション(ParallaxRenderer)は異なる速度で動いていきます。

あなたが地平線あたりを見ながら移動したときに、近くのものが早く動いて、遠くのものが遅く動いてみえますよね。

このコンポーネントは上記の効果をシミュレートして、リアルな背景効果を作ります。

一番シンプルなParallaxComponentは以下の通りです:

@override
Future<void> onLoad() async {
  final parallaxComponent = await loadParallaxComponent([
    ParallaxImageData('bg.png'),
    ParallaxImageData('trees.png'),
  ]);
  add(parallax);
}

上記のParallaxComponentはonLoadメソッドをオーバーライドしてParallaxComponentをロードしています。

class MyParallaxComponent extends ParallaxComponent with HasGameRef<MyGame> {
  @override
  Future<void> onLoad() async {
    parallax = await gameRef.loadParallax([
      ParallaxImageData('bg.png'),
      ParallaxImageData('trees.png'),
    ]);
  }
}

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    add(MyParallaxComponent());
  }
}

これは静止している背景を作っています。

動くパララックスを作りたければいくつかの方法があります。これはそれぞれのレイヤーをどれだけ細かくセッティングしたいかによってきます。

一番シンプルなのは名前付きパラメタのbaseVelocityとvelocityMultiplierDeltaをヘルパー関数のロードのときに設定すること。例えば、背景イメージをX軸方向でより近い画像を早く動かしたければ、以下のようにします:

final parallaxComponent = await loadParallaxComponent(
  _dataList,
  baseVelocity: Vector2(20, 0),
  velocityMultiplierDelta: Vector2(1.8, 1.0),
);

ベースのスピードとレイヤーごとの差分(velocityMultiplierDelta)はいつでも変更することができます。

final parallax = parallaxComponent.parallax;
parallax.baseSpeed = Vector2(100, 0);
parallax.velocityMultiplierDelta = Vector2(2.0, 1.0);

デフォルトでは、画像は左下にくっついていて、X軸方向に繰り返されて画面をカバーします。この振る舞いを変えたい場合、例えばシングルスクロールではないゲームを作っているような場合などで、画像の繰り返し方向などを変えたいときはParallaxRendererとParallaxLayersのそれぞれを適切に設定してParallaxComponentのコンストラクタに与えます。

final images = [
  loadParallaxImage('stars.jpg', repeat: ImageRepeat.repeat, alignment: Alignment.center, fill: LayerFill.width),
  loadParallaxImage('planets.jpg', repeat: ImageRepeat.repeatY, alignment: Alignment.bottomLeft, fill: LayerFill.none),
  loadParallaxImage('dust.jpg', repeat: ImageRepeat.repeatX, alignment: Alignment.topRight, fill: LayerFill.height),
];
final layers = images.map((image) => ParallaxLayer(await image, velocityMultiplier: images.indexOf(image) * 2.0));
final parallaxComponent = ParallaxComponent.fromParallax(
  Parallax(
    await Future.wait(layers),
    baseVelocity: Vector2(50, 0),
  ),
);

上記の例でスターの画像はX軸Y軸の両方向に繰り返し表示され、中央寄せで、画面を埋めるまでスケールされます。

惑星の画像はY軸方向に繰り返され、左下寄せでスケールはされません。

ダストの画像はX軸方向に繰り返され、右上寄せで画面の高さを埋めるようにスケールされます。

一度ParallaxComponentを設定するとあとはほかのComponentと同様にゲームに加えることができます(game.add(parallaxComponent)のように)。pubspec.yamlに画像のassetを登録しておくことは忘れないでくださいね。


もしフルスクリーンのParallaxComponentを使いたければ、size引数を省略すればOKです。ゲームが画面サイズや画面の方向を変えた場合でも自動的にリサイズします。

Flameは2つの種類のParallaxRendererを提供します。ParallaxImageとParallaxAnimationです。ParallaxImageは静止画で、ParallaxAnimationは名前で分かる通りアニメーションを描画するものです。ParallaxRendererを継承してさらにカスタマイズすることも可能です。

これらの実装例はexampleディレクトリで見ることができます。
https://docs.flame-engine.org/1.0.0/components.html

文中で紹介されているサンプルのディレクトリはこちらです:


github.com

上記でParallax関連のソースコード例が読めますが、根本のフォルダに行ってFlame全体のソースコードをダウンロードしてFlutterでビルドすると、Parallax以外の例も全部動かしてみることができます。これとソースを突き合わせるといろいろとわかりやすいのでぜひやってみてください。

ParallaxのBasicの例だと下記のような画面が横スクロールします。月や山が遅く、手前の木が早いために奥行き感がでています。これがパララックス効果ですね。


f:id:linkedsort:20211214221554p:plain:w300

画面を横長に変形させてみると、X軸方向にリピートされています。これが上記の引用のなかでいっているX軸報告に繰り返す、という機能になります。月がいくつもあるのは変ですが、きれいに画面が埋まっています。


f:id:linkedsort:20211214221726p:plain

ちなみに縦方向に伸ばしてもリピートはされず、画像がスケール(拡大)される形で画面を埋めます。



ShapeComponents

ShapeComponentは幾何学的な図形を画面に描画するときに使える基本的なComponentです。ShapeComponentはPositionComponentなので、その効果を利用することができます。すべてのShapeComponentはPaintを引数にとって形状を決定します。

ShapeComponentには以下の3つの実装があります:

CircleComponent
CircleComponentはradius(=半径)のみを決定することで最小限の作成を行うことができますが、positionによって位置の指定、あるいはpaintによって色(デフォルトは白)の指定ができます。

final paint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
final circle = CircleComponent(radius: 200.0, position: Vector2(100, 200), paint: paint);

RectangleComponent
RectangleComponentは2つの方法で生成することができます。これらは正方形かどうかでわかれます。幅300、高さ200のRectangleComponentを作るには下記のようにします:

final paint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
final rectangle = RectangleComponent(
  size: Vector2(300.0, 200.0),
  position: Vector2(100, 200),
  paint: paint,
);

正方形であれば名前付きコンストラクタのRectangleComponent.squareでほんの少し単純にすることができます。例えば1辺が200の正方形を作るには下記のようにします:

final paint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
final square = RectangleComponent.square(
  size: 200.0,
  position: Vector2(100, 200),
  paint: paint,
);

PolygonComponent
PolygonComponentはShapeComponentのなかでも、もっとも複雑なものです。ポリゴンのすべての角の定義を与えていく必要があります。PolygonComponentは二つの方法で生成することができます。そのどちらもデフォルトコンストラクタを用い、Vector2のリストを引数として与えます。

1つ目の方法は、Vector2のリストの値において-1.0から1.0の値の範囲で表すものです。この値の範囲で割合を用いて形を定義します。

なので[Vector2(1.0, 1.0), Vector2(1.0, -1.0), Vector2(-1.0, -1.0), Vector2(-1.0, 1.0)]が、フルサイズの長方形を表しています。ここでリストの要素は反時計回りに定義することを覚えておいてください。(Y軸が下向きであることに注意してくださいね)

ではPolygonComponentを使ってダイヤモンドの形を作る例を見てみます:

final vertices = ([
  Vector2(0.0, 0.9),  // Middle of top wall
  Vector2(-0.9, 0.0), // Middle of left wall
  Vector2(0.0, -0.9), // Middle of bottom wall
  Vector2(0.9, 0.0),  // Middle of right wall
]);

final diamond = PolygonComponent(
  normalizedVertices: vertices,
  size: Vector2(200, 300),
  position: Vector2.all(500),
)

もう一つの方法として絶対座標でポリゴンを定義する方法があります。この場合PolygonComponent.fromPointsというfactoryコンストラクタを使います。これを使う場合は必ずしもsizeとpositionを指定する必要はありません。それでもsizeなどを指定すれば、それに合うように調整されます。

再びダイヤモンドの形の例を示します:

final vertices = ([
  Vector2(100, 100),  // Middle of top wall
  Vector2(50, 150), // Middle of left wall
  Vector2(100, 200), // Middle of bottom wall
  Vector2(200, 150),  // Middle of right wall
]);

final diamond = PolygonComponent.fromPoints(vertices);
)

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

ポリゴンは点をリストで与えて、それをつなぎ合わせることで形状を定義します。このとき2つの方法があって、-1.0~1.0の値の範囲で座標を表現してサイズや位置をあとで与えるか、絶対座標系で直接ポリゴンを描画するか、です。

絶対座標系の場合は、これ自体に大きさと位置の意味も含んでいますので、sizeやpositionを後で与える必要はありません。サンプルコードでもそうなっていますよね。しかし与えればそれに合わせて調整されますので、形を定義するために絶対座標系のコンストラクタを使っても問題ありません。



SpriteBodyComponent

SpriteBodyComponentについてはForge2Dのドキュメントを参照してください。

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

前回もとりあげましたがForge2DとはBox2DのFlameポーティング版です:


pub.dev

でもこちらのAPIリファレンスにはSpriteBodyComponentの記載が見当たらず…

Flameのドキュメントのその他のモジュールのところにあるForge2Dのドキュメントには少し記載がありますね:

SpriteBodyComponent

Forge2DGameにおいて、BodyComponentの描画にスプライトを使いたいと思う時があると思います。

このコンポーネントはスプライト画像をボディのトップにおいて、スケーリングとポジショニングを行います。

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

これは要するに、Forge2Dという物理エンジン内で使われるBodyComponentの画像の初期化に、スプライトを使えるということです。APIリファレンスにドキュメントがないのでソースコードから引っ張ってきますと:

import 'package:flame/components.dart';

import 'position_body_component.dart';

abstract class SpriteBodyComponent extends PositionBodyComponent {
  /// Make sure that the [size] of the sprite matches the bounding shape of the
  /// body that is create in createBody()
  SpriteBodyComponent(
    Sprite sprite,
    Vector2 spriteSize,
  ) : super(SpriteComponent(size: spriteSize, sprite: sprite), spriteSize);
}

(これがSpriteBodyComponentの全部)ということで、スプライトとスプライトのサイズを与えることによってBodyComponentを形成しているもの、ということになりますね。


TiledComponent

現状タイル状のコンポーネントは非常に基礎的な実装の段階です。APIはtiled.dartを使ってマップファイルを読み込み、マップレイヤーを描画します。

どのようにAPIを使うかは、こちらを参照してください。

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

ここで言っているマップファイルはTMXファイルと呼ばれるタイル状のマップデータフォーマットのファイルです。

TMXファイルを生成する代表的なマップエディタとしては、例えば下記があります:


www.mapeditor.org

これは必須のツールだと思いますが、現状は非常に基礎的な段階であるとしています。その完成度は慎重に見ていく必要がありそうです。


IsometricTileMapComponent

IsometricTileMapComponentは直交座標系状の等尺のタイルセットからなるマップを描画できます。

シンプルな例は下記の通りです:

// tilesetを作る。ブロックのidは自動的に0から始まる形でアサインされる。
// 左から右、上から下の順。
final tilesetImage = await images.load('tileset.png');
final tileset = IsometricTileset(tilesetImage, 32);
// 各要素はブロックidで、-1は「無い」ことを表す。
final matrix = [[0, 1, 0], [1, 0, 0], [1, 1, 1]];
add(IsometricTileMapComponent(tileset, matrix));

クリックやホバーやタイルの上に何かを描画することや選択するカーソルを含めることなどのメソッドが用意されています。

タイルの高さも指定できるので、一番下からトップまでの垂直の距離を調整できます。基本的に、その高さは一番前の辺の高さを意味します。通常、1辺の半分(デフォルト)かあるいは、1/4の高さになります。

f:id:linkedsort:20211210151243p:plain

下は、1/4の高さのマップがどういうふうに見えるかを表したものです:

f:id:linkedsort:20211210151258p:plain

Flameのサンプルアプリはもっと深い例を含んでいます。それらは選択カーソルの使い方などを含んでいますので参照してください。

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

公式ドキュメントの最後のリンクが現在404で切れていますが、おそらくこれのことです:


github.com

実行している様子は下記のなかで左上のボタンから「Rendering」→「Isometric Tile Map」を選択すると見えます:


examples.flame-engine.org

これはタクティクス・オウガのような視点の格好いいシミュレーションなどを作る上で非常に強力ですよね。是非活用を検討したいところです。



NineTileBoxComponent

Nine Tile Boxはグリッドスプライトを用いた長方形です。グリッドスプライトは3x3のグリッドを持つ9つのブロックがあります。4隅に4つのブロック、サイドに4つのブロックです。

隅のブロックは同じサイズで描画され、横のブロックは引き伸ばされて描画されます。

これを使うことでどんなサイズの四角形も書くことができます。これによってパネルや対話ウィンドウを簡単に作ることができます。

サンプルをチェックしてみてください。

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

サンプルは上記に上げたサンプルページの左上のボタン→「Rendering」→「Nine Tile Box」でみることができます。


f:id:linkedsort:20211216232052p:plain

こういう四角が表示されて、画面クリックする毎に、ずいずいと拡大されていきますね。

なにこれ?っていう感じかもしれませんが、四角は大きくなったり小さくなったりしても、四隅のグラフィックスは引き伸ばされたりしないで同じ絵柄をキープしているというのがポイントです。

よくドラクエのメッセージウィンドウのように、大きさは色々あるものの、角の丸みは同一、というのがありますよね。ああいう風に角は拡大されたりしないけれども、上下左右のグラフィックスは引き伸ばされて全体的に四角の領域が拡大縮小する、というものを表現するのに使えるものです。


CustomPainterComponent

CustomPainterはCustomPaintウィジェットと共にFlutterのアプリケーションの中でカスタム図形を描画するために使われるものです。

FlameはCustomPainterをレンダリングするためのCustomPainterComponentを提供します。これはCustomPainterを受け取り、ゲームキャンバス上で描画を行います。

これはFlameゲームとFlutterウィジェットで描画ロジックを共有するために用いられます。

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

CustomPaintというのはFlutter(Flameではなく)一般の様々な図形を簡単に描けるクラスです。


www.youtube.com

このCustomPaintによって定義してもComponentの描画を定義できるというものです。

すでにCustomPaintで多くの図形資産があったり、FlutterとFlameで同じ図形を書きたいというときにこれを使うと便利です。



Effects

Flameは特定のタイプのコンポーネントに適用できる特殊効果を提供します。これはコンポーネントに動きを与えるものです。

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

Effectについてはまたこのシリーズのしばらく先(公式ドキュメントでも次の次の章)で扱いますので、詳しくはそちらを参照してください。








おわりに

Componentの章を完走しました。お疲れさまです。

最後にEffectが顔をのぞかせますが、すぐにEffect編は始まりません。というかまだ大分あとになります。このあたりの変な構成がまた独特の味というところでしょうか。

ドキュメントの読み下しはそこそこ量があって大変ですが、全体像を掴むには一番いい方法ですので、とにかくざっと見ていくのがいいですね。

途中あれこれ調べながら、ソースコードをちょいちょい読みながら。そうしていくうちに、パッケージ全体の設計思想や方向性がみえてきます。つまみ食いのサンプルだけだと、やっぱりあとで詰まってしまったりしたときに困りますからね。

f:id:linkedsort:20211216235750j:plain