Skip to content

Commit 0096373

Browse files
committed
Fixed null-reference exception when domain reload is enabled in Play Mode options
1 parent 5cf1fc2 commit 0096373

File tree

3 files changed

+134
-10
lines changed

3 files changed

+134
-10
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
namespace UnityHierarchyFolders.Editor
2+
{
3+
using System;
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using UnityEditor;
7+
using UnityEngine;
8+
9+
/// <summary>
10+
/// A singleton that contains info about edited prefabs and persists changes between domain reloads.
11+
/// </summary>
12+
[Serializable]
13+
internal class ChangedPrefabs : IEnumerable<ValueTuple<string, string>>
14+
{
15+
private const string KeyName = nameof(ChangedPrefabs);
16+
17+
[SerializeField] private string[] _paths;
18+
[SerializeField] private string[] _contents;
19+
20+
private static ChangedPrefabs _instance;
21+
22+
public static ChangedPrefabs Instance
23+
{
24+
get
25+
{
26+
// _instance is null only in PrefabFolderStripper.RevertChanges() when Instance is called for the first time.
27+
// If _instance is null at that point, it means the domain reloaded, so the instance must be retrieved from PlayerPrefs.
28+
// In all other cases, _instance is created with help of Initialize before operating on it, so FromDeserialized won't be called.
29+
if (_instance == null)
30+
{
31+
_instance = FromDeserialized();
32+
}
33+
34+
return _instance;
35+
}
36+
}
37+
38+
public (string path, string content) this[int index]
39+
{
40+
get => (_paths[index], _contents[index]);
41+
set
42+
{
43+
_paths[index] = value.path;
44+
_contents[index] = value.content;
45+
}
46+
}
47+
48+
public static void Initialize(int length)
49+
{
50+
_instance = new ChangedPrefabs
51+
{
52+
_paths = new string[length],
53+
_contents = new string[length]
54+
};
55+
}
56+
57+
public static void SerializeIfNeeded()
58+
{
59+
// Serialization is only needed if prefabs are edited before entering play mode and the domain will reload.
60+
// In all other cases, changes to prefabs will be reverted before a domain reload.
61+
#if UNITY_2019_3_OR_NEWER
62+
if (EditorSettings.enterPlayModeOptions.HasFlag(EnterPlayModeOptions.DisableDomainReload))
63+
return;
64+
#endif
65+
66+
string serializedObject = EditorJsonUtility.ToJson(Instance);
67+
PlayerPrefs.SetString(KeyName, serializedObject);
68+
}
69+
70+
private static ChangedPrefabs FromDeserialized()
71+
{
72+
string serializedObject = PlayerPrefs.GetString(KeyName);
73+
PlayerPrefs.DeleteKey(KeyName);
74+
var instance = new ChangedPrefabs();
75+
EditorJsonUtility.FromJsonOverwrite(serializedObject, instance);
76+
return instance;
77+
}
78+
79+
public Enumerator GetEnumerator() => new Enumerator(this);
80+
81+
IEnumerator<(string, string)> IEnumerable<ValueTuple<string, string>>.GetEnumerator()
82+
{
83+
return GetEnumerator();
84+
}
85+
86+
IEnumerator IEnumerable.GetEnumerator()
87+
{
88+
return GetEnumerator();
89+
}
90+
91+
public struct Enumerator : IEnumerator<ValueTuple<string, string>>
92+
{
93+
private readonly ChangedPrefabs _instance;
94+
private int _index;
95+
96+
public Enumerator(ChangedPrefabs instance)
97+
{
98+
_instance = instance;
99+
_index = -1;
100+
}
101+
102+
public bool MoveNext()
103+
{
104+
return ++_index < Instance._paths.Length;
105+
}
106+
107+
public void Reset() => _index = 0;
108+
109+
public (string, string) Current => _instance[_index];
110+
111+
object IEnumerator.Current => Current;
112+
113+
public void Dispose() { }
114+
}
115+
}
116+
}

Editor/Prefab Handling/ChangedPrefabs.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/Prefab Handling/PrefabFolderStripper.cs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,10 @@
88
using UnityEditor.Build;
99
using UnityEditor.Build.Reporting;
1010
using UnityEngine;
11-
using Object = UnityEngine.Object;
1211

1312
[InitializeOnLoad]
1413
public class PrefabFolderStripper : IPreprocessBuildWithReport, IPostprocessBuildWithReport
1514
{
16-
private static (string path, string assetContent)[] _changedPrefabs;
17-
1815
static PrefabFolderStripper()
1916
{
2017
EditorApplication.playModeStateChanged += HandlePrefabsOnPlayMode;
@@ -39,10 +36,12 @@ private static void HandlePrefabsOnPlayMode(PlayModeStateChange state)
3936
if ( ! StripSettings.StripFoldersFromPrefabsInPlayMode || StripSettings.PlayMode == StrippingMode.DoNothing)
4037
return;
4138

39+
// Calling it not in EnteredPlayMode because scripts may instantiate prefabs in Awake or OnEnable
40+
// which happens before EnteredPlayMode.
4241
if (state == PlayModeStateChange.ExitingEditMode)
4342
{
4443
// Stripping folders from all prefabs in the project instead of only the ones referenced in the scenes
45-
// because a prefab can be hot-swapped in Play Mode.
44+
// because a prefab may be hot-swapped in Play Mode.
4645
StripFoldersFromAllPrefabs();
4746
}
4847
else if (state == PlayModeStateChange.EnteredEditMode)
@@ -60,21 +59,23 @@ private static void StripFoldersFromDependentPrefabs()
6059
AssetDatabase.GetLabels(GetAssetForLabel(path)).Contains(LabelHandler.FolderPrefabLabel))
6160
.ToArray();
6261

63-
_changedPrefabs = new (string, string)[prefabsWithLabel.Length];
62+
ChangedPrefabs.Initialize(prefabsWithLabel.Length);
6463

6564
for (int i = 0; i < prefabsWithLabel.Length; i++)
6665
{
6766
string path = prefabsWithLabel[i];
68-
_changedPrefabs[i] = (path, File.ReadAllText(path));
67+
ChangedPrefabs.Instance[i] = (path, File.ReadAllText(path));
6968
StripFoldersFromPrefab(path, StripSettings.Build);
7069
}
70+
71+
// Serialization of ChangedPrefabs is not needed here because domain doesn't reload before changes are reverted.
7172
}
7273

7374
private static
7475
#if UNITY_2020_1_OR_NEWER
7576
GUID
7677
#else
77-
Object
78+
UnityEngine.Object
7879
#endif
7980
GetAssetForLabel(string path)
8081
{
@@ -88,16 +89,20 @@ private static
8889
private static void StripFoldersFromAllPrefabs()
8990
{
9091
var prefabGUIDs = AssetDatabase.FindAssets($"l: {LabelHandler.FolderPrefabLabel}");
91-
_changedPrefabs = new (string, string)[prefabGUIDs.Length];
92+
ChangedPrefabs.Initialize(prefabGUIDs.Length);
9293

9394
for (int i = 0; i < prefabGUIDs.Length; i++)
9495
{
9596
string guid = prefabGUIDs[i];
9697
string path = AssetDatabase.GUIDToAssetPath(guid);
9798

98-
_changedPrefabs[i] = (path, File.ReadAllText(path));
99+
ChangedPrefabs.Instance[i] = (path, File.ReadAllText(path));
99100
StripFoldersFromPrefab(path, StripSettings.PlayMode);
100101
}
102+
103+
// If domain reload is enabled in Play Mode Options, serialization of the changed prefabs is necessary
104+
// so that changes can be reverted after leaving play mode.
105+
ChangedPrefabs.SerializeIfNeeded();
101106
}
102107

103108
private static void StripFoldersFromPrefab(string prefabPath, StrippingMode strippingMode)
@@ -115,7 +120,7 @@ private static void StripFoldersFromPrefab(string prefabPath, StrippingMode stri
115120

116121
private static void RevertChanges()
117122
{
118-
foreach ((string path, string content) in _changedPrefabs)
123+
foreach ((string path, string content) in ChangedPrefabs.Instance)
119124
{
120125
File.WriteAllText(path, content);
121126
}

0 commit comments

Comments
 (0)