「Unityでenumを使うと状態管理とか便利だからやってみよう」
「新しい状態増えたから追加するか」
「スクリプト内での見た目も整えよう」
「…あれ?さっきまで設定してた要素じゃなくなってるな…」
こんな経験ありませんか?
私は何度もこれに悩まされていたため、ついにエディター拡張をして問題解決に挑戦してみました
この記事ではその過程と成果についてまとめていこうと思います
問題のきっかけ
これは冒頭にも述べた通りですが、Unityではenumを使うと状態管理などがしやすくも、 要素の追加には対応できていない というところで不便さを感じたことがきっかけです
具体例
以下はテストとして作成したScriptableObjectのクラスです
TestStateというenumを定義しており、SerializeField 属性をつけることでInspectorから編集できるようにしています
using UnityEngine;
[CreateAssetMenu(fileName = "Test", menuName = "Scriptable Objects/Test")]
public class Test : ScriptableObject
{
[SerializeField] TestState _test;
}
enum TestState
{
Idle,
Move,
Attack,
Jump
}
これを実際にInspectorで見てみると…
このように、TestStateに定義したものを選べるようになっています
ここでDieという要素を追加してみましょう
enum TestState
{
Idle,
Move,
Attack,
Jump,
Die // 追加
}
問題なく追加されていますね
では Dieという要素をTestStateの一番上に持っていった場合 どうなるでしょうか?
enum TestState
{
Die, // 一番上に移動
Idle,
Move,
Attack,
Jump
}
さっきまでInspector上では"Idle"を選択していましたが、この変更を保存すると…
要素の順番が反映されるとともに、選択していた要素も"Die"になってしまいました…
当たり前といえば当たり前ですが、私はこれに不便さを感じてしまいました
問題について
上記の例のように、enumの定義の順番が変わってしまうような要素の追加をした場合に、Inspectorに表示される順番とともに設定した要素も変わってしまうことが問題だと感じています
このような挙動になる理由として
- enumに定義したものは、自動で 連続な値 が割り振られている
- Unityはenumをシリアライズするときに、その 割り振られた値を保存する
ということがあげられます
先の例で言えば、
enum TestState
{
Idle, // 0
Move, // 1
Attack, // 2
Jump // 3
}
というように値が割り振られていたということであり、Inspectorで "Idle" を設定した場合にUnity内部では "Idle" ではなく、そこに割り振られていた "0" を保存していたというわけです
あとから "Die" を追加しても問題が起きなかったのは割り振られた値に変更がなかったためです
enum TestState
{
Idle, // 0 <= 番号に変更なし
Move, // 1
Attack, // 2
Jump, // 3
Die // 4
}
しかしその後、要素の順番を入れ替えた時に問題が起きたのは、 順番を入れ替えたことによって割り振られる番号が変わってしまったから というわけですね
enum TestState
{
Die, // 0
Idle, // 1 <= 順番が変わって番号も変わった
Move, // 2
Attack, // 3
Jump // 4
}
ここまでの流れをまとめると、
- Idle = 0をInspectorで設定した
- Unityは "Idle" ではなく "0" を保存した
- Die = 4を追加した(ここでは変化なし)
- Die = 0に変更した
- Idel = 1に変わった
- Unityは保存されている "0" に対応する、現在のenumの要素である "Die" を表示した
ということになります
ちょっとの修正で済むのであれば気にすることもないのですが、
- あらゆる場所でenumを使っている
- 要素を順番の初めの方に追加する
この条件下では、ほとんどの場所で さっきまで設定していた要素とは一個以上ずれている わけですから、それらをすべて修正しなければいけないなんて考えたくもありません…
全くあり得ない条件ではないということもわかると思います
問題解消について
ここまでで、
自分で番号を割り振ればいいのでは?
と思った人もいると思いますが、 "全くその通り"です
enumには自分で番号を振ることもでき、自分で番号の管理ができれば問題も解消するわけです
Unityがenumを番号で管理していることとも相性がいいですね
実際に以下のようにすれば、ほとんどの場合苦労しないでしょう
enum TestState
{
Idle = 0,
Move = 10,
Attack = 20,
Jump = 30
// 番号に幅を持たせることで要素を追加してもある程度は対応できる
}
…とはいえ、わざわざ番号を振らなければならなかったり、いくら幅を持たせても幅以上に要素を追加したくなった場合は結局番号の振り直しをしたり、と面倒ごとは残ってしまいます(そんなに手間じゃないんだからあきらめろ、と言われればそれまでですが…)
なにより、最初はこのような定義で作ったとしても、
enum TestState
{
Idle = 0,
Move = 1,
Attack = 2,
Jump = 3
}
あとからこのような定義の方がきれいだとなった場合、
enum TestState
{
Move = 1,
Jump = 3,
Attack = 2,
Idle = 0
// 番号に変更なし
}
限定的な状況かもしれませんが、これ、気持ち悪くないですか…?
問題解決に挑戦
というわけで、前振りが長くなりましたが、ここからはどのように問題解決をしようとしたかについて説明していきます
と、さぞ凄いことを考えたかのように言いましたが、問題解決のために考えたロジックは極めて単純です
- 変更前のenum(割り振られた番号と名前)を保存する
- Inspectorで好きな要素を設定
- enumを変更
- 現在Unityに保存されている値に対応する"変更前のenumの要素の名前"を取得
- 取得した名前が"変更後のenumで割り振られている値"を取得し、Unityに現在保存されている値をこの値で上書きする
- Inspectorの表示を更新
文で説明してもわかりにくいため、具体例で見ていきましょう
1.まずはenumの定義を保存します
enum TestState
{
Idle,
Move,
Attack,
Jump
}
Idle : 0
Move : 1
Attack : 2
Jump : 3
💾 保存しましょう
2.Inspectorで好きな要素を設定
今回は仮で "Idle" を設定しているとします
つまり、Unityでは "0" を保存しています
3.enumを変更
"Idle" を一番下に移動しました
enum TestState
{
Move,
Attack,
Jump,
Idle // 一番下に移動
}
💾 保存しましょう
4.現在Unityに保存されている値に対応する"変更前のenumの要素の名前"を取得
現在Unityには "0" が保存されていたため、変更前のenumに照らし合わせると "Idle" を取得できます
Idle : 0 // <= ここを参照
Move : 1
Attack : 2
Jump : 3
5.取得した名前が"変更後のenumで割り当てられている値"を取得し、Unityに現在保存されている値をこの値で上書きする
変更後のenumにおいてIdleは "3" が割り当てられているため、現在Unityに保存されている "0" という値を "3" に書き換えます
enum TestState
{
Move,
Attack,
Jump,
Idle // <= ここを参照
}
6.Inspectorの表示を更新
最後にInspectorの表示を更新すれば、Unityは現在保存している値に対応するenumの要素を表示してくれるため、変更後のenumでも変わらず "Idle" を選択していると表示されます
成果物について
以下がプロジェクトGithubリンクになりますが、README.mdに書いてある通りの手順を踏んでいただくと作成した機能をご使用いただけます
お試し程度に作ってみたものでありまだまだプロトタイプレベルの完成度ではありますが、ぜひ試しに使ってみていただければと思います
現在はScriptableObjectにしか対応していませんが、いずれは(やる気と時間があれば)enumを使っているすべての箇所へ対応できるようにしたいと思っています
バグがあった場合はぜひともお伝えいただけると幸いです