1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Unityのデシリアライズの不思議な挙動に悩まされた話

Posted at

はじめに

Unityで32種類までしか使えないレイヤーを実質無限にする拡張を作ってたときにデシリアライズの仕様でハマったことをメモ。

環境

  • Unity2021.3.0f1
  • Unity2021.3.11f1
  • Unity2022.1.0f1

3つのUnityバージョンで試しましたが全部で起きたのでどうやら仕様っぽいです。

Inspectorのバグみたいな挙動

まずは以下のクラス群を実装します。

Layer.cs
using System;
using UnityEngine;

[Serializable]
public class Layer
{
    [SerializeField] private string _name;

    public Layer(string name)
    {
        _name = name ?? "";
    }
}
BuiltinLayer.cs
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));
}
LayerSettingsAsset.cs
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(""),
    };
}

次に AssetsCreateMy ScriptableObjectsLayer Settings でアセットを2つ作ります。

それらの中身はこんな感じにします。

Layer Settings 1.asset
%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: 
Layer Settings 2.asset
%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

Oct-14-2022 12-24-44.gif

この不思議な挙動はファイルをReimportしても直らず、Unityを再起動すると一時的にリセットされます。

最初は「 _layers のインスタンスが共有されてる?」と思ったのですが、 _layers[3] は正しく別の値になるので _layers のインスタンス自体が共有されてるわけではなさそうです。

デシリアライズの挙動(推測)

この現象は以下の修正を加えると起きなくなります。

 (a) Layer クラスを構造体に変更する
 (b) BuiltinLayer を使わないようにする
 (c) 要素数を足したり減らしたりして32じゃない個数にする

ここまで書いたら勘のいい人ならわかると思いますが、 _layers の一部の要素が実際に共有されていると推測されます。

もう一度ソースコードを確認してみます。

Layer.cs
using System;
using UnityEngine;

[Serializable]
public class Layer
{
    [SerializeField] private string _name;

    public Layer(string name)
    {
        _name = name ?? "";
    }
}
BuiltinLayer.cs
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));
}
LayerSettingsAsset.cs
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は(少なくとも配列は)以下の手順でデシリアライズを行うということです。

  1. 宣言と同時の初期化が実装されていればまずそれを読み込む
  2. シリアライズされた値があれば読み込み済みのインスタンスに対して値を適用する

クラスを構造体に変更したら起きなくなるのは構造体は都度コピーされて別モノになるからです。

ただ、(c)の挙動も合わせて考えると、以下のような場合分けになると推測されます。

 

  • シリアライズされた配列の要素数が宣言と同時の初期化と同じ場合
    1. 宣言と同時の初期化が実装されていればまずそれを読み込む
    2. シリアライズされた値があれば読み込み済みのインスタンスに対して値を適用する
  • シリアライズされた配列の要素数が宣言と同時の初期化と異なる場合
    1. 内部的にインスタンスを new する
    2. 読み込み済みのインスタンスに対してシリアライズされた値を適用する

 
もろもろを勘案すると、当初の LayerBuiltinLayer の実装は

  • BuiltinLayer[Serializable] にしないよう変更する
    • Layer とは別のクラスを使う
  • BuiltinLayer.Default などを BuiltinLayer.Default.AsSerializable() とかに変更する
    • シリアライズ可能なインスタンスが使い回されないようにする

とするのが良いかなと思いました。

雑感

謎の場合分けはパフォーマンスチューニングの一環なんだろうと思いつつそのせいで調査が難航したので、どっちかに統一してほしいなという気持ち。

気になるからフォーラムで中の人に聞いてみようかな。

  1. このときは _layers[0]2 で固定されてしまいましたが、 1 で固定される場合もあります(どうやら InstanceID の小さい方で固定されるっぽい)。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?