Treasure chest of Unity developer tools. Professional inspector tooling, high-performance utilities, spatial queries, and 20+ editor tools.
Visual
Auto-wire components in your hierarchy without GetComponent boilerplate. These attributes make common relationships explicit, robust, and easy to maintain.
SiblingComponent — same GameObjectParentComponent — up the transform hierarchyChildComponent — down the transform hierarchy (breadth-first)Collection Type Support: Each attribute works with:
Transform)Collider2D[])List<Rigidbody2D>)HashSet<Renderer>)All attributes support optional assignment, filters (tag/name), depth limits, max results, and interface/base-type resolution.
Having issues? Jump to Troubleshooting: see Troubleshooting.
Related systems: For data‑driven gameplay effects (attributes, tags, cosmetics), see Effects System and the README section Effects, Attributes, and Tags.
Curious how these attributes stack up against manual GetComponent* loops? Check the Relational Component Performance Benchmarks for operations-per-second and allocation snapshots.
Before (The Old Way):
void Awake()
{
sprite = GetComponent<SpriteRenderer>();
if (sprite == null) Debug.LogError("Missing SpriteRenderer!");
rigidbody = GetComponentInParent<Rigidbody2D>();
if (rigidbody == null) Debug.LogError("Missing Rigidbody2D in parent!");
colliders = GetComponentsInChildren<Collider2D>();
if (colliders.Length == 0) Debug.LogWarning("No colliders in children!");
// Repeat for every component...
// 15-30 lines of boilerplate per script
}
After (Relational Components):
[SiblingComponent] private SpriteRenderer sprite;
[ParentComponent] private Rigidbody2D rigidbody;
[ChildComponent] private Collider2D[] colliders;
void Awake() => this.AssignRelationalComponents();
// That's it. 4 lines total, all wired automatically with validation.
Pick the right attribute
SiblingComponent.ParentComponent.ChildComponent.One‑minute setup
[SiblingComponent] private SpriteRenderer sprite;
[ParentComponent(OnlyAncestors = true)] private Rigidbody2D rb;
[ChildComponent(OnlyDescendants = true, MaxDepth = 1)] private Collider2D[] childColliders;
void Awake() => this.AssignRelationalComponents();
GetComponent and fragile manual wiringusing UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;
public class Player : MonoBehaviour
{
// Same-GameObject
[SiblingComponent] private SpriteRenderer sprite;
// First matching ancestor (excluding self)
[ParentComponent(OnlyAncestors = true)] private Rigidbody2D ancestorRb;
// Immediate children only, collect many
[ChildComponent(OnlyDescendants = true, MaxDepth = 1)]
private Collider2D[] immediateChildColliders;
private void Awake()
{
// Wires up all relational fields on this component
this.AssignRelationalComponents();
}
}
Decorate private (or public) fields on a MonoBehaviour with a relational attribute, then call one of:
this.AssignRelationalComponents() — assign all three categoriesthis.AssignSiblingComponents() — only siblingsthis.AssignParentComponents() — only parentsthis.AssignChildComponents() — only childrenAssignments happen at runtime (e.g., Awake/OnEnable), not at edit-time serialization.
ParentComponent (searches UP the hierarchy):
Grandparent ←────────── (included unless OnlyAncestors = true)
↑
│
Parent ←────────────── (always included)
↑
│
[YOU] ←──────────────── Component with [ParentComponent]
│
Child
│
Grandchild
ChildComponent (searches DOWN the hierarchy, breadth-first):
Grandparent
│
Parent
│
[YOU] ←───────────────── Component with [ChildComponent]
↓
├─ Child 1 ←────────── (depth = 1)
│ ├─ Grandchild 1 (depth = 2)
│ └─ Grandchild 2 (depth = 2)
│
└─ Child 2 ←────────── (depth = 1)
└─ Grandchild 3 (depth = 2)
Breadth-first means all Children (depth 1) are checked
before any Grandchildren (depth 2).
SiblingComponent (searches same GameObject):
Parent
│
└─ [GameObject] ←────── All components on this GameObject
├─ [YOU] ←─────── Component with [SiblingComponent]
├─ Component A
├─ Component B
└─ Component C
OnlyAncestors / OnlyDescendants:
OnlyAncestors = true → Excludes self, searches only parents/grandparentsOnlyDescendants = true → Excludes self, searches only children/grandchildrenMaxDepth:
MaxDepth = 1 with OnlyDescendants = true → immediate children onlyMaxDepth = 2 → children + grandchildren (or parents + grandparents)💡 Having Issues? Components not being assigned? Fields staying null? Jump to Troubleshooting for solutions to common problems.
GameObjectExamples:
[SiblingComponent] private Animator animator; // required by default
[SiblingComponent(Optional = true)] private Rigidbody2D rb; // optional
[SiblingComponent(TagFilter = "Visual", NameFilter = "Sprite")] private Component[] visuals;
[SiblingComponent(MaxCount = 2)] private List<Collider2D> firstTwo; // List<T> supported
[SiblingComponent] private HashSet<Renderer> allRenderers; // HashSet<T> supported
Performance note: Sibling lookups do not cache results between calls. In profiling we found these assignments typically run once per GameObject (e.g., during
Awake), so the extra bookkeeping and invalidation cost of a cache outweighed the benefits. If you need updated references later, callAssignSiblingComponentsagain after the hierarchy changes.
OnlyAncestors, MaxDepthExamples:
// Immediate parent only
[ParentComponent(OnlyAncestors = true, MaxDepth = 1)] private Transform directParent;
// Up to 3 levels with a tag
[ParentComponent(OnlyAncestors = true, MaxDepth = 3, TagFilter = "Player")] private Collider2D playerAncestor;
// Interface/base-type resolution is supported by default
[ParentComponent] private IHealth healthProvider;
OnlyDescendants, MaxDepthExamples:
// Immediate children only
[ChildComponent(OnlyDescendants = true, MaxDepth = 1)] private Transform[] immediateChildren;
// First matching descendant with a tag
[ChildComponent(OnlyDescendants = true, TagFilter = "Weapon")] private Collider2D weaponCollider;
// Gather into a List (preserves insertion order)
[ChildComponent(OnlyDescendants = true)] private List<MeshRenderer> childRenderers;
// Gather into a HashSet (unique results, no duplicates) and limit count
[ChildComponent(OnlyDescendants = true, MaxCount = 10)] private HashSet<Rigidbody2D> firstTenRigidbodies;
Performance note: When you avoid depth limits and interface filtering, child assignments run through a cached
GetComponentsInChildren<T>()delegate to stay allocation-free. Turning onMaxDepthor interface searches still works, but the assigner reverts to the breadth-first traversal to honour those constraints.
Optional (default: false)
false, logs a descriptive error when no match is foundtrue, suppresses the error (field remains null/empty)IncludeInactive (default: true)
true, includes disabled components and inactive GameObjectsfalse, only assigns enabled components on active-in-hierarchy objectsSkipIfAssigned (default: false)
true, preserves existing non-null value (single) or non-empty collectionMaxCount (default: 0 = unlimited)
TagFilter
CompareTagNameFilter
AllowInterfaces (default: true)
true, can assign by interface or base type; set false to restrict to concrete typesUse Arrays (T[]) when:
Use Lists (List<T>) when:
[] operatorUse HashSets (HashSet<T>) when:
Contains())// Arrays: Fixed size, minimal overhead
[ChildComponent] private Collider2D[] colliders;
// Lists: Dynamic, ordered, index-based access
[ChildComponent] private List<Renderer> renderers;
// HashSets: Unique, fast lookups, unordered
[ChildComponent] private HashSet<AudioSource> audioSources;
UI hierarchy references
[ParentComponent(OnlyAncestors = true, MaxDepth = 2)] private Canvas canvas;
[ChildComponent(OnlyDescendants = true, NameFilter = "Button")] private Button[] buttons;
Sensors/components living on children
[ChildComponent(OnlyDescendants = true, TagFilter = "Sensor")] private Collider[] sensors;
Modular systems via interfaces
public interface IInputProvider { Vector2 Move { get; } }
[ParentComponent] private IInputProvider input; // PlayerInput, AIInput, etc.
Awake() or OnEnable() so references exist earlyAssignSibling/Parent/Child) when you only use one categoryMaxDepth to cap traversal cost in deep treesMaxCount to reduce allocations when you only need a subsetOptional = true to avoid noiseRelational components build high‑performance reflection helpers on first use. To eliminate this lazy cost and avoid first‑frame stalls on large projects or IL2CPP builds, explicitly pre‑initialize caches at startup:
// Call during bootstrap/loading
using WallstopStudios.UnityHelpers.Core.Attributes;
void Start()
{
RelationalComponentInitializer.Initialize();
}
Notes:
RelationalComponentInitializer.Initialize(new[]{ typeof(MyComponent) });Stop choosing between DI and clean hierarchy references - Unity Helpers provides seamless integrations with Zenject/Extenject, VContainer, and Reflex that automatically wire up your relational component fields right after dependency injection completes.
Without these integrations, you’re stuck writing Awake() methods full of GetComponent boilerplate even when using a DI framework:
public class Enemy : MonoBehaviour
{
[Inject] private IHealthSystem _health; // ✅ DI handles this
private Animator _animator; // ❌ Still manual boilerplate
private Rigidbody2D _rigidbody; // ❌ Still manual boilerplate
void Awake()
{
_animator = GetComponent<Animator>();
_rigidbody = GetComponent<Rigidbody2D>();
// ... 15 more lines of GetComponent hell
}
}
With the DI integrations, everything just works:
public class Enemy : MonoBehaviour
{
[Inject] private IHealthSystem _health; // ✅ DI injection
[SiblingComponent] private Animator _animator; // ✅ Relational auto-wiring
[SiblingComponent] private Rigidbody2D _rigidbody; // ✅ Relational auto-wiring
// No Awake() needed! Both DI and hierarchy references wired automatically
}
Awake() method needed, no manual GetComponent calls, no validation codeUnity Helpers automatically detects these packages via UPM:
com.extenject.zenject, com.modesttree.zenject, com.svermeulen.extenjectjp.cysharp.vcontainer, jp.hadashikick.vcontainercom.gustavopsantos.reflex💡 UPM packages work out-of-the-box - No scripting defines needed!
If you import Zenject/VContainer/Reflex as source code, .unitypackage, or raw DLLs (not via UPM), you need to manually add scripting defines:
Project Settings > Player > Other Settings > Scripting Define SymbolsZENJECT_PRESENT - When using Zenject/ExtenjectVCONTAINER_PRESENT - When using VContainerREFLEX_PRESENT - When using ReflexRuntime/Integrations/* will activate automaticallyEnable once per scope
builder.RegisterRelationalComponents(
new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true),
enableAdditiveSceneListener: true
);
_resolver.InstantiateComponentWithRelations(componentPrefab, parent)_resolver.InstantiateGameObjectWithRelations(rootPrefab, parent, includeInactiveChildren: true)_resolver.AssignRelationalHierarchy(existingRoot, includeInactiveChildren: true)RelationalObjectPools.CreatePoolWithRelations(...) + pool.GetWithRelations(resolver)RelationalComponentsInstaller to your SceneContext._container.InstantiateComponentWithRelations(componentPrefab, parent)_container.InstantiateGameObjectWithRelations(rootPrefab, parent, includeInactiveChildren: true)_container.AssignRelationalHierarchy(existingRoot, includeInactiveChildren: true)RelationalMemoryPool<T> to hydrate pooled items on spawn.SceneScope in each scene. Add RelationalComponentsInstaller to the same GameObject (or a child) to bind the relational assigner, run the initial scene scan, and optionally register the additive-scene listener._container.InjectWithRelations(existingComponent) to inject DI fields and hydrate relational attributes on existing objects._container.InstantiateComponentWithRelations(componentPrefab, parent) for component prefabs._container.InstantiateGameObjectWithRelations(rootPrefab, parent, includeInactiveChildren: true) for full hierarchies._container.AssignRelationalHierarchy(existingRoot, includeInactiveChildren: true) to hydrate arbitrary hierarchies after manual instantiation.Full walkthrough: DI – Reflex sample
AssignRelationalComponents() directly so you can adopt incrementally.Notes
component.AssignRelationalComponents() call path if the DI container does not expose the assigner binding, so you can adopt them incrementally without breaking existing behaviour.AssignRelationalComponents() or the specific Assign*Components() in Awake() or OnEnable().TagFilter must match an existing tag; NameFilter is case-sensitive.OnlyAncestors/OnlyDescendants may exclude self; MaxDepth may be too small.AllowInterfaces = true (default) or use a concrete type.IncludeInactive = false to restrict to enabled components on active GameObjects.MaxCount and/or MaxDepth. Prefer List<T> or HashSet<T> when you plan to mutate the collection after assignment.AssignParentComponents / AssignChildComponents / AssignSiblingComponents) instead of the all-in-one method for clarity and potentially less work.Q: Does this run in Edit Mode or serialize values?
Q: Are interfaces supported?
AllowInterfaces = true (default). Set it to false to restrict to concrete types.Q: What about performance?
MaxDepth, TagFilter, NameFilter, and MaxCount to limit work. Sibling lookups are O(1) when no filters are applied.For quick examples in context, see the README’s “Auto Component Discovery” section. For API docs, hover the attributes in your IDE for XML summaries and examples.
Beginner-friendly overview
ZENJECT_PRESENT, VCONTAINER_PRESENT). With UPM, these are added via asmdef versionDefines. Without UPM (manual import), add them in Project Settings → Player → Scripting Define Symbols.IRelationalComponentAssigner) and provide a scene initializer/entry point to hydrate relational fields once the container is ready.VContainer (1.16.x)
Runtime usage (LifetimeScope): Call builder.RegisterRelationalComponents() in LifetimeScope.Configure. The entry point runs automatically after the container builds. You can enable an additive-scene listener and customize scan options:
using VContainer;
using VContainer.Unity;
using WallstopStudios.UnityHelpers.Integrations.VContainer;
protected override void Configure(IContainerBuilder builder)
{
// Single-pass scan + additive scene listener
var options = new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true);
builder.RegisterRelationalComponents(options, enableAdditiveSceneListener: true);
}
Tests without LifetimeScope: Construct the entry point and call Initialize() yourself, and register your AttributeMetadataCache instance so the assigner uses it:
var cache = ScriptableObject.CreateInstance<AttributeMetadataCache>();
// populate cache._relationalTypeMetadata with your test component types
cache.ForceRebuildForTests(); // rebuild lookups so the initializer can discover your types
var builder = new ContainerBuilder();
builder.RegisterInstance(cache).AsSelf();
builder.Register<RelationalComponentAssigner>(Lifetime.Singleton)
.As<IRelationalComponentAssigner>()
.AsSelf();
var resolver = builder.Build();
var entry = new RelationalComponentEntryPoint(
resolver.Resolve<IRelationalComponentAssigner>(),
cache,
RelationalSceneAssignmentOptions.Default
);
entry.Initialize();
resolver.InjectWithRelations(component) to inject + assign in one call, or resolver.Inject(component) then resolver.AssignRelationalComponents(component).resolver.InstantiateComponentWithRelations(prefab, parent) or resolver.InstantiateGameObjectWithRelations(prefab, parent); to inject existing hierarchies use resolver.InjectGameObjectWithRelations(root).[UnityTest] and yield return null after creating objects and after initializing the entry point so Unity has a frame to register new objects before FindObjectsOfType runs and to allow assignments to complete.SceneManager.CreateScene, set it active, and move your test hierarchy into it before calling Initialize().RelationalSceneAssignmentOptions(includeInactive: bool).Zenject/Extenject
RelationalComponentsInstaller to your SceneContext. It binds IRelationalComponentAssigner and runs RelationalComponentSceneInitializer once the container is ready. The installer exposes toggles to assign on initialize and to listen for additive scenes.AttributeMetadataCache instance and construct the assigner with that cache. Then resolve IInitializable and call Initialize().[UnityTest] with a yield return null after creating objects and after calling Initialize() to allow Unity to register objects and complete assignments.container.InstantiateComponentWithRelations(...), container.InstantiateGameObjectWithRelations(...), or container.InjectGameObjectWithRelations(root); to inject + assign a single instance: container.InjectWithRelations(component).RelationalMemoryPool<T> (or <TParam, T>) to assign relational fields in OnSpawned automatically.RelationalObjectPools.CreatePoolWithRelations(...) and rent via pool.GetWithRelations(resolver) to inject + assign.Common pitfalls and how to avoid them
LifetimeScope. Construct the entry point manually as shown above.versionDefines; manual imports require adding them in Player Settings.AttributeMetadataCache has the relational metadata for your test component types and that the DI container uses the same cache instance (register it and prefer constructors that accept the cache).Core Guides:
Related Features:
DI Integration Samples:
| Need help? Open an issue | Troubleshooting |