How to do simple injection of multiple items, scoped to gameObject?
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:
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)
any assistance is welcomed.
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.
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...
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 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/Itembroadcast changes filtered bypuzzleId, though it's not ideal cause everyPuzzleItemevent is broadcasted to the entire system and needs to be filtered by the correct manager/item. Maybe it can be optimized by injecting the thepub-subitself instead ofpuzzleIdin thePuzzleLifetimeScopeto 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, 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();
}
}
@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