// 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();
}
}
}