Flutter でゲームを作ってみた Part2

7 min読了の目安(約4700字TECH技術記事

はじめに

初めまして、私は普段は Swift / Kotlin で iOS / Android アプリを開発しているエンジニアです。

この度、初めて Flutter を使ったのですが、その所感をまとめたいと思います。
また、Flutter はノンゲームのアプリを開発するのがメジャーだと思いますが、今回はあえてゲームを作ってみました。

ペコシューというレトロ風味のシンプルな横シューティングゲームです( ・∀・ )ゞ
AppStore : https://apps.apple.com/us/app/id1527790378
GooglePlay : https://play.google.com/store/apps/details?id=jp.forestonegame.PekopekoShooting

Flutter はとにかく初なのでツッコミあればお手柔らかにお願いします!

前回

前回は主に SpriteWidget に関してお話ししました( ・∀・ )ゞ
https://zenn.dev/asteroid/articles/07df478053f209613058

今回はサウンド関連について追っていたいと思います!

サウンド再生

サウンド再生には AudioPlayer というライブラリを使用します。
https://github.com/luanpotter/audioplayers

AudioPlayer

AudioPlayer はシンプルながら中々高機能のサウンド再生ライブラリです。
シングル再生・ループ再生・ポーズ・停止の機能はもちろん、早送り再生とかも出来てしまいます!

ただ、おそらくあまりゲームで使われる想定で作られておらず、所々クセもあるのでそれも合わせて解説していきたいと思います( ・∀・ )ゞ

シンプルな処理

まずはシンプル処理を解説したいと思います。
とりあえずインスタンス生成はこちら

AudioCache _player = AudioCache();

シングル再生

_player.play('sound/se/se.wav');
_player.play('sound/bgm/bgm.wav');

ループ再生

_player.loop('sound/bgm/bgm.wav');

停止やポーズは play or loop メソッドから返却される AudioPlayer から行います。
await で呼ぶ必要があるので注意です。
(この辺も個人的には若干使いづらい)

AudioPlayer audioSource = await _player.loop('sound/bgm/bgm.wav');

ここから停止やポーズを行えます。

audioSource.stop();   // 停止.
audioSource.pause();  // ポーズ.

AudioPlayer の(ゲームならではの)ハマりどころ

ここからはゲームならでらはの落とし穴について解説します。
私がゲーム開発中にハマったところになります。

SE の再生が段々著しく重くなる

ある程度、ゲーム部分も作って一段落していた頃、SE がすぐに再生されず・・・しばらくすると再生される現象が多発しました。
そして、それは再生数が増えるほど顕著になりました。

この時点でメモリリークしているのかと懸念しました。

そこで AudioPlayeraudio_cache.dart のこの部分が気になりました。

AudioPlayer _player(PlayerMode mode) { 
  return fixedPlayer ?? new AudioPlayer(mode: mode); 
}

AudioCache を生成するときに fixedPlayer を渡さないと AudioPlayer が new されていました。

もしかしたらハードに耐えられない量の AudioPlayer が生成されてしまい上記のような不具合
が起きているのかなと推察しました。

なので、AudioCache 生成時に fixedPlayer も設定してみました。

AudioCache _player = AudioCache(fixedPlayer: new AudioPlayer());

このようにしたところ、無事に滞るなくスムーズに再生できました。
しかし、このやり方だと SE の再生が重複したときに前になっている SE が途中で途切れてしまいました。(これはこれで使いどころがありますが)

なので、SE 毎に AudioCache を生成することにしました。

Map _audioMap = Map();
_audioMap.addAll({
  'sound/se/a.wav' : new AudioCache(fixedPlayer: new AudioPlayer()),
  'sound/se/b.wav' : new AudioCache(fixedPlayer: new AudioPlayer()),
  'sound/se/c.wav' : new AudioCache(fixedPlayer: new AudioPlayer()),
});

// ロードが必要か分からないが一応やっている.
_audioMap.forEach((key, value) {
  value.load(key);
});

このアプローチが正しいかどうかわかりませんが、今のところ問題はなかったです。
この内容は一応、issue に上げておきました。
https://github.com/luanpotter/audioplayers/issues/619

諸注意

ここは私独自で行ったのですが、その対応が正しいのかどうかは未だ判断できておりません。
ただ、一応そちらの対応でリリースまでは行っているので、無責任に提示しているわけではないということはご了承ください。

また、不具合などが発生しても一切責任は取りません。
技術系の記事でこのようなことを述べてしまい申し訳ありません。

BGM をシングル再生できない

二つ目の落とし穴です。
通常の BGM はループ再生していましたが、エンディングの BGM のみシングル再生したかったので、 play メソッドで再生したのですが、なんとループで再生されてしまいます・・・。

これも AudioPlayeraudio_cache.dartplay メソッドと loop メソッドを比較してみると loop の方では

player.setReleaseMode(ReleaseMode.LOOP);

が記述されていました。
なので、play メソッドでも release モードを指定してやれば良いのかなと思いました。

Future<AudioPlayer> play(String fileName,
      {double volume = 1.0,
      bool isNotification,
      PlayerMode mode = PlayerMode.MEDIA_PLAYER,
      bool stayAwake}) async {
    String url = await getAbsoluteUrl(fileName);
    AudioPlayer player = _player(mode);
    player.setReleaseMode(ReleaseMode.STOP);    // added.
    await player.play(
      url,
      volume: volume,
      respectSilence: isNotification ?? respectSilence,
      stayAwake: stayAwake,
    );
    return player;
  }

こうすると上手く行きました。おそらく、これも fixedPlayer を使用している影響で起こったことだと思います。loop メソッドでループ再生した後は releaseModeReleaseMode.LOOP のままになっていたのだと思います。

こちらは、issue に上げておいて最終的に私の PR がマージされました( ・∀・ )ゞ
https://github.com/luanpotter/audioplayers/pull/632

以上がサウンド再生を行う方法になります。
途中色々書きましたが、基本的にはシンプルな API で使いやすくて便利なライブラリです。
諸注意にも書きましたが、バグフィックスに関しては正しいのかどうか分からないので注意して下さい。

Flutter でゲームを作ったけどどうなのさ?

こちらの結論はやはり Unity や Unreal Engine にはかなり劣ります。SpriteWidget はゲームエンジンを名乗っていますが、正直機能的にはまだ色々と足りないですし、今回使用したサウンドライブラリの Audio Player もゲーム用途では不安定です。

それと Flutter の State を中心に行う宣言的な UI とゲームの組み合わせも結構難しかったです。この辺りは何も考えずに実装していたので、苦労しました・・・。
さらにホットリロードもゲーム部分にはあまりうまくいかずでした。

なので、総合的に考えて Flutter でゲームが作りたいっていう意外の人にはオススメ出来ません。
また 2D ゲームで作るならという条件も付きます。

・・・ですが、触っていて楽しかったですし、iOS / Android のゲームが一緒に作れるのはやっぱり強力です。それに設計ももっと考えて実装すれば、Flutter との相性も格段に良くなると思います(╹◡╹)

そして何よりゲームとしてちゃんとリリースできたのですごく嬉しかったです!
ちょっとディスり強めですが簡単な 2D ゲームなら全然作れますよ♪

おまけ

実は何気に OSS に初めてコミットしましたw
不具合自体は割とすぐに発見できたので、issue は割とすぐに作れました。PR はちょっと手順ありましたが、ネットでググりながら他の方のものを参考に作りました。

こういう経験も積めてよかったとリリースできた今本当に思います。
これからもガンガン個人開発してゲームやアプリをリリースしてきたいです(╹◡╹)