com.wallstop-studios.unity-helpers
Version:
Treasure chest of Unity developer tools
520 lines (368 loc) • 17.6 kB
Markdown
# VContainer Integration - Unity Helpers
## Why This Integration Matters
**Stop Writing GetComponent Boilerplate in Every Single Script**
When using dependency injection with VContainer, you've solved half the problem - your service dependencies get injected cleanly. But you're **still stuck** writing repetitive `GetComponent` boilerplate for hierarchy references in every. single. MonoBehaviour.
**The Painful Reality:**
1. **Dependencies** → ✅ Handled by VContainer (IHealthSystem, IAudioService, etc.)
2. **Hierarchy references** → ❌ Still manual hell (SpriteRenderer, Rigidbody2D, child colliders, etc.)
You're using a modern DI framework but still writing 2008-era Unity boilerplate. **Unity Helpers fixes this.**
**The Solution:** This integration automatically wires up relational component fields **right after** DI injection completes - giving you the best of both worlds with **literally zero extra code per component**.
### ⚡ Quick Example: Before vs After
**Before (Manual):**
```csharp
public class Enemy : MonoBehaviour
{
[Inject] private IHealthSystem _healthSystem;
private Animator _animator;
private Rigidbody2D _rigidbody;
private Collider2D[] _childColliders;
void Awake()
{
_animator = GetComponent<Animator>();
_rigidbody = GetComponent<Rigidbody2D>();
_childColliders = GetComponentsInChildren<Collider2D>();
// 10+ more lines of GetComponent calls...
if (_animator == null) Debug.LogError("Missing Animator!");
if (_rigidbody == null) Debug.LogError("Missing Rigidbody2D!");
// More validation...
}
}
```
**After (With Integration):**
```csharp
public class Enemy : MonoBehaviour
{
[Inject] private IHealthSystem _healthSystem;
[SiblingComponent] private Animator _animator;
[SiblingComponent] private Rigidbody2D _rigidbody;
[ChildComponent] private Collider2D[] _childColliders;
// That's it! No Awake() needed - both DI and relational fields are auto-wired
// Automatic validation with helpful error messages included
}
```
**⏱️ Time Saved:** 10-20 lines of boilerplate per component × hundreds of components = **weeks** of development time.
**🧠 Mental Load Eliminated:** No more context-switching between DI patterns and Unity hierarchy patterns.
**🐛 Bugs Prevented:** Automatic validation catches missing references **before** they cause runtime errors.
---
## 🚀 Quick Setup (2 Minutes)
### Step 1: Register the Integration
In your `LifetimeScope`, enable the integration (the sample exposes the three toggles below in the inspector so you can experiment without touching code):
```csharp
using UnityEngine;
using VContainer;
using VContainer.Unity;
using WallstopStudios.UnityHelpers.Integrations.VContainer;
public sealed class GameLifetimeScope : LifetimeScope
{
[SerializeField] private bool _includeInactiveSceneObjects = true;
[SerializeField] private bool _useSinglePassScan = true;
[SerializeField] private bool _listenForAdditiveScenes = true;
protected override void Configure(IContainerBuilder builder)
{
// Your existing registrations...
builder.Register<PlayerController>(Lifetime.Singleton);
builder.Register<IHealthSystem, HealthSystem>(Lifetime.Scoped);
RelationalSceneAssignmentOptions options = new RelationalSceneAssignmentOptions(
_includeInactiveSceneObjects,
_useSinglePassScan
);
// ✨ Scene scan + optional additive-scene listener
builder.RegisterRelationalComponents(options, _listenForAdditiveScenes);
}
}
```
**That's it!** All scene components with relational attributes are now automatically wired after DI injection, and additively loaded scenes can opt-in to the same treatment.
> 💡 **Beginner tip:** Not sure what these options do? Leave them all enabled (the defaults). You can always tune them later.
>
> - `includeInactiveSceneObjects` → Wires disabled GameObjects too (usually what you want)
> - `useSinglePassScan` → Faster scanning (always leave this on)
> - `listenForAdditiveScenes` → Auto-wires newly loaded scenes (great for multi-scene setups)
### Step 2: Use With Runtime Instantiation
When spawning prefabs at runtime, use the helpers that combine instantiation, DI, and relational assignment:
```csharp
using UnityEngine;
using VContainer;
using WallstopStudios.UnityHelpers.Integrations.VContainer;
public sealed class EnemySpawner : MonoBehaviour
{
[Inject] private IObjectResolver _resolver;
[SerializeField] private Enemy _enemyPrefab;
[SerializeField] private GameObject _enemySquadPrefab;
public Enemy SpawnEnemy(Transform parent)
{
return _resolver.InstantiateComponentWithRelations(_enemyPrefab, parent);
}
public GameObject SpawnEnemySquad(Transform parent)
{
return _resolver.InstantiateGameObjectWithRelations(
_enemySquadPrefab,
parent,
includeInactiveChildren: true
);
}
public void HydrateExisting(GameObject root)
{
_resolver.AssignRelationalHierarchy(root, includeInactiveChildren: true);
}
}
```
---
## 📦 What's Included in This Sample
This sample provides a complete working example:
- **Scripts/GameLifetimeScope.cs** - LifetimeScope with inspector-driven options for include-inactive, scan strategy, and additive-scene listening
- **Scripts/Spawner.cs** - Demonstrates `InstantiateComponentWithRelations`, `InstantiateGameObjectWithRelations`, pooling helpers, and hydrating existing hierarchies
- **Scripts/RelationalConsumer.cs** - Component demonstrating relational attributes
- **Prefabs/RelationalConsumer.prefab** - Example prefab with relational fields
- **Prefabs/Spawner.prefab** - Spawner prefab wired to the helper methods above
- **Scenes/VContainer_Sample.unity** - Complete working scene ready to play
### How to Import This Sample
1. Open Unity Package Manager
2. Find **Unity Helpers** in the package list
3. Expand the **Samples** section
4. Click **Import** next to "DI - VContainer"
5. Open `Scenes/VContainer_Sample.unity` and press Play
---
## 🎯 Common Use Cases (By Experience Level)
### 🟢 Beginner: "I just want my components to work"
**Perfect for:** Player controllers, enemy AI, simple gameplay scripts
**What you get:** No more `GetComponent` calls, no more null reference exceptions from missing components
**Example:**
```csharp
public class PlayerController : MonoBehaviour
{
// Injected dependencies
[Inject] private IInputService _input;
[Inject] private IAudioService _audio;
// Hierarchy references (auto-wired)
[SiblingComponent] private Animator _animator;
[SiblingComponent] private Rigidbody2D _rigidbody;
[ChildComponent(TagFilter = "Weapon")] private Weapon _weapon;
// Everything wired automatically - no Awake() needed!
void Update()
{
Vector2 input = _input.GetMovementInput();
_rigidbody.velocity = input * moveSpeed;
_animator.SetFloat("Speed", input.magnitude);
}
}
```
### 🟡 Intermediate: "I'm spawning objects at runtime"
**Perfect for:** Enemy spawners, projectile systems, object pooling
**What you get:** One-line instantiation that handles DI injection + hierarchy wiring automatically
**Example:**
```csharp
public sealed class ProjectileSpawner : MonoBehaviour
{
[Inject] private IObjectResolver _resolver;
[SerializeField] private Projectile _projectilePrefab;
public Projectile Fire(Vector3 position, Vector3 forward)
{
Projectile projectile = _resolver.InstantiateComponentWithRelations(_projectilePrefab);
projectile.transform.SetPositionAndRotation(position, Quaternion.LookRotation(forward));
projectile.Launch(forward);
return projectile;
}
}
```
### 🔴 Advanced: "I have complex hierarchies and custom workflows"
**Perfect for:** UI systems, vehicles with multiple parts, procedural generation, custom object pools
**What you get:** Full control over when and how wiring happens, with helpers for every scenario
**Example:**
```csharp
public sealed class VehicleFactory : MonoBehaviour
{
[Inject] private IObjectResolver _resolver;
[SerializeField] private GameObject _vehiclePrefab;
public GameObject CreateVehicle(Transform parent)
{
return _resolver.InstantiateGameObjectWithRelations(
_vehiclePrefab,
parent,
includeInactiveChildren: true
);
}
}
```
---
## 💡 Real-World Impact: A Day in the Life
### Without This Integration
**Morning:** You start work on a new enemy type.
```csharp
public class FlyingEnemy : MonoBehaviour
{
[Inject] private IHealthSystem _health;
[Inject] private IAudioService _audio;
private Animator _animator;
private Rigidbody2D _rigidbody;
private SpriteRenderer _sprite;
private Collider2D[] _hitboxes;
private Transform _weaponMount;
void Awake()
{
_animator = GetComponent<Animator>();
if (_animator == null) Debug.LogError("Missing Animator on FlyingEnemy!");
_rigidbody = GetComponent<Rigidbody2D>();
if (_rigidbody == null) Debug.LogError("Missing Rigidbody2D on FlyingEnemy!");
_sprite = GetComponent<SpriteRenderer>();
if (_sprite == null) Debug.LogError("Missing SpriteRenderer on FlyingEnemy!");
_hitboxes = GetComponentsInChildren<Collider2D>();
if (_hitboxes.Length == 0) Debug.LogWarning("No hitboxes found on FlyingEnemy!");
_weaponMount = transform.Find("WeaponMount");
if (_weaponMount == null) Debug.LogError("Missing WeaponMount on FlyingEnemy!");
// Finally, actual game logic can start...
}
}
```
**10 minutes later:** You've written 20+ lines of boilerplate before writing any actual game logic.
**30 minutes later:** Null reference exception in the build! You forgot to add the SpriteRenderer to the prefab.
**60 minutes later:** You're manually wiring up the 8th enemy variant of the day...
### With This Integration
**Morning:** You start work on a new enemy type.
```csharp
public class FlyingEnemy : MonoBehaviour
{
[Inject] private IHealthSystem _health;
[Inject] private IAudioService _audio;
[SiblingComponent] private Animator _animator;
[SiblingComponent] private Rigidbody2D _rigidbody;
[SiblingComponent] private SpriteRenderer _sprite;
[ChildComponent] private Collider2D[] _hitboxes;
[ChildComponent(NameFilter = "WeaponMount")] private Transform _weaponMount;
// Start writing game logic immediately
void Start() => _animator.Play("Idle");
}
```
**2 minutes later:** You're done with wiring and writing game logic.
**10 minutes later:** You've shipped 5 enemy variants with zero boilerplate.
**Never:** You never see "Missing component" runtime errors because validation happens automatically with helpful messages.
---
## 🔧 Advanced Configuration
### Exclude Inactive GameObjects from Scene Scanning
By default, inactive GameObjects are included in the initial scene scan. To scan only active objects:
```csharp
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterRelationalComponents(
new RelationalSceneAssignmentOptions(includeInactive: false),
enableAdditiveSceneListener: false
);
}
```
### Manual Wiring Helpers
If you need to hydrate instances that were created outside of the resolver:
```csharp
[Inject] private IObjectResolver _resolver;
void WireComponentOnly(MonoBehaviour component)
{
// Only assigns relational component fields, skips DI injection
_resolver.AssignRelationalComponents(component);
}
void WireHierarchy(GameObject root)
{
_resolver.AssignRelationalHierarchy(root, includeInactiveChildren: true);
}
```
### Performance: Prewarming Reflection Caches
For large projects, prewarm reflection caches during loading to avoid first-use stalls:
```csharp
using WallstopStudios.UnityHelpers.Core.Attributes;
void Start()
{
// Call once during bootstrap/loading screen
RelationalComponentInitializer.Initialize();
}
```
Or enable auto-prewarm on the `AttributeMetadataCache` asset:
1. Find the asset: `Assets > Create > Wallstop Studios > Unity Helpers > Attribute Metadata Cache`
2. Enable **"Prewarm Relational On Load"** in the Inspector
---
## 🧰 Additional Helpers & Recipes
### One-liners for DI + Relational Wiring
```csharp
// Inject + assign a single component (generic method)
resolver.InjectWithRelations<Enemy>(enemy);
// Build up an existing instance + assign relational fields
Enemy enemy = resolver.BuildUpWithRelations<Enemy>(existingEnemy);
// Instantiate a component prefab + inject + assign
Enemy comp = resolver.InstantiateComponentWithRelations(prefabComp, parent);
// Inject + assign a whole hierarchy
resolver.InjectGameObjectWithRelations(root, includeInactiveChildren: true);
// Instantiate a GameObject prefab + inject + assign hierarchy
GameObject go = resolver.InstantiateGameObjectWithRelations(prefabGo, parent);
```
### Additive Scenes & Options
The registration can enable an additive-scene listener that hydrates relational fields in newly loaded scenes, and you can customize scan behavior:
```csharp
protected override void Configure(IContainerBuilder builder)
{
var options = new RelationalSceneAssignmentOptions(
includeInactive: true,
useSinglePassScan: true
);
builder.RegisterRelationalComponents(options, enableAdditiveSceneListener: true);
}
```
### Pools
Use DI-aware pools to auto-inject and assign on rent:
```csharp
// Component pool
var pool = RelationalObjectPools.CreatePoolWithRelations(
createFunc: () => Instantiate(componentPrefab)
);
var item = pool.GetWithRelations(resolver);
// GameObject pool
var goPool = RelationalObjectPools.CreateGameObjectPoolWithRelations(prefab);
var instance = goPool.GetWithRelations(resolver);
```
---
## ❓ Troubleshooting
### My relational fields are null even with the integration
**Check these common issues:**
1. **Did you register the integration?**
- Ensure `builder.RegisterRelationalComponents()` is called in your `LifetimeScope.Configure()`
2. **Are you using the right attributes?**
- Fields need `[SiblingComponent]`, `[ParentComponent]`, or `[ChildComponent]` attributes
- These are different from `[Inject]` - you can use both on the same component
3. **Runtime instantiation not working?**
- Use `_resolver.InstantiateComponentWithRelations(...)`, `_resolver.InstantiateGameObjectWithRelations(...)`, or `_resolver.AssignRelationalHierarchy(...)`
- Regular `Instantiate()` on its own won't trigger relational wiring
4. **Check your filters:**
- `TagFilter` must match an existing Unity tag exactly
- `NameFilter` is case-sensitive
### Do I need to call AssignRelationalComponents() in Awake()?
**No!** The integration handles this automatically:
- **Scene objects:** Wired during scene initialization (after container builds)
- **Runtime objects:** Wired when you call any of the helper methods (`InstantiateComponentWithRelations`, `InstantiateGameObjectWithRelations`, `AssignRelationalHierarchy`, or the pooling `GetWithRelations` helpers)
Only call `AssignRelationalComponents()` manually if you're not using the DI integration.
### Does this work without VContainer?
**Yes!** The integration gracefully falls back to standard Unity Helpers behavior if VContainer isn't detected. You can:
- Adopt incrementally without breaking existing code
- Use in projects that mix DI and non-DI components
- Remove VContainer later without refactoring all your components
### Performance impact?
**Minimal:** Relational component assignment happens once per component at initialization time. After that, there's zero runtime overhead - the references are just regular fields.
**Optimization tips:**
- Use `MaxDepth` to limit hierarchy traversal
- Use `TagFilter` or `NameFilter` to narrow searches
- Use `OnlyDescendants`/`OnlyAncestors` to exclude self when appropriate
---
## 📚 Learn More
**Unity Helpers Documentation:**
- [Relational Components Guide](../../docs/features/relational-components/relational-components.md) - Complete attribute reference and recipes
- [Getting Started](../../docs/overview/getting-started.md) - Unity Helpers quick start guide
- [Main README](../../README.md) - Full feature overview
**VContainer Documentation:**
- [VContainer Official Docs](https://vcontainer.hadashikick.jp/) - Complete VContainer guide
- [VContainer GitHub](https://github.com/hadashiA/VContainer) - Source code and examples
**Troubleshooting:**
- [Relational Components Troubleshooting](../../docs/features/relational-components/relational-components.md#troubleshooting) - Detailed solutions
- [DI Integration Testing Guide](../../docs/features/relational-components/relational-components.md#di-integrations-testing-and-edge-cases) - Advanced scenarios
---
## 🎓 Next Steps
1. **Try the sample scene:** Open `VContainer_Sample.unity` and press Play
2. **Read the scripts:** See how `GameLifetimeScope` and `Spawner` work
3. **Add to your project:** Copy the pattern to your own LifetimeScope
4. **Explore attributes:** Check out the [Relational Components Guide](../../docs/features/relational-components/relational-components.md) for all options
---
## Made with ❤️ by Wallstop Studios
*Unity Helpers is production-ready and actively maintained. [Star the repo](https://github.com/wallstop/unity-helpers) if you find it useful!*