Unity Helpers

Logo

Treasure chest of Unity developer tools. Professional inspector tooling, high-performance utilities, spatial queries, and 20+ editor tools.

Relational Component Attributes

Visual

Relational Wiring

Auto-wire components in your hierarchy without GetComponent boilerplate. These attributes make common relationships explicit, robust, and easy to maintain.

Collection Type Support: Each attribute works with:

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.

TL;DR — What Problem This Solves

The Productivity Advantage

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

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

Why Use These?

Quick Start

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

How It Works

Decorate private (or public) fields on a MonoBehaviour with a relational attribute, then call one of:

Assignments happen at runtime (e.g., Awake/OnEnable), not at edit-time serialization.

Visual Search Patterns

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

Key Options

OnlyAncestors / OnlyDescendants:

MaxDepth:


💡 Having Issues? Components not being assigned? Fields staying null? Jump to Troubleshooting for solutions to common problems.


Attribute Reference

SiblingComponent

Examples:

[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, call AssignSiblingComponents again after the hierarchy changes.

ParentComponent

Examples:

// 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;

ChildComponent

Examples:

// 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 on MaxDepth or interface searches still works, but the assigner reverts to the breadth-first traversal to honour those constraints.

Common Options (All Attributes)

Choosing the Right Collection Type

Use Arrays (T[]) when:

Use Lists (List<T>) when:

Use HashSets (HashSet<T>) when:

// 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;

Recipes

Best Practices

Explicit Initialization (Prewarm)

Relational 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:

Dependency Injection Integrations

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.

The DI Pain Point

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
    }
}

The Integration Solution

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
}

Why Use the DI Integrations

Supported Packages (Auto-detected)

Unity Helpers automatically detects these packages via UPM:

💡 UPM packages work out-of-the-box - No scripting defines needed!

Manual or Source Imports (Non-UPM)

If you import Zenject/VContainer/Reflex as source code, .unitypackage, or raw DLLs (not via UPM), you need to manually add scripting defines:

  1. Open Project Settings > Player > Other Settings > Scripting Define Symbols
  2. Add the appropriate define(s) for your target platforms:
    • ZENJECT_PRESENT - When using Zenject/Extenject
    • VCONTAINER_PRESENT - When using VContainer
    • REFLEX_PRESENT - When using Reflex
  3. Unity will recompile and the integration assemblies under Runtime/Integrations/* will activate automatically

VContainer at a Glance

Zenject at a Glance

Reflex at a Glance

Notes


Troubleshooting

FAQ

Q: Does this run in Edit Mode or serialize values?

Q: Are interfaces supported?

Q: What about performance?


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.

DI Integrations: Testing and Edge Cases

Beginner-friendly overview

VContainer (1.16.x)

Zenject/Extenject

Object Pools (DI-aware)

Common pitfalls and how to avoid them


Core Guides:

Related Features:

DI Integration Samples:

Need help? Open an issue Troubleshooting