🔥

FlutterのBonfireでタッチエフェクトを表示したい

2024/10/06に公開

BonfireやFlameの記事は少ないので、同じように詰まっている人の助けになれば…

Bonfireとは

BonfireはFlutterでRPGなどのゲームを作成できるフレームワークです。
Flutter公式のゲームエンジンであるFlameをベースとしているため、BonfireにFlameのコンポーネントを使用することもできます。
Bonfireの入口はBonfireWidgetというStatefulWidgetなので、FutterのWidgetと組み合わせることができます。

作りたかったもの

スマホゲームでよく見る、タップした箇所にエフェクトを表示するやつです。

実装

TouchEffectorの作成

class TouchEffector extends GameComponent with TapGesture {
  /// コンストラクタ
  TouchEffector() {
    // 他のコンポーネントよりも上に表示する
    renderAboveComponents = true;
  }

  /// タッチエフェクトの表示を行います。
  
  void onTapDownScreen(GestureEvent event) {
    add(_TouchEffectParticle(position: event.worldPosition));
    super.onTapDownScreen(event);
  }

  // do nothing
  
  void onTap() {}

  /// GameComponentはデフォルトでは表示しないようになっているので表示
  
  bool get isVisible => true;
}

/// タッチ時に表示するパーティクルです。
class _TouchEffectParticle extends ParticleSystemComponent with HasPaint {
  _TouchEffectParticle({
    required super.position,
  }) : super(
          anchor: Anchor.center,
        ) {
    particle = CircleParticle(
      paint: paint..color = Colors.cyan,
      lifespan: 1.5,
    );
  }

  
  Future<void> onLoad() async {
    // フェードアウト
    add(
      OpacityEffect.fadeOut(
        EffectController(
          duration: 1.5,
          curve: Curves.easeOutQuad,
        ),
      ),
    );
    // 縮小
    add(
      ScaleEffect.to(
        Vector2.zero(),
        EffectController(duration: 1.5),
      ),
    );
    return super.onLoad();
  }
}

BonfireWidgetに追加

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Touch Effect'),
      ),
      body: BonfireWidget(
        map: WorldMap.empty(),
        backgroundColor: Colors.transparent,
        components: [
          TouchEffector(),
        ],
      ),
    );
  }
}

解説

追加したコンポーネント(TouchEffector)について

touch_effector.dart
class TouchEffector extends GameComponent with TapGesture {

TouchEffectorGameComponentを継承しています。
Bonfire(その元になっているFlameも)は基本的にComponentを使用することでゲーム上のあらゆる機能を実現していく作りになっています。
BonfireではGameComponentまたはそれを継承したクラスを使用することで、Bonfireの様々なComponentを使用できます。

TapGestureはこのコンポーネントにタップ機能を追加するために追加しています。
Flame(Bonfire)はこれに限らずmixinで機能を追加する形が多いため、車輪の再発明にならないようにドキュメントリポジトリで探してみると意外に用意されていることがあるかもしれません。

touch_effector.dart
 TouchEffector() {
    // 他のコンポーネントよりも上に表示する
    renderAboveComponents = true;
  }

Bonfireはコンポーネントごとにレイヤー(優先度)が設定されていますが、renderAboveComponents = true;を使用して、他のレイヤーよりも上に表示できるようにしています。

touch_effector.dart
  
  void onTapDownScreen(GestureEvent event) {
    add(_TouchEffectParticle(position: event.worldPosition));
    super.onTapDownScreen(event);
  }

onTapDownScreenで画面上のタップした位置に_TouchEffectParticleを追加しています。
追加にはaddを使用していますが、これはこのコンポーネントの子要素にaddしています。
一方でgameRef.add()も目にするかと思いますが、こちらはBonfireGameの(内部で使用されている)ベースとなるコンポーネントに追加するものです。
ただし今回はレイヤー的に後ろに回り込んでしまうので renderAboveComponentsを設定したTouchEffectorにaddします

touch_effector.dart
  
  bool get isVisible => true;

GameComponentはデフォルトでは非表示となっているため、isVisibleをtrueにして、タッチエフェクトを画面上に表示できるようにしています。

_TouchEffectParticleパーティクルについて

パーティクルは指定時間が経過すると自動的に消滅するコンポーネントです。これはFlameの機能です。

touch_effector.dart
class _TouchEffectParticle extends ParticleSystemComponent with HasPaint {

ParticleSystemComponentを継承し、パーティクルとして扱えるようになります。

HasPaintはBonfireで定義されているmixinですが、Componentを継承しているのでFlameでも使えます。
後述するエフェクトを使用するために必要なPaintを生成してくれます。

touch_effector.dart
_TouchEffectParticle({
    required super.position,
  }) : super(
          anchor: Anchor.center,
        ) {
    particle = CircleParticle(
      paint: paint..color = Colors.cyan,
      lifespan: 1.5,
    );
  }

このあたりはパラメータ(position,anchor,particle)の設定です。
円形のParticleを設定しています。

touch_effector.dart
 
  Future<void> onLoad() async {
    // フェードアウト
    add(
      OpacityEffect.fadeOut(
        EffectController(
          duration: 1.5,
          curve: Curves.easeOutQuad,
        ),
      ),
    );
    // 縮小
    add(
      ScaleEffect.to(
        Vector2.zero(),
        EffectController(duration: 1.5),
      ),
    );
    return super.onLoad();
  }

onLoadはゲーム上に追加されたときに一度だけ呼び出されるメソッドです。このパーティクルが生成されるタイミングでParticleSystemComponentにaddします。

onLoadでは透明度を変えるOpacityEffectと、大きさを変えるScaleEffect.toを使用しています。

参考

Effectを使わずupdateメソッドで自力で透明度(大きさ等も)を処理させる方法もありますが、Effectを使うほうがシンプルに感じたのでそちらを使っています。

  
  void update(double dt) {
    final double opacity = max(0.75 - progress * lifespan / lifespan, 0);
    paint.color = paint.color.withOpacity(opacity);
    super.update(dt);
  }

追加したBonfireWidgetについて

touch_effect_sample_page.dart
      body: BonfireWidget(
        map: WorldMap.empty(),
        backgroundColor: Colors.transparent,
        components: [
          TouchEffector(),
        ],
      ),

Bonfireを使用するにはBonfireWidgetを使用します。
今回はmapを空で設定していますが、WorldMapByTiledなどでマップファイルを使用することができます。
最後にcomponentsには今回作成したTouchEffectorコンポーネントを設定しています。

まとめ

今回は、Bonfireを使用してタッチエフェクトを表示するために、GameComponentonTapDownScreenParticleSystemComponentを使用しました。
また、便利なmixinのTapGesture,HasPaintも確認しました。

もっと良い方法があるかもしれませんが、ドキュメントに掲載されていない内容も割とあったためこのやり方で実現しました。
なので詰まったときはリポジトリ(RafaelBarbosatec/bonfire)から読み解く必要があるなと感じるとともに、理解が進むと割とサクッとできるな、というのが現状のBonfireの感想です。

今後はGridViewやListViewなどと組み合わせられるのもやっていきたいです。

Discussion