VContainer is an DI (Dependency Injection) library running on Unity (Game Engine).
"V" means making Unity's initial "U" more thinner and solid ... !
- Fast resolve. Minimum GC allocation.
- Extra small code size. Few internal types.
- Application can freely create nested Lifetime Scope.
- IoC : Create script entry point without MonoBehaviour. (Using own Unity's PlayerLoopSystem)
- ECS integration.
- Immutable Container.
Basically, VContainer is 5-10x faster than Zenject.
- By default, both VContainer and Zenject use reflection at runtime.
- "VContainer (CodeGen)" means optimization feature by pre-generation IL code of Inject methods by ILPostProcessor.
- See Optimization section more information.
- Zenject also has a pre-code generation feature called "Reflection Baking", but it was excluded in this benchmark. (Because it didn't succeed in working in the same environment..)
And in resolve, We have zero allocation (without resolved instances):
- From 0.0.x
- Remove MonoInstaller/ScriptableObjectInstaller and instead inherit LifetimeScope
- If you are using an earlier version, please check Getting Started again.
- From 0.2.x
- Use
VContainerSettings
instead of automatically loading Resources - If you were using "ProjectLifetimeScope" in Resources, please check How to create project root LifetimeScope
- Use
- What is DI ?
- Installation
- Getting Started
- Resolving
- Registering
- Controlling Scope and Lifetime
- Dispatching Unity Lifecycle
- Integrating with ECS
- Optimization
- Best Practices and Recommendations
DI (Dependency Injection) is a general technique in OOP that all about removing unconcerned dependencies from your code. It brings testability, maintainability, extensibility or any kind of exchangeability to your object graph.
In all programming paradigms, the basic design is, weak module coupling and strong module cohesion. As you know, OOP(Object Oriented Programming) does it through objects.
- Objects hides the details of its responsibility (encapsulation).
- Objects transfer work outside their responsibilities to other objects
Actually, There is a problem in doing this fundamentally. If you write the delegation object in the class code, it means tight coupling at the source code level. The only way to exclude a unconcerned dependency from a class is to pass from outside.
Then, if your class receives dependencies externally, need help from outside. DI is a technique that facilitates a place to resolve dependencies completely outside.
Terminology:
- DI Container: Have dependent references all at once and execute auto-wiring.
- Composition Root: The place where you make settings to resolve dependencies.
- Auto-wiring: It is a function that allows you to manage services in the container with minimal configuration. DI library usually does this.
- IoC (Inversion of Control): Make the object with control flow responsibility an entry point. In simple and traditional programming, the entry point is where the responsibility for interrupting user input is. In Unity, the entry point is
MonoBehaviour
, but in order to separate presentation and domain logic, it is effective to create an entry point that does not depend onMonoBehaviour
. VContainer supports this with its own PlayerLoop. See Dispatching Unity Lifecycle section.
Note:
- Don't try to DI what an object should hide inside. What's hidden is leaking and the design is getting worse.
- The word DI does not necessarily mean magic or library. Creating your own Composition Root without using the library is called
Poor man's DI
orDIY DI
. This is not an anti-pattern.
Further reading:
- Unity Package Manager (upm)
Window -> Package Manager -> +▼ -> Add package from git url
https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer
- Or add this to your
Package/manifest.json
"jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer"
- unitypackage
- The releases page provides downloadable .unitypackage files.
- To use the unitypackage, the following entry is required in your
Package/manifest.json
."nuget.mono-cecil": "0.1.6-preview"
The basic way for integrating VContainer into your application is:
- Create a component that inherits
LifetimeScope
in your scene. It has one container and one scope. - Register dependencies with C# code in a subclass of LifetimeScope. This is the composition root.
- When playing scene, LifetimeScope automatically build Container and dispatch to the own PlayerLoopSystem.
Note:
- Normally, "scope" is repeatedly created and destroyed during the game.
LifetimeScope
assumes this and has a parent-child relationship.
1. Write a class that depends on other classes
Let's say Hello world.
namespace MyGame
{
public class HelloWorldService
{
public void Hello()
{
UnityEngine.Debug.Log("Hello world");
}
}
}
2. Define composition root
Next, let's write a setting that can auto-wiring the class.
- Right click in a folder within the Project Tab and Choose Create -> C# Script.
- Name it
GameLifetimeScope.cs
.
Note:
- VContainer will automatically template C# scripts ending in
*LifetimeScope
.
You instruct builder
and register the class above.
using VContainer;
using VContainer.Unity;
namespace MyGame
{
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
+ builder.Register<HelloWorldService>(Lifetime.Singleton);
}
}
}
Note:
- VContainer always required a
Lifetime
argument explicitly. This gives us transparency and consistency.
3. Create GameObject attached your LifetimeScope
Right Click inside the Hierarchy tab and select Create Empty. And name GameLifetimeScope
it.
Then attach the Component created above.
4. How to use your new HelloWorldService ?
Registered objects will automatically have dependency injection. Like below:
using VContainer;
using VContainer.Unity;
namespace MyGame
{
public class GamePresenter
{
readonly HelloWorldService helloWorldService;
public GamePresenter(HelloWorldService helloWorldService)
{
this.helloWorldService = helloWorldService;
}
}
}
And let's also register this class.
builder.Register<HelloWorldService>(Lifetime.Singleton);
+ builder.Register<GamePresenter>(Lifetime.Singleton);
5. Execute your registerd object on PlayerLoopSystem
To write an application in Unity, we have to interrupt Unity's lifecycle events. (Typically MonoBehaviour's Start / Update / OnDestroy / etc..)
Objects registered with VContainer can do this independently of MonoBehaviour. This is done automatically by implementing and registering some marker interfaces.
using VContainer;
using VContainer.Unity;
namespace MyGame
{
- public class GamePresenter
+ public class GamePresenter : ITickable
{
readonly HelloWorldService helloWorldService;
public GamePresenter(HelloWorldService helloWorldService)
{
this.helloWorldService = helloWorldService;
}
+ void ITickable.Tick()
+ {
+ helloWorldService.Hello();
+ }
}
}
Now, Tick()
will be executed at the timing of Unity's Update.
As such, it's a good practice to keep any side effect entry points through the marker interface.
( By design, for MonoBehaviour is enough to use Start / Update etc. The marker interface of VContainer is a function to separate the entry point of domain logic and presentation logic. )
We should register this as running on Unity's life cycle events.
- builder.Register<GamePresenter>(Lifetime.Singleton);
+ builder.RegisterEntryPoint<GamePresenter>(Lifetime.Singleton)
Note:
RegisterEntryPoint<GamePresenter>(Lifetime.Singleton)
is an alias to register interfaces related to Unity's PlayerLoop event. ( Simular toRegister<GamePresenter>(Lifetime.Singleton).As<ITickable>()
)- Registering lifecycle events without relying on MonoBehaviour facilitates decupling of domain logic and presentation !
If you have multiple EntryPoints, you can also use the following declaration as grouping.
builder.UseEntryPoints(Lifetime.Singleton, entryPoints =>
{
entryPoints.Add<GamePresenter>();
// entryPoints.Add<OtherSingletonEntryPointA>();
// entryPoints.Add<OtherSingletonEntryPointB>();
// entryPoints.Add<OtherSingletonEntryPointC>();
})
This makes it clearer that EntryPoints are given special treatment by design.
VContainer automatically collects and calls registered class constructors.
Note:
- At this time, all the parameters of the constructor must be registered.
- If the dependency cannot be resolved, throws exception when validating LifetimeScope or building Container.
Here is basic idiom with DI.
class ClassA
{
readonly IServiceA serviceA;
readonly IServiceB serviceB;
readonly SomeUnityComponent component;
public ClassA(
IServiceA serviceA,
IServiceB serviceB,
SomeUnityComponent component)
{
this.serviceA = serviceA;
this.serviceB = serviceB;
this.component = component;
}
}
[Inject]
Attribute explicitly.
[Inject]
public ClassA(
IServiceA serviceA,
IServiceB serviceB,
SomeUnityComponent component)
{
// ...
}
Note:
- If class has multiple constructors, the one with
[Inject]
has priority.
Recommendation: Use Constructor Injection whenever possible. The constructor & readonly field idiom is:
- The instantiated object has a compiler-level guarantee that the dependencies have been resolved.
- No magic in the class code. Instantiate easily without DI container. (e.g. Unit testing)
- If you look at the constructor, the dependency is clear.
- If too many constructor arguments, it can be considered overly responsible.
If constructor injection is not available, use method injection.
Typically this is for MonoBehaviour.
public class SomeBehaviour : MonoBehaviour
{
float speed;
[Inject]
public void Construct(GameSettings settings)
{
speed = settings.speed;
}
}
Recommendation: Consider whether injection to MonoBehaviour is necessary. In a code base where domain logic and presentation are well decoupled, MonoBehaviour should act as a View component.
In my opinion, View components should only be responsible for rendering and should be flexible.
Of course, In order for the View component to work, it needs to pass state at runtime. But the "state" of an object and its dependency of functionality of other objects are different.
It's enough to pass the state as arguments instead of [Inject]
.
If the object has a local default and Inject is optional, Property/Field Injection can be used.
class ClassA
{
[Inject]
IServiceA serviceA { get; set; } // Will be overwritten if something is registered.
public ClassA()
{
serviceA = ServiceA.GoodLocalDefault;
}
}
You can also use field.
[Inject]
IServiceA serviceA;
VContainer supports automatically resolving particular types implicitly to support special relationships.
Duplicate registered interfaces can be resolved together with IEnumerable or IReadOnlyList.
builder.Register<IDisposable, A>(Lifetime.Scoped);
builder.Register<IDisposable, B>(Lifetime.Scoped);
class ClassA
{
public ClassA(IEnumerable<IDisposable> disposables) { /* ... */ }
}
OR
class ClassA
{
public ClassA(IReadOnlyList<IDisposable> disposables) { /* ... */ }
}
Note:
- This is mainly used by internal functions such as
ITickable
marker etc.
- Singleton : Single instance per container (includes all parents and children).
- Transient : Instance per resolving.
- Scoped : Instance per
LifetimeScope
.- If LifetimeScope is single, similar to Singleton.
- If you create a LifetimeScope child, the instance will be different for each child.
- When LifetimeScope is destroyed, release references and calls all the registered
IDisposable
.
See more information: Controlling Object Lifetime
There are various ways to use Register. Let's take the following complex type as an example.
class ServiceA : IServiceA, IInputPort, IDisposable { /* ... */ }
builder.Register<ServiceA>(Lifetime.Singleton);
It can resolve like this:
class ClassA
{
public ClassA(IServiceA serviceA) { /* ... */ }
}
builder.Register<IServiceA, ServiceA>();
It can resolve like this:
class ClassA
{
public ClassA(IServiceA serviceA) { /* ... */ }
}
builder.Register<ServiceA>(Lifetime.Singleton)
.As<IServiceA, IInputPort>();
It can resolve like this:
class ClassA
{
public ClassA(IServiceA serviceA) { /* ... */ }
}
class ClassB
{
public ClassB(IInputPort handlerA) { /* ... */ }
}
builder.Register<ServiceA>(Lifetime.Singleton)
.AsImplementedInterfaces();
It can resolve like this:
class ClassA
{
public ClassA(IServiceA serviceA) { /* ... */ }
}
class ClassB
{
public ClassB(IHandlerB handlerA) { /* ... */ }
}
builder.Register<ServiceA>(Lifetime.Singleton)
.AsImplementedInterfaces()
.AsSelf();
It can resolve like this:
class ClassA
{
public ClassA(IServiceA serviceA) { /* ... */ }
}
class ClassB
{
public ClassB(IHandlerB handlerA) { /* ... */ }
}
class ClassB
{
public ClassB(ServiceA serviceA) { /* ... */ }
}
class GameController : IInitializable, ITickable, IDisposable { /* ... */ }
builder.RegisterEntryPoint<GameController>(Lifetime.Singleton);
Note:
- this is similar to
Register<GameController>(Lifetime.Singleton).AsImplementedInterfaces()
If you have multiple EntryPoints, you have the option to use the following declaration as grouping.
builder.UseEntryPoints(Lifetime.Scoped, entryPoints =>
{
entryPoints.Add<ScopedEntryPointA>();
entryPoints.Add<ScopedEntryPointB>();
entryPoints.Add<ScopedEntryPointC>().AsSelf();
});
This is the same as:
builder.RegisterEntryPoint<ScopedEntryPointA>(Lifetime.Scoped);
builder.RegisterEntryPoint<ScopedEntryPointB>(Lifetime.Scoped);
builder.RegisterEntryPoint<ScopedEntryPointC>(Lifetime.Scoped).AsSelf();
// ...
var obj = new ServiceA();
// ...
builder.RegisterInstance(obj);
Note:
RegisterIntance
alwaysScoped
lifetime. So it has no arguments.
It can resolve like this:
class ClassA
{
public ClassA(ServiceA serviceA) { /* ... */ }
}
Allow As*
decrelations.
builder.RegisterInstance<IInputPort>(serviceA);
builder.RegisterInstance(serviceA)
.As<IServiceA, IInputPort>();
builder.RegisterInstance()
.AsImplementedInterfaces();
If the types are not unique, but you have a dependency you want to inject at startup, you can use below:
builder.Register<SomeService>(lifetime.Singleton)
.WithParameter<string>("http://example.com");
Or, You can parameter name as a key.
builder.Register<SomeService>(Lifetime.Singleton)
.WithParameter("url", "http://example.com");
It can resolve like this:
class SomeService
{
public SomeService(string url) { /* ... */ }
}
This Register is with only SomeService
.
class OtherClass
{
// ! Error
public OtherClass(string hogehoge) { /* ... */ }
}
Register from LifetimeScope's [SerializeField]
[SerializeField]
YourBehaviour yourBehaviour;
// ...
builder.RegisterComponent(yourBehaviour);
Note:
RegisterComponent
simular toRegisterInstance
. The only difference is that MonoBehaviour registered withRegisterComponent
will be injected even if not Resolved.
Register from scene with LifetimeScope
builder.RegisterComponentInHierarchy<YourBehaviour>();
Note:
RegisterComponentInHierarchy
always.Scoped
lifetime. Because lifetime is equal to the scene.
Register component that Instantiate from prefab when resolving
[SerializeField]
YourBehaviour prefab;
// ...
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped);
Register component that with new GameObject when resolving
builder.RegisterComponentOnNewGameObject<YourBehaciour>(Lifetime.Scoped, "NewGameObjectName");
Register component as interface
builder.RegisterComponentInHierarchy<YourBehaviour>()
.AsImplementedInterfaces();
Register component to specific parent Transform
builder.RegisterComponentFromInNewPrefab<YourBehaviour>(Lifetime.Scoped)
.UnderTransform(parent);
Or find at runtime.
builder.RegisterComponentFromInNewPrefab<YourBehaviour>(Lifetime.Scoped)
.UnderTransform(() => {
// ...
return parent;
});
builder.UseComponents(components =>
{
components.AddInstance(yourBehaviour);
components.AddInHierarchy<YourBehaviour>();
components.AddFromNewPrefab(prefab, Lifetime.Scoped);
components.AddOnNewGameObject<YourBehaviour>(Lifetime.Scoped, "name");
})
This is the same as:
builder.RegisterComponent(yourBehaviour);
builder.RegisterComponentInHierarchy<YourBehaviour>();
builder.RegisterComponentFromNewPrefab(prefab, Lifetime.Scoped);
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped, "name");
It is useful to register the setting information saved as ScriptableObject Asset as follows.
[Serializable]
public class CameraSettings
{
public float MoveSpeed = 10f;
public float DefaultDistance = 5f;
public float ZoomMax = 20f;
public float ZoomMin = 5f;
}
[Serializable]
public class ActorSettings
{
public float MoveSpeed = 0.5f;
public float FlyingTime = 2f;
public Vector3 FlyingInitialVelocity = Vector3.zero;
}
[CreateAssetMenu(fileName = "GameSettings", menuName = "MyGame/Settings")]
public class GameSettings : ScriptableObject
{
[SerializeField]
public FieldCameraSettings cameraSetting
[SerializeField]
public ActorSettings actorSettings;
}
And
- Create
GameSettings
assets from menu. - Register the created asset with LifetimeScope.
public class SomeLifetimeScope : LifetimeScope
{
[SerializeField]
GameSettings settings;
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterInstance(settings.cameraSettings);
builder.RegisterInstance(settings.actorSettings);
}
}
LifetimeScope
can build parent-child relationship.
it has following behaviours:
- If registered object is not found,
LifetimeScope
will look for a parentLifetimeScope
. - For
Lifetime.Singleton
- Always returns the same instance.
- Parent and child cannot register the same type.
- For
LifeTime.Transient
- Instance creating for each resolving.
- If parent and child have the same type, child will prioritize itself.
- For
Lifetime.Scoped
:- Instance will be different for each child.
- If same child, returns same instance.
- If parent and child have the same type, child will prioritize itself.
- When a
LifetimeScope
is destroyed, objects withIDisposable
implemented are calledDispose()
.
- Instance will be different for each child.
LifetimeScope
is destroyed, MonoBehaviour registered as Lifetime.Scoped
is not automatically destroyed.
If you want to destroy with LifetimeScope
, make it a child transform of LifetimeScope
or consider implement IDisposable.
You can parent it by specifying a LifetimeScope
object before loading the scene.
// Find LifetimeScope by type in all loaded scenes
var parent = LifetimeScope.Find<BaseLifetimeScope>();
// The LifetimeScope generated inside this block will have the specified parent
using (LifetimeScope.PushParent(parent))
{
// If this scene has a LifetimeScope, its parent will be `parent`.
var loading = SceneManager.LoadSceneAsync("...", LoadSceneMode.Additive);
while (!loading.isDone)
{
yield return null;
}
}
using (LifetimeScope.PushParent(parent))
{
// UniTask example
await SceneManager.LoadSceneAsync("...", LoadSceneMode.Additive);
}
LifetimeScope
can be serialized by specifying the type of parent in other scenes.
In base scene.
In additional scene.
Child can also be generated from code.
Can be used to get a reference to the LifetimeScope
if the key is set.
var lifetimeScope = LifetimeScope.Find<BaseLifetimeScope>();
And below is an example of creating a child.
// Create no extra registered child
var childLifetimeScope = lifetimeScope.CreateChild();
// Create a extra registered child
var childLifetimeScope = lifetimeScope.CreateChild(builder =>
{
builder.Register<ExtraClass>(Lifetime.Scoped);
builder.RegisterInstance(extraInstance);
// ...
});
// Create a extra registered child with IInstaller.
class ChildInstaller : IInstaller
{
public void Install(IContainerBuilder builder)
{
// ...
}
}
var childLifetimeScope = lifetimeScope.CreateChild(childInstaller);
// Create from LifetimeScope prefab
var childLifetimeScope = lifetimeScope.CreateChildFromPrefab(prefab);
// Allow extra registrations
var childLifetimeScope = lifetimeScope.CreateChildFromPrefab(prefab, builder =>
{
// Extra Registration
});
// Dispose child scope
UnityEngine.Object.Destroy(childLifetimeScope.gameObject);
Often, you may want to add additional Registers to the loaded scenes.
For example, when context is finalized after assets are loaded asynchronously.
In that case you could use:
using (LifetimeScope.Push(builder =>
{
// Register for the next scene not yet loaded
builder.RegisterInstance(extraInstance);
builder.Register<ExtraType>(Lifetime.Scoped);
}))
{
// Load the scene here.
var loading = SceneManager.LoadSceneAsync("NextScene");
while (!loading.isDone)
{
yield return null;
}
}
// Example with UniTask
using (LifetimeScope.Push(builder =>
{
// Register for the next scene not yet loaded
builder.RegisterInstance(extraInstance);
builder.Register<ExtraType>(Lifetime.Scoped);
}))
{
// Load the scene here
await SceneManager.LoadSceneAsync("NextScene");
}
// Use registration as type
class FooInstaller : IInstaller
{
public void Install(IContainerBuilder builder)
{
builder.Register<ExtraType>(Lifetime.Scoped);
}
}
using (LifetimeScope.Push(fooInstaller)
{
// ...
}
You can specify a root LifetimeScope that will be the parent of all LifetimeScopes.
-
- Create your root LifetimeScope prefab
-
- Create
VContainerSettings
- Choose
Assets -> Create -> VContaienr -> VContainer Settings
- Create
-
- From your VContainerSettings inspector, set your prefab to the Root Lifetime Scope section.
Note:
- If you create VContainerSettings from this menu, it will be automatically registered in preload assets.
VContainer has own PlayerLoop sub systems.
If you register a class that implements the marker interface, it will be scheduled in Unity's PlayerLoop cycle.
Since it uses PlayerLoopSystem, it works even if you register at any time (e.g: IInitilizable
etc)
The following interfaces and timings are available.
IInitializable
: NearlyStart
IPostInitializable
: AfterStart
IFixedTickable
: NearlyFixedUpdate
IPostFixedTickable
: AfterFixedUpdate
ITickable
: NearlyUpdate
IPostTickable
: AfterUpdate
ILateTickable
: NearlyLateUpdate
IPostLateTickabl
: AfterLateUpdate
And
IDisposable
: With container disposes. (ForLifetime.Scoped
)
Note:
VContainer supports integration between Unity's ECS (Entity Component System) and regular C# World.
( This is an experimental feature. Any feedback is wellcome! :0 )
ECS features for VContainer is enabled if the project has the com.unity.entities
package installed.
- Currently, ECS is a preview version. You may need the settings:
[Windows] -> [Package Manager]
and[Advanced] -> [Show preview packages]
. - Select package of
Entities
and Press[Install]
.
If the com.unity.entities
package exists, the VCONTAINER_ECS_INTEGRATION
symbol is defined and the following features are enabled.
By default, ECS will automatically instantiate classes that inherits the ComponentSystemBase
defined in your project, add it to the default world, and running on it.
In this mode, you can use method injection to ECS System
.
( The constructor is automatically used by Unity, so it cannot be used.)
class SystemA : SystemBase
{
[Inject]
public void Construct(Settings settings)
{
// ...
}
protected override void OnUpdate()
{
// ...
}
}
// Inject the `System` to Unity's default World
builder.RegisterSystemFromDefaultWorld<SystemA>();
// builder.RegisterSystemFromDefaultWorld<SystemB>();
// builder.RegisterSystemFromDefaultWorld<SystemC>();
// Other dependencies can be injected into the System.
builder.RegisterInstance(settings);
// ...
(Optional) The above can also be declared by grouping as below:
builder.UseDefaultWorld(systems =>
{
systems.Add<SystemA>();
// systems.Add<SystemB>();
// systems.Add<SystemC>();
// ...
});
Internaly, this is an automation of the following processes:
var system = World.DefaultGameObjectInjectionWorld.GetExistingSystem<SystemA>();
system.Construct(settings);
Note:
- In default (
UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP is **not** used), The SystemGroup to which the System belongs can be controlled by Attribute ( e.g:
[UpdateInGroup(typeof(SystemGroupType))]` .
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.UseDefaultWorld(systems =>
{
systems.Add<SystemA>();
})
}
}
public class SystemA : SystemBase
{
[Inject]
public void Construct(Foo foo)
{
// Setup entities...
var archtype = World.EntityManager.CreateArchetype(typeof(ComponentDataA));
World.EntityManager.CreateEntity(archtype);
}
protected override void OnUpdate()
{
Entities.ForEach((ref ComponentDataA data) =>
{
// Do something...
})
.Schedule();
}
}
ECS also allows you to create and register your own system.
There are two ways to disable Unity's automatic system bootstrap.
- By setting the define symbol
UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP
will disable all World and System auto-bootstrap. - Or, By adding the
[DisableAutoCreation]
attribute to the class definition to disable auto bootstrap per system.
For System that have auto bootstrap disabled, constructor injection can be used.
public class SystemA : SystemBase
{
readonly ServiceA serviceA;
// Constructor injection
public SampleSystem(ServiceA serviceA)
{
this.serviceA = serviceA;
}
protected override void OnUpdate()
{
// ...
}
}
To use this System, you need to set up World yourself.
VContainer supports to instantiate of World and configuration.
// Register of new World under the control of VContainer.
builder.RegisterNewWorld("My World 1", Lifetime.Scoped);
// Register System by specifying the name of World to be added.
builder.RegisterSystemIntoWorld<SystemA>("My World 1");
// builder.RegisterSystemIntoWorld<SystemB>("My World 1");
// builder.RegisterSystemIntoWorld<SystemC>("My World 1");
// Other dependencies can be injected into the System.
builder.Register<ServiceA>(Lifetime.Singleton);
(Optional) The above can also be declared by grouping as below:
builder.UseNewWorld("My World 1", Lifetime.Scoped, systems =>
{
systems.Add<SystemA>();
// systems.Add<SystemB>();
// systems.Add<SystemC>();
// ...
});
Internaly, If you use above methods, the following setup will be performed automatically:
// When resolving world ...
var world = new World("My World 1");
world.CreateSystem<InitializationSystemGroup>();
world.CreateSystem<SimulationSystemGroup>();
world.CreateSystem<PresentationSystemGroup>();
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world, PlayerLoop.GetCurrentPlayerLoop());
// Resolving dependencies ...
var systemA = new SystemA(new ServiceA());
world.AddSystem(systemA);
var systemGroup = (ComponentSystemGroup)world.GetOrCreateSystem<SimulationSystemGroup>();
systemGroup.AddSystemToUpdateList(systemA);
// After container build ...
foreach (var system in world.Systems)
{
if (system is ComponentSystemGroup group)
group.SortSystems();
}
Note:
- Currently, VContainer is registering the World using
ScriptBehaviourUpdateOrder.UpdatePlayerLoop
. - This is an alias that registers 3 SystemGroups to PlayerLoop, so VContainer also creates these SystemGroups internally.
By default, VContainer will register System to SimulationSystemGroup
. If you want to change this, you can use .IntoGroup<T>()
:
// Example
builder.RegisterSystemIntoWorld<SystemA>("My World 1")
.IntoGroup<PresentationSystemGroup>();
RegisterNewWorld(...)
or UseNewWorld(...)
can accept Lifetime as an argument.
- This new World is placed under the control of VContainer.
- World holds System. Therefore, the lifetime of System is the same as World.
- If
Lifetime.Scoped
is specified, when the scope is destroyed, Dispose of all the systems belonging to that World will be called.
builder.RegisterNewWorld("My World 1", Lifetime.Scoped);
builder.RegisterSystemIntoWorld("My World 1");
public class SystemA : SystemBase, IDisposable
{
protected override void OnUpdate()
{
// ...
}
// Called when scope is disposed.
public void Dispose()
{
// ...
}
}
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.UseNewWorld("My World 1", Lifetime.Scoped, systems =>
{
systems.Add<SystemA>();
})
}
}
public class SystemA : SystemBase
{
public void SystemA(Foo foo)
{
// Injected dependencies...
}
protected override void OnCreate()
{
// Setup entities...
var archtype = World.EntityManager.CreateArchetype(typeof(ComponentDataA));
World.EntityManager.CreateEntity(archtype);
}
protected override void OnUpdate()
{
Entities.ForEach((ref ComponentDataA data) =>
{
// Do something...
})
.Schedule();
}
}
// When only one world is registered:
class ClassA
{
public ClassA(World world) { /* ... */ }
}
// When multiple worlds is registered
class ClassA
{
public ClassA(IEnumerable<World> worlds)
{
var world = worlds.First(x => x.Name == "My new world");
// ...
}
}
Zenject is awesome. but VContainer is:
- Most parts of reflections and assertions are isolated to the Container's build stage.
- Easy to read implementation.
- Code first, transparent API.
Zenject | VContainer |
---|---|
Container.Bind<Service>() .AsTransient() |
builder.Register<Service>(Lifetime.Transient) |
Container.Bind<Service>() .AsCached() |
builder.Register<Service>(Lifetime.Scoped) |
Container.Bind<Service>() .AsSingle() |
builder.Register<Service>(Lifetime.Singleton) |
Container.Bind<IService>() .To<Service>() .AsCache() |
builder.Register<IService, Service>(Lifetime.Scoped) |
Container.Bind(typeof(IInitializable), typeof(IDisposable)) .To<Service>() .AsCached(); |
builder.Register<Service>(Lifetime.Scoped) .As<IInitializable, IDisposable>() |
Container.BindInterfacesTo<Service>() .AsCached() |
builder.Register<Service>(Lifetime.Scoped) .AsImplementedInterfaces() |
Container.BindInterfacesAndSelfTo<Foo>() .AsCached() |
builder.Register<Service>(Lifetime.Scoped) .AsImplementedInterfaces() .AsSelf() |
Container.BindInstance(obj) | builder.RegisterInstance(obj) |
Container.Bind<IService>() .FromInstance(obj) |
builder.RegisterInstance<IService>(obj) |
Container.Bind(typeof(IService1), typeof(IService2)) .FromInstance(obj) |
builder.RegisterInstance(obj) .As<IService1, IService2>() |
Container.BindInterfacesTo<Service>() .FromInstance(obj) |
builder.RegisterInstance(obj) .AsImplementedInterfaces() |
Container.BindInterfacesAndSelfTo<Service>() .FromInstance(obj) |
builder.RegisterInstance(obj) .AsImplementedInterfaces() .AsSelf() |
Container.Bind<Foo>() .FromComponentInHierarchy() .AsCached(); |
builder.RegisterComponentInHierarchy<Foo>() |
Container.Bind<Foo>() .FromComponentInNewPrefab(prefab) .AsCached() |
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped) |
Container.Bind<Foo>() .FromNewComponentOnNewGameObject() .AsCached() .WithGameObjectName("Foo1") |
builder.RegisterComponentOnNewGameObject<Foo>(Lifetime.Scoped, "Foo1") |
.UnderTransform(parentTransform) | .UnderTransform(parentTransform) |
.UnderTransform(() => parentTransform) | .UnderTransform(() => parentTransform) |
Container.Bind<Foo>() .FromComponentInNewPrefabResource("Some/Path/Foo") |
Not supported We should load Resources using LoadAsync family.You can use RegisterInstance() etc after loading the Resource. |
Container.Bind<Foo>() .WithId("foo").AsCached() |
Not supported Duplicate type Resolve is not recommended. You can instead use type-specific Register builder.Register<Service>(Lifetime.Scoped) .WithParameter("foo", foo) |
wip
VContainer has the ability to perform the meta programming part faster by generating IL code at compile time.
As a result, there is no reflection at runtime, and it is expected to be 3-6x faster.
It works in the IL2CPP environment.
With VContainer, for example, the following code (as IL) is automatically generated:
class ClassA
{
private sealed class __GeneratedInjector : IInjector
{
public object CreateInstance(IObjectResolver resolver, IReadOnlyList<IInjectParameter> parameters)
{
I6 fromConstructor = resolver.ResolveOrParameter<I6>("fromConstructor1", parameters);
I7 fromConstructor2 = resolver.ResolveOrParameter<I7>("fromConstructor2", parameters);
return new ClassA(fromConstructor, fromConstructor2);
}
public void Inject(object instance, IObjectResolver resolver, IReadOnlyList<IInjectParameter> parameters)
{
ClassA clasA = (ClassA)instance;
I3 service = resolver.ResolveOrParameter<I3>("service3", parameters);
I4 service2 = resolver.ResolveOrParameter<I4>("service4", parameters);
allInjectionFeatureService.MethodInjectable1(service, service2);
I5 service3 = resolver.ResolveOrParameter<I5>("service5", parameters);
I6 service4 = resolver.ResolveOrParameter<I6>("service6", parameters);
classA.MethodInjectable2(service3, service4);
classA.PrivatePropertyInjectable = resolver.Resolve<I2>();
classA.PublicPropertyInjectable = resolver.Resolve<I3>();
classA.privateFieldInjectable = resolver.Resolve<I4>();
classA.PublicFieldInjectable = resolver.Resolve<I5>();
}
}
Choose Assets -> Create -> VContainer -> VContainer Settings
.
And select directory.
Note:
- This menu will automatically add VContainerSettings to preload-assets.
Set the following from the inspector of VContainerSettings.asset.
- Prepare Code Generation section
- Enabled : enable/disable code generation feature at build time.
- Target Assemblies : Specify the assembly you want to pre-IL generate.
- "+" button shows valid choices
- The Assembly specified here must reference VContainer.dll.
- If not specified, no pre-code generation will be done
- Namespace Filter : You can filter the target by specifying namespace.
- "+" button shows valid choices.
The next time the specified assembly is compiled, VContainer will make changes to the compiled dll.
If it succeeds, it will be logged. For example:
VContainer code generation optimization for MyGame.dll 24 types (40.0355ms)
VContainer code generation optimization for VContainer.Benchmark.Fixtures 24 types (40.0355ms)
VContainer executes pre-processing such as reflection when building a container. To reduce main thread blocking time for your game:
- Set
false
toLifetimeScope.autoRun
- After scene loading, Call
LifetimeScope.Build
manually.
For example:
var nextScene = await LoadSceneAsync(...);
var lifetimeScope = LifetimeScope.Find<MyLifetimeScope>(nextScene.Scene);
// Your async solution here.
await UniTask.Run(() => lifetimeScope.Build());
builder.RegisterComponentInHierarcky()
do not work on background threads.
If this causes an error, use Awake instead.
For example:
class GameLifetimeScope : LifetimeScope
{
Ground groundInScene;
protected override void Awake()
{
// Run main thread.
groundInScene = FindObjectOfType<Ground>();
base.Awake();
}
protected override void Configure(IContainerBuilder builder)
{
// It can run background thread.
builder.RegisterInstance(groundInScene);
}
}
If you enable VCONTAINER_PARALLEL_CONTAINER_BUILD
compilation flag,
VContainer will perform container builds in Parallel.
This is the solution when you have a lot of objects to register.
- Use Constructor Injection whenever possible. see Constructor Injection section.
- Consider whether injection to MonoBehaviour is necessary. see Method Injection section.
- wip
MIT