Skip to content

Commit 1f5836b

Browse files
authored
Merge pull request #195 from Unity-Technologies/ordered-tag-queries
Fixed issues with determinism in Unity Simulation
2 parents a4da870 + 260be46 commit 1f5836b

File tree

9 files changed

+265
-90
lines changed

9 files changed

+265
-90
lines changed

com.unity.perception/CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1313

1414
### Added
1515

16+
Added Register() and Unregister() methods to the RandomizerTag API so users can implement RandomizerTag compatible GameObject caching
17+
1618
### Changed
1719

20+
Switched accessibility of scenario MonoBehaviour lifecycle functions (Awake, Start, Update) from private to protected to enable users to define their own overrides when deriving the Scenario class.
21+
22+
The GameObjectOneWayCache has been made public for users to cache GameObjects within their own custom Randomizers.
23+
1824
### Deprecated
1925

2026
### Removed
2127

2228
### Fixed
2329

24-
Fixed the math offsetting the iteration index of each Unity Simulation instance directly after they deserialize their app-params
30+
Fixed the math offsetting the iteration index of each Unity Simulation instance directly after they deserialize their app-params.
31+
32+
The RandomizerTagManager now uses a LinkedHashSet data structure to register tags to preserve insertion order determinism in Unity Simulation.
33+
34+
GameObjectOneWayCache now correctly registers and unregisters RandomizerTags on cached GameObjects.
2535

2636
## [0.7.0-preview.1] - 2021-02-01
2737

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
4+
namespace UnityEngine.Perception.Randomization.Randomizers
5+
{
6+
/// <summary>
7+
/// This collection has the properties of a HashSet that also preserves insertion order. As such, this data
8+
/// structure demonstrates the following time complexities:
9+
/// O(1) lookup, O(1) insertion, O(1) removal, and O(n) traversal
10+
/// </summary>
11+
/// <typeparam name="T">The item type to store in this collection</typeparam>
12+
class LinkedHashSet<T> : ICollection<T>
13+
{
14+
readonly IDictionary<T, LinkedListNode<T>> m_Dictionary;
15+
readonly LinkedList<T> m_LinkedList;
16+
17+
public LinkedHashSet() : this(EqualityComparer<T>.Default) { }
18+
19+
public LinkedHashSet(IEqualityComparer<T> comparer)
20+
{
21+
m_Dictionary = new Dictionary<T, LinkedListNode<T>>(comparer);
22+
m_LinkedList = new LinkedList<T>();
23+
}
24+
25+
public int Count => m_Dictionary.Count;
26+
27+
public bool IsReadOnly => m_Dictionary.IsReadOnly;
28+
29+
void ICollection<T>.Add(T item)
30+
{
31+
Add(item);
32+
}
33+
34+
public bool Add(T item)
35+
{
36+
if (m_Dictionary.ContainsKey(item)) return false;
37+
var node = m_LinkedList.AddLast(item);
38+
m_Dictionary.Add(item, node);
39+
return true;
40+
}
41+
42+
public void Clear()
43+
{
44+
m_LinkedList.Clear();
45+
m_Dictionary.Clear();
46+
}
47+
48+
public bool Remove(T item)
49+
{
50+
var found = m_Dictionary.TryGetValue(item, out var node);
51+
if (!found) return false;
52+
m_Dictionary.Remove(item);
53+
m_LinkedList.Remove(node);
54+
return true;
55+
}
56+
57+
public IEnumerator<T> GetEnumerator()
58+
{
59+
return m_LinkedList.GetEnumerator();
60+
}
61+
62+
IEnumerator IEnumerable.GetEnumerator()
63+
{
64+
return GetEnumerator();
65+
}
66+
67+
public bool Contains(T item)
68+
{
69+
return m_Dictionary.ContainsKey(item);
70+
}
71+
72+
public void CopyTo(T[] array, int arrayIndex)
73+
{
74+
m_LinkedList.CopyTo(array, arrayIndex);
75+
}
76+
}
77+
}

com.unity.perception/Runtime/Randomization/Randomizers/LinkedHashSet.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.

com.unity.perception/Runtime/Randomization/Randomizers/RandomizerExamples/Utilities/GameObjectOneWayCache.cs

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,76 @@ namespace UnityEngine.Perception.Randomization.Randomizers.Utilities
99
/// Facilitates object pooling for a pre-specified collection of prefabs with the caveat that objects can be fetched
1010
/// from the cache but not returned. Every frame, the cache needs to be reset, which will return all objects to the pool
1111
/// </summary>
12-
class GameObjectOneWayCache
12+
public class GameObjectOneWayCache
1313
{
1414
static ProfilerMarker s_ResetAllObjectsMarker = new ProfilerMarker("ResetAllObjects");
1515

16-
// Objects will reset to this origin when not being used
1716
Transform m_CacheParent;
1817
Dictionary<int, int> m_InstanceIdToIndex;
19-
List<GameObject>[] m_InstantiatedObjects;
18+
List<CachedObjectData>[] m_InstantiatedObjects;
2019
int[] m_NumObjectsActive;
2120
int NumObjectsInCache { get; set; }
21+
22+
/// <summary>
23+
/// The number of active cache objects in the scene
24+
/// </summary>
2225
public int NumObjectsActive { get; private set; }
2326

27+
/// <summary>
28+
/// Creates a new GameObjectOneWayCache
29+
/// </summary>
30+
/// <param name="parent">The parent object all cached instances will be parented under</param>
31+
/// <param name="prefabs">The prefabs to cache</param>
2432
public GameObjectOneWayCache(Transform parent, GameObject[] prefabs)
2533
{
2634
m_CacheParent = parent;
2735
m_InstanceIdToIndex = new Dictionary<int, int>();
28-
m_InstantiatedObjects = new List<GameObject>[prefabs.Length];
36+
m_InstantiatedObjects = new List<CachedObjectData>[prefabs.Length];
2937
m_NumObjectsActive = new int[prefabs.Length];
3038

3139
var index = 0;
3240
foreach (var prefab in prefabs)
3341
{
3442
var instanceId = prefab.GetInstanceID();
3543
m_InstanceIdToIndex.Add(instanceId, index);
36-
m_InstantiatedObjects[index] = new List<GameObject>();
44+
m_InstantiatedObjects[index] = new List<CachedObjectData>();
3745
m_NumObjectsActive[index] = 0;
3846
++index;
3947
}
4048
}
4149

50+
/// <summary>
51+
/// Retrieves an existing instance of the given prefab from the cache if available.
52+
/// Otherwise, instantiate a new instance of the given prefab.
53+
/// </summary>
54+
/// <param name="prefab"></param>
55+
/// <returns></returns>
56+
/// <exception cref="ArgumentException"></exception>
4257
public GameObject GetOrInstantiate(GameObject prefab)
4358
{
4459
if (!m_InstanceIdToIndex.TryGetValue(prefab.GetInstanceID(), out var index))
45-
{
4660
throw new ArgumentException($"Prefab {prefab.name} (ID: {prefab.GetInstanceID()}) is not in cache.");
47-
}
4861

4962
++NumObjectsActive;
5063
if (m_NumObjectsActive[index] < m_InstantiatedObjects[index].Count)
5164
{
5265
var nextInCache = m_InstantiatedObjects[index][m_NumObjectsActive[index]];
5366
++m_NumObjectsActive[index];
54-
return nextInCache;
55-
}
56-
else
57-
{
58-
++NumObjectsInCache;
59-
var newObject = Object.Instantiate(prefab, m_CacheParent);
60-
++m_NumObjectsActive[index];
61-
m_InstantiatedObjects[index].Add(newObject);
62-
return newObject;
67+
foreach (var tag in nextInCache.randomizerTags)
68+
tag.Register();
69+
return nextInCache.instance;
6370
}
71+
72+
++NumObjectsInCache;
73+
var newObject = Object.Instantiate(prefab, m_CacheParent);
74+
++m_NumObjectsActive[index];
75+
m_InstantiatedObjects[index].Add(new CachedObjectData(newObject));
76+
return newObject;
6477
}
6578

79+
/// <summary>
80+
/// Return all active cache objects back to an inactive state
81+
/// </summary>
6682
public void ResetAllObjects()
6783
{
6884
using (s_ResetAllObjectsMarker.Auto())
@@ -71,13 +87,27 @@ public void ResetAllObjects()
7187
for (var i = 0; i < m_InstantiatedObjects.Length; ++i)
7288
{
7389
m_NumObjectsActive[i] = 0;
74-
foreach (var obj in m_InstantiatedObjects[i])
90+
foreach (var cachedObjectData in m_InstantiatedObjects[i])
7591
{
7692
// Position outside the frame
77-
obj.transform.localPosition = new Vector3(10000, 0, 0);
93+
cachedObjectData.instance.transform.localPosition = new Vector3(10000, 0, 0);
94+
foreach (var tag in cachedObjectData.randomizerTags)
95+
tag.Unregister();
7896
}
7997
}
8098
}
8199
}
100+
101+
struct CachedObjectData
102+
{
103+
public GameObject instance;
104+
public RandomizerTag[] randomizerTags;
105+
106+
public CachedObjectData(GameObject instance)
107+
{
108+
this.instance = instance;
109+
randomizerTags = instance.GetComponents<RandomizerTag>();
110+
}
111+
}
82112
}
83113
}

com.unity.perception/Runtime/Randomization/Randomizers/RandomizerTag.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,34 @@ public abstract class RandomizerTag : MonoBehaviour
1111
{
1212
RandomizerTagManager tagManager => RandomizerTagManager.singleton;
1313

14-
void Awake()
14+
/// <summary>
15+
/// Awake is called when this RandomizerTag is created or instantiated
16+
/// </summary>
17+
protected virtual void Awake()
18+
{
19+
Register();
20+
}
21+
22+
/// <summary>
23+
/// OnDestroy is called when this RandomizerTag is destroyed
24+
/// </summary>
25+
protected virtual void OnDestroy()
26+
{
27+
Unregister();
28+
}
29+
30+
/// <summary>
31+
/// Registers this tag with the tagManager
32+
/// </summary>
33+
public void Register()
1534
{
1635
tagManager.AddTag(this);
1736
}
1837

19-
void OnDestroy()
38+
/// <summary>
39+
/// Unregisters this tag with the tagManager
40+
/// </summary>
41+
public void Unregister()
2042
{
2143
tagManager.RemoveTag(this);
2244
}

com.unity.perception/Runtime/Randomization/Randomizers/RandomizerTagManager.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class RandomizerTagManager
1414
public static RandomizerTagManager singleton { get; } = new RandomizerTagManager();
1515

1616
Dictionary<Type, HashSet<Type>> m_TypeTree = new Dictionary<Type, HashSet<Type>>();
17-
Dictionary<Type, HashSet<RandomizerTag>> m_TagMap = new Dictionary<Type, HashSet<RandomizerTag>>();
17+
Dictionary<Type, LinkedHashSet<RandomizerTag>> m_TagMap = new Dictionary<Type, LinkedHashSet<RandomizerTag>>();
1818

1919
/// <summary>
2020
/// Enumerates over all RandomizerTags of the given type present in the scene
@@ -60,15 +60,15 @@ void AddTagTypeToTypeHierarchy(Type tagType)
6060
if (m_TypeTree.ContainsKey(tagType))
6161
return;
6262

63-
m_TagMap.Add(tagType, new HashSet<RandomizerTag>());
63+
m_TagMap.Add(tagType, new LinkedHashSet<RandomizerTag>());
6464
m_TypeTree.Add(tagType, new HashSet<Type>());
6565

6666
var baseType = tagType.BaseType;
6767
while (baseType != null && baseType != typeof(RandomizerTag))
6868
{
6969
if (!m_TypeTree.ContainsKey(baseType))
7070
{
71-
m_TagMap.Add(baseType, new HashSet<RandomizerTag>());
71+
m_TagMap.Add(baseType, new LinkedHashSet<RandomizerTag>());
7272
m_TypeTree[baseType] = new HashSet<Type> { tagType };
7373
}
7474
else
@@ -84,7 +84,7 @@ void AddTagTypeToTypeHierarchy(Type tagType)
8484

8585
internal void RemoveTag<T>(T tag) where T : RandomizerTag
8686
{
87-
var tagType = typeof(T);
87+
var tagType = tag.GetType();
8888
if (m_TagMap.ContainsKey(tagType) && m_TagMap[tagType].Contains(tag))
8989
m_TagMap[tagType].Remove(tag);
9090
}

0 commit comments

Comments
 (0)