はじめに
Unityで32種類までしか使えないレイヤーを実質無限にする拡張を作ってたときにデシリアライズの仕様でハマったことをメモ。
環境
- Unity2021.3.0f1
- Unity2021.3.11f1
- Unity2022.1.0f1
3つのUnityバージョンで試しましたが全部で起きたのでどうやら仕様っぽいです。
Inspectorのバグみたいな挙動
まずは以下のクラス群を実装します。
using System;
using UnityEngine;
[Serializable]
public class Layer
{
[SerializeField] private string _name;
public Layer(string name)
{
_name = name ?? "";
}
}
public static class BuiltinLayer
{
public static readonly Layer Default = new(nameof(Default));
public static readonly Layer TransparentFX = new(nameof(TransparentFX));
public static readonly Layer IgnoreRaycast = new("Ignore Raycast");
public static readonly Layer Water = new(nameof(Water));
public static readonly Layer UI = new(nameof(UI));
}
using UnityEngine;
[CreateAssetMenu(menuName = "My ScriptableObjects/Layer Settings")]
public class LayerSettingsAsset : ScriptableObject
{
[SerializeField] private Layer[] _layers =
{
BuiltinLayer.Default,
BuiltinLayer.TransparentFX,
BuiltinLayer.IgnoreRaycast,
new(""),
BuiltinLayer.Water,
BuiltinLayer.UI,
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
new(""),
};
}
次に Assets
→ Create
→ My ScriptableObjects
→ Layer Settings
でアセットを2つ作ります。
それらの中身はこんな感じにします。
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 00dcba1fefff445680661ee99e49dafa, type: 3}
m_Name: Layer Settings 1
m_EditorClassIdentifier:
_layers:
- _name: 1
- _name: TransparentFX
- _name: Ignore Raycast
- _name: 1
- _name: Water
- _name: UI
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 00dcba1fefff445680661ee99e49dafa, type: 3}
m_Name: Layer Settings 2
m_EditorClassIdentifier:
_layers:
- _name: 2
- _name: TransparentFX
- _name: Ignore Raycast
- _name: 2
- _name: Water
- _name: UI
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
- _name:
_layers[0]
と _layers[3]
だけファイルごとに異なるデータにします。
↑この編集をしようとInspectorから値をいじってる最中にすでにおかしなことが起きるので、直接ファイルを開いて書き換えたほうがわかりやすいです。
これら2つのアセットを交互に選択すると、Inspector上では _layers[0]
だけ変化しなくなります。1
この不思議な挙動はファイルをReimportしても直らず、Unityを再起動すると一時的にリセットされます。
最初は「 _layers
のインスタンスが共有されてる?」と思ったのですが、 _layers[3]
は正しく別の値になるので _layers
のインスタンス自体が共有されてるわけではなさそうです。
デシリアライズの挙動(推測)
この現象は以下の修正を加えると起きなくなります。
(a) Layer
クラスを構造体に変更する
(b) BuiltinLayer
を使わないようにする
(c) 要素数を足したり減らしたりして32じゃない個数にする
ここまで書いたら勘のいい人ならわかると思いますが、 _layers
の一部の要素が実際に共有されていると推測されます。
もう一度ソースコードを確認してみます。
using System;
using UnityEngine;
[Serializable]
public class Layer
{
[SerializeField] private string _name;
public Layer(string name)
{
_name = name ?? "";
}
}
public static class BuiltinLayer
{
public static readonly Layer Default = new(nameof(Default));
public static readonly Layer TransparentFX = new(nameof(TransparentFX));
public static readonly Layer IgnoreRaycast = new("Ignore Raycast");
public static readonly Layer Water = new(nameof(Water));
public static readonly Layer UI = new(nameof(UI));
}
using UnityEngine;
[CreateAssetMenu(menuName = "My ScriptableObjects/Layer Settings")]
public class LayerSettingsAsset : ScriptableObject
{
[SerializeField] private Layer[] _layers =
{
BuiltinLayer.Default,
BuiltinLayer.TransparentFX,
BuiltinLayer.IgnoreRaycast,
new(""),
BuiltinLayer.Water,
BuiltinLayer.UI,
...
_layers[0]
が _layers
のインスタンスに依らず常に BuiltinLayer.Default
を参照している場合、Inspectorからの書き換えは他のインスタンスに影響します。
_layers[3]
は new
で個別に生成されてるので共有されておらず、他に影響がなかったわけです。
今回は BuiltinLayer
のフィールドが private
だったり readonly
をつけてたりして深く考えてなかったのですが、調査中はシリアライズ経由でインスタンスの中身が書き換えられることを完全に見落としてました。
* * * * *
(a)と(b)の挙動でわかることは、Unityは(少なくとも配列は)以下の手順でデシリアライズを行うということです。
- 宣言と同時の初期化が実装されていればまずそれを読み込む
- シリアライズされた値があれば読み込み済みのインスタンスに対して値を適用する
クラスを構造体に変更したら起きなくなるのは構造体は都度コピーされて別モノになるからです。
ただ、(c)の挙動も合わせて考えると、以下のような場合分けになると推測されます。
- シリアライズされた配列の要素数が宣言と同時の初期化と同じ場合
- 宣言と同時の初期化が実装されていればまずそれを読み込む
- シリアライズされた値があれば読み込み済みのインスタンスに対して値を適用する
- シリアライズされた配列の要素数が宣言と同時の初期化と異なる場合
- 内部的にインスタンスを
new
する - 読み込み済みのインスタンスに対してシリアライズされた値を適用する
- 内部的にインスタンスを
もろもろを勘案すると、当初の Layer
と BuiltinLayer
の実装は
-
BuiltinLayer
を[Serializable]
にしないよう変更する-
Layer
とは別のクラスを使う
-
-
BuiltinLayer.Default
などをBuiltinLayer.Default.AsSerializable()
とかに変更する- シリアライズ可能なインスタンスが使い回されないようにする
とするのが良いかなと思いました。
雑感
謎の場合分けはパフォーマンスチューニングの一環なんだろうと思いつつそのせいで調査が難航したので、どっちかに統一してほしいなという気持ち。
気になるからフォーラムで中の人に聞いてみようかな。
-
このときは
_layers[0]
が2
で固定されてしまいましたが、1
で固定される場合もあります(どうやらInstanceID
の小さい方で固定されるっぽい)。 ↩