UNPKG

com.wallstop-studios.unity-helpers

Version:

Treasure chest of Unity developer tools

520 lines (368 loc) 17.6 kB
# 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!*