Swiftで「RPG 勇者の冒険」というゲームを作った時のノウハウ

More than 1 year has passed since last update.

はじめに

今回作成したiPhoneアプリであるRPG 勇者の冒険を作った際に行った施策とその効果を残しておこうと思います。
今回作成したのはゲームですが、ゲーム以外にも参考になる箇所は多々あると思います。

どんなゲーム?

RPG 勇者の冒険は冒険する洞窟を決めたら、送り込む勇者(モンスター)・装備・スキルを設定して出陣させるゲームです。
その後の冒険は勇者が勝手に進めてくれるという、放置系RPGゲームです。(無料です。)

qiita.jpg
-1日5分から遊べるRPG-

qiita2.jpg
-様々なカードを集めて冒険しよう!-

こんなゲームです。

ゲーム自体は「戦闘準備→出陣」の単純なゲームですが、内部で保持するコンテンツは膨大で250MBほどの大量コンテンツのゲームに仕上がってます。
無料なので、興味のある人は是非ダウンロードにご協力ください!
勇者の冒険はこちらからDLLできます

RxSwiftについて

ゲームを作成する時に真っ先に入れたのがこのRxSwiftです。
今回は描画処理が煩雑だったので何かライブラリを入れたい気持ちと、勉強のためにRxSwiftの導入を入れてみました。

イベントが発火したタイミングでイベント発火から離れた箇所の描画をすることは多々あります。
例えば、今回のケースだと洞窟を冒険中に剣を手に入れた場合です。
ただのデータ上で剣を手に入れただけならデータを追加すれば良いのですが、それをユーザーに通知しなければゲームとしての面白さがありません。
(勇者の冒険はバックグランドでゲームが進行していくので、データだけ更新していくと何も変化が無いゲームになってしまいます。)

その時に出番となるのがRxSwiftです。
剣を手に入れたというイベントをトリガーにオブジェクトへ通知を出します。
その通知を受け取って描画を開始します。
例えば、剣を手に入れるとフッターにバッジがつきます。
装備リストを表示ししてる時には取得した剣のカードが現れてnewマークが付きます。

他にもデータ保存中に画面の端にsaveの文言を出したり、冒険している勇者たちのステータスを逐一マップに表示して必要に応じて交換を出したりしています。
とにかくオブザーバーパターンを簡単に使えるて、なおかつカスタマイズが容易にできるので超オススメです。

メリット

ゲーム内では大いに存在するイベントのトリガーで描画する処理が楽に、綺麗に書ける。
APIなどの非同期処理の待ち受け処理も容易に書ける。
メモリリークが発生しにくいプログラムが作りやすい。
(購買解除の処理はインスタンスが破棄されたら自動的に行うなどの設定ができる。)

デメリット

あまりない。
強いて言うと、CocoaPodsで入れるとコンパイルが遅くなる。くらいかな?
あ!、あと学習コストが少し高い。

総評

使う使わないに限らず入れておくと便利なスタンダード装備。

NSNotificationCenterはもう古いにも書いていますが、NSNotificationCenterを使うならRxSwiftを使ったほうが良いです。

データ保存

データ保存というとRealmが有名ですが・・・
今回は見送りました。
そのあと少し後悔しました・・・。

見送った理由としてはゲームの性質です。
ゲームを作ると装備スキルなどの一概に一つのスキーマーに収まりにくいデータが発生します。
装備は武器・盾・鎧のように分類すればRealmRDBに格納できますが、スキルはほぼ不可能でした。(少なくてもその時の僕には)
なので「NSKeyedArchiver+NSCoding」でデータを保持しました。

※Realmを検討した時点では継承したクラスの管理ができなかった。(Realmで指定したクラスしか使えなかった。今はどうなってるかわからないが、将来的には対応する方針の模様。)

メリット

データの保存に関することをほとんど考えなくて良いので、軽量アプリではオーソドックスに使えると思います。
どのデータパターンでも保存・復元が容易なので、オンメモリの感覚でデータを扱えます。
小規模アプリにはオススメです!

スレッド間でのデータやりとりに強い。
(realmを使うと別スレッドにデータを渡すことが面倒だったりしますが、この方法は特に何かに依存していないためスレッドをまたぐ時に障壁がほとんどありません。)

デメリット

トランザクションが無いため、関連するデータは一括で保存する必要がある。そのため、データ数が増えてくると5〜6秒くらい保存処理に時間がかかることがあります。
上記の派生で、データ保存する際に大量のメモリを使うことがあり大容量データを保存すると落ちやすくなります。(固まります。)
中規模以降で行う場合は保存パターンを検討する必要がある。
(大規模の場合はデータ暗号化についても検討する必要があると思うので、このような方法は取らないと思いますが・・・)

総評

僕は最終的に「NSKeyedArchiver+NSCoding」を使いながら、データの分割保存をして大容量のデータを保存しました。
保存途中で落ちてもデータが壊れないように細工もしました。
自前のマイグレーションも作りました。
しかし、これらをわざわざ作るくらいならRealmで保存できる実装を考えたほうが現実的です。
なので、この方法は小規模アプリ(小データアプリ)には超オススメですが、それ以降の場合はやめたほうが良いです。

余談

Realmではデータの検索にfilterなどが使えるところに優位性がありますが、検索の速度だけで考えるならSwiftfilterも十分早いです。
Swiftの場合はオンメモリになってしまう点が欠点ですが、メモリに乗せてしまえばfilterflatMapを使えば高速にデータへアクセスできます。
なので、Realmのデータアクセスに関する優位性はメモリ外を検索できるところかなと思います。

データ管理

データ保存と話が似てますが、本件はSwiftに限った話です。Objective-cではこの欠点はあまり気になりません。
Swiftの欠点はコンパイルの遅さです。
ある程度の規模までは気になりませんが、ファイル数が500〜1000を超えたあたりから急激にコンパイルが遅く感じます。
一つは僕のPCがMacbook Airのためメモリ不足で毎回全ビルドが走っているからかもしれません・・・。(部分的な修正の場合は一瞬でコンパイルされるのですが・・・)
文献を調べて色々試したのですが、抜本的な解決には至らず。

で、その原因ですが。
上記に記載している通りで僕はカード(モンスター、武器、盾、鎧、スキルetc..)などを種類ごとにクラスを作っているのが問題でした。
通常なら

装備クラス
   ↑
武器クラス
   ↑
固有武器(エクスカリバーなど)

このような構成でコードを書くことは大いにあると思いますが、Swiftでこの構成にすると固有武器(クラス)の数だけコンパイルが発生します。
なので、武器を増やせば増やすほどコンパイルが遅くなります。

対策

我慢。

・・・いや、まじです。
本来なら固有クラスを減らして別管理をすべきなのですが、保存方式でも書いた通り「NSKeyedArchiver+NSCoding」の形式でデータを管理しているため様々なところ直さないと改善できないことがわかったのです。
なので、今回はこの方式でデータを管理してコンパイル中は別の調べ物をしたりしてました。

次回からはデータ保存形式をRealmRDBなどに準ずる形式まで落としこんで、プログラム部分のクラスを減らそうと思います。

余談

パラメータはあとで変更できるように、武器攻撃力などの固有データはセーブデータには保存してません。
武器の攻撃力とかを保存してもあまりメリットが無いですからね・・・
(だったらプログラムを変えることもできるのでは無いかと言われるとおっしゃる通りなのですが、、、100個以上のクラスを変えるのは本当にもう、、、なんというか・・・orzでした。)

この部分は今後のゲーム作成をする上でも課題の一つになりました。
次のゲームで答えが得られたらまた記事にしようと思います。
自分向けTODO データ領域とプログラム領域を同一にしないようにしつつ、開発効率をあまり落とさない設計を考える。)

保存処理を別スレッド

これは最後までやって良かった系です。
プログラムの保存処理をsaveメソッドとforceSaveメソッドの二つに分けておきました。
とりあえず、データを書き換えたから保存したい場合はsaveメソッドを実施して課金などのクリティカル要素が強い場合はforceSaveメソッドで保存するようにしました。

両者の違いは実施を促す実施するの違いです。
もともとはsaveのみだったのですが、本ゲームはスレッドを多用するためあちらこちらでデータの保存を行われれるとデータ保存の処理が頻繁に走ってしまいます。
なので保存処理を別スレッドにしておき、保存が促されると定期的に保存させてました。

具体的に言うとsaveメソッドが実行されるとsaveFlgのようなフラグを立てます。そしてsaveFlgを監視するスレッドを常駐させて、saveFlgフラグが立っている場合に保存するようにしました。
このようにすることで、保存する処理が一箇所に固まるため修正が容易にできました。
(ただし、この方法はすべてのデータを一括で保存する場合にしか使えません。)

なお、保存した場合などの通知をするときにはスレッドをまたぐため

observeOn(MainScheduler.sharedInstance)

上記のようにRxSwftを通して通知を送る必要があります。
勇者の冒険では様々なスレッドを駆使しているので、RxSwiftの恩恵を大いに感じました。
(Realmではこの方法で生データの受け渡しができないので、基本はIDを渡して受け取ったスレッドで検索する形になります。しかし、自前でデータを持っている場合は生データも渡せるので開発効率の観点では[少しですが]メリットがありました。)

メリット

一括保存をする際にデッドロックが起きにくい。(スレッドセーフなども駆使するとより強固に!)

デメリット

個別保存する場合はこの方法は使えないし、使うメリットも無い。

課金周り

課金周りはこちらの記事に書いたように行いました。
(実際はちょっと違いますが・・・。)

特筆することはあまり書けません。メソッド名もあまり書かないほうが良いとのことなので具体的なことは書きませんが・・・。
課金だけを行う場合はリンク先をみてください。

メリット/デメリットもなく、無料アプリでは課金、広告周りのコードは必須になります。
動画広告は一旦見送りました。

TODOリスト

僕は何かしらのモノ作りをする時に、必ずTODOリストを作ります。
大体においてEverNoteを使って、コーディング(PC)の後にスマホでデバッグを延々と行いEverNoteにバグや足りない機能を書いていきます。

この方法の良いところは、電車の中だろうが昼休みだろうがどこでもTODOリストを作ってPCと同期できる点です。
(EverNoteが2端末のみになった時に僕はとても悲しみました・・・)

メリット

いつでもメモが取れる。
次にやることが明確になる。

デメリット

特になし。
気づくとTODOに押しつぶされて精神が病む・・・orz

コンテンツ作りの縛り

今回は自分に一つの縛りを入れました。
コンテンツ作りで飽きないゲームを作ろうと。
前回の偽りの物語を作った時はマップの作成が苦行で苦行で仕方がありませんでした。
終盤ではモチベーションも下がり、ひたすら作業を行っていた感じがありました・・・。
それでも頑張れたのはRPGゲームを作ってみたいという一心だったのです。

しかし、今回は違います。RPGゲームは前回作ったので、コンテンツを絞って苦行を減らすようにしました。
RPGのシステムを作ることは好きなのですが、プログラムやパラメータ調整以外に時間を取られたく無いためとにかく苦手な要素を減らしてコンテンツ作りに従事できるように工夫しました。

その一つが、モンスターを仲間にすることでした。

敵を作ることで、パーティーメンバーのコンテンツも一緒に出来上がる

となるので、コンテンツ作りがだいぶ楽になりました。
また、マップの移動も無くしましたのでプログラムに集中しやすく前回よりも楽しく作れました。
(プログラム量は前回よりもかなり多いのですが、作成期間は半分ちょっとで出来上がりました。)

テスト

勇者の冒険は戦闘を自動で行ってくれるので、コンテンツが増えてもテストがとても楽にできました。
僕はテストコードを一切書かないので、この方法でテストができると負担がかなり減りました。

勇者の冒険のインターフェースは単純で

パーティーを決めて洞窟に送り込む

だけなので、作成した敵やスキルの確認はPCが手元に無くてもできます。
あとはバグをEverNoteに書き出していって、まとまった時間が取れたら直していくというのを繰り返しました。

期間

本ゲームは偽りの物語の作成後、1ヶ月後の2016/5に着手したので約6ヶ月で作成しました。
(2016/10末に完成。)

P.R.

勇者の冒険の発売記念で偽りの物語をしばらくセールをしてます。(本記事公開から約3週間ほど行ってます。)
よければご購入お願いします。

勇者の冒険は無料で始めれるので、記事を読んだ方は職人気質としてダウンロードお願いします!

次回作について

Swiftはとても面白い言語だし、将来性もある素晴らしい言語だと思う。
しかし、iPhoneアプリしか使えない上にCocos2dなどのフレームワークが手入れされなくなってきているため次回の選択としては難しい局面を迎えていると思う。

なので、次回作はCocos creatorUnityに手を出さなければならないかと(Swiftから離れるので)憂鬱な気持ちでいっぱいです。
もし、お勧めのフレームワークを知っている方がいたら教えて欲しいです。
一番の理想はSwiftでAndroidとiPhoneのアプリが作れることなのだが・・・

最後に

ゲームを作るとわかると思うが、ゲーム作成は自由度がとてもWebアプリのようなセオリーが無いためプログラミングをとても楽しめます。
一方で「どうすればこれを実行できるのだろうか?」という回答がネットに無いので、詰むときはあっさり詰んでしまう。

なので、初めにゲームを作る時にはやりたいことの技術を事前に一つ一つ検証しておく慎重さが重要になります。
しかし、それでも自分が作ったゲームが完成してAppStoreなどに陳列されると言いようのない感動があります。

僕はプログラムが好きだから今後もプログラムメインのゲームを考えて作っていこうと思うし、このまま趣味で利益を上げれればと思ってます。
長い記事を読んでいただきありがとうございました!