Unityでメンバをプロパティとしてシュッと書けることとインスペクタに表示することをいい感じに両立させられる方法ってないんですか?

はじめに

これはinonoa advent calendar 2020 の3日目の記事です。そこのあなたも寄稿してみませんか?

adventar.org

本題

C#にはプロパティという機能があります。クラスの実装を隠蔽しつつ実装・使用の煩わしさを緩和してくれる優れものだと思います。たぶん。ここでは主に、getとsetでアクセシビリティを変えたい(値を外から設定することはできないが見ることはできる)という場面での使用を想定します。

public class Hoge
{
    public float Fuga{ get; private set; } = 10;
}

また、Unityでは[SerializeField]という属性をメンバにつけることで、そのメンバがprivateであってもその値をインスペクタで表示できる機能があります。これも中身を(他クラスから)隠蔽しつつ値を調整したりほかのオブジェクトへの参照を与えてやったりできる便利機能です。たぶん。

using UnityEngine;

public class Hoge : MonoBehaviour
{
    [SerializeField] float _Fuga = 10;
}

問題はこれらを両立したいときです。変数を直接外部から設定させたくはない。が値を見たくはある。ついでに細かい調整をインスペクタ上でやりたい。

例えばこのように書けたらいいですね。

using UnityEngine;

public class Hoge : MonoBehaviour
{
    [SerializeField] public float Fuga{ get; private set; } = 10;
}

しかしこうは書けません。というよりインスペクタに表示されません。というのも[SerializeField]は変数しかシリアライズできないらしく、あくまで表記上変数に見えるだけであるプロパティは対象外みたいです。

解決法1

思いついた解決法の一つは下記です。

using UnityEngine;

public class Hoge : MonoBehaviour
{
    [SerializeField] float _Fuga = 10;
    public float Fuga => _Fuga;
}

ほかのクラスから見える部分はプロパティであるFugaで、getしか実装していないので値の設定もされ得ません。 また、インスペクタでは_Fugaが表示されるので((インスペクタ上ではアンダーバーが省略されて表示されるようで、単に Fuga を表示しているように見えます。))、微調整も可能です。

しかし、イメージとしては一つのものを指しているはずのFugaを表現するのに2識別子*1を使うのは個人的には奇妙に感じます。内部で値を参照したいときに_FugaFugaかどっち使うか迷ったり、いい感じのエディタを使う場合は補完に2つ出てきて??になったりしそうです(潔癖症すぎるかも……)。

解決法1.5

ところで、

    public float Fuga{ get; private set; } = 10;

このように使用したプロパティは単にgetやsetとしか書かれておらず実装を何も書いていません。C#ではこのように書いた場合、自動的に内部に変数を作ってその値を返したり設定したりするようにしてくれているらしいです。 [SerializeField]は変数にしか使えないと書きましたが、自動的に生成されるその変数に[SerializeField]をつけるようなことができればいい感じになるのでは……!

ufcpp.net

C#7.3*2からそのような変数(backing field)に対して属性をつけられるようになったらしい!

using UnityEngine;

public class Hoge : MonoBehaviour
{
    [field: SerializeField] public float Fuga{ get; private set; } = 10;
}

これで当初の目的は達成されました。Fuga一つで中身は隠され、しかし見ることはでき、インスペクタ上での設定も可能!めでたしめでたし。

f:id:inonoa:20201214050555p:plain

うーんこの…… 表示はできたものの、変数はプロパティFugaとは別物であって、インスペクタが表示する名前はC#が内部的に決めている変数名になるようです。 頭にFugaとはついていて一応何を指しているかはわかるので、見た目が多少美しくなくても割り切って使うこともできそう……?

解決法2

こちらが解になります。

baba-s.hatenablog.com

こちらを導入することでインスペクタでの表示を上書きすることができるようです。つまりこうなります。

using UnityEngine;

public class Hoge : MonoBehaviour
{
    // ͡コガネブログ様では[]を2つ並べる表記だったがこのように,で並べることもできる
    [field: SerializeField, RenameField("Fuga")] public float Fuga{ get; private set; } = 10;
}

大団円です。

蛇足

このままではコガネブログ様のステマ以上の意味がないので少し追加します。

Odin

自分のプロジェクトではOdinというアセットを使用していることが多いのですが、そちらの[LabeText]属性も同様にインスペクタでの表記を書き換える機能を持っています(自分ではこっちを使ってます)。ほかにもインスペクタをいい感じにする機能がたくさんあるのでオススメです。

assetstore.unity.com

baba-s.hatenablog.com

つまり結局ステマということになります。

文字列を直書きしない

先ほどの [RenameField("Fuga")]のうち "Fuga" の部分ですが、このように文字列を直接書いてしまうのはなるたけ避けたいです。名前が長いとタイポしそう*3

C#6.0からnameof演算子というものが導入されました。変数名などの識別子名を取得できる機能です。この場合エディタでの補完も利くほか、タイポするとコンパイルエラーになるので気づかずに変な誤字をしているような事態にはならないでしょう。

最終形態

最終的には自分ではこのような形に落ち着いています。

using UnityEngine;
using Sirenix.OdinInspector;

public class Hoge : MonoBehaviour
{
    [field: SerializeField, LabelText(nameof(Fuga))] public float Fuga{ get; private set; } = 10;
}

変数を直接外部から設定させたくはない。が値を見たくはある。ついでに細かい調整をインスペクタ上でやりたい。インスペクタの表示も意図した名前であってほしい。という要求をすべて解決しています。

とはいえかなり長くてごちゃごちゃした見た目なので、もっとシュッと書ける方法があったら教えてください。

*1:識別子の意味これであってる……?

*2:少なくとも自分の使っているUnity2019.4.14などはC#7.3に対応しています

*3:とはいえ1か所にしか書かないし、タイポしてもインスペクタの表示がそうなるだけなので気にしすぎの可能性が高い