// 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 UnityEngine; using WallstopStudios.UnityHelpers.Core.Extension; using WallstopStudios.UnityHelpers.Utils; using static RelationalComponentProcessor; /// /// Automatically assigns sibling components (components on the same ) 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(). /// /// Use optional filters to refine results: (by tag), /// (substring match on name), and /// (include disabled/inactive components). /// /// 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. /// /// /// /// /// /// /// Assign common sibling components with filters and collections: /// allSiblingColliders; /// /// // Filter by tag and name substring /// [SiblingComponent(TagFilter = "Visual", NameFilter = "Sprite")] /// private Component[] visualComponents; /// /// private void Awake() /// { /// this.AssignSiblingComponents(); /// // or: this.AssignRelationalComponents(); /// } /// } /// ]]> /// [AttributeUsage(AttributeTargets.Field)] public sealed class SiblingComponentAttribute : BaseRelationalComponentAttribute { } public static class SiblingComponentExtensions { private static readonly Dictionary< Type, FieldMetadata[] > FieldsByType = new(); /// /// 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 AssignSiblingComponents(this Component component) { FieldMetadata[] fields = FieldsByType.GetOrAdd( component.GetType(), type => GetFieldMetadata(type) ); AssignSiblingComponents(component, fields); } internal static void AssignSiblingComponents( Component component, FieldMetadata[] fields ) { if (component == null || fields == null || fields.Length == 0) { return; } foreach (FieldMetadata metadata in fields) { if (ShouldSkipAssignment(metadata, component)) { continue; } bool foundSibling; if (metadata.kind == FieldKind.Single) { foundSibling = TryAssignSingleSibling(component, metadata); } else { FilterParameters filters = metadata.Filters; if ( !metadata.isInterface && !filters.RequiresPostProcessing && metadata.attribute.MaxCount <= 0 ) { foundSibling = TryAssignSiblingCollectionFast(component, metadata); } else { switch (metadata.kind) { case FieldKind.Array: { using PooledResource> componentBuffer = Buffers.List.Get(out List components); GetComponentsOfType( component, metadata.elementType, metadata.isInterface, metadata.attribute.AllowInterfaces, components ); int filteredCount = !filters.RequiresPostProcessing && metadata.attribute.MaxCount <= 0 ? components.Count : FilterComponentsInPlace( components, filters, metadata.attribute, metadata.elementType, metadata.isInterface ); Array correctTypedArray = metadata.arrayCreator(filteredCount); for (int i = 0; i < filteredCount; ++i) { correctTypedArray.SetValue(components[i], i); } metadata.SetValue(component, correctTypedArray); foundSibling = filteredCount > 0; break; } case FieldKind.List: { using PooledResource> componentBuffer = Buffers.List.Get(out List components); GetComponentsOfType( component, metadata.elementType, metadata.isInterface, metadata.attribute.AllowInterfaces, components ); int filteredCount = !filters.RequiresPostProcessing && metadata.attribute.MaxCount <= 0 ? components.Count : FilterComponentsInPlace( components, filters, metadata.attribute, metadata.elementType, metadata.isInterface ); object existing = metadata.GetValue(component); if (existing is IList instance) { instance.Clear(); } else { instance = metadata.listCreator(filteredCount); metadata.SetValue(component, instance); } for (int i = 0; i < filteredCount; ++i) { instance.Add(components[i]); } foundSibling = filteredCount > 0; break; } case FieldKind.HashSet: { using PooledResource> componentBuffer = Buffers.List.Get(out List components); GetComponentsOfType( component, metadata.elementType, metadata.isInterface, metadata.attribute.AllowInterfaces, components ); int filteredCount = !filters.RequiresPostProcessing && metadata.attribute.MaxCount <= 0 ? components.Count : FilterComponentsInPlace( components, filters, metadata.attribute, metadata.elementType, metadata.isInterface ); object instance = metadata.GetValue(component); if (instance != null && metadata.hashSetClearer != null) { metadata.hashSetClearer(instance); } else { instance = metadata.hashSetCreator(filteredCount); metadata.SetValue(component, instance); } for (int i = 0; i < filteredCount; ++i) { metadata.hashSetAdder(instance, components[i]); } foundSibling = filteredCount > 0; break; } default: { foundSibling = TryAssignSingleSibling(component, metadata); break; } } } } if (!foundSibling) { LogMissingComponentError(component, metadata, "sibling"); AssignNullToSingleField(component, metadata); } } } internal static FieldMetadata[] GetOrCreateFields(Type type) { return FieldsByType.GetOrAdd(type, t => GetFieldMetadata(t)); } private static bool TryAssignSingleSibling( Component component, FieldMetadata metadata ) { SiblingComponentAttribute attribute = metadata.attribute; if (metadata.isInterface && !attribute.AllowInterfaces) { return false; } bool hasSimpleFilters = attribute.IncludeInactive && attribute.TagFilter == null && attribute.NameFilter == null; if (!metadata.isInterface && hasSimpleFilters) { if (component.TryGetComponent(metadata.elementType, out Component sibling)) { metadata.SetValue(component, sibling); return true; } return false; } FilterParameters filters = new(attribute); if ( TryResolveSingleComponent( component, filters, metadata.elementType, metadata.isInterface, attribute.AllowInterfaces, null, out Component resolved ) ) { metadata.SetValue(component, resolved); return true; } return false; } private static bool TryAssignSiblingCollectionFast( Component component, FieldMetadata metadata ) { Array componentsArray = SiblingComponentFastInvoker.GetArray( component, metadata.elementType ); return AssignComponentsFromArray(component, metadata, componentsArray); } private static bool AssignComponentsFromArray( 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: { metadata.SetValue(component, componentsArray); return count > 0; } case FieldKind.List: { if (metadata.GetValue(component) is IList instance) { instance.Clear(); } else { instance = metadata.listCreator(count); metadata.SetValue(component, instance); } for (int i = 0; i < count; ++i) { instance.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 TryAssignSingleSibling(component, metadata); } } } } internal static class SiblingComponentFastInvoker { private static readonly Dictionary> ArrayGetters = new(); private static readonly MethodInfo GetComponentsGenericDefinition = FindGetComponentsMethod(); private static MethodInfo FindGetComponentsMethod() { 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.GetComponents) && method.IsGenericMethodDefinition && method.GetParameters().Length == 0 ) { return method; } } throw new InvalidOperationException( "Could not find GetComponents() method on Component type." ); } internal static Array GetArray(Component component, Type elementType) { if (!ArrayGetters.TryGetValue(elementType, out Func getter)) { getter = CreateArrayGetter(elementType); ArrayGetters[elementType] = getter; } return getter(component); } private static Func CreateArrayGetter(Type elementType) { MethodInfo closedMethod = GetComponentsGenericDefinition.MakeGenericMethod(elementType); ParameterExpression componentParameter = Expression.Parameter( typeof(Component), "component" ); MethodCallExpression invoke = Expression.Call(componentParameter, closedMethod); UnaryExpression convert = Expression.Convert(invoke, typeof(Array)); return Expression.Lambda>(convert, componentParameter).Compile(); } } }