こんにちは、個人でアプリ開発をしているYuKiOです。
これまでツール系アプリを12本リリースしていますが、このたび、メッセージを選択してアンチを論破する「論破王」という、初めてのゲームアプリをリリースしました。
iPhone版 https://apps.apple.com/app/id1632034935
Android版 https://play.google.com/store/apps/details?id=com.king_of_refutation
ぜひ、遊んでみてください(切実!)笑
Flutterでゲームを作った理由
- 稼ぎやすいジャンルだから
- Flutterのノウハウがあるから
- プログラミング技術向上
Flutterでゲームアプリを作ろうと思った理由は、一つにゲームがツール系アプリと比べて稼ぎやすいジャンルだからです。個人開発では年間の収益目標を立ているのですが、達成の可能性を高めるために新作アプリではゲームジャンルを選択しました。
Unitiyなどを使ってゲームを作る方法もありますが、一から習得するのは時間的にもリスクが高いため、慣れ親しんだFlutterで作ることにしました。
これだけの理由だと「ハングリーさが足りない!」と思われるのも嫌なので、アピールのためにあとづけの理由を付け加えると、今まで作ったことがなかったジャンルを作ることで、プログラミング技術の向上にもなるかなと考えました。
参考文献した資料
Flutter公式動画
英語は得ではなくて何を言っているか分からないし(字幕?笑)、「この動画の人、誰かに似ている?誰だろう?」と意識が別のところいってしまう状況を必死に抑えながら見てみました。
テキトーにざっくりまとめると、Flutterはカードゲームやボードゲームみたいな静的なゲームには向いているみたいなことを言ってると思います。知らんけど。
主にゲームシステムの設計は、この動画で紹介されているレポジトリを参考にしました。
Youtube Flutter公式動画
GitHub - filiph/tictactoe
参考にした本
良いコード/悪いコードで学ぶ設計入門
言わずと知れた技術本なので説明不要だと思いますが、この本が存在しなかったら、このアプリを完成できなかったと思うくらいに参考にしていました。
特に分岐処理、引数、間違いを減らすために、値の受け渡しについて、クラスのインスタンスで受け渡すというのはすごく参考になりました。。
良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方 https://amzn.asia/d/7GOQaqh
※アフィリエイトリンクではありません。
Flutter × Flameを使わなかった理由
- 衝突判定が必要ない
- Flutterとは異なる技術領域だから
- Flutterの良さを活かせないから
Flutterをやっている方からすると、ゲーム作成にあたってflame | Flutter Packageを使わないの?と思われる方もいると思います。
たしかにFlameで2Dのアクションゲームを作るなら適しているかもしれません。下記はキャッチアップのために作ってみたゲームです。
Flutter × Flameでゲームを使ってみました。
— YuKiO|アプリ個人開発|Flutter × Firebase (@oo_forward) May 26, 2022
Flutterとは全然違うので面白くて、これくらいなら簡単にできますが、次回の新作ゲームでは要件に合わないので使用しないことにしました。
操作用の十字キーやボタンや画面構成はFlutter側で、Flameをインスタンス化して、画面のところに組み込んでます。 pic.twitter.com/Vry7VhCARv
ただ今回作るゲームは、ユーザーが選択してから動作が決まる静的なゲームなので、特に衝突判定も必要がなく、Flameを使いませんでした。
また、FlameはFlutterというフレームワークの中で、さらに違うフレームワークを触るようなイメージで、Widgetなどのやりとりもできますが、煩雑になりFlutterのUI作成が簡単というメリットも潰してしまうところも、採用しなかった点です。
Flame自体はとてもすばらしいパッケージなので、2Dゲームを作りたいなら、ぜひ試してみてください。
Flutterのゲーム構成
ゲームの構成について
参考にしたのはFlutter公式の動画およびそこで紹介されているGitHubレポジトリを参考に構成しました。
状態管理はriverpodで、MVVMで作りはじめましたが、結局MV的な構成になっています。
ゲームの表示や勝敗判定などのコントロールや、シナリオの進捗のコントロールについては、ChangeNotifierのnotifyListeners()でビューに変更を通知しています。
おそらく、riverpodは、monoさんが発狂しそうな使い方していますし、設計もミノ駆動さんが発狂するようなことやっていると思います笑
とりあえず、目的のゲームを作ることを優先しました(←言い訳)
ゲームで使った主要なパッケージ
個人的な開発方針として、極力パッケージは使わないようにしています。以前、Flutter2に移行する際に、かなりパッケージのバージョンで苦労したからです。
そんなに多くないですが、今回使った主要なパッケージを紹介します。
flutter_riverpod
アプリの状態管理、知らない人のために説明すると、画面を更新を管理するパッケージです。使うのはそれなりにお勉強が必要です。
go_router
最近流行っている?Navigater2?を簡単に使えるようになるパッケージで、Flutterの公式レポジトリが使っていたので使ってみました。Webっぽいリンク構成にできる感じですが、個人的にNavigaterでやっていた方法とあまり変わらない感じでしたが、修行不足が原因だと思います。
google_mobile_ads
収益化するには絶対必要ですね。何かと困ったちゃんの印象がありましたが、今回はえらく素直に動いてくれています。
慣れてしまえば実装も簡単なので、他のパッケージが開発が滞っていたりするので、こちら一択になっていますね。
firebase_analytics
何かとどれくらい使われているかなどチェックするには必要なので、最近は最初から入れています。
shared_preferences
今回はDBを採用しない方法で設計しましたが、どうしてもクリア状況だけを記録しておく必要があるので、こちらのパッケージでデータを永続化しています。
ゲーム制作で苦労した点
データ作りが大変
ツール系ではアプリが解決する問題が決まれば、すぐに設計に入れますが、ゲームアプリの場合、シナリオやキャラクター設定を考える必要があります。
特にシナリオで分岐を考えるのが、とても大変でしたね。
ツール系と比べて、ゲームアプリの開発が長期化しやすい部分かなと思いました。
ゲームシステムをどうするか?
今回一番悩んだ部分が、ゲームシステムをどうするかでした。
Flutterでツール系アプリを作ってはいますが、ツール系は基本的に、ユーザーが行う1つのアクションに対して、1つの結果を返すことが多いです。
しかしゲームの場合、1つのアクションに関して、複数の結果を、時間差で自動的に返すことが必要です。
今回のゲームでは、メッセージ選択型クイズゲームというジャンルで、SNSのリプ欄を模した画面上で、ユーザーと敵のメッセージを交互に表示後、選択肢を出す、もしくは勝敗表示、という流れを繰り返します。
そこで1連の流れをブロック化して、各ブロックにコンテンツをリストで入れることにしました。
この時にどのコンテンツなのか?また異なるコンテンツが入ってしまうことを防ぐために、独自の抽象クラスを作成し、implementsした各コンテンツのクラスを作成しました。これによってリストに入る値を限定し、且つ各コンテンツの判定をruntimeTypeのみで判定できるようになります。(一部例外ありますが。)
PostBlockリストから適宜必要なものを呼び出して、PostBlockを自動展開し、それぞれのクラスによる動作の違いを、時間差で読み込むことで、まるでSNSで自動返信されているような画面になったり、選択肢を表示したり、または勝敗判定をしています。
abstract class GameData {
GameData();
}
abstract class PostData implements GameData {
const PostData({required this.content});
final String content;
}
class UserPostData implements PostData {
const UserPostData({required this.content, required this.nextPostBlockID});
@override
final String content;
final int nextPostBlockID;
}
class EnemyPostData implements PostData {
const EnemyPostData({required this.content});
@override
final String content;
}
class EnemyPostDataWithImage implements EnemyPostData {
const EnemyPostDataWithImage(
{required this.content, required this.imageFileName});
@override
final String content;
final String imageFileName;
}
class UserSelectAnser implements GameData {
const UserSelectAnser({required this.userPosts});
final List<UserPostData> userPosts;
}
class UserInputAnser<T> implements GameData {
const UserInputAnser({
required this.question,
required this.anser,
this.unit = "",
required this.correctData,
required this.worngData,
});
final String question;
final String unit;
final List<T> anser;
final UserPostData correctData;
final UserPostData worngData;
}
class FollowedWin implements GameData {
const FollowedWin({required this.enemyName});
final String enemyName;
get comment => "$enemyNameがフォローしました。";
}
class BlockWin implements GameData {
const BlockWin({required this.enemyName});
final String enemyName;
get comment => "$enemyNameがブロックしました";
}
class Lose implements GameData {
const Lose();
get comment => "論破されました。";
}
class PostBlock {
const PostBlock({required this.id, required this.dataList});
final int id;
final List<GameData> dataList;
}
シナリオデータの管理をどうするか?
すべてのシナリオはデータクラスとして作成し、クリアするごとに指定のストーリーデータクラスを読み出す形にしました。一部過去の実装の成り行きで、finalになってますが、static constでもいいはず。
class Tutorial01 implements Story {
const Tutorial01();
static const String _enemyName = "入川鉄郎";
static const String _enemyAcountId = "@king_of_reaction";
static const _charactor = "伝説のリアクション芸人";
static const _title = "$_charactorを論破せよ!";
static const Level _level = Level.tutorial;
static const String _iconImageFileName = "irikawa-icon.jpg";
@override
final UserPostData firstPost =
const UserPostData(content: "やっぱりお風呂の温度は、熱湯に限るね〜!", nextPostBlockID: 0);
@override
final List<PostBlock> dataList = const [
PostBlock(id: 0, dataList: [
EnemyPostData(content: "やばいよ!やばいよ!お風呂は熱湯って・・・普通に火傷するからねw"),
UserSelectAnser(userPosts: [
UserPostData(content: "いや、火傷する温度に入るわけないでしょw", nextPostBlockID: 1),
UserPostData(content: "火傷って、何度だと思ってるの?", nextPostBlockID: 2),
]),
]),
PostBlock(id: 1, dataList: [
EnemyPostData(content: "知らないの?熱湯は厚生労働省の定義では、約100度以上のことをさすからね。"),
UserSelectAnser(userPosts: [
UserPostData(
content: "「ねっとう」じゃなくて、「あつゆ」といって熱いお風呂という意味で書いたんだけど・・・",
nextPostBlockID: 11),
UserPostData(
content: "じゃあ、リアクション芸人は100度のお湯に入っているんですか?", nextPostBlockID: 12),
]),
]),
//省略
PostBlock(id: 21, dataList: [
EnemyPostData(content: "知らないの?www厚生労働省の定義で、熱湯は約100度以上のことを指すんだよ!"),
Lose(),
]),
PostBlock(id: 22, dataList: [
EnemyPostData(content: "「ねっとう」しかないだろう。厚生労働省の定義では約100度以上のことを指すんだよ。"),
UserPostData(
content:
"「ねっとう」なんて書いてませんけど。熱いお風呂という意味の「あつゆ」って書いたんですが、43度くらいだから火傷しませんよ。",
nextPostBlockID: 0),
BlockWin(enemyName: _enemyName)
]),
];
@override
getCurrentPostBlock(int currentPostBlockID) {
final currentPostBlock = dataList.firstWhere(
(PostBlock postblock) => postblock.id == currentPostBlockID);
return currentPostBlock;
}
}
ゲームを作ってみたメリット
知識の体感が上がった。
今回ゲームを作ったメリットは、クラスやインスタンスなど、頭では理解していたことがより深く理解というか、体感できる機会が多かったことです。
ツール系アプリは、言い方悪いですが、そこまでインスタンスやクラスを意識しなくても作れてしまいます。
ただ、ゲームの場合、キャラクターを何個も作成したり、そのインスタンスでないとダメ!ということも多々あるので、かなり意識していました。
このゲームを作ってから、ツール系アプリのリファクタリングも捗るようになりましたね。
Flutterでゲームを作るべきか?
Flutterでゲームを作るべきか?についてですが、ツール系アプリも作っていて、ゲームも作りたいならアリだと思います。
ただし、現状は情報が少ないので、自走力が求められるので、まだアプリを全然リリースしたことがない人は、まずはツール系をやった方がいいと思います。
本格的にゲームを作りたいなら、Unitiy(使ったことない)をやるほうがいいとは思いますね〜。
ただFlutterの今後の可能性として、ツール系もゲームが作れる、OSに囚われないというのは大きいかもしれません。
ゲームみたいなツール系アプリ、ツール系みたいなゲームなんていうのも作ったら楽しそうですね。
とはいえ、何かを作るのも、どの言語でつくるかも、自由だ〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜!
さあ、作ろう!
アプリ開発についていろいろ発信しています。
よかったらどうぞ!
ちなみに1ヶ月でAppStoreのレビューが200件増えた方法ですが、下記の音声チャンネルで収録した内容を意識してレビュー依頼を実装した結果です。https://t.co/83Kxy6rVKd
— YuKiO|アプリ個人開発|Flutter × Firebase (@oo_forward) April 12, 2022
HackerMemoを大幅にアップデートしたので、
— YuKiO|アプリ個人開発|Flutter × Firebase (@oo_forward) January 10, 2021
海外向けのPVを作ってみました😆
夜なべして頑張って作ったので、拡散頂けたら泣いて喜びます😭🙏
音有りがオススメです!
元ネタわかる人いるかな〜🤔
アプリはこちら🔽
■iPhoneの方https://t.co/6T10L94Ld1
■Androidの方https://t.co/uBevCXrNw2 pic.twitter.com/wiGLv46kaG