DI and ECS in unity
Architecture ECS

How to Use Dependency Injection with ECS for Scalable Game Development

I have worked extensively with various Dependency Injection containers throughout my career. So, when ECS started gaining traction in 2018, I decided to experiment with it in a pet project to familiarize myself with this approach. At the time, I was already using Zenject in production, so it was natural to incorporate it from the start and build infrastructure to integrate the DI container with ECS systems.

Fast forward to today, and I have multiple projects built with ECS and different DI containers, including Unity Entities. While integrating DI with ECS felt intuitive to me, I’ve often seen questions in various ECS-related discussions about whether it’s even possible. To clarify this once and for all, I’m writing this post along with a GitHub repo showcasing a simple way to make it work.

Since I’ve used DI + ECS in multiple combinations and all of them followed the same structure and principle, this sample can be easily adapted to any variation you might use. This post is intended for developers familiar with any DI and ECS framework.

Setup

For this particular tutorial, I use VContainer and Morpeh ECS Framework, but the same principle applies to other combinations too.

Code Organization For ECS and DI

The structure is straightforward. For DI, we need a composition root and an entry point. In VContainer terms, this means creating a LifetimeScope:

using Sample.ECS;
using Scellecs.Morpeh;
using UnityEngine;
using VContainer;
using VContainer.Unity;

namespace Sample
{
    public class GameLifetimeScope : LifetimeScope
    {
        [SerializeField] private UnitConfig _unitConfig;

        protected override void Configure(IContainerBuilder builder)
        {
            // Register the entry point that initializes the ECS world
            builder.RegisterEntryPoint<RootWorld>();

            // Register a factory for creating unit instances dynamically
            builder.Register<IFactory<Vector3, GameObject>, UnitFactory>(Lifetime.Transient);

            // Register the unit configuration, which contains a reference to the unit prefab
            builder.RegisterInstance(_unitConfig);

            // Register the default ECS world so it can be injected into the entry point
            builder.RegisterInstance(World.Default);

            // Register all necessary systems
            RegisterSystems(builder);
        }

        private void RegisterSystems(IContainerBuilder builder)
        {
            // Register core ECS systems
            builder.RegisterSystem<SpawnerSystem>();
            
            // Register cleanup systems that handle entity removal
            builder.RegisterCleanupSystem<DeadUnitsCleanupSystem>();
        }
    }
}

And as an entry point, I use the wrapper for World:

using System;
using System.Collections.Generic;
using Scellecs.Morpeh;
using VContainer.Unity;

namespace Sample
{
    public class RootWorld : IInitializable, IDisposable
    {
        private readonly IEnumerable<ICleanupSystem> _cleanUpSystems;
        private readonly IEnumerable<IFixedSystem> _fixedUpdateSystems;
        private readonly IEnumerable<ISystem> _updateSystems;
        private readonly World _world;

        public RootWorld(
            World world,
            IEnumerable<ISystem> updateSystems,
            IEnumerable<IFixedSystem> fixedUpdateSystems,
            IEnumerable<ICleanupSystem> cleanUpSystems)
        {
            _world = world;
            
            // Inject all the bound systems.
            // Since they are created via the container then all dependencies are already provided
            _updateSystems = updateSystems;
            _fixedUpdateSystems = fixedUpdateSystems;
            _cleanUpSystems = cleanUpSystems;
        }

        public void Initialize()
        {
            // Add all systems to the default group
            var defaultGroup = _world.CreateSystemsGroup();
            foreach (var updateSystem in _updateSystems)
            {
                defaultGroup.AddSystem(updateSystem);
            }

            foreach (var fixedUpdateSystem in _fixedUpdateSystems)
            {
                defaultGroup.AddSystem(fixedUpdateSystem);
            }

            foreach (var cleanUpSystem in _cleanUpSystems)
            {
                defaultGroup.AddSystem(cleanUpSystem);
            }

            // Add the default group to the world and now all your systems and world are hooked up
            _world.AddSystemsGroup(0, defaultGroup);
        }

        public void Dispose()
        {
            _world?.Dispose();
        }
    }
}

The same structure and principle apply to other ECS frameworks I’ve used in my projects, including Unity Entities and Entitas.

In this sample, prefabs are spawned via a factory instead of calling Object.Instantiate directly. Any system can inject the factory into its constructor, ensuring:

  • Encapsulated creation logic – The instantiation logic is handled within the factory.
  • Flexibility – You can change the object creation process based on new requirements without modifying existing systems.
  • Reduced boilerplate – Systems don’t need to manually handle object instantiation, keeping code cleaner and more modular.



Built-in DI

You might point out that Morpeh ECS Framework already allows manual injection via the inspector—and you’d be absolutely right. However, in this ECS framework, manually creating scriptable objects for each system and adding them to the world felt tedious. Not gonna lie, I sometimes just modified existing systems, adding more filters and logic to avoid the hassle of manual setup in the editor. While this was fine for a prototype, you should think twice before doing the same in a production project. That’s why integrating DI to automatically hook up systems was a natural step to optimize the workflow. Plus, AI agents can add new systems to the scope via code much more easily than through the scriptable object flow.


Further Steps

To reduce manual work, you can also add an automated step that finds all systems using the reflection and binds them at the start of the game. Or you can even write a source generator to bake it. That way it would work similarly to Entities. But for me and my prototype, the current solution is enough, as I have finer control over what system I add at the game start.


Conclusion

Using DI containers with ECS results in a cleaner, more modular, and scalable codebase. While some ECS frameworks provide built-in dependency management, integrating a DI container like VContainer simplifies system registration and reduces boilerplate, making development more efficient.

This approach has improved my workflow across multiple projects, allowing for greater flexibility and maintainability. If you’re looking to integrate DI with ECS in your own game, I encourage you to explore the provided GitHub repo, experiment with the setup, and adapt it to your needs. Let me know if you have any questions or insights—I’d love to hear how it works for you.


AlexeyMerzlikin-TechLead-Gamedeveloper-Unity


Alexey Merzlikin

Experienced game developer and tech lead with a passion for writing educational content about game development and programming. I have over 10 years of industry experience focused on performance optimization, clean code practices, and robust game architecture. I share regular posts on my Telegram channel about applying software engineering best practices to build high-quality games. My goal is to help other game developers level up their skills.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.