com.wallstop-studios.unity-helpers 2.0.0-rc39 → 2.0.0-rc41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.github/workflows/npm-publish.yml +22 -26
  2. package/Editor/AnimationCopier.cs +25 -2
  3. package/Editor/AnimationCreator.cs +18 -27
  4. package/Editor/AnimatorControllerCopier.cs +7 -7
  5. package/Editor/EnsureTextureSizeWizard.cs +14 -9
  6. package/Editor/PrefabCheckWizard.cs +1 -1
  7. package/Editor/Utils/{ReadOnlyPropertyDrawer.cs → DxReadOnlyPropertyDrawer.cs} +2 -2
  8. package/README.md +37 -1
  9. package/Runtime/Core/Attributes/ChildComponentAttribute.cs +39 -19
  10. package/Runtime/Core/Attributes/DxReadOnlyAttribute.cs +6 -0
  11. package/Runtime/Core/Attributes/ParentComponent.cs +16 -15
  12. package/Runtime/Core/Attributes/SiblingComponentAttribute.cs +7 -9
  13. package/Runtime/Core/Helper/Partials/TransformHelpers.cs +26 -0
  14. package/Runtime/Core/Helper/ReflectionHelpers.cs +167 -22
  15. package/Tests/Runtime/Components/{RelationalComponentTester.cs → RelationalComponentTesterComplex.cs} +1 -1
  16. package/Tests/Runtime/Components/RelationalComponentsTesterSimple.cs +40 -0
  17. package/Tests/Runtime/Components/RelationalComponentsTesterSimple.cs.meta +3 -0
  18. package/Tests/Runtime/Helper/ReflectionHelperTests.cs +215 -0
  19. package/Tests/Runtime/Helper/ReflectionHelperTests.cs.meta +3 -0
  20. package/Tests/Runtime/Performance/RelationComponentPerformanceTests.cs +27 -3
  21. package/package.json +1 -1
  22. package/.github/workflows/unity-package.yml +0 -105
  23. package/Editor/BuildScript.cs +0 -33
  24. package/Editor/BuildScript.cs.meta +0 -3
  25. package/Editor/Scenes/SampleScene.unity +0 -221
  26. package/Editor/Scenes/SampleScene.unity.meta +0 -7
  27. package/Editor/Scenes.meta +0 -3
  28. package/Runtime/Core/Attributes/ReadOnlyAttribute.cs +0 -6
  29. /package/Editor/Utils/{ReadOnlyPropertyDrawer.cs.meta → DxReadOnlyPropertyDrawer.cs.meta} +0 -0
  30. /package/Runtime/Core/Attributes/{ReadOnlyAttribute.cs.meta → DxReadOnlyAttribute.cs.meta} +0 -0
  31. /package/Tests/Runtime/Components/{RelationalComponentTester.cs.meta → RelationalComponentTesterComplex.cs.meta} +0 -0
@@ -7,15 +7,6 @@ on:
7
7
  paths:
8
8
  - 'package.json'
9
9
  workflow_dispatch:
10
- inputs:
11
- logLevel:
12
- description: 'Log level'
13
- required: true
14
- default: 'warning'
15
- environment:
16
- description: 'Environment to deploy'
17
- required: false
18
- default: 'staging'
19
10
 
20
11
  jobs:
21
12
  publish_npm:
@@ -36,27 +27,32 @@ jobs:
36
27
  - name: Check if version changed
37
28
  id: version_check
38
29
  run: |
39
- PREV_VERSION=$(git show HEAD~1:package.json | jq -r '.version' || echo "0.0.0")
40
- NEW_VERSION=$(jq -r '.version' package.json)
41
- echo "Previous version: $PREV_VERSION"
42
- echo "New version: $NEW_VERSION"
43
-
44
- if [ "$PREV_VERSION" != "$NEW_VERSION" ]; then
45
- echo "Version changed, proceeding..."
46
- echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
30
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
31
+ echo "Manual trigger detected. Skipping version check."
47
32
  echo "should_publish=true" >> $GITHUB_ENV
33
+ else
34
+ PREV_VERSION=$(git show HEAD~1:package.json | jq -r '.version' || echo "0.0.0")
35
+ NEW_VERSION=$(jq -r '.version' package.json)
36
+ echo "Previous version: $PREV_VERSION"
37
+ echo "New version: $NEW_VERSION"
48
38
 
49
- # Detect pre-releases (versions with "rc" or similar tags)
50
- if [[ "$NEW_VERSION" == *"rc"* ]]; then
51
- echo "This is a pre-release (next tag)."
52
- echo "NPM_TAG=next" >> $GITHUB_ENV
39
+ if [ "$PREV_VERSION" != "$NEW_VERSION" ]; then
40
+ echo "Version changed, proceeding..."
41
+ echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
42
+ echo "should_publish=true" >> $GITHUB_ENV
43
+
44
+ # Detect pre-releases (versions with "rc" or similar tags)
45
+ if [[ "$NEW_VERSION" == *"rc"* ]]; then
46
+ echo "This is a pre-release (next tag)."
47
+ echo "NPM_TAG=next" >> $GITHUB_ENV
48
+ else
49
+ echo "This is a stable release (latest tag)."
50
+ echo "NPM_TAG=latest" >> $GITHUB_ENV
51
+ fi
53
52
  else
54
- echo "This is a stable release (latest tag)."
55
- echo "NPM_TAG=latest" >> $GITHUB_ENV
53
+ echo "Version did not change, skipping..."
54
+ echo "should_publish=false" >> $GITHUB_ENV
56
55
  fi
57
- else
58
- echo "Version did not change, skipping..."
59
- echo "should_publish=false" >> $GITHUB_ENV
60
56
  fi
61
57
 
62
58
  - name: Install Dependencies
@@ -14,12 +14,35 @@
14
14
  private string _fullSourcePath;
15
15
  private string _fullDestinationPath;
16
16
 
17
- [ReadOnly]
17
+ [DxReadOnly]
18
18
  public string animationSourcePath;
19
19
 
20
- [ReadOnly]
20
+ [DxReadOnly]
21
21
  public string animationDestinationPath;
22
22
 
23
+ private void OnEnable()
24
+ {
25
+ if (string.IsNullOrWhiteSpace(_fullSourcePath))
26
+ {
27
+ _fullSourcePath = $"{Application.dataPath}/Sprites";
28
+ int assetIndex = _fullSourcePath.IndexOf("Assets", StringComparison.Ordinal);
29
+ if (0 <= assetIndex)
30
+ {
31
+ animationSourcePath = _fullSourcePath.Substring(assetIndex);
32
+ }
33
+ }
34
+
35
+ if (string.IsNullOrWhiteSpace(_fullDestinationPath))
36
+ {
37
+ _fullDestinationPath = $"{Application.dataPath}/Animations";
38
+ int assetIndex = _fullDestinationPath.IndexOf("Assets", StringComparison.Ordinal);
39
+ if (0 <= assetIndex)
40
+ {
41
+ animationDestinationPath = _fullDestinationPath.Substring(assetIndex);
42
+ }
43
+ }
44
+ }
45
+
23
46
  [MenuItem("Tools/Unity Helpers/Animation Copier")]
24
47
  public static void CopyAnimations()
25
48
  {
@@ -14,9 +14,9 @@
14
14
  {
15
15
  public const int DefaultFramesPerSecond = 12;
16
16
 
17
- public List<Texture2D> frames;
17
+ public List<Texture2D> frames = new();
18
18
  public int framesPerSecond = DefaultFramesPerSecond;
19
- public string animationName;
19
+ public string animationName = string.Empty;
20
20
  }
21
21
 
22
22
  public sealed class AnimationCreator : ScriptableWizard
@@ -90,11 +90,13 @@
90
90
  int lastNumericIndex = frameName.Length - 1;
91
91
  for (int i = frameName.Length - 1; 0 <= i; --i)
92
92
  {
93
- if (!char.IsNumber(frameName[i]))
93
+ if (char.IsNumber(frameName[i]))
94
94
  {
95
- lastNumericIndex = i + 1;
96
- break;
95
+ continue;
97
96
  }
97
+
98
+ lastNumericIndex = i + 1;
99
+ break;
98
100
  }
99
101
 
100
102
  int lastUnderscoreIndex = frameName.LastIndexOf('_');
@@ -118,29 +120,18 @@
118
120
  if (0 < texturesByPrefixAndAssetPath.Count)
119
121
  {
120
122
  animationData.Clear();
121
- foreach (
122
- KeyValuePair<
123
- string,
124
- Dictionary<string, List<Texture2D>>
125
- > assetPathAndTextures in texturesByPrefixAndAssetPath
126
- )
127
- {
128
- foreach (
129
- KeyValuePair<
130
- string,
131
- List<Texture2D>
132
- > textureAndPrefix in assetPathAndTextures.Value
123
+ animationData.AddRange(
124
+ texturesByPrefixAndAssetPath.SelectMany(assetPathAndTextures =>
125
+ assetPathAndTextures.Value.Select(
126
+ textureAndPrefix => new AnimationData
127
+ {
128
+ frames = textureAndPrefix.Value,
129
+ framesPerSecond = data.framesPerSecond,
130
+ animationName = $"Anim_{textureAndPrefix.Key}",
131
+ }
132
+ )
133
133
  )
134
- {
135
- AnimationData newData = new()
136
- {
137
- frames = textureAndPrefix.Value,
138
- framesPerSecond = data.framesPerSecond,
139
- animationName = $"Anim_{textureAndPrefix.Key}",
140
- };
141
- animationData.Add(newData);
142
- }
143
- }
134
+ );
144
135
  }
145
136
  }
146
137
  }
@@ -14,11 +14,11 @@
14
14
  private string _fullSourcePath;
15
15
  private string _fullDestinationPath;
16
16
 
17
- [ReadOnly]
17
+ [DxReadOnly]
18
18
  public string controllerSourcePath;
19
19
 
20
- [ReadOnly]
21
- public string controllerDestinationpath;
20
+ [DxReadOnly]
21
+ public string controllerDestinationPath;
22
22
 
23
23
  [MenuItem("Tools/Unity Helpers/Animator Controller Copier")]
24
24
  public static void CopyAnimations()
@@ -61,8 +61,8 @@
61
61
  return false;
62
62
  }
63
63
 
64
- _fullDestinationPath = controllerDestinationpath = sourcePath ?? string.Empty;
65
- controllerDestinationpath = controllerDestinationpath.Substring(assetIndex);
64
+ _fullDestinationPath = controllerDestinationPath = sourcePath ?? string.Empty;
65
+ controllerDestinationPath = controllerDestinationPath.Substring(assetIndex);
66
66
  return true;
67
67
  }
68
68
 
@@ -78,7 +78,7 @@
78
78
 
79
79
  if (
80
80
  string.IsNullOrEmpty(controllerSourcePath)
81
- || string.IsNullOrEmpty(controllerDestinationpath)
81
+ || string.IsNullOrEmpty(controllerDestinationPath)
82
82
  )
83
83
  {
84
84
  return;
@@ -130,7 +130,7 @@
130
130
  }
131
131
 
132
132
  string destination =
133
- controllerDestinationpath + partialPath + relativePath.Substring(prefixIndex);
133
+ controllerDestinationPath + partialPath + relativePath.Substring(prefixIndex);
134
134
  bool copySuccessful = AssetDatabase.CopyAsset(path, destination);
135
135
  if (copySuccessful)
136
136
  {
@@ -22,22 +22,27 @@
22
22
  {
23
23
  textures ??= new List<Texture2D>();
24
24
  textureSourcePaths ??= new List<Object>();
25
- HashSet<string> texturePath = new();
26
- foreach (Object textureSource in textureSourcePaths)
25
+ HashSet<string> texturePaths = new();
26
+ foreach (
27
+ string assetPath in textureSourcePaths
28
+ .Select(AssetDatabase.GetAssetPath)
29
+ .Where(assetPath => !string.IsNullOrWhiteSpace(assetPath))
30
+ )
27
31
  {
28
- string assetPath = AssetDatabase.GetAssetPath(textureSource);
29
- if (!string.IsNullOrWhiteSpace(assetPath))
30
- {
31
- _ = texturePath.Add(assetPath);
32
- }
32
+ _ = texturePaths.Add(assetPath);
33
+ }
34
+
35
+ if (!textures.Any() && !texturePaths.Any())
36
+ {
37
+ texturePaths.Add("Assets");
33
38
  }
34
39
 
35
- if (texturePath.Any())
40
+ if (texturePaths.Any())
36
41
  {
37
42
  foreach (
38
43
  string assetGuid in AssetDatabase.FindAssets(
39
44
  "t:texture2D",
40
- texturePath.ToArray()
45
+ texturePaths.ToArray()
41
46
  )
42
47
  )
43
48
  {
@@ -21,7 +21,7 @@
21
21
  [Tooltip(
22
22
  "Drag a folder from Unity here to validate all prefabs under it. Defaults to Assets/Prefabs and Assets/Resources if none specified."
23
23
  )]
24
- public List<Object> assetPaths;
24
+ public List<Object> assetPaths = new();
25
25
 
26
26
  [MenuItem("Tools/Unity Helpers/Prefab Check Wizard")]
27
27
  public static void CreatePrefabCheckWizard()
@@ -6,8 +6,8 @@
6
6
  using UnityEngine;
7
7
 
8
8
  // https://www.patrykgalach.com/2020/01/20/readonly-attribute-in-unity-editor/
9
- [CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
10
- public sealed class ReadOnlyPropertyDrawer : PropertyDrawer
9
+ [CustomPropertyDrawer(typeof(DxReadOnlyAttribute))]
10
+ public sealed class DxReadOnlyPropertyDrawer : PropertyDrawer
11
11
  {
12
12
  public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
13
13
  {
package/README.md CHANGED
@@ -3,7 +3,6 @@ Various Unity Helpers. Includes many deterministic, seedable random number gener
3
3
 
4
4
  # CI/CD Status
5
5
  ![Npm Publish](https://github.com/wallstop/unity-helpers/actions/workflows/npm-publish.yml/badge.svg)
6
- ![Deploy](https://github.com/wallstop/unity-helpers/actions/workflows/unity-package.yml/badge.svg)
7
6
 
8
7
  # Compatibility
9
8
  | Platform | Compatible |
@@ -51,6 +50,43 @@ Check out the latest [Releases](https://github.com/wallstop/unity-helpers/releas
51
50
  - A randomizable PerlinNoise implementation
52
51
  - And more!
53
52
 
53
+ # Auto Get(Parent/Sibling/Child)Component
54
+ Are you tired of constantly writing GetComponent<T>() all over the place? Waste time no more!
55
+
56
+ ```csharp
57
+ public sealed class MyCoolScript : MonoBehaviour
58
+ {
59
+ [SiblingComponent] // If it doesn't exist, will log an error
60
+ private SpriteRenderer _spriteRenderer;
61
+
62
+ [SiblingComponent(optional = true)] // Ok if it doesn't exist, no errors logged
63
+ private BoxCollider2D _boxCollider;
64
+
65
+ [ParentComponent] // Similar to GetComponentInParent<AIController>(includeInactive: true)
66
+ private AIController _parentAIController;
67
+
68
+ // Only include components in parents, Unity by default includes sibling components in the GetComponentsInParent call
69
+ [ParentComponent(onlyAncestors = true)]
70
+ private Transform [] _allParentTransforms; // Works with arrays!
71
+
72
+ [ParentComponent(includeInactive = false)] // Don't include inactive components
73
+ private List<PolygonCollider2> _parentColliders; // Works with lists!
74
+
75
+ [ChildComponent(onlyDescendents = true)] // Similar to GetComponentInChildren<EdgeCollider2D>(includeInactive: true)
76
+ private EdgeCollider2D _childEdgeCollider;
77
+
78
+ private void Awake()
79
+ {
80
+ /*
81
+ Make sure this is called somewhere, usually in Awake, OnEnable, or Start - wherever this is called,
82
+ values will be injected into the annotated fields and errors will be logged
83
+ */
84
+ this.AssignRelationalComponents();
85
+ }
86
+ }
87
+
88
+ ```
89
+
54
90
  # Random Number Generators
55
91
  This package implements several high quality, seedable, and serializable random number generators. The best one (currently) is the PCG Random. This has been hidden behind the `PRNG.Instance` class, which is thread-safe. As the package evolves, the exact implementation of this may change.
56
92
 
@@ -15,6 +15,7 @@
15
15
  public sealed class ChildComponentAttribute : Attribute
16
16
  {
17
17
  public bool optional = false;
18
+ public bool includeInactive = true;
18
19
  public bool onlyDescendents = false;
19
20
  }
20
21
 
@@ -37,7 +38,7 @@
37
38
  );
38
39
  return fields
39
40
  .Where(field => Attribute.IsDefined(field, typeof(ChildComponentAttribute)))
40
- .Select(field => (field, ReflectionHelpers.CreateFieldSetter(type, field)))
41
+ .Select(field => (field, ReflectionHelpers.GetFieldSetter(field)))
41
42
  .ToArray();
42
43
  }
43
44
  );
@@ -49,7 +50,9 @@
49
50
  Type childComponentType = isArray ? fieldType.GetElementType() : fieldType;
50
51
 
51
52
  bool foundChild;
52
- if (field.GetCustomAttribute<ChildComponentAttribute>().onlyDescendents)
53
+ ChildComponentAttribute customAttribute =
54
+ field.GetCustomAttribute<ChildComponentAttribute>();
55
+ if (customAttribute.onlyDescendents)
53
56
  {
54
57
  if (isArray)
55
58
  {
@@ -57,7 +60,10 @@
57
60
  foreach (Transform child in component.IterateOverAllChildren())
58
61
  {
59
62
  children.AddRange(
60
- child.GetComponentsInChildren(childComponentType, true)
63
+ child.GetComponentsInChildren(
64
+ childComponentType,
65
+ customAttribute.includeInactive
66
+ )
61
67
  );
62
68
  }
63
69
 
@@ -83,7 +89,10 @@
83
89
  Component childComponent in component
84
90
  .IterateOverAllChildren()
85
91
  .SelectMany(child =>
86
- child.GetComponentsInChildren(childComponentType, true)
92
+ child.GetComponentsInChildren(
93
+ childComponentType,
94
+ customAttribute.includeInactive
95
+ )
87
96
  )
88
97
  )
89
98
  {
@@ -97,14 +106,27 @@
97
106
  {
98
107
  foundChild = false;
99
108
  Component childComponent = null;
100
- foreach (Transform child in component.IterateOverAllChildren())
109
+ foreach (
110
+ Transform child in component.IterateOverAllChildrenRecursivelyBreadthFirst()
111
+ )
101
112
  {
102
113
  childComponent = child.GetComponent(childComponentType);
103
- if (childComponent != null)
114
+ if (
115
+ childComponent == null
116
+ || (
117
+ !customAttribute.includeInactive
118
+ && (
119
+ !childComponent.gameObject.activeInHierarchy
120
+ || childComponent is Behaviour { enabled: false }
121
+ )
122
+ )
123
+ )
104
124
  {
105
- foundChild = true;
106
- break;
125
+ continue;
107
126
  }
127
+
128
+ foundChild = true;
129
+ break;
108
130
  }
109
131
  if (foundChild)
110
132
  {
@@ -118,7 +140,7 @@
118
140
  {
119
141
  Component[] childComponents = component.GetComponentsInChildren(
120
142
  childComponentType,
121
- true
143
+ customAttribute.includeInactive
122
144
  );
123
145
  foundChild = 0 < childComponents.Length;
124
146
 
@@ -149,7 +171,7 @@
149
171
  foreach (
150
172
  Component childComponent in component.GetComponentsInChildren(
151
173
  childComponentType,
152
- true
174
+ customAttribute.includeInactive
153
175
  )
154
176
  )
155
177
  {
@@ -163,7 +185,7 @@
163
185
  {
164
186
  Component childComponent = component.GetComponentInChildren(
165
187
  childComponentType,
166
- true
188
+ customAttribute.includeInactive
167
189
  );
168
190
  foundChild = childComponent != null;
169
191
  if (foundChild)
@@ -173,15 +195,13 @@
173
195
  }
174
196
  }
175
197
 
176
- if (!foundChild)
198
+ if (
199
+ !foundChild
200
+ && field.GetCustomAttributes(typeof(ChildComponentAttribute), false)[0]
201
+ is ChildComponentAttribute { optional: false }
202
+ )
177
203
  {
178
- if (
179
- field.GetCustomAttributes(typeof(ChildComponentAttribute), false)[0]
180
- is ChildComponentAttribute { optional: false } _
181
- )
182
- {
183
- component.LogError($"Unable to find child component of type {fieldType}");
184
- }
204
+ component.LogError($"Unable to find child component of type {fieldType}");
185
205
  }
186
206
  }
187
207
  }
@@ -0,0 +1,6 @@
1
+ namespace UnityHelpers.Core.Attributes
2
+ {
3
+ using PropertyAttribute = UnityEngine.PropertyAttribute;
4
+
5
+ public sealed class DxReadOnlyAttribute : PropertyAttribute { }
6
+ }
@@ -15,6 +15,7 @@
15
15
  public sealed class ParentComponentAttribute : Attribute
16
16
  {
17
17
  public bool optional = false;
18
+ public bool includeInactive = true;
18
19
  public bool onlyAncestors = false;
19
20
  }
20
21
 
@@ -39,7 +40,7 @@
39
40
  .Where(field =>
40
41
  Attribute.IsDefined(field, typeof(ParentComponentAttribute))
41
42
  )
42
- .Select(field => (field, ReflectionHelpers.CreateFieldSetter(type, field)))
43
+ .Select(field => (field, ReflectionHelpers.GetFieldSetter(field)))
43
44
  .ToArray();
44
45
  }
45
46
  );
@@ -51,6 +52,8 @@
51
52
  Type parentComponentType = isArray ? fieldType.GetElementType() : fieldType;
52
53
 
53
54
  bool foundParent;
55
+ ParentComponentAttribute customAttribute =
56
+ field.GetCustomAttribute<ParentComponentAttribute>();
54
57
  if (field.GetCustomAttribute<ParentComponentAttribute>().onlyAncestors)
55
58
  {
56
59
  Transform parent = component.transform.parent;
@@ -62,7 +65,7 @@
62
65
  {
63
66
  Component[] parentComponents = parent.GetComponentsInParent(
64
67
  parentComponentType,
65
- true
68
+ customAttribute.includeInactive
66
69
  );
67
70
  foundParent = 0 < parentComponents.Length;
68
71
 
@@ -82,7 +85,7 @@
82
85
 
83
86
  Component[] parents = parent.GetComponentsInParent(
84
87
  parentComponentType,
85
- true
88
+ customAttribute.includeInactive
86
89
  );
87
90
 
88
91
  IList instance = ReflectionHelpers.CreateList(
@@ -103,7 +106,7 @@
103
106
  {
104
107
  Component childComponent = parent.GetComponentInParent(
105
108
  parentComponentType,
106
- true
109
+ customAttribute.includeInactive
107
110
  );
108
111
  foundParent = childComponent != null;
109
112
  if (foundParent)
@@ -118,7 +121,7 @@
118
121
  {
119
122
  Component[] parentComponents = component.GetComponentsInParent(
120
123
  parentComponentType,
121
- true
124
+ customAttribute.includeInactive
122
125
  );
123
126
  foundParent = 0 < parentComponents.Length;
124
127
 
@@ -138,7 +141,7 @@
138
141
 
139
142
  Component[] parents = component.GetComponentsInParent(
140
143
  parentComponentType,
141
- true
144
+ customAttribute.includeInactive
142
145
  );
143
146
 
144
147
  IList instance = ReflectionHelpers.CreateList(
@@ -158,7 +161,7 @@
158
161
  {
159
162
  Component childComponent = component.GetComponentInParent(
160
163
  parentComponentType,
161
- true
164
+ customAttribute.includeInactive
162
165
  );
163
166
  foundParent = childComponent != null;
164
167
  if (foundParent)
@@ -168,15 +171,13 @@
168
171
  }
169
172
  }
170
173
 
171
- if (!foundParent)
174
+ if (
175
+ !foundParent
176
+ && field.GetCustomAttributes(typeof(ParentComponentAttribute), false)[0]
177
+ is ParentComponentAttribute { optional: false }
178
+ )
172
179
  {
173
- if (
174
- field.GetCustomAttributes(typeof(ParentComponentAttribute), false)[0]
175
- is ParentComponentAttribute { optional: false } _
176
- )
177
- {
178
- component.LogError($"Unable to find parent component of type {fieldType}");
179
- }
180
+ component.LogError($"Unable to find parent component of type {fieldType}");
180
181
  }
181
182
  }
182
183
  }
@@ -38,7 +38,7 @@
38
38
  .Where(field =>
39
39
  Attribute.IsDefined(field, typeof(SiblingComponentAttribute))
40
40
  )
41
- .Select(field => (field, ReflectionHelpers.CreateFieldSetter(type, field)))
41
+ .Select(field => (field, ReflectionHelpers.GetFieldSetter(field)))
42
42
  .ToArray();
43
43
  }
44
44
  );
@@ -103,15 +103,13 @@
103
103
  }
104
104
  }
105
105
 
106
- if (!foundSibling)
106
+ if (
107
+ !foundSibling
108
+ && field.GetCustomAttributes(typeof(SiblingComponentAttribute), false)[0]
109
+ is SiblingComponentAttribute { optional: false }
110
+ )
107
111
  {
108
- if (
109
- field.GetCustomAttributes(typeof(SiblingComponentAttribute), false)[0]
110
- is SiblingComponentAttribute { optional: false } _
111
- )
112
- {
113
- component.LogError($"Unable to find sibling component of type {fieldType}");
114
- }
112
+ component.LogError($"Unable to find sibling component of type {fieldType}");
115
113
  }
116
114
  }
117
115
  }
@@ -1,6 +1,7 @@
1
1
  namespace UnityHelpers.Core.Helper
2
2
  {
3
3
  using System.Collections.Generic;
4
+ using System.Linq;
4
5
  using UnityEngine;
5
6
 
6
7
  public static partial class Helpers
@@ -163,5 +164,30 @@
163
164
  }
164
165
  }
165
166
  }
167
+
168
+ public static IEnumerable<Transform> IterateOverAllChildrenRecursivelyBreadthFirst(
169
+ this Component component
170
+ )
171
+ {
172
+ if (component == null)
173
+ {
174
+ return Enumerable.Empty<Transform>();
175
+ }
176
+
177
+ Queue<Transform> results = new();
178
+ Queue<Transform> iteration = new();
179
+ iteration.Enqueue(component.transform);
180
+ while (iteration.TryDequeue(out Transform current))
181
+ {
182
+ for (int i = 0; i < current.childCount; ++i)
183
+ {
184
+ Transform childTransform = current.GetChild(i);
185
+ results.Enqueue(childTransform);
186
+ iteration.Enqueue(childTransform);
187
+ }
188
+ }
189
+
190
+ return results;
191
+ }
166
192
  }
167
193
  }