Flutter でゲームを作ってみた Part1
はじめに
初めまして、私は普段は 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 を鳴らす
基本はこれだけです。
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
だとちょっとやりにくいです。
なので、私は少しだけ改造しちゃいました。
と言っても、SpriteWidget
の sprite_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 をしっかりサポートしているのがそれが仇となってしまいました(╹_╹)
おまけのおまけ
上記のバグ自体はなんだか面白かったのでゲームに組み込みました。
固定フレームレート
なので倍速自体はゲームループを少し加工するだけでそんなに難しくなかったです(╹◡╹)
後半へ続く
前半パートはこれにて終了。
後半は AudioPlayers
を使用したサウンド再生処理を作っていきます。
ゲームならではの落とし穴もあったのでそれも共有したいなと思います!
それでは後半でまたお会いしましょう〜(๑•᎑•๑)
後半です
Discussion