SerializeReferenceはUnity 2019.3から使用できる機能で、従来のSerializable + SerializeFieldよりも柔軟なシリアライズが行えます。
Unknown managed type referenced
SerializeReferenceを使ってこんなコードを書いたとします。
using System;
using UnityEngine;
namespace SRQuest
{
[CreateAssetMenu(menuName = "SerializeReferenceQuest/Move Class Sample")]
public class MoveClassAsset : ScriptableObject
{
[SerializeReference] private ISomething something;
}
public interface ISomething
{
}
[Serializable]
public class Hoge : ISomething
{
[SerializeField] private string someField = default;
}
}
ScriptableObjectであるMoveClassAsset
をアセットとして作成すると、Hoge
のインスタンスが代入された状態になります。
何らかの理由でHoge
の命名が気に入らなくなったので、Hoge
をFuga
に改名したくなったとします。
// public class Hoge : ISomething
public class Fuga : ISomething
すると、ちょっと困った事態になります。
2021.1までのUnityを使用している場合、Unknown managed type referenced: [Assembly-CSharp] SRQuest.Hoge
というエラーが表示されます。「Hoge
という名前のクラスが見つからないよ!」と言っているわけです。
この状態になると、このアセットはクラス名の見つからない状態を解消しない限り、一切保存されなくなります。Fuga
のインスタンスを代入しなおそうがnullを代入しようが無駄で、いくら変更を加えようと保存されなくなります。上記の例ではScriptableObjectを使っていますが、コンポーネントを使用している場合はコンポーネント単位で保存がされなくなります。
2021.2以降ではMissing types referenced from component ~~~ on game object ~~~
みたいなWarningが出ます。前述のような「全く保存されない」という事態には陥らなくなっており、内容を上書きすることが可能になっています。ただ、その場合既にHogeに設定していたデータは失われてしまいます。
今回は上記のようなクラス名の変更に対処してみます。
内部表現
問題を理解するため、SerializeReferenceにセットしたデータがどのように保存されているのかを見てみます。
上記の例で作ったScriptableObjectをテキストエディタで開いてみるとこんな感じになります(抜粋):
something:
id: 0
references:
version: 1
00000000:
type: {class: Hoge, ns: SRQuest, asm: Assembly-CSharp}
data:
someField: a
SerializeReferenceのフィールドsomething
にはid
が記録され、実際の内容は末尾のreferences
リストに分離して記録されます。
インスタンスの型はクラス class
、名前空間 ns
、アセンブリ asm
で文字列によって指定されています。
こんなふうに、SerializeReferenceに代入したインスタンスは型名、名前空間、アセンブリ名で記録されているため、コード側でそのいずれかが変更された場合に、変更を追いかけることができないわけです。
上記のYAMLは2021.1での例で、2021.2からはちょっと内容が異なりますが、基本は同じです。
余談ですが、「クラス名の変更の際にクラスの同一性をトラックできない」という問題はScriptableObjectやMonoBehaviourを継承したクラスでも起きそうな気がしますが、実際にはそれらのクラス名を変更しても問題が発生することはありません。
これはUnityがMonoBehaviourやScriptableObjectをクラス名ではなくスクリプトファイルに設定されたGUIDによって認識しているからです。スクリプトファイルを削除したりしない限りGUIDによって同一性が保たれます。
さらに、ひとつのC#スクリプトファイルは複数のクラス定義を持つことができてしまいますので、「どのクラス定義を使うか」を決定する必要があります。そこで「MonoBehaviourやScriptableObjectのクラス名はファイル名と同じでなければならない」という制約が課されている、というわけです。
SerializeReferenceではあらゆる非UnityEngine.Objectクラスがシリアライズできてしまうので、GUIDによるトラックができず、今回のような問題がおきることになります。
SerializeField + Serializableによるシリアライズの場合は、そもそもフィールドの型とインスタンスの方は常に同一であるため、型情報を保存する必要がありません。クラス名を変えても、持っているフィールドの構造が同じであればデータを保持したまま移行することができます。
対処法
本題に戻ります。こちらのIssueに対処法が記載されていました。
UnityEngine.Scripting.APIUpdating.MovedFromAttributeを使うとデータを保持したままクラスの改名が行えるほか、上記の2021.1までのUnityで発生するバグも回避することができます。
MovedFromAttributeはUnity内部で使用されるクラスらしく、ググってもほぼ使用例が出てきませんが、UnityCsReferenceに説明文がありました。
UnityCsReference/UpdatedFromAttribute.cs at master · Unity-Technologies/UnityCsReference
What is this : Attribute that can be used to indicate that a type has been moved/renamed.
Motivation(s):
- When a class is moved from one namespace to an other (potentialy in a different assembly), the APIUpdater needs
a way to be informed of this.- Serialization by reference of plain C# classes needs to be informed of classes being renamed so that it can
read data that was saved by an earlier version.
雑翻訳:
これは?:型が移動したり改名したことを示すことができるAttribute
モチベーション:
- クラスが別の名前空間(や別のアセンブリ)に移動した際、APIUpdaterに知らせる必要がある。
- プレーンなC#クラスの参照のシリアライズは、以前のバージョンで保存されたデータを読むためにクラスの改名を把握する必要がある。
改名したクラスに対し、こんな感じで設定すると動きました。アセンブリ・名前空間・クラス名のそれぞれについて変更前の名称を指定し、変更がない場合はnullを指定すればいいようです。
[Serializable, UnityEngine.Scripting.APIUpdating.MovedFrom(true, "Present.Name.Space", "Assembly-Present", "PresentClass")]
public class Fuga : ISomething
{
}
クラスを消したいときは?
今回はクラスの改名(正確にはアセンブリ名、名前空間、クラス名のいずれかの変更)に対処する方法を紹介しました。
2021.1までのUnityでは、改名と同様のメカニズムで、クラスの定義を消去した場合にも変更内容が保存されなくなる問題が発生します。クラスを消しちゃってるのでMovedFrom
属性は使えません。あらかじめそのクラスのインスタンスを全部nullで代入しなおせば問題は回避できますが、広範に使用されているクラスの場合はそうもいきません。ではどうするか……?
……全くおすすめできないワークアラウンドですが、テキストエディタで該当のSerializeReferenceを手動で消去する以外の方法を知りません。(ご存じの方がいたら教えてください……。)