Unity 库开发与游戏开发的不同:Kumiko、Narimono 开发者的体会
ライブラリ開発はゲーム開発と勝手が違うと気づいた話
はじめに
こんにちは、空音です!
普段のゲーム制作で使い回していた仕組みを改良して、Unity向けのライブラリとしてGitHubで公開する取り組みをしています。これまでに、UIライブラリのKumikoと、サウンド再生ライブラリのNarimonoを公開しました。
先日、この2つのライブラリと、現在開発中のライブラリのコードスタイルを統一する作業をしました。最初は書き方をそろえるだけのつもりでしたが、作業を進めるうちに、ライブラリ開発はゲーム開発とは勝手が違うということを強く感じました。
この記事では、ライブラリを実装していて気づいたことを、感想としてまとめていきます!
公開しているライブラリ
まずは、公開している2つのライブラリを簡単に紹介します。
Kumiko(組子)
Unity(uGUI)向けの、ゲームパッド・マウス&キーボード・タッチに対応したUIインタラクションライブラリです。
- 入力の種類に合わせて、ホバー・選択・押下などの表示状態を自動で切り替えます。
- ダブルクリックやクールダウンなど、決定入力に条件を付けられます。
- 方向入力で、上下左右にある要素へ選択を移せます。
Narimono(鳴物)
Unity向けの、同時再生数の制御・3Dサウンド・フェードに対応したサウンド再生ライブラリです。
- 鳴らす音をScriptableObjectで定義し、そのまま渡して再生します。
- Cueごと・カテゴリごと・全体の3段階で、同時再生数の上限を設定できます。
- 上限に達したとき、古い音をいきなり止めずに、フェードアウトさせることもできます。
コードスタイルを統一した
今回の作業のきっかけは、ライブラリごとに少しずつ書き方が違っていたことです。そこで、.editorconfigと.sln.DotSettings(Riderの設定)にルールをまとめ、すべてのライブラリに適用しました。
ブレースや空行、命名規則などの書式は、設定ファイルに書いておけば、IDEの整形でそろえられます。一方で、コメントの書き方や引数の渡し方、ログの文言などは、設定では強制できません。そこで、これらのルールも一つずつ決めていきました。
その中で、ゲーム開発のときには意識していなかったことに、いくつも気づきました。
気づき1:コメントは利用者が読むもの
ゲーム開発で書くコメントは、基本的に自分やチームのメンバーが、ソースコードと一緒に読むものです。
一方でライブラリの場合、利用者の多くはソースコードを開きません。IDEのツールチップに表示されるドキュメントコメントが、利用者にとっての説明書になります。
そこで、ドキュメントコメントで型名やメンバー名を書くときは、<see cref>を使うことにしました。
/// <summary>
/// 音を停止する。すでに終了している音や無効なハンドルを渡した場合は何もしない。
/// </summary>
/// <param name="playback">停止する音のハンドル。</param>
/// <param name="fadeOutDuration">フェードアウトにかける秒数。0以下の場合は即座に停止する。</param>
/// <remarks>
/// フェードアウトを指定すると、フェードアウトの開始と同時にハンドルが無効になる。
/// そのため、フェードアウト中の音に<see cref="Stop" />・<see cref="Pause" />・<see cref="Resume" />を呼んでも何もしない。
/// 一時停止中の音は、秒数を指定しても即座に停止する。
/// </remarks>
void Stop(AudioPlayback playback, float fadeOutDuration = 0f);
<see cref>で書いた名前は、ツールチップ上でリンクになり、そのまま定義に移動できます。また、メンバーの名前を変えたときにはRiderが参照を書き換えてくれるので、コメントの中に古い名前が残ることもありません。
ほかにも、次のようなことを決めました。
- 書く内容:そのメンバー自身の役割・前提・挙動だけを書き、ほかのクラスでの使われ方は書かない
-
書く範囲:public・internal・protectedのメンバーには必ず書き、privateには必要なときだけ
//で書く - 文体:文末に「。」を付け、表記(「ほかの」「すでに」など)もそろえる
気づき2:ゲーム開発では、このレベルのコメントは向いていない
ここまで書いて、逆のことにも気づきました。ゲーム開発の途中で、このレベルのコメントを書くのは向いていないということです。
ゲーム開発では、仕様の変更や調整で実装が頻繁に変わります。そのたびに細かいドキュメントコメントを直すのは大変ですし、直し忘れたコメントは、間違った説明として残ってしまいます。
ライブラリとゲーム開発の違いを、表にまとめてみました。
| ライブラリ | ゲーム開発 | |
|---|---|---|
| コードを読む人 | ソースを開かない利用者 | 自分やチームのメンバー |
| 主な説明の場所 | ドキュメントコメント・README | ソースコードそのもの |
| 実装の変わりやすさ | 公開した部分は簡単に変えられない | 仕様の変更で頻繁に変わる |
| コメントの重点 | 使い方・前提・挙動 | なぜそうしたか |
ゲーム開発では、コメントは「なぜそう実装したのか」を中心に書き、使い方は名前や型で伝わるようにするほうが合っていると感じました。同じコメントでも、誰が、どれくらいの期間読むのかによって、最適な書き方が変わるということがよくわかりました。
気づき3:コメント以外も利用者のことを考える
利用者のことを考える必要があるのは、コメントだけではありませんでした。
公開レベル
開発中のセーブライブラリでは、JSONの読み書きに使うコンバーターをpublicにしていました。しかし、コンバーターはライブラリの内部でしか使っておらず、セーブライブラリに渡して使う方法もない、利用者が使うことを想定していないものでした。
ライブラリでpublicにしたものは、利用者がそれに依存できるものになります。一度公開すると、後から名前や挙動を変えたときに、利用者のコードを壊してしまいます。そのため、内部でしか使わないものはinternalにして、公開するものを必要最低限に絞りました。
ゲーム開発では「とりあえずpublic」でも困ることは少ないですが、ライブラリでは公開レベルがそのまま利用者との約束になります。
ログ
ライブラリのログを読むのは、ライブラリの中身を知らない利用者です。そのため、次のことを意識するようにしました。
- 何が原因かわかる:どのアセットの、どの設定が問題なのかを書く
- どうすればよいかわかる:直し方まで書く
- 原因のオブジェクトを渡す:ログをクリックすると、問題のアセットがハイライトされるようにする
- 大量に出さない:繰り返し起きる警告は、最初の1回だけ出す
Debug.LogError($"{cue.GetType().Name}: {cue.name} の {nameof(cue.AudioResource)} が設定されていません。", cue);
また、英語と日本語のログが混ざっていたので、日本語に統一しました。
引数名
true・false・nullを引数に渡すときは、必ず名前付き引数にすることにしました。
SetValue(_value, notify: true);
EventSystem.current.SetSelectedGameObject(selected: null);
ここで気づいたのが、名前付き引数で呼ばれると、引数名も公開APIの一部になるということです。公開しているメソッドの引数名を変えると、名前付きで呼んでいる利用者の側でコンパイルエラーになります。引数名も、利用者のコードから見える名前として慎重に決める必要があります。
バージョン
ライブラリでは、変更の内容に合わせてバージョンを上げる必要があります。
- パッチバージョン(1.0.0 → 1.0.1):公開しているAPIが変わらない、不具合の修正
- マイナーバージョン(1.0.0 → 1.1.0):互換性を保ったままの、機能の追加
- メジャーバージョン(1.0.0 → 2.0.0):APIの削除や名前空間の変更など、利用者のコードに影響する変更
なお、v1.0.0になる前の開発版では、利用者のコードに影響する変更でもマイナーバージョンを上げるのが一般的です。
Git URLで導入する場合、タグでバージョンを指定している利用者も多く、新しいタグを付けないと、その利用者には修正を届けられません。また、利用者のコードに影響する変更は、リリースノートで目立つように伝える必要があります。
気づき4:説明と実装が食い違う怖さ
もう一つ、印象に残った出来事があります。
セーブライブラリでは、手で編集されたファイルにVector2Intの成分として小数が入っていた場合、四捨五入して読み込むようにしました。コメントとREADMEにも「四捨五入する」と書いていました。
ところが、READMEを見直していて、実装と説明が食い違っていることに気づきました。
// 引数を指定しないMath.Roundは、.5を偶数側に丸める(銀行型丸め)。
Math.Round(2.5); // 2
Math.Round(3.5); // 4
// 0から遠い側に丸めると、四捨五入になる。
Math.Round(2.5, MidpointRounding.AwayFromZero); // 3
引数を指定しないMath.Roundは、.5のときに偶数側へ丸めるため、四捨五入ではありませんでした。ちなみに、UnityのMathf.RoundやMathf.RoundToIntも同じ丸め方です。
ゲーム開発であれば、気づいた人が実装を見れば済む話かもしれません。しかし、ライブラリの利用者はドキュメントを信じて使います。ライブラリのドキュメントは、利用者にとっての仕様そのものだと実感しました。
ライブラリ開発で意識するようになったこと
今回の作業を通して、ライブラリを開発するときに次のことを意識するようになりました。
- ドキュメントコメントは説明書:利用者がツールチップだけで使い方を理解できるように書く
-
公開するものは約束:
publicにするもの、引数名、ログの文言まで、利用者から見えるものを意識する - 説明と実装を一致させる:コメントやREADMEに書いたことは、実装で必ず守る
- 変更は利用者に伝える:バージョンとリリースノートで、何が変わったかを伝える
一方で、ゲーム開発では実装が変わりやすいため、同じレベルのコメントを書くのではなく、「なぜそうしたか」を中心に書くほうが合っていると思います。ただ、ログの書き方や名前付き引数のルールなどは、ゲーム開発でもそのまま役に立つと感じました。
おわりに
最後まで読んでいただき、ありがとうございました!
ライブラリを公開する前は、「普段使っている仕組みを、そのまま公開すればよい」と考えていました。しかし実際には、自分ではない誰かが使うことを前提にすると、コメントからログ、公開レベル、バージョンまで、考えることが一気に増えました。ゲーム開発とはまた違った難しさがあり、とても勉強になっています。
現在は、3つ目のライブラリとしてセーブライブラリのTansu(箪笥)を開発しています。こちらもv1.0.0として公開できる状態になったら、紹介したいと思います!
感想や改善案などがあれば、ZennのコメントやGitHubで教えていただけると嬉しいです!
InputとUpdate 昨日の知識を、今日の技術へ
来源:zenn · 游戏开发社区 · zenn.dev