Skip to main content

VContainer + ECS (beta)

VContainer supports integration between Unity's ECS (Entity Component System) and the regular C# World. (This is an experimental feature. Any feedback is welcome! :0 )

caution

Currently, this feature requires Unity 2019.3 or later versions. Some features (e.g. RegisterUnmanagedSystemIntoWorld) require Unity 2022.2 or later.

Setup

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 the Entities package and press [Install].

If the com.unity.entities package exists, the VCONTAINER_ECS_INTEGRATION symbol is defined and the following features are enabled.

When using Unity's default World

By default, ECS will automatically instantiate classes that inherit the ComponentSystemBase defined in your project, add them to the default world, and run them.

In this mode, you can use method injection for ECS Systems. (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>();
// ...
});

Internally, this is an automation of the following processes:

var system = World.DefaultGameObjectInjectionWorld.GetExistingSystem<SystemA>();
system.Construct(settings);
note

By default (UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP is not used), the SystemGroup to which the System belongs can be controlled by attributes (e.g., [UpdateInGroup(typeof(SystemGroupType))]).

Example of setup entities (with Default World)

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();
}
}

When to use your custom world

ECS also allows you to create and register your own system.

There are two ways to disable Unity's automatic system bootstrap.

  • Setting the define symbol UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP will disable all World and System auto-bootstrapping.
  • Alternatively, adding the [DisableAutoCreation] attribute to the class definition will disable auto-bootstrapping per system.

For Systems that have auto-bootstrapping 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 the World yourself.

VContainer supports the instantiation of Worlds and their configuration.

// Register a new World under the control of VContainer.
builder.RegisterNewWorld("My World 1", Lifetime.Scoped);

// Register a System by specifying the name of the World to be added.
builder.RegisterSystemIntoWorld<SystemA>("My World 1");
// builder.RegisterSystemIntoWorld<SystemB>("My World 1");
// builder.RegisterSystemIntoWorld<SystemC>("My World 1");

// For Unity 2022.2+ (Unmanaged System)
builder.RegisterUnmanagedSystemIntoWorld<MyUnmanagedSystem>("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>();
// ...
});

Internally, if you use the above methods, the following setup will be performed automatically:

YourLifetimeScope.cs
// 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>();

Lifetime of World and Systems

RegisterNewWorld(...) or UseNewWorld(...) can accept Lifetime as an argument.

  • This new World is placed under the control of VContainer.
  • The World holds Systems. Therefore, the lifetime of the Systems is the same as the World.
  • If Lifetime.Scoped is specified, when the scope is destroyed, Dispose will be called on all systems belonging to that World.
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()
{
// ...
}
}

Example of setup entities (with Custom World)

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();
}
}

Resolving World

// When only one world is registered:
class ClassA
{
public ClassA(World world) { /* ... */ }
}

// When multiple worlds are registered
class ClassA
{
public ClassA(IEnumerable<World> worlds)
{
var world = worlds.First(x => x.Name == "My new world");
// ...
}
}