この記事は traP夏のブログリレー 9 日目の記事です
こんにちは、23B期待の新星(自称)のcobaltです。エディター拡張について書こうと思っていたのですが、その前に前提について書いた記事があってもいいと思ったので変更しました。
動機
言わずと知れた有名ゲームエンジン、Unity。Unityは様々な面でプログラマの面倒を見てくれますが、人間がゲームを作っている以上どうしてもミスというものは生じてしまいます。コード上のバグや、数値設定のミスなどです。
今回はUnityを使う上でのお作法というか、ミスを減らすための仕組みづくりの方法について紹介していきます。
ドキュメントコメントを書こう
さっきまでの書き出しに反してこれはUnity自体の話ではないのですが、C#には『XML ドキュメント コメント』という機能があります。これはC#のソースファイルにXML形式でコメントを書いておくと、コンパイラやIDEがそれに応じて便利機能を提供してくれるというものです。
Visual Studioユーザーであれば、シンボルをマウスオーバーするとこのような表示が出てくることがあるはずです。
ドキュメントコメントを使えば、自分で作ったクラスにもこのような説明を付けることができます。例えば以下のようなクラスを作ったとします。
クラスを使う人には、以下のような説明が表示されます。
こういった説明文は、変数名などから見えにくいクラスの情報をプログラマに素早く伝え、バグを未然に防ぐための強力な手段になりえます。ドキュメントコメントには他にも、他のメンバーやWebサイトへのリンク、型パラメーターへの注釈、サンプルコードといった様々な内容を書くことができます。ただのコメントの代わりにドキュメントコメントを書いて、自然言語での説明も提供しましょう!
RequireComponentを使おう
Unityでコンポーネントの操作をするとき、GetComponentを使うことがあると思います。GetComponent<T>()はGameObjectにアタッチされているT型(またはその派生型)のコンポーネントを取得するという、Unityのコンポーネント指向を支える重要なメソッドです。
しかしGetComponentをするまでは、本当にオブジェクトにそのコンポーネントがアタッチされているのかは分かりません。もちろんTryGetComponentなどを使って例外処理やAddComponentをすればよいのですが、GetComponentの処理が走る前に仕組みで対策出来たらうれしいですよね?そこで登場するのがRequireComponentAttributeです。例えば下のような、BGMを再生するスクリプトを書いたとしましょう。
3行目にある[RequireComponent(typeof(AudioSource))]が肝です。このBgmPlayerクラスは、アタッチされたGameObjectにAudioSourceが付いていることを前提とした内容がStartメソッド内に書いてあります。この前提をRequireComponentという形でコードに書くことで、Unityは「このクラスはAudioSourceを必要とする」ということを認識し、BgmPlayerをアタッチしたときに自動的にAudioSourceを追加します(既にAudioSourceがある場合以外)。こうすればPlayモードに入ってNullReferenceExceptionでエラーが出るより前に、自動的にアタッチし忘れ問題を防ぐことができます。
Rangeを使おう
RequireComponentの他にミスを減らすのに便利なAttributeとして、RangeAttributeがあります。これはシンプルに言うと、変数にセットできる値の範囲を制限するために使うAttributeです。
Unityのインスペクターからステータスなどを設定するとき、プレイヤーのジャンプ力を強く設定し過ぎて吹き飛んで行ってしまったり、弾速を遅くしすぎてゲームにならなかったりした経験がある人もいるのではないでしょうか。intやfloatは人間の感覚では値の範囲が極めて広いので、適正な数値設定がどれくらいなのか分からなくなりがちです。そこでRangeAttributeの出番です。メンバ変数に[Range(/*定数値*/,/*定数値*/)]と付けてみましょう
public class PlayerMovement : MonoBehaviour
{
[SerializeField][Range(3.0f, 6.0f)] float MovementSpeed;
[SerializeField][Range(2.0f, 5.0f)] float JumpHeight;
(略)
}
するとインスペクタの表示がいい感じのスライダー表示に変わり、その範囲の値を設定することが一目でわかります。
負の値を入力して欲しくない時や、スライダーで簡単に値を調整したいときなどに使うといいでしょう。ちなみにint型にも使えます。
(発展編)OnDrawGizmoは便利だよ
UnityにはGizmo(ギズモ)という機能があります。これはプレビューやデバッグなどによく使われる、ゲームには登場しない視覚要素の一種で、例えばDirectionalLightを選択すると表示される光のアイコンと光の方向を表す黄色い線は、どちらもGizmoの一種です。
Gizmoは自作したスクリプトなどにも付けることができます。例えば弾幕ゲームで、3-Way弾を撃つ方向をプレビューする以下のようなギズモは
次のようなたった四行のメソッドで描くことができます。
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, transform.position + new Vector3(0, 5));
Gizmos.DrawLine(transform.position, transform.position + Quaternion.Euler(0, 0, 20) * new Vector3(0, 5));
Gizmos.DrawLine(transform.position, transform.position + Quaternion.Euler(0, 0, -20) * new Vector3(0,5));
}
常に表示されるのが邪魔であれば、OnDrawGizmosの代わりにOnDrawGizmosSelectedを使うと良いです。3-Way弾の拡散の角度をインスペクタからスライダーを弄って調整し、その結果をリアルタイムにGizmoでプレビューする、といったことが出来るわけです。そう考えると便利そうに聞こえませんか?
(発展編)エディター拡張を書こう
なんだかんだ私はエディター拡張が大好きなので最後はこれです。エディター拡張は機能がとてつもなく多いので別の記事にまとめて書こうと思うのですが、エディター拡張それ自体は特に難しい技術ではありません。インスペクター拡張でコンポーネントに不正な値を入力できないようにしたり、Property Drawerで自作クラスをいい感じに表示できるようにしたり、ウィンドウを追加してステージに登場する敵の情報を一括管理したりと、プログラマやレベルデザイナーの助けになる様々な機能を拡張することができるようになっています。特に他のプログラマやレベルデザイナーが触れる部分は、いい感じのUIになっていると嬉しいでしょう。
まとめ
プログラマであれば、人間はミスをするということを(多くは自分がミスをした経験を通して)痛いほど知っていることでしょう。C#やUnityも人間がミスをするということをよく知っているので、それを防ぐような様々な機能を提供してくれています。ここに書いた機能はいずれも「無くても動く」ものでゲーム開発で必須ではありませんが、ミスを減らし効率や体験を向上させるためにもぜひ活用していただきたいです。
明日は@karoさんの記事です。お楽しみに!