👾

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

2020/09/22に公開

はじめに

初めまして、私は普段は 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 もとにかく初なのでツッコミあればお手柔らかにお願いします!

なぜ Flutter でゲームを作ったのか

ぶっちゃけノリです(╹◡╹)
スマホアプリエンジニアとして流行り(?)のクロスプラットフォーム開発をやってみたかった感じです。
ゲームなのは元々個人でゲーム開発をしていてプライベートはゲームでないとモチベを保てないという理由ですねw

使用したライブラリ

前置きはこの辺にして技術的な話をします。
ライブラリは以下のような物を使用しました。

SpriteWidget - 画像表示のライブラリ。ちょっとしたゲームエンジンっぽい。メインで使用する
AudioPlayers - サウンドライブラリ。BGM と SE を鳴らす

https://github.com/spritewidget/spritewidget
https://github.com/luanpotter/audioplayers

基本はこれだけです。

SpriteWidget

画像表示とゲームループを構成するライブラリです。
そのため、ゲーム部分はこの SpriteWidget 上で表現することになります。

オブジェクトは Node で構成されてレイヤーも Node みたいです。

レイヤーの実装

下記のコードはゲームのレイヤーになります。

class GameNode extends NodeWithSize {
  GameNode(Size size) : super(size)
}


void update(double dt) {
  // TOOD: 更新処理.
}

タッチの有効化

さらにタッチを有効にしたい場合はこんな感じです。

class GameNode extends NodeWithSize {
  GameNode(Size size) : super(size) {
    userInteractionEnabled = true;  // タッチを有効にする.
  }
  
  
  bool handleEvent(SpriteBoxEvent event) {
    
    switch(event.type) {
      case PointerDownEvent:
        break;
      case PointerMoveEvent:
        break;
      case PointerUpEvent:
        break;
      case PointerCancelEvent:
        break;
    }
    
    return true;
  }
}

userInteractionEnabled を有効にして、それぞれのタッチイベントのハンドルを handleEvent で定義していく感じです。

Widget の描画

SpriteWidget の描画は簡単です。

class GameWidgetState extends State<GamePage> with TickerProviderStateMixin {

  SpriteWidget _spriteWidget = null;

  
  void initState() {
    super.initState();
  }
  
  
  void Widget build(BuildContext context) {
    super.initState();

    _spriteWidget = new SpriteWidget(new GameNode(MediaQuery.of(context).size));

    return new Scaffold(
      body: new Stack(
        children: <Widget>[
          _spriteWidget,
          // ...
        ],
      )
    )
  }
}

このように作成したレイヤーの Node を SpriteWidget でラップしてやると Widget として扱えます。
以上でレイヤーの構成は終了です。

画像のロード

いよいよ画像を描画してみます・・・と言いたいところですが、まずはイメージデータをロードする必要があります。

class Objects {
  static ImageMap images;
}

class GameWidgetState extends State<GamePage> with TickerProviderStateMixin {

  SpriteWidget _spriteWidget = null;
  ImageMap images;
  bool _loaded;

  
  void initState() {
    super.initState();
    _loaded = false;
    
    Objects.images = new ImageMap(rootBundle);
    Objects.images.load(<String> [
      // 画像のファイルパスを列挙する.
    ]).then((List<ui.Image> images) {
      setState(() => _loaded = true);
    });
  }
}

ImageMap で使用する画像をロードします。個別でロードもできますし上記のように一括でロードすることも可能です。ロード完了した後は通知してあげましょう。
ロードはどこでも出来ますが、initState がやりやすいかと思います。
場合によってはロード処理を分けてもいいでしょうね(╹◡╹)

画像の描画

それでは画像の描画をしましょう。描画自体はここまで下準備するとすごく簡単です。

class GameNode extends NodeWithSize {
  Sprite _sprite;
  GameNode(Size size) : super(size) {
    _sprite = Sprite.fromImage(Objects.images[ロードしたファイルパス]);
      ..position = position
      ..zPosition = 0;
    addChild(_sprite);
  }
}

fromImage でロード済みの ImageMap から Sprite を生成し、座標と Z 座標は設定します。Z 座標は重ね順です。SpriteWidget は昇順で重ねていきます。

画像の切り抜き

ペコシューは画像の切り抜きによるアニメーションを実装する必要がありました。
しかし、画像の切り抜きは SpriteWidget だとちょっとやりにくいです。
なので、私は少しだけ改造しちゃいました。

と言っても、SpriteWidgetsprite_texture.dart

final Rect frame;Rect frame;

にしちゃうだけです。しちゃうだけですが、これが正しいアプローチではない気がします・・・が、今のところ問題は起こってないです。

※追記:これは良くないです。ちゃんとカスタマイズクラスを作りましょう。基本的にコピペで済むのでそんなに難しくないと思います。

それで拡張関数も作ります。

extension SpriteExt on SpriteTexture {
  void setTextureRect(Rect rect) {
    this.frame = rect;
  }
}
Rect rect =  Rect.fromLTWH(0, 0, 88, 88);
_sprite.texture.setTextureRect(rect);
_sprite.size = rect.size; // これは一回だけで良い.

みたいにやると切り抜きが出来ます。
これをゲームループ(update 関数)で切り抜きの座標を変えるとアニメーションさせることができます( ・∀・ )b

おまけ

iPad Pro だとゲームスピードが爆速になってしまいました。原因は今回のゲームは 60FPS を想定した 固定フレームレート 方式を採用していたのですが(レトロ風味ということで)、iPad Pro はデフォルトだと 120FPS で描画されるので、このようになってしまいました。
普通のゲームエンジンは FPS の指定ができるのですが、SpriteWidget はパッと見そのような機能はなかったです。
Flutter 自体は 120 FPS をしっかりサポートしているのがそれが仇となってしまいました(╹_╹)

https://www.youtube.com/watch?v=dEYFN0mVsQI&feature=youtu.be

おまけのおまけ

上記のバグ自体はなんだか面白かったのでゲームに組み込みました。
固定フレームレート なので倍速自体はゲームループを少し加工するだけでそんなに難しくなかったです(╹◡╹)
https://twitter.com/Asteroid_1/status/1308728959719227392?s=20

後半へ続く

前半パートはこれにて終了。
後半は AudioPlayers を使用したサウンド再生処理を作っていきます。
ゲームならではの落とし穴もあったのでそれも共有したいなと思います!
それでは後半でまたお会いしましょう〜(๑•᎑•๑)

後半です
https://zenn.dev/asteroid/articles/31104fc13cd19b256381

Discussion