Skip to main content

VContainer + ECS (beta)

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

caution

Currently, this feature requires Unity 2019.3 or later versions.

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 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.

When using Unity's default World

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))] .

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.

  • 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:

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

Lifeime of World and Systems

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()
{
// ...
}
}

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 is registered
class ClassA
{
public ClassA(IEnumerable<World> worlds)
{
var world = worlds.First(x => x.Name == "My new world");
// ...
}
}