VContainer icon indicating copy to clipboard operation
VContainer copied to clipboard

How to do simple injection of multiple items, scoped to gameObject?

Open orijel opened this issue 6 months ago • 6 comments

Hi, I'm fairly new to VContainer and i'm struggling with injection of gameobjects. I have multiple objects in scene that are not added at runtime (placed manually). each of these objects have a Puzzle class that should have all IPuzzleItem registered to it as an IEnumerable<IPuzzleItem> and each IPuzzleItem also needs to have a reference to the Puzzle class.

the classes should look something like this:

public class Puzzle : MonoBehaviour
{
    private IEnumerable<IPuzzleItem> _puzzleItems;

    [Inject]
    private void Construct(IEnumerable<IPuzzleItem> puzzleItems)
    {
        _puzzleItems = puzzleItems;
        Debug.Log("items: " + string.Join(", ", _puzzleItems.Select(item => item.Name)));
    }
}

public interface IPuzzleItem
{
    PuzzleItemId Id { get; }
    string Name { get; }
    Puzzle Puzzle { get; }
}

public abstract class PuzzleItemBase : MonoBehaviour, IPuzzleItem
{
  [SerializeField] private PuzzleItemId id;

  [Inject]
  private void Construct(Puzzle puzzle)
  {
      Puzzle = puzzle;
      Debug.Log(Puzzle.name);
  }
  
  public PuzzleItemId Id => id;
  public string Name => gameObject.name;
  public Puzzle Puzzle { get; private set; }
}

public class TogglablePuzzleItem : PuzzleItemBase 
{
    private void Start()
    {
        Debug.Log(Puzzle.name);
    }
}

scene structure looks like this: Image

Image

I tried to create a PuzzleLifetimeScope to have my initializations scoped per objects and make it simpler, but i can't seem to get it working:

public class PuzzleLifetimeScope : LifetimeScope
{
  [SerializeField] private Puzzle puzzle;
  
  protected override void Configure(IContainerBuilder builder)
  {
      builder.RegisterInstance(puzzle);
      var componentsInChildren = puzzle.GetComponentsInChildren<IPuzzleItem>();
      foreach (var puzzleItem in componentsInChildren)
      {
          // the Puzzle class is still null
          builder.RegisterInstance(puzzleItem);
      }
      // builder.RegisterComponentInHierarchy<InteractiblePuzzleItemBase>().As<IPuzzleItem>(); // works only for the first time
  }

The error:

VContainerException: Conflict implementation type : Registration TogglablePuzzleItem ContractTypes=[Code.Scripts.PuzzleItems.IPuzzleItem] Singleton VContainer.Internal.ExistingInstanceProvider
VContainer.Internal.CollectionInstanceProvider.Add (VContainer.Registration registration) (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/Internal/InstanceProviders/CollectionInstanceProvider.cs:61)
VContainer.Internal.Registry.AddToBuildBuffer (System.Collections.Generic.IDictionary`2[TKey,TValue] buf, System.Type service, VContainer.Registration registration) (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/Registry.cs:70)
VContainer.Internal.Registry.Build (VContainer.Registration[] registrations) (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/Registry.cs:28)
VContainer.ContainerBuilder.BuildRegistry () (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/ContainerBuilder.cs:166)
VContainer.ScopedContainerBuilder.BuildScope () (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/ContainerBuilder.cs:38)
VContainer.ScopedContainer.CreateScope (System.Action`1[T] installation) (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/Container.cs:126)
VContainer.Container.CreateScope (System.Action`1[T] installation) (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/Container.cs:263)
VContainer.Unity.LifetimeScope.Build () (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/Unity/LifetimeScope.cs:199)
VContainer.Unity.LifetimeScope.Awake () (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/Unity/LifetimeScope.cs:145)
VContainer.Unity.LifetimeScope.OnSceneLoaded (UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode) (at ./Library/PackageCache/jp.hadashikick.vcontainer@94276e73bc7c/Runtime/Unity/LifetimeScope.AwakeScheduler.cs:88)
UnityEngine.SceneManagement.SceneManager.Internal_SceneLoaded (UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode) (at <0022d4fb3cd44d45a62e51c39f257e7c>:0)

Image

any assistance is welcomed.

orijel avatar Jun 28 '25 15:06 orijel

You can probably use the new Keyed implementation, as long as you have a unique identifier to supply. For example:

// Assuming puzzleItem.Name is unique
builder.RegisterInstance(puzzleItem).Keyed(puzzleItem.Name);

But I think the real issue runs deeper. It looks like you’re introducing circular dependencies - your PuzzleItem knows about the Puzzle, and vice versa. That’s generally a bad idea, and it might even break registration altogether.

I’d recommend rethinking your architecture and keeping the direction one-way. Let the puzzle manager handle the puzzle items, or use something neutral like an event aggregator or pub-sub. Personally, I’d go with the first option.

migus88 avatar Jul 06 '25 17:07 migus88

Maybe in DI it can cause issues, but in a real life game it makes sense to do a two way SerializeField relationship. In my case i want each puzzle item to tell the PuzzleManager whether if it's in a solved state or not on change and that each PuzzleManager will unlock all children when the puzzle becomes available to the player which requires some sort of a two way relationship. I don't see the Keyed method, so I guess this is available in a recent/nightly version? For now I decided to go with the pub-sub solution using this scope:

public class PuzzleLifetimeScope : LifetimeScope
{
    [SerializeField] private Puzzle puzzle;
    
    protected override void Configure(IContainerBuilder builder)
    {
        var puzzleName = puzzle.name;
        if (!Enum.TryParse(typeof(PuzzleId), puzzleName, false, out var parseId))
        {
            Debug.LogError(
                $"GameObject '{puzzleName}' does not have a valid PuzzleId. Ensure the name matches one of the PuzzleId enum values. PuzzleId enum values are used to register puzzles in the puzzle system.");
            return;
        }

        var puzzleId = (PuzzleId)parseId;
        builder.RegisterInstance(puzzleId);
    }
}

And via MessagePipe framework in GameLifetimeScope:

private void RegisterMessagePipe(IContainerBuilder builder)
    {
        var options = builder.RegisterMessagePipe(/* configure option */);   
        builder.RegisterBuildCallback(c => GlobalMessagePipe.SetProvider(c.AsServiceProvider()));
        builder.RegisterMessageBroker<PuzzleItemStateChangedEvent>(options);
        builder.RegisterMessageBroker<PuzzleUnlocked>(options);
    }

and have each PuzzleManager/Item broadcast changes filtered by puzzleId, though it's not ideal cause every PuzzleItem event is broadcasted to the entire system and needs to be filtered by the correct manager/item. Maybe it can be optimized by injecting the the pub-sub itself instead of puzzleId in the PuzzleLifetimeScope to scope it, but i'm not sure if that's the recommended thing to do or not...

orijel avatar Jul 10 '25 21:07 orijel

Yep, same problem with much easier sample.

public abstract class UiElement : MonoBehaviour, IInitializable
{
    public virtual void Initialize()
    {
    }

    public virtual void Foo()
    {
    }

    public virtual void Bar()
    {
    }
}

public class UiPanel : UiElement
{
}

public class UiButton : UiElement
{
}

How to bind all UiElement's?

Zenject has very simple solution

container
    .BindInterfacesAndSelfTo<UiElement>()
    .FromMethodMultiple(ctx => ctx.Container.Resolve<Canvas>().GetComponentsInChildren<UiElement>(true));

But VContainer only binds first

if (scene.IsValid())
{
    var gameObjectBuffer = UnityEngineObjectListBuffer<GameObject>.Get();
    scene.GetRootGameObjects(gameObjectBuffer);
    foreach (var gameObject in gameObjectBuffer)
    {
        component = gameObject.GetComponentInChildren(componentType, true);
        if (component != null) break;
    }
    if (component == null)
    {
        throw new VContainerException(componentType, $"{componentType} is not in this scene {scene.path} : {this}");
    }
}

Quawetim avatar Jul 15 '25 14:07 Quawetim

@Quawetim you can use keyed bindings feature for this one. this is how i use it in my projects:

[SerializeField]
private UIDocument firstDocument;

[SerializeField]
private UIDocument secondDocument;

builder.RegisterInstance<UIDocument>(firstDocument).Keyed("first");
builder.RegisterInstance<UIDocument>(secondDocument).Keyed("second");

[Inject]
[Key("first")]
private readonly UIDocument _first;

[Inject]
[Key("second")]
private readonly UIDocument _second;

and have each PuzzleManager/Item broadcast changes filtered by puzzleId, though it's not ideal cause every PuzzleItem event is broadcasted to the entire system and needs to be filtered by the correct manager/item. Maybe it can be optimized by injecting the the pub-sub itself instead of puzzleId in the PuzzleLifetimeScope to scope it, but i'm not sure if that's the recommended thing to do or not...

@orijel you can use the EventChannel pattern for this type of implementation, whereby events only pass through the relevant channels. this is how i use it in my projects:

public class EventChannels {
    [Inject]
    public TowerTargetEventChannel TowerTarget { get; set; }

    // ...
}

public class EventScope : LifetimeScope {
    [SerializeField]
    private TowerTargetEventChannel towerTarget;

    // ...

    protected override void Configure(IContainerBuilder builder) {
        builder.RegisterInstance(towerTarget);
        // ...
        builder.Register<EventChannels>(Lifetime.Singleton);
    }
}

public class ComponentTowerTargetFinder : MonoBehaviour, ICanTarget {
    [Inject]
    private readonly EventChannels _channels;

    // ...

    public void Target(IEntityEnemy newTarget) {
        // ...
        _channels.TowerTarget.Fire(new TowerTargetEvent(_entity, newTarget));
        // ...
    }
}

portlek avatar Jul 18 '25 10:07 portlek

@portlek, yep, i thought about it. I don't like this method because i need to write code every time i add new UiElement to Canvas. New UiElement => new code.

Solution for now: custom class who finds all MonoBehaviour with IInitializable interface and invokes it.

public interface IBicycleSlave { }

public sealed class Bicycle: IInitializable
{
    public void Initialize()
    {
        Object
            .FindObjectsByType<MonoBehaviour>(FindObjectsInactive.Include, FindObjectsSortMode.None)
            .OfType<IBicycleSlave>()
            .OfType<IInitializable>()
            .ForEach(x => x.Initialize());
    }

    public static void Install(IContainerBuilder builder)
    {
        builder.Register<Bicycle>(Lifetime.Singleton).AsSelf().AsImplementedInterfaces();
    }
}

Quawetim avatar Jul 18 '25 13:07 Quawetim

@portlek so your solution is indeed "injecting the the pub-sub itself". Since i'm using MessagePipe framework my only concern is possible performance impact, but can't know for sure till I try. Regardless of that I think there's a strong case for "find all descendants/siblings/ancestors of type and inject as IEnumerable" to be implemented into framework

orijel avatar Jul 18 '25 13:07 orijel