🎮

BonfireをListViewに組み込みたい

2024/12/05に公開

FlutterのWidgetの一部としてBonfireを使えないかなと思いやってみました。
Bonfireを小さなWidgetとして使うのは良いのか?という疑問はありつつも、簡単にキャラクター用のアニメーションを使うことができるため限定的に使うならば良いのではないか?という思いです。

なお、今回のとは逆に「Bonfire上にWidgetを表示する」機能はデフォルトで用意されているので、それはまた別途記事にしたいと思います。

やってみたかったこと

ListView内でBonfireを表示し、リストをタップするとidle状態からrun状態に切り替わる

キャラクターを表示するまでの実装

ページ(Scaffold)の作成

bonfire_sprite_page.dart
class BonfireSpritePage extends StatelessWidget {
  /// コンストラクタ
  const BonfireSpritePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sprite List Sample'),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
          return ListTile(
            leading: const PlayerCharacter(
              size: 30,
            ),
            title: Text('Sprite $index'),
          );
        },
        itemCount: 30,
      ),
    );
}

ListViewでPlayerCharacter ウィジェットを表示しています。
ここはいつも通りのFlutterです。

BonfireWidgetの作成

player_character.dart
class PlayerCharacter extends StatelessWidget {
  /// コンストラクタ
  const PlayerCharacter({
    super.key,
    required this.size,
  });

  /// サイズ
  final double size;

  
  Widget build(BuildContext context) {
    return SizedBox(
      width: size,
      height: size,
      child: BonfireWidget(
        map: WorldMap.empty(
          size: Vector2.all(size),
        ),
        backgroundColor: Colors.transparent,
        player: PlayerCharacterComponent(
          size: Vector2.all(size),
        ),
      ),
    );
  }
}

BonfireWidgetを使用してプレイヤーを表示します。
mapは画面にマップを表示する必須のパラメータですが、今回は特に必要ないのでWorldMap.emptyを使用しています。
playerには後述するPlayerCharacterComponentを指定しています。

Sprite用の画像ファイルの準備

画像のダウンロード

今回は公式のリポジトリからダウンロードしましょう

画像の配置

[PorjectRoot]/assets/imagesに配置してください。pubspec.yamlへの記述も忘れずに。

pubspec.yaml
flutter:
  assets:
    - assets/images/

スプライトシートの読み込み

player_sprite_sheet.dart
SimpleDirectionAnimation get simpleDirectionAnimation =>
    SimpleDirectionAnimation(
      idleRight: _idleRight,
      runRight: _runRight,
    );

Future<SpriteAnimation> get _idleRight => SpriteAnimation.load(
      'knight_idle.png',
      SpriteAnimationData.sequenced(
        amount: 6,
        stepTime: 0.1,
        textureSize: Vector2(16, 16),
      ),
    );

Future<SpriteAnimation> get _runRight => SpriteAnimation.load(
      'knight_run.png',
      SpriteAnimationData.sequenced(
        amount: 6,
        stepTime: 0.1,
        textureSize: Vector2(16, 16),
      ),
    );

画像をBonfire(Flame)に読み込ませる処理です。
詳細はFlameのSpriteSheetを参照してください

PlayerCharacterComponent

player_character_component.dart
class PlayerCharacterComponent extends SimplePlayer with TapGesture {
  /// コンストラクタ
  PlayerCharacterComponent({
    required super.size,
  }) : super(
          animation: simpleDirectionAnimation,
          position: Vector2.zero(),
          initDirection: Direction.left,
        ) {
    anchor = Anchor.center;
  }

  
  void onTap() {
    _switchAnimation();
  }

  /// アニメーションをidleとRunで切り替えます。
  void _switchAnimation() {
    SimpleAnimationEnum newAnimationEnum;
    if (animation?.currentType == SimpleAnimationEnum.idleLeft) {
      newAnimationEnum = SimpleAnimationEnum.runLeft;
    } else {
      newAnimationEnum = SimpleAnimationEnum.idleLeft;
    }
    animation?.play(newAnimationEnum);
  }
}

SimplePlayerを継承してキャラクターの待機・走りアニメーションを設定します。
このコンポーネントをタップしたときにアニメーションを切り替えたかったので、TapGestureを使用して、onTapswitchAnimation()を呼び出しています。

リストをタップして走らせるまでの実装

実現方法は色々あると思いますが、やり方の1つとして実装していきます。

PlayerCharacterComponentの修正

player_character_component.dart
- class PlayerCharacterComponent extends SimplePlayer with TapGesture {
+ class PlayerCharacterComponent extends SimplePlayer {
  /// コンストラクタ
  PlayerCharacterComponent({
    required super.size,
  }) : super(
          animation: simpleDirectionAnimation,
          position: Vector2.zero(),
          initDirection: Direction.left,
        ) {
    anchor = Anchor.center;
  }

-  
-  void onTap() {
-    _switchAnimation();
-  }

  /// アニメーションをidleとRunで切り替えます。
-  void _switchAnimation() {
+  void switchAnimation() {
    SimpleAnimationEnum newAnimationEnum;
    if (animation?.currentType == SimpleAnimationEnum.idleLeft) {
      newAnimationEnum = SimpleAnimationEnum.runLeft;
    } else {
      newAnimationEnum = SimpleAnimationEnum.idleLeft;
    }
    animation?.play(newAnimationEnum);
  }
}

このコンポーネント自体のタップの処理は削除しました。
代わりにswitchAnimation()をpublicにしています。

PlayerCharacterControllerの作成

player_character_controller.dart
class PlayerCharacterController {
  /// プレイヤーキャラクターのアニメーションを切り替えます。
  late void Function() switchPlayerAnimation;
}

このControllerを通じて、リストからプレイヤーのアニメーション切り替えを行います。

PlayerCharacterの修正

player_character
class PlayerCharacter extends StatelessWidget {
  /// コンストラクタ
  const PlayerCharacter({
    super.key,
    required this.size,
+   this.controller,
  });

  /// サイズ
  final double size;

+  /// プレイヤーキャラクターのController
+  final PlayerCharacterController? controller;

  
  Widget build(BuildContext context) {
    return SizedBox(
      width: size,
      height: size,
      child: BonfireWidget(
        map: WorldMap.empty(
          size: Vector2.all(size),
        ),
        backgroundColor: Colors.transparent,
        player: PlayerCharacterComponent(
          size: Vector2.all(size),
        ),
+       onReady: (gameInterface) {
+         final player = gameInterface.player! as PlayerCharacterComponent;
+         controller?.switchPlayerAnimation = player.switchAnimation;
+       },
      ),
    );
  }
}

onReadyはBonfireの準備が完了したら呼び出されるコールバックです。
onReadyにて、PlayerCharacterComponentを取得し、コントローラーにswitchAnimationのFunctionを設定しています。
onReadyで使用しているBonfireGameInterfaceは、ゲーム上に追加されたものを扱うことができます。

BonfireWidetの修正

bonfire_sprite_page.dart
class BonfireSpritePage extends StatelessWidget {
  /// コンストラクタ
  const BonfireSpritePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sprite List Sample2'),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
+         // PlayerCharacterControllerを作成
+         final controller = PlayerCharacterController();
          return ListTile(
            leading: const PlayerCharacter(
                 size: 30,
+                controller: controller.value,
            ),
            title: Text('Sprite $index'),
+           onTap: () {
+             controller.value.switchPlayerAnimation();
+           }
          );
        },
        itemCount: 30,
      ),
    );
  }
}

最後に、PlayerCharacterControllerを通じて、リストのタップ時にキャラクターのアニメーションを切り替えています。

まとめ

FlutterのWidget側からBonfireを表示し、さらにそこからBonfire側を操作するという少々トリッキーなことをやってみました。
スプライト画像など、Bonfireの資産をそのまま流用できるので限定的に使うことが良いのかなと思います。
なお、スクロール後の変更後のアニメーションの復元などは課題として残っていますが、これはこれで一旦完結といたします。
とりあえず今回でBonfireをWidgetとして扱えるということが確認できました。

次回はBonfire側からWidgetを表示する機能について触っていこうと思います。

Discussion