BonfireをListViewに組み込みたい
FlutterのWidgetの一部としてBonfireを使えないかなと思いやってみました。
Bonfireを小さなWidgetとして使うのは良いのか?という疑問はありつつも、簡単にキャラクター用のアニメーションを使うことができるため限定的に使うならば良いのではないか?という思いです。
なお、今回のとは逆に「Bonfire上にWidgetを表示する」機能はデフォルトで用意されているので、それはまた別途記事にしたいと思います。
やってみたかったこと
ListView内でBonfireを表示し、リストをタップするとidle状態からrun状態に切り替わる
キャラクターを表示するまでの実装
ページ(Scaffold)の作成
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の作成
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への記述も忘れずに。
flutter:
assets:
- assets/images/
スプライトシートの読み込み
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
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
を使用して、onTap
でswitchAnimation()
を呼び出しています。
リストをタップして走らせるまでの実装
実現方法は色々あると思いますが、やり方の1つとして実装していきます。
PlayerCharacterComponentの修正
- 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の作成
class PlayerCharacterController {
/// プレイヤーキャラクターのアニメーションを切り替えます。
late void Function() switchPlayerAnimation;
}
このControllerを通じて、リストからプレイヤーのアニメーション切り替えを行います。
PlayerCharacterの修正
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の修正
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