// MIT License - Copyright (c) 2023 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Attributes
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Extension;
using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Helper;
using WallstopStudios.UnityHelpers.Utils;
using static RelationalComponentProcessor;
#if UNITY_EDITOR && UNITY_2020_2_OR_NEWER
using Unity.Profiling;
#endif
///
/// Automatically assigns child components (components down the transform hierarchy) to the decorated field.
/// Supports single components, s, ,
/// and collection types.
///
///
/// Call (or
/// ) to populate the field.
/// This is typically done in Awake() or OnEnable().
///
/// By default, searches include the current ; set to exclude it.
/// Limit traversal with (depth 1 = immediate children only). Children are visited in breadth-first order.
/// Combine with filters like and
/// . Interfaces and base types are supported when
/// is true (default).
///
/// IMPORTANT: This attribute populates fields at runtime, not during Unity serialization in Edit mode.
/// Fields populated by this attribute will not be serialized by Unity.
///
///
///
///
///
///
/// Typical child searches with depth limits and collections:
/// firstTenRigidbodies;
///
/// private void Awake()
/// {
/// this.AssignChildComponents();
/// }
/// }
/// ]]>
///
[AttributeUsage(AttributeTargets.Field)]
public sealed class ChildComponentAttribute : BaseRelationalComponentAttribute
{
///
/// If true, excludes components on the current and only searches descendant transforms.
/// If false, includes components on the current in the search. Default: false.
///
public bool OnlyDescendants { get; set; } = false;
///
/// Maximum depth to search down the hierarchy. 0 means unlimited. Default: 0.
/// Depth 1 = immediate children only, depth 2 = children and grandchildren, etc.
///
///
/// Negative values are treated as 0 (unlimited). The search is breadth-first, so closer
/// descendants are found before distant ones regardless of depth limit.
///
public int MaxDepth
{
get => _maxDepth;
set => _maxDepth = value < 0 ? 0 : value;
}
private int _maxDepth;
}
public static class ChildComponentExtensions
{
private static readonly Dictionary<
Type,
FieldMetadata[]
> FieldsByType = new();
#if UNITY_EDITOR && UNITY_2020_2_OR_NEWER
private static readonly ProfilerMarker ChildFastPathMarker = new(
"RelationalComponents.Child.FastPath"
);
private static readonly ProfilerMarker ChildFallbackMarker = new(
"RelationalComponents.Child.Fallback"
);
#endif
///
/// Assigns fields on marked with .
///
/// The component whose fields will be populated.
///
/// Typical call site is Awake() or OnEnable(). For convenience, you can also call
/// to assign all relational attributes.
///
///
///
///
public static void AssignChildComponents(this Component component)
{
FieldMetadata[] fields = FieldsByType.GetOrAdd(
component.GetType(),
type => GetFieldMetadata(type)
);
AssignChildComponents(component, fields);
}
internal static void AssignChildComponents(
Component component,
FieldMetadata[] fields
)
{
if (component == null || fields == null || fields.Length == 0)
{
return;
}
foreach (FieldMetadata metadata in fields)
{
if (ShouldSkipAssignment(metadata, component))
{
continue;
}
bool foundChild = false;
if (metadata.kind == FieldKind.Single)
{
if (TryAssignChildSingleFast(component, metadata, out Component childComponent))
{
metadata.SetValue(component, childComponent);
foundChild = true;
}
else
{
FilterParameters filters = metadata.Filters;
using PooledResource> childBufferResource =
Buffers.List.Get(out List childBuffer);
if (
TryAssignChildSingleFallback(
component,
metadata,
filters,
childBuffer,
out childComponent
)
)
{
metadata.SetValue(component, childComponent);
foundChild = true;
}
}
}
else
{
FilterParameters filters = metadata.Filters;
if (
TryAssignChildCollectionFast(
component,
metadata,
filters,
out bool assignedAny
)
)
{
foundChild = assignedAny;
}
else
{
using PooledResource> childBufferResource =
Buffers.List.Get(out List childBuffer);
switch (metadata.kind)
{
case FieldKind.Array:
{
using PooledResource> cacheResource =
Buffers.List.Get(out List cache);
cache.Clear();
int filteredCount = EnumerateFilteredChildComponents(
component,
metadata,
filters,
childBuffer,
candidate =>
{
cache.Add(candidate);
return true;
}
);
Array correctTypedArray = metadata.arrayCreator(filteredCount);
for (int i = 0; i < filteredCount; ++i)
{
correctTypedArray.SetValue(cache[i], i);
}
metadata.SetValue(component, correctTypedArray);
foundChild = filteredCount > 0;
break;
}
case FieldKind.List:
{
object existing = metadata.GetValue(component);
IList list = existing as IList;
if (list == null)
{
int initialCapacity =
metadata.attribute.MaxCount > 0
? metadata.attribute.MaxCount
: 0;
list = metadata.listCreator(initialCapacity);
metadata.SetValue(component, list);
}
else
{
list.Clear();
}
int added = EnumerateFilteredChildComponents(
component,
metadata,
filters,
childBuffer,
candidate =>
{
list.Add(candidate);
return true;
}
);
foundChild = added > 0;
break;
}
case FieldKind.HashSet:
{
object instance = metadata.GetValue(component);
if (instance != null && metadata.hashSetClearer != null)
{
metadata.hashSetClearer(instance);
}
else
{
int initialCapacity =
metadata.attribute.MaxCount > 0
? metadata.attribute.MaxCount
: 0;
instance = metadata.hashSetCreator(initialCapacity);
metadata.SetValue(component, instance);
}
int added = EnumerateFilteredChildComponents(
component,
metadata,
filters,
childBuffer,
candidate =>
{
metadata.hashSetAdder(instance, candidate);
return true;
}
);
foundChild = added > 0;
break;
}
default:
{
break;
}
}
}
}
if (!foundChild)
{
LogMissingComponentError(component, metadata, "child");
AssignNullToSingleField(component, metadata);
}
}
}
internal static FieldMetadata[] GetOrCreateFields(Type type)
{
return FieldsByType.GetOrAdd(type, t => GetFieldMetadata(t));
}
private static bool TryAssignChildSingleFast(
Component component,
FieldMetadata metadata,
out Component childComponent
)
{
childComponent = null;
ChildComponentAttribute attribute = metadata.attribute;
if (
metadata.isInterface
|| attribute.MaxDepth != 0
|| attribute.TagFilter != null
|| attribute.NameFilter != null
)
{
return false;
}
Component[] results = component.GetComponentsInChildren(
metadata.elementType,
attribute.IncludeInactive
);
if (results == null || results.Length == 0)
{
return false;
}
Transform componentTransform = component.transform;
for (int i = 0; i < results.Length; ++i)
{
Component candidate = results[i];
if (candidate == null)
{
continue;
}
if (attribute.OnlyDescendants && candidate.transform == componentTransform)
{
continue;
}
childComponent = candidate;
return true;
}
return false;
}
private static bool TryAssignChildCollectionFast(
Component component,
FieldMetadata metadata,
FilterParameters filters,
out bool assignedAny
)
{
assignedAny = false;
ChildComponentAttribute attribute = metadata.attribute;
if (metadata.isInterface || filters.RequiresPostProcessing || attribute.MaxDepth > 0)
{
#if UNITY_EDITOR && UNITY_2020_2_OR_NEWER
ChildFallbackMarker.Begin();
ChildFallbackMarker.End();
#endif
return false;
}
#if UNITY_EDITOR && UNITY_2020_2_OR_NEWER
using (ChildFastPathMarker.Auto())
#endif
{
Array children = ChildComponentFastInvoker.GetArray(
component,
metadata.elementType,
attribute.IncludeInactive
);
Array filtered = FilterChildArray(component, metadata, children);
Array ordered = EnsureBreadthFirstOrder(component, metadata, filtered);
assignedAny = AssignChildComponentsFromArray(component, metadata, ordered);
return true;
}
}
private static Array FilterChildArray(
Component component,
FieldMetadata metadata,
Array source
)
{
Type elementType = metadata.elementType;
if (source == null || source.Length == 0)
{
return Array.CreateInstance(elementType, 0);
}
ChildComponentAttribute attribute = metadata.attribute;
bool onlyDescendants = attribute.OnlyDescendants;
Transform self = component.transform;
int maxCount = attribute.MaxCount;
if (!onlyDescendants && maxCount <= 0)
{
return source;
}
int limit = maxCount > 0 ? Math.Min(maxCount, source.Length) : source.Length;
Array staged = Array.CreateInstance(elementType, limit);
int writeIndex = 0;
for (int i = 0; i < source.Length && writeIndex < limit; ++i)
{
Component candidate = source.GetValue(i) as Component;
if (candidate == null)
{
continue;
}
if (onlyDescendants && candidate.transform == self)
{
continue;
}
staged.SetValue(candidate, writeIndex++);
}
if (writeIndex == staged.Length)
{
return staged;
}
Array result = Array.CreateInstance(elementType, writeIndex);
if (writeIndex > 0)
{
Array.Copy(staged, 0, result, 0, writeIndex);
}
return result;
}
private static Array EnsureBreadthFirstOrder(
Component component,
FieldMetadata metadata,
Array source
)
{
Type elementType = metadata.elementType;
if (source == null)
{
return Array.CreateInstance(elementType, 0);
}
int length = source.Length;
if (length <= 1)
{
return source;
}
ChildComponentAttribute attribute = metadata.attribute;
using PooledResource> traversalResource = Buffers.List.Get(
out List traversal
);
component.IterateOverAllChildrenRecursivelyBreadthFirst(
traversal,
includeSelf: !attribute.OnlyDescendants,
attribute.MaxDepth
);
using PooledResource>> groupedResource =
DictionaryBuffer>.Dictionary.Get(
out Dictionary> grouped
);
using PooledResource> positionsResource = DictionaryBuffer<
Transform,
int
>.Dictionary.Get(out Dictionary positions);
using PooledResource>>> groupedLeaseTracker =
Buffers>>.List.Get(
out List>> groupedListLeases
);
for (int i = 0; i < length; ++i)
{
Component candidate = source.GetValue(i) as Component;
if (candidate == null)
{
continue;
}
Transform key = candidate.transform;
if (!grouped.TryGetValue(key, out List list))
{
PooledResource> lease = Buffers.List.Get(out list);
groupedListLeases.Add(lease);
grouped.Add(key, list);
positions.Add(key, 0);
}
list.Add(candidate);
}
Array ordered = Array.CreateInstance(elementType, length);
int writeIndex = 0;
for (int i = 0; i < traversal.Count && writeIndex < length; ++i)
{
Transform transform = traversal[i];
if (!grouped.TryGetValue(transform, out List list))
{
continue;
}
int position = positions[transform];
while (position < list.Count && writeIndex < length)
{
ordered.SetValue(list[position], writeIndex++);
position++;
}
if (position >= list.Count)
{
grouped.Remove(transform);
positions.Remove(transform);
}
else
{
positions[transform] = position;
}
}
if (writeIndex < length && grouped.Count > 0)
{
foreach (KeyValuePair> pair in grouped)
{
List list = pair.Value;
int position = positions[pair.Key];
while (position < list.Count && writeIndex < length)
{
ordered.SetValue(list[position], writeIndex++);
position++;
}
if (writeIndex >= length)
{
break;
}
}
}
if (writeIndex >= length)
{
DisposeGroupLeases(groupedListLeases);
return ordered;
}
if (writeIndex == 0)
{
DisposeGroupLeases(groupedListLeases);
return Array.CreateInstance(elementType, 0);
}
Array trimmed = Array.CreateInstance(elementType, writeIndex);
Array.Copy(ordered, 0, trimmed, 0, writeIndex);
DisposeGroupLeases(groupedListLeases);
return trimmed;
}
private static void DisposeGroupLeases(
List>> groupedListLeases
)
{
if (groupedListLeases == null)
{
return;
}
for (int i = groupedListLeases.Count - 1; i >= 0; --i)
{
groupedListLeases[i].Dispose();
}
groupedListLeases.Clear();
}
private static bool AssignChildComponentsFromArray(
Component component,
FieldMetadata metadata,
Array componentsArray
)
{
if (componentsArray == null)
{
componentsArray = Array.CreateInstance(metadata.elementType, 0);
}
int count = componentsArray.Length;
switch (metadata.kind)
{
case FieldKind.Array:
{
Array instance = metadata.arrayCreator(count);
for (int i = 0; i < count; ++i)
{
instance.SetValue(componentsArray.GetValue(i), i);
}
metadata.SetValue(component, instance);
return count > 0;
}
case FieldKind.List:
{
if (metadata.GetValue(component) is IList list)
{
list.Clear();
}
else
{
list = metadata.listCreator(count);
metadata.SetValue(component, list);
}
for (int i = 0; i < count; ++i)
{
list.Add(componentsArray.GetValue(i));
}
return count > 0;
}
case FieldKind.HashSet:
{
object hashSet = metadata.GetValue(component);
if (hashSet != null && metadata.hashSetClearer != null)
{
metadata.hashSetClearer(hashSet);
}
else
{
hashSet = metadata.hashSetCreator(count);
metadata.SetValue(component, hashSet);
}
for (int i = 0; i < count; ++i)
{
metadata.hashSetAdder(hashSet, componentsArray.GetValue(i));
}
return count > 0;
}
default:
{
return false;
}
}
}
private static bool TryAssignChildSingleFallback(
Component component,
FieldMetadata metadata,
FilterParameters filters,
List childBuffer,
out Component childComponent
)
{
bool needsScratch = metadata.isInterface || filters.RequiresPostProcessing;
List scratchList = null;
PooledResource> scratch = default;
if (needsScratch)
{
scratch = Buffers.List.Get(out scratchList);
}
childComponent = null;
foreach (
Transform child in component.IterateOverAllChildrenRecursivelyBreadthFirst(
childBuffer,
includeSelf: !metadata.attribute.OnlyDescendants,
maxDepth: metadata.attribute.MaxDepth
)
)
{
if (
TryResolveSingleComponent(
child,
filters,
metadata.elementType,
metadata.isInterface,
metadata.attribute.AllowInterfaces,
scratchList,
out Component resolved
)
)
{
childComponent = resolved;
break;
}
}
if (needsScratch)
{
scratch.Dispose();
}
return childComponent != null;
}
private static int EnumerateFilteredChildComponents(
Component component,
FieldMetadata metadata,
FilterParameters filters,
List childBuffer,
Func onComponent
)
{
if (component == null)
{
return 0;
}
ChildComponentAttribute attribute = metadata.attribute;
int maxAssignments = attribute.MaxCount > 0 ? attribute.MaxCount : int.MaxValue;
int added = 0;
using PooledResource> componentBuffer = Buffers.List.Get(
out List components
);
foreach (
Transform child in component.IterateOverAllChildrenRecursivelyBreadthFirst(
childBuffer,
includeSelf: !attribute.OnlyDescendants,
maxDepth: attribute.MaxDepth
)
)
{
GetComponentsOfType(
child,
metadata.elementType,
metadata.isInterface,
attribute.AllowInterfaces,
components
);
for (int i = 0; i < components.Count; ++i)
{
Component candidate = components[i];
if (!PassesStateAndFilters(candidate, filters, filterDisabledComponents: true))
{
continue;
}
if (!onComponent(candidate))
{
return added;
}
added++;
if (added >= maxAssignments)
{
return added;
}
}
}
return added;
}
}
internal static class ChildComponentFastInvoker
{
private static readonly Dictionary> ArrayGetters = new();
private static readonly MethodInfo GetComponentsInChildrenGeneric =
FindGetComponentsInChildrenMethod();
private static MethodInfo FindGetComponentsInChildrenMethod()
{
MethodInfo[] methods = typeof(Component).GetMethods(
BindingFlags.Instance | BindingFlags.Public
);
for (int i = 0; i < methods.Length; i++)
{
MethodInfo method = methods[i];
if (
method.Name == nameof(Component.GetComponentsInChildren)
&& method.IsGenericMethodDefinition
&& method.GetParameters().Length == 1
&& method.GetParameters()[0].ParameterType == typeof(bool)
)
{
return method;
}
}
throw new InvalidOperationException(
"Could not find GetComponentsInChildren(bool) method on Component type."
);
}
internal static Array GetArray(Component component, Type elementType, bool includeInactive)
{
if (!ArrayGetters.TryGetValue(elementType, out Func getter))
{
getter = CreateArrayGetter(elementType);
ArrayGetters[elementType] = getter;
}
return getter(component, includeInactive);
}
private static Func CreateArrayGetter(Type elementType)
{
MethodInfo closedMethod = GetComponentsInChildrenGeneric.MakeGenericMethod(elementType);
ParameterExpression componentParameter = Expression.Parameter(
typeof(Component),
"component"
);
ParameterExpression includeInactiveParameter = Expression.Parameter(
typeof(bool),
"includeInactive"
);
MethodCallExpression invoke = Expression.Call(
componentParameter,
closedMethod,
includeInactiveParameter
);
UnaryExpression convert = Expression.Convert(invoke, typeof(Array));
return Expression
.Lambda>(
convert,
componentParameter,
includeInactiveParameter
)
.Compile();
}
}
}