🔥

【Flame】Flutterでさくっとゲームを作る方法

2021/07/08に公開

https://twitter.com/7oh_2020/status/1412826714590244870

https://flame-engine.org/

Flameとは

FlameはFlutterで2Dゲームを制作するためのパッケージです。
FlameComponent System(FCS)というコンポーネントベースの構造によりシンプルにゲームを実装できます。
公式ドキュメントはこちら

インストール

pubspec.yamlに依存関係を記述します。
また、ゲーム内で使用する画像を格納するフォルダを作成しassetsとして宣言します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flame: 1.0.0-rc9

・・・

flutter:
  assets:
    - assets/images/

PositionComponentの作成

コンポーネントは更新と描画メソッドを持つゲーム内の要素です。
FlameではPositionComponentという基本的なコンポーネントが用意されています。
下記のコードはPositionComponentクラスを継承したEnemySpriteクラスの例です。

onGameResize()メソッドは初期化時と画面サイズ変更時に呼び出されます。
画面サイズを基準にして位置やサイズを変更できるためレスポンシブな表示ができます。

update()メソッドには移動などの更新処理を記述します。

render()メソッドではCanvasが使えるため自身のプロパティを使用して自由な描画が可能です。
Canvasの基準座標は左上ですが、コンポーネント自体の基準座標はanchorプロパティで指定します。

enemy_sprite
class EnemySprite extends PositionComponent {
  EnemySprite({
    required double length,
    Color? color,
  })  : this.length = length,
        this.color = color ?? Colors.greenAccent,
        super(
          anchor: Anchor.center,
          size: Vector2.all(length),
        );
  late double length;
  late Color color;

  
  void onGameResize(Vector2 gameSize) {
    position = Vector2(
      gameSize.x / 2.0,
      gameSize.y / 5.0,
    );
  }

  
  void update(double dt) {
    super.update(dt);
  }

  
  void render(Canvas canvas) {
    super.render(canvas);
    canvas.drawRect(
      Rect.fromLTWH(0, 0, length, length),
      Paint()..color = color,
    );
  }

}

SpriteComponentの作成

SpriteComponentは画像を持ったコンポーネントです。
PositionComponentクラスを継承していますがrender()メソッドが省略できるためPositionComponentより簡単に扱えます。

player_sprite.dart
class PlayerSprite extends SpriteComponent {
  PlayerSprite({
    required Image image,
    required Vector2 size,
  }) : super(
          sprite: Sprite(image),
          size: size,
        ) {
    anchor = Anchor.center;
  }

  
  void onGameResize(Vector2 gameSize) {
    position = Vector2(
      gameSize.x / 2.0,
      gameSize.y / 5.0 * 5.0,
    );
  }

  
  void update(double dt) {
    super.update(dt);
  }
}

TextComponentの作成

TextComponentクラスはテキストを持ったコンポーネントです。
こちらもPositionComponentクラスを継承しているため動的な移動や回転などが可能です。
フォントサイズや色などのフォント情報はconfigプロパティに指定します。

info_sprite
class InfoSprite extends TextComponent {
  InfoSprite({
    String? text,
    Color? color,
    double? fontSize,
  }) : super(
          text ?? "default",
          config: TextConfig(
            color: color ?? Colors.white,
            fontSize: fontSize ?? 32.0,
          ),
        ) {
    anchor = Anchor.center;
  }
}

コンポーネントをグループ化する

コンポーネントはaddChild()メソッドで入れ子にできます。
※入れ子にした場合、親コンポーネントは子コンポーネントのonGameResize()メソッドやupdate()メソッドを呼び出してあげる必要があります。

addChild(component);

ゲームループ

コンポーネントが作成できたら次はそれらをゲームループに追加します。
FlameにはBaseGameという便利なゲームループが用意されています。

BaseGameクラスを継承したクラスをGameWidgetというウィジェットに渡すだけで画面表示は完了です。

GameWidgetはStatefulWidgetなのでそのままrunApp()関数にわたすことも他のウィジェットに追加することも可能です。

onLoad()メソッドはゲーム初期化時に呼び出される非同期メソッドです。
ここで画像の読み込みやコンポーネントの初期化を行います。

画像の読み込み方法はいくつかありますが、BaseGameクラスのimagesプロパティを使用するのが簡単です。
ファイルパスには「assets/images/」フォルダ内のPNGファイル名のみを指定します。
※PNGファイル以外は対応していないようです。

await images.load('player.png')

コンポーネントはadd()メソッドでゲームループに追加できます。
追加されたコンポーネントのupdate()メソッドやrender()メソッドは自動的に呼び出されます。

下記はゲームループの例です。

main.dart
void main() => runApp(GameWidget(game: MainGame()));

class MainGame extends BaseGame {
  late PlayerSprite player;
  late EnemySprite enemy;

  /// ロード処理
  
  Future<void> onLoad() async {
    // 敵の初期化
    enemy = EnemySprite(length: 100.0);
    add(enemy);

    // プレイヤーの初期化
    player = PlayerSprite(
      image: await images.load('player.png'),
      size: Vector2(100.0, 100.0),
    );
    add(player);
  }
}

カメラの設定

BaseGameクラスにはcameraプロパティが用意されています。
カメラを使用するとプレイヤーを追従したり大きな背景の一部を表示などができます。
例えば下記の1行を追加するだけでカメラがプレイヤーコンポーネントを追従するようになります。

 camera.followComponent(player);

入力イベントを処理する

Flameには画面タップやドラッグや長押しなど様々な入力イベントを受け取るMixinが用意されています。

使い方は簡単で、入力イベント毎のMixinをゲームループクラスに追加して、各メソッドをオーバーライドするだけで入力イベントを受け取れます。

main.dart
class MainGame extends BaseGame
    with
        TapDetector,
        VerticalDragDetector,
        HorizontalDragDetector {

・・・

  /// 垂直方向にドラッグしたときの処理
  /// tokino syori
  
  void onVerticalDragUpdate(DragUpdateDetails details) {
    // プレイヤーを移動
    player.position += Vector2(
      details.delta.dx,
      details.delta.dy,
    );
  }

  /// 水平方向にドラッグしたときの処理
  
  void onHorizontalDragUpdate(DragUpdateDetails details) {
    // プレイヤーを移動
    player.position += Vector2(
      details.delta.dx,
      details.delta.dy,
    );
  }

  /// タップ or クリックしたときの処理
  
  void onTap() {
    //
  }
}

当たり判定

コンポーネントに当たり判定を追加したいときは、コンポーネントにHitbox MixinとCollidable Mixinを追加します。

Hitboxはその名の通り当たり判定の形状(Shape)を表します。
ヒットボックスの形状にはHitboxRectangle(四角形)やHitboxCircle(円形)などがあります。
※単位はピクセル数ではなく中心からの倍率です。

そしてaddShape()メソッドでコンポーネントにヒットボックスの形状を追加します。
ヒットボックスは複数追加可能なので組み合わせて複雑な形状にも対応できます。

final hitbox = HitboxRectangle(relation: Vector2(1.0, 1.0));
addShape(hitbox);

他のコンポーネントと接触した時の処理はonCollision()メソッドに記述します。
その際に「is」キーワードを使ってどのコンポーネントと接触したかを判定できます。

また、コンポーネントの当たり判定をゲームループが検知するためには、ゲームループクラスにHasCollidables Mixinを追加する必要があります。

class MainGame extends BaseGame with HasCollidables {
  ・・・
}

下記の例では接触したEnemySpriteをゲームループから削除しています。
コンポーネントのshouldRemoveプロパティをtrueにすると、ゲームループは自動的にそのコンポーネントをゲームから削除します。

また、接触するためにはもちろんEnemySprite側も同様にaddShape()メソッドで自身のヒットボックスを定義している必要があります。

player_sprite.dart
class PlayerSprite extends SpriteComponent with Hitbox, Collidable {
  PlayerSprite({
    required Image image,
    required Vector2 size,
  }) : super(
          sprite: Sprite(image),
          size: size,
        ) {
    // ヒットボックスを定義 
    hitbox = HitboxRectangle(relation: Vector2(1.0, 1.0));
    addShape(hitbox);
  }
  late HitboxRectangle hitbox;

  
  void onGameResize(Vector2 gameSize) {
    // ヒットボックスとコンポーネントの位置を合わせる
    hitbox.position = position;
  }
  
  ・・・

  /// 接触したときの処理
  
  void onCollision(Set<Vector2> intersectionPoints, Collidable other) {
    if (other is EnemySprite) {
      // 敵を消す
      other.shouldRemove = true;
    }
  }
}

アニメーション

Flameではアニメーション処理をEffectと呼びます。

コンポーネントの移動や回転処理などはもちろんupdate()メソッドに書いても良いのですが、コンポーネントのaddEffect()メソッドにエフェクト情報を渡すとFlameが自動的にアニメーション処理を行ってくれます。

下記は5秒かけて左右に移動するMoveEffectの例です。
Flameには他にも回転処理を行うRotateEffectや拡大縮小を行うScaleEffectなどが用意されています。

addEffect(MoveEffect(
      path: [
        Vector2(-200.0, 0.0),
        Vector2(400.0, 0.0),
        Vector2(-200.0, 0.0),
      ],
      duration: 5.0, 
      curve: Curves.linear,
      isInfinite: true,  // ループするかどうか
      isAlternating: true,  // 反転するかどうか
      isRelative: true,  // 相対座標にするかどうか
    ));

まとめ

image_1

だいぶ長くなりましたが、ここで紹介した機能はほんの一例です。
Flameはシンプルながら多機能で本格的なゲームが作れるので面白いですね。

今回作成したサンプルコードは私のGithubからも見れます。
https://github.com/7oh2020/flame_example

この記事で少しでもFlameに興味を持っていただけたら幸いです。
以上です。

Discussion