Skip to content
This repository was archived by the owner on May 9, 2023. It is now read-only.

Commit 0afccad

Browse files
committed
Improve TransformTree efficiency
- Added batching to the update method so that a maximum of 2000 GameObjects are traversed each frame. - Changed from OrderedDictionary.Remove to OrderedDictionary.RemoveAt when pruning entries as the former needs to iterate through all entries to find the index of the key, whereas the latter is constant time.
1 parent 0e37e80 commit 0afccad

File tree

3 files changed

+127
-75
lines changed

3 files changed

+127
-75
lines changed

src/Inspectors/GameObjectInspector.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public void ChangeTarget(GameObject newTarget)
8282
this.Target = newTarget;
8383
GOControls.UpdateGameObjectInfo(true, true);
8484
GOControls.UpdateTransformControlValues(true);
85-
TransformTree.RefreshData(true, false);
85+
TransformTree.RefreshData(true, false, true);
8686
UpdateComponents();
8787
}
8888

@@ -109,7 +109,7 @@ public override void Update()
109109

110110
GOControls.UpdateGameObjectInfo(false, false);
111111

112-
TransformTree.RefreshData(true, false);
112+
TransformTree.RefreshData(true, false, false);
113113
UpdateComponents();
114114
}
115115
}
@@ -220,7 +220,7 @@ private void OnAddChildClicked(string input)
220220
var newObject = new GameObject(input);
221221
newObject.transform.parent = GOTarget.transform;
222222

223-
TransformTree.RefreshData(true, false);
223+
TransformTree.RefreshData(true, false, true);
224224
}
225225

226226
private void OnAddComponentClicked(string input)

src/ObjectExplorer/SceneExplorer.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void Update()
6464
public void UpdateTree()
6565
{
6666
SceneHandler.Update();
67-
Tree.RefreshData(true);
67+
Tree.RefreshData(true, false, false);
6868
}
6969

7070
public void JumpToTransform(Transform transform)
@@ -94,7 +94,7 @@ private void OnSceneSelectionDropdownChanged(int value)
9494

9595
SceneHandler.SelectedScene = SceneHandler.LoadedScenes[value];
9696
SceneHandler.Update();
97-
Tree.RefreshData(true);
97+
Tree.RefreshData(true, true, true);
9898
OnSelectedSceneChanged(SceneHandler.SelectedScene.Value);
9999
}
100100

@@ -158,7 +158,7 @@ private void OnFilterInput(string input)
158158
}
159159

160160
Tree.CurrentFilter = input;
161-
Tree.RefreshData(true, true);
161+
Tree.RefreshData(true, false, true);
162162
}
163163

164164
private void TryLoadScene(LoadSceneMode mode, Dropdown allSceneDrop)
@@ -239,6 +239,17 @@ public override void ConstructUI(GameObject content)
239239

240240
refreshRow.SetActive(false);
241241

242+
// tree labels row
243+
244+
var labelsRow = UIFactory.CreateHorizontalGroup(toolbar, "LabelsRow", true, true, true, true, 2, new Vector4(2, 2, 2, 2));
245+
UIFactory.SetLayoutElement(labelsRow, minHeight: 30, flexibleHeight: 0);
246+
247+
var nameLabel = UIFactory.CreateLabel(labelsRow, "NameLabel", "Name", TextAnchor.MiddleLeft, color: Color.grey);
248+
UIFactory.SetLayoutElement(nameLabel.gameObject, flexibleWidth: 9999, minHeight: 25);
249+
250+
var indexLabel = UIFactory.CreateLabel(labelsRow, "IndexLabel", "Sibling Index", TextAnchor.MiddleLeft, fontSize: 12, color: Color.grey);
251+
UIFactory.SetLayoutElement(indexLabel.gameObject, minWidth: 100, flexibleWidth: 0, minHeight: 25);
252+
242253
// Transform Tree
243254

244255
var scrollPool = UIFactory.CreateScrollPool<TransformCell>(uiRoot, "TransformTree", out GameObject scrollObj,
@@ -248,7 +259,7 @@ public override void ConstructUI(GameObject content)
248259

249260
Tree = new TransformTree(scrollPool, GetRootEntries);
250261
Tree.Init();
251-
Tree.RefreshData(true, true);
262+
Tree.RefreshData(true, true, true);
252263
//scrollPool.Viewport.GetComponent<Mask>().enabled = false;
253264
//UIRoot.GetComponent<Mask>().enabled = false;
254265

src/UI/Widgets/TransformTree/TransformTree.cs

Lines changed: 109 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Collections.Specialized;
5-
using System.Linq;
6-
using System.Text;
5+
using System.Diagnostics;
76
using UnityEngine;
8-
using UnityEngine.UI;
97
using UniverseLib;
10-
using UniverseLib.UI.Widgets;
118
using UniverseLib.UI.Widgets.ScrollView;
129
using UniverseLib.Utility;
1310

@@ -24,17 +21,18 @@ public class TransformTree : ICellPoolDataSource<TransformCell>
2421
/// Key: UnityEngine.Transform instance ID<br/>
2522
/// Value: CachedTransform
2623
/// </summary>
27-
internal readonly OrderedDictionary cachedTransforms = new OrderedDictionary();
24+
internal readonly OrderedDictionary cachedTransforms = new();
2825

2926
// for keeping track of which actual transforms are expanded or not, outside of the cache data.
30-
private readonly HashSet<int> expandedInstanceIDs = new HashSet<int>();
31-
private readonly HashSet<int> autoExpandedIDs = new HashSet<int>();
27+
private readonly HashSet<int> expandedInstanceIDs = new();
28+
private readonly HashSet<int> autoExpandedIDs = new();
3229

33-
private readonly HashSet<int> visited = new HashSet<int>();
30+
private readonly HashSet<int> visited = new();
3431
private bool needRefresh;
3532
private int displayIndex;
33+
int prevDisplayIndex;
3634

37-
public int ItemCount => cachedTransforms.Count;
35+
public int ItemCount => prevDisplayIndex;
3836

3937
public bool Filtering => !string.IsNullOrEmpty(currentFilter);
4038
private bool wasFiltering;
@@ -56,45 +54,15 @@ public string CurrentFilter
5654
}
5755
private string currentFilter;
5856

57+
private Coroutine refreshCoroutine;
58+
private readonly Stopwatch traversedThisFrame = new();
59+
5960
public TransformTree(ScrollPool<TransformCell> scrollPool, Func<IEnumerable<GameObject>> getRootEntriesMethod)
6061
{
6162
ScrollPool = scrollPool;
6263
GetRootEntriesMethod = getRootEntriesMethod;
6364
}
6465

65-
public void OnCellBorrowed(TransformCell cell)
66-
{
67-
cell.OnExpandToggled += OnCellExpandToggled;
68-
cell.OnGameObjectClicked += OnGameObjectClicked;
69-
cell.OnEnableToggled += OnCellEnableToggled;
70-
}
71-
72-
private void OnGameObjectClicked(GameObject obj)
73-
{
74-
if (OnClickOverrideHandler != null)
75-
OnClickOverrideHandler.Invoke(obj);
76-
else
77-
InspectorManager.Inspect(obj);
78-
}
79-
80-
public void OnCellExpandToggled(CachedTransform cache)
81-
{
82-
var instanceID = cache.InstanceID;
83-
if (expandedInstanceIDs.Contains(instanceID))
84-
expandedInstanceIDs.Remove(instanceID);
85-
else
86-
expandedInstanceIDs.Add(instanceID);
87-
88-
RefreshData(true);
89-
}
90-
91-
public void OnCellEnableToggled(CachedTransform cache)
92-
{
93-
cache.Value.gameObject.SetActive(!cache.Value.gameObject.activeSelf);
94-
95-
RefreshData(true);
96-
}
97-
9866
public void Init()
9967
{
10068
ScrollPool.Initialize(this);
@@ -129,7 +97,7 @@ public void JumpAndExpandToTransform(Transform transform)
12997
}
13098

13199
// Refresh cached transforms (no UI rebuild yet)
132-
RefreshData(false);
100+
RefreshData(false, false, false);
133101

134102
int transformID = transform.GetInstanceID();
135103

@@ -167,57 +135,82 @@ public void Rebuild()
167135
autoExpandedIDs.Clear();
168136
expandedInstanceIDs.Clear();
169137

170-
RefreshData(true, true);
138+
RefreshData(true, true, true);
171139
}
172140

173-
public void RefreshData(bool andReload = false, bool jumpToTop = false)
141+
public void RefreshData(bool andRefreshUI, bool jumpToTop, bool stopExistingCoroutine)
174142
{
143+
if (refreshCoroutine != null)
144+
{
145+
if (stopExistingCoroutine)
146+
{
147+
RuntimeHelper.StopCoroutine(refreshCoroutine);
148+
refreshCoroutine = null;
149+
}
150+
else
151+
return;
152+
}
153+
175154
visited.Clear();
176155
displayIndex = 0;
177156
needRefresh = false;
157+
traversedThisFrame.Reset();
158+
traversedThisFrame.Start();
178159

179-
var rootObjects = GetRootEntriesMethod.Invoke();
160+
IEnumerable<GameObject> rootObjects = GetRootEntriesMethod.Invoke();
180161

181-
//int displayIndex = 0;
182-
foreach (var obj in rootObjects)
183-
if (obj) Traverse(obj.transform);
162+
refreshCoroutine = RuntimeHelper.StartCoroutine(RefreshCoroutine(rootObjects, andRefreshUI, jumpToTop));
163+
}
164+
165+
private IEnumerator RefreshCoroutine(IEnumerable<GameObject> rootObjects, bool andRefreshUI, bool jumpToTop)
166+
{
167+
var thisCoro = refreshCoroutine;
168+
foreach (var gameObj in rootObjects)
169+
{
170+
if (gameObj)
171+
{
172+
var enumerator = Traverse(gameObj.transform);
173+
while (enumerator.MoveNext())
174+
yield return enumerator.Current;
175+
}
176+
}
184177

185178
// Prune displayed transforms that we didnt visit in that traverse
186179
for (int i = cachedTransforms.Count - 1; i >= 0; i--)
187180
{
188-
var obj = (CachedTransform)cachedTransforms[i];
189-
if (!visited.Contains(obj.InstanceID))
181+
var cached = (CachedTransform)cachedTransforms[i];
182+
if (!visited.Contains(cached.InstanceID))
190183
{
191-
cachedTransforms.Remove(obj.InstanceID);
184+
cachedTransforms.RemoveAt(i);
192185
needRefresh = true;
193186
}
194187
}
195188

196-
if (!needRefresh)
197-
return;
189+
if (andRefreshUI && needRefresh)
190+
ScrollPool.Refresh(true, jumpToTop);
198191

199-
//displayedObjects.Clear();
192+
prevDisplayIndex = displayIndex;
193+
}
200194

201-
if (andReload)
195+
private IEnumerator Traverse(Transform transform, CachedTransform parent = null, int depth = 0)
196+
{
197+
// Let's only tank 2ms of each frame (60->53fps)
198+
if (traversedThisFrame.ElapsedMilliseconds > 2)
202199
{
203-
if (!jumpToTop)
204-
ScrollPool.Refresh(true);
205-
else
206-
ScrollPool.Refresh(true, true);
200+
yield return null;
201+
traversedThisFrame.Reset();
202+
traversedThisFrame.Start();
207203
}
208-
}
209204

210-
private void Traverse(Transform transform, CachedTransform parent = null, int depth = 0)
211-
{
212205
int instanceID = transform.GetInstanceID();
213206

214207
if (visited.Contains(instanceID))
215-
return;
208+
yield break;
216209

217210
if (Filtering)
218211
{
219212
if (!FilterHierarchy(transform))
220-
return;
213+
yield break;
221214

222215
visited.Add(instanceID);
223216

@@ -231,8 +224,21 @@ private void Traverse(Transform transform, CachedTransform parent = null, int de
231224
if (cachedTransforms.Contains(instanceID))
232225
{
233226
cached = (CachedTransform)cachedTransforms[(object)instanceID];
227+
int prevSiblingIdx = cached.SiblingIndex;
234228
if (cached.Update(transform, depth))
229+
{
235230
needRefresh = true;
231+
232+
// If the sibling index changed, we need to shuffle it in our cached transforms list.
233+
if (prevSiblingIdx != cached.SiblingIndex)
234+
{
235+
cachedTransforms.Remove(instanceID);
236+
if (cachedTransforms.Count <= displayIndex)
237+
cachedTransforms.Add(instanceID, cached);
238+
else
239+
cachedTransforms.Insert(displayIndex, instanceID, cached);
240+
}
241+
}
236242
}
237243
else
238244
{
@@ -249,7 +255,11 @@ private void Traverse(Transform transform, CachedTransform parent = null, int de
249255
if (IsCellExpanded(instanceID) && cached.Value.childCount > 0)
250256
{
251257
for (int i = 0; i < transform.childCount; i++)
252-
Traverse(transform.GetChild(i), cached, depth + 1);
258+
{
259+
var enumerator = Traverse(transform.GetChild(i), cached, depth + 1);
260+
while (enumerator.MoveNext())
261+
yield return enumerator.Current;
262+
}
253263
}
254264
}
255265

@@ -276,13 +286,44 @@ public void SetCell(TransformCell cell, int index)
276286
if (Filtering)
277287
{
278288
if (cell.cachedTransform.Name.ContainsIgnoreCase(currentFilter))
279-
{
280289
cell.NameButton.ButtonText.color = Color.green;
281-
}
282290
}
283291
}
284292
else
285293
cell.Disable();
286294
}
295+
296+
public void OnCellBorrowed(TransformCell cell)
297+
{
298+
cell.OnExpandToggled += OnCellExpandToggled;
299+
cell.OnGameObjectClicked += OnGameObjectClicked;
300+
cell.OnEnableToggled += OnCellEnableToggled;
301+
}
302+
303+
private void OnGameObjectClicked(GameObject obj)
304+
{
305+
if (OnClickOverrideHandler != null)
306+
OnClickOverrideHandler.Invoke(obj);
307+
else
308+
InspectorManager.Inspect(obj);
309+
}
310+
311+
public void OnCellExpandToggled(CachedTransform cache)
312+
{
313+
var instanceID = cache.InstanceID;
314+
if (expandedInstanceIDs.Contains(instanceID))
315+
expandedInstanceIDs.Remove(instanceID);
316+
else
317+
expandedInstanceIDs.Add(instanceID);
318+
319+
RefreshData(true, false, true);
320+
}
321+
322+
public void OnCellEnableToggled(CachedTransform cache)
323+
{
324+
cache.Value.gameObject.SetActive(!cache.Value.gameObject.activeSelf);
325+
326+
RefreshData(true, false, true);
327+
}
287328
}
288329
}

0 commit comments

Comments
 (0)