// MIT License - Copyright (c) 2025 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.Utils; using static RelationalComponentProcessor; #if UNITY_EDITOR && UNITY_2020_2_OR_NEWER using Unity.Profiling; #endif /// /// Automatically assigns parent components (components up 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 parent only). 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 parent searches with depth and filters: /// /// [AttributeUsage(AttributeTargets.Field)] public sealed class ParentComponentAttribute : BaseRelationalComponentAttribute { /// /// If true, excludes components on the current and only searches parent transforms. /// If false, includes components on the current in the search. Default: false. /// public bool OnlyAncestors { get; set; } = false; /// /// Maximum depth to search up the hierarchy. 0 means unlimited. Default: 0. /// Depth 1 = immediate parent only, depth 2 = parent and grandparent, etc. /// /// /// Negative values are treated as 0 (unlimited). The search proceeds from closest /// to most distant ancestors. /// public int MaxDepth { get => _maxDepth; set => _maxDepth = value < 0 ? 0 : value; } private int _maxDepth; } public static class ParentComponentExtensions { private static readonly Dictionary< Type, FieldMetadata[] > FieldsByType = new(); #if UNITY_EDITOR && UNITY_2020_2_OR_NEWER private static readonly ProfilerMarker ParentFastPathMarker = new( "RelationalComponents.Parent.FastPath" ); private static readonly ProfilerMarker ParentFallbackMarker = new( "RelationalComponents.Parent.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 AssignParentComponents(this Component component) { FieldMetadata[] fields = FieldsByType.GetOrAdd( component.GetType(), type => GetFieldMetadata(type) ); AssignParentComponents(component, fields); } internal static void AssignParentComponents( Component component, FieldMetadata[] fields ) { if (component == null || fields == null || fields.Length == 0) { return; } foreach (FieldMetadata field in fields) { if (ShouldSkipAssignment(field, component)) { continue; } FilterParameters filters = field.Filters; Transform root = component.transform; if (field.attribute.OnlyAncestors) { root = root.parent; } if (root == null) { SetEmptyCollection(component, field); LogMissingComponentError(component, field, "parent"); AssignNullToSingleField(component, field); continue; } else { bool foundParent; if (field.kind == FieldKind.Single) { if ( TryAssignParentSingleFast( root, field, filters, out Component parentComponent ) || TryGetFirstParentComponent( root, filters, field.elementType, field.attribute, field.isInterface, out parentComponent ) ) { field.SetValue(component, parentComponent); foundParent = true; } else { foundParent = false; } } else { switch (field.kind) { case FieldKind.Array: { if ( TryAssignParentCollectionFast( component, root, field, filters, out bool assignedAny ) ) { foundParent = assignedAny; break; } using PooledResource> parentComponentBuffer = Buffers.List.Get( out List parentComponents ); GetParentComponents( root, field.elementType, field.attribute, field.isInterface, parentComponents ); int filteredCount = !filters.RequiresPostProcessing && field.attribute.MaxCount <= 0 ? parentComponents.Count : FilterComponentsInPlace( parentComponents, filters, field.attribute, field.elementType, field.isInterface, filterDisabledComponents: false ); Array correctTypedArray = field.arrayCreator(filteredCount); for (int i = 0; i < filteredCount; ++i) { correctTypedArray.SetValue(parentComponents[i], i); } field.SetValue(component, correctTypedArray); foundParent = filteredCount > 0; break; } case FieldKind.List: { if ( TryAssignParentCollectionFast( component, root, field, filters, out bool assignedAny ) ) { foundParent = assignedAny; break; } using PooledResource> parentComponentBuffer = Buffers.List.Get( out List parentComponents ); GetParentComponents( root, field.elementType, field.attribute, field.isInterface, parentComponents ); int filteredCount = !filters.RequiresPostProcessing && field.attribute.MaxCount <= 0 ? parentComponents.Count : FilterComponentsInPlace( parentComponents, filters, field.attribute, field.elementType, field.isInterface, filterDisabledComponents: false ); if (field.GetValue(component) is IList instance) { instance.Clear(); } else { instance = field.listCreator(filteredCount); field.SetValue(component, instance); } for (int i = 0; i < filteredCount; ++i) { instance.Add(parentComponents[i]); } foundParent = filteredCount > 0; break; } case FieldKind.HashSet: { if ( TryAssignParentCollectionFast( component, root, field, filters, out bool assignedAny ) ) { foundParent = assignedAny; break; } using PooledResource> parentComponentBuffer = Buffers.List.Get( out List parentComponents ); GetParentComponents( root, field.elementType, field.attribute, field.isInterface, parentComponents ); int filteredCount = FilterComponentsInPlace( parentComponents, filters, field.attribute, field.elementType, field.isInterface, filterDisabledComponents: false ); object instance = field.GetValue(component); if (instance != null && field.hashSetClearer != null) { field.hashSetClearer(instance); } else { instance = field.hashSetCreator(filteredCount); field.SetValue(component, instance); } for (int i = 0; i < filteredCount; ++i) { field.hashSetAdder(instance, parentComponents[i]); } foundParent = filteredCount > 0; break; } default: { foundParent = false; break; } } } if (!foundParent) { LogMissingComponentError(component, field, "parent"); AssignNullToSingleField(component, field); } } } } internal static FieldMetadata[] GetOrCreateFields(Type type) { return FieldsByType.GetOrAdd(type, t => GetFieldMetadata(t)); } private static bool TryAssignParentCollectionFast( Component component, Transform root, FieldMetadata metadata, FilterParameters filters, out bool assignedAny ) { assignedAny = false; ParentComponentAttribute attribute = metadata.attribute; if ( metadata.isInterface || filters.RequiresPostProcessing || attribute.MaxDepth > 0 || root == null ) { #if UNITY_EDITOR && UNITY_2020_2_OR_NEWER ParentFallbackMarker.Begin(); ParentFallbackMarker.End(); #endif return false; } #if UNITY_EDITOR && UNITY_2020_2_OR_NEWER using (ParentFastPathMarker.Auto()) #endif { Array parents = ParentComponentFastInvoker.GetArray( root, metadata.elementType, attribute.IncludeInactive ); Array filtered = FilterParentArray(metadata, parents); assignedAny = AssignParentComponentsFromArray(component, metadata, filtered); return true; } } private static Array FilterParentArray( FieldMetadata metadata, Array source ) { Type elementType = metadata.elementType; if (source == null || source.Length == 0) { return Array.CreateInstance(elementType, 0); } int maxCount = metadata.attribute.MaxCount; if (maxCount <= 0) { return source; } int limit = Math.Min(maxCount, 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; } 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 bool AssignParentComponentsFromArray( Component component, FieldMetadata metadata, Array parents ) { if (parents == null) { parents = Array.CreateInstance(metadata.elementType, 0); } int count = parents.Length; switch (metadata.kind) { case FieldKind.Array: { Array instance = metadata.arrayCreator(count); for (int i = 0; i < count; ++i) { instance.SetValue(parents.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(parents.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, parents.GetValue(i)); } return count > 0; } default: { return false; } } } private static List GetParentComponents( Transform root, Type elementType, ParentComponentAttribute attribute, bool isInterface, List buffer ) { buffer.Clear(); if (isInterface && attribute.AllowInterfaces) { // For interfaces, we need to manually traverse the hierarchy Transform current = root; int depth = 0; int maxDepth = attribute.MaxDepth > 0 ? attribute.MaxDepth : int.MaxValue; using PooledResource> parentComponentBuffer = Buffers.List.Get(out List components); while (current != null && depth < maxDepth) { GetComponentsOfType( current, elementType, isInterface, attribute.AllowInterfaces, components ); buffer.AddRange(components); current = current.parent; depth++; } return buffer; } // Use Unity's built-in method for concrete types Component[] allParents = root.GetComponentsInParent( elementType, includeInactive: attribute.IncludeInactive ); // Filter by depth if needed if (attribute.MaxDepth > 0) { foreach (Component comp in allParents) { int depth = GetDepthFromTransform(root, comp.transform); // depth is steps from root: 0 = root itself, 1 = root.parent, etc. // MaxDepth is how many levels to search, so depth should be < MaxDepth if (depth < attribute.MaxDepth) { buffer.Add(comp); } } } else { buffer.AddRange(allParents); } return buffer; } private static bool TryAssignParentSingleFast( Transform root, FieldMetadata metadata, FilterParameters filters, out Component parentComponent ) { parentComponent = null; if ( root == null || metadata.isInterface || filters.RequiresPostProcessing || metadata.attribute.IncludeInactive || metadata.attribute.MaxDepth > 0 ) { return false; } Component candidate = root.GetComponentInParent(metadata.elementType); if (candidate == null) { return false; } parentComponent = candidate; return true; } private static bool TryGetFirstParentComponent( Transform root, FilterParameters filters, Type elementType, ParentComponentAttribute attribute, bool isInterface, out Component result ) { Transform current = root; int depth = 0; int maxDepth = attribute.MaxDepth > 0 ? attribute.MaxDepth : int.MaxValue; bool needsScratch = isInterface || filters.RequiresPostProcessing; List components = null; PooledResource> scratch = default; if (needsScratch) { scratch = Buffers.List.Get(out components); } while (current != null && depth < maxDepth) { if ( TryResolveSingleComponent( current, filters, elementType, isInterface, attribute.AllowInterfaces, components, out Component resolved, filterDisabledComponents: false ) ) { if (needsScratch) { scratch.Dispose(); } result = resolved; return true; } current = current.parent; depth++; } if (needsScratch) { scratch.Dispose(); } result = null; return false; } private static int GetDepthFromTransform(Transform start, Transform target) { int depth = 0; Transform current = start; while (current != null && current != target) { current = current.parent; depth++; } return current == target ? depth : int.MaxValue; } } internal static class ParentComponentFastInvoker { private static readonly Dictionary> ArrayGetters = new(); private static readonly MethodInfo GetComponentsInParentGeneric = FindGetComponentsInParentMethod(); private static MethodInfo FindGetComponentsInParentMethod() { 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.GetComponentsInParent) && method.IsGenericMethodDefinition && method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(bool) ) { return method; } } throw new InvalidOperationException( "Could not find GetComponentsInParent(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 = GetComponentsInParentGeneric.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(); } } }