Лёгкий DI-фреймворк для Unity. Основа — контейнер с поддержкой:
- биндингов по типу, инстансу и фабрике,
- тегов для параллельных реализаций,
- инъекций через конструктор (в том числе с атрибутами
[InjectionConstructor]
и[Tag]
), - контейнеров проекта и сцены,
- вспомогательных биндингов для MonoBehaviour (создание GO, префабы, захват существующих инстансов).
Фреймворк сделан прежде всего для внутреннего использования в AbyssMoth Studios. Используйте на свой страх и риск; API может эволюционировать.
-
Импортируйте пакет. Папка плагина окажется здесь:
Assets/Plugins/RimuruDev/CuteDI
-
Откройте пример:
Assets/Plugins/RimuruDev/CuteDI/Example
— там готовые сцены Boot, MainMenu, Gameplay, Other и демонстрация всех видов биндинга.
Проектный контейнер вы создаёте сами в точке входа (как в примере):
using UnityEngine;
namespace AbyssMoth.CuteDI.Example
{
public sealed class Bootstrapper : MonoBehaviour
{
private IProjectContext projectContext;
private IGameNavigation navigation;
private void Awake()
{
projectContext = new ProjectContext();
projectContext.Register();
projectContext.Resolve();
navigation = projectContext.Container.Resolve<IGameNavigation>();
DontDestroyOnLoad(gameObject);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Alpha1)) navigation.GoToMainMenu();
if (Input.GetKeyDown(KeyCode.Alpha2)) navigation.GoToGameplay();
if (Input.GetKeyDown(KeyCode.Alpha3)) navigation.GoToOther();
}
private void OnDestroy() => projectContext?.Release();
}
}
ProjectContext
внутри создаёт DIContainer
, делает базовые регистрации (например, ICoroutineRunner
, IGameNavigation
), и передаёт контейнер во фреймворк через DiProvider.SetProject(...)
.
Такой подход даёт полный контроль над тем, что находится на уровне проекта.
На каждой сцене положите SceneContext
(есть готовый SceneContextBootstrap
, который гарантирует наличие контекста).
В SceneContext
можно назначить ScriptableObject-инсталлеры — они вызываются в Awake
и наполняют контейнер сцены.
Пример простого инсталлера меню:
using UnityEngine;
namespace AbyssMoth.CuteDI.Example
{
[CreateAssetMenu(fileName = "MainMenuInstaller", menuName = "DI/Scene Installers/MainMenu")]
public sealed class MainMenuInstaller : SceneInstallerSO
{
[SerializeField] private GameObject menuRootPrefab;
public override void Compose(in DIContainer scene, in DIContainer project)
{
scene.RegisterInstance(this).AsSingle().NonLazy();
scene.RegisterType<IMenuService, MenuService>().AsSingle().NonLazy();
scene.Register(c => new MenuViewModel(c.Resolve<IMenuService>())).AsSingle().NonLazy();
if (menuRootPrefab)
scene.BindPrefab<IMenuRoot, MenuRoot>(menuRootPrefab, isUI: true);
}
}
}
Вдохновлено Reflex. Делает конструкторную инъекцию автоматически.
scene.RegisterType<IEnemySpawner, EnemySpawner>().AsSingle().NonLazy();
scene.RegisterType(typeof(IAnalytics), typeof(DummyAnalytics)).AsSingle().NonLazy();
scene.RegisterInstance(this).AsSingle().NonLazy();
scene.RegisterInstance(configSo).AsSingle().NonLazy();
scene.Register(c => new GameplayController(
c.Resolve<IEnemySpawner>(),
project.Resolve<IGameNavigation>()))
.AsSingle()
.NonLazy();
Ключ в контейнере — (tag, serviceType)
. Для параллельных реализаций используйте теги:
scene.RegisterType<IClock, UtcClock>("utc").AsSingle().NonLazy();
scene.RegisterType<IClock, GameClock>("game").AsSingle().NonLazy();
Инъекция нужной реализации через [Tag]
:
public sealed class AttrConsumer
{
public IStorage File { get; }
public IClock Clock { get; }
[InjectionConstructor]
public AttrConsumer([Tag("file")] IStorage file, [Tag("utc")] IClock clock)
{
File = file;
Clock = clock;
}
}
Сбор всех реализаций интерфейса:
scene.RegisterType<IProcessor, ProcA>("proc_a").AsSingle();
scene.RegisterType<IProcessor, ProcB>("proc_b").AsSingle();
scene.Register(c => new ProcessorAggregator(c.ResolveAll<IProcessor>().ToArray()))
.AsSingle()
.NonLazy();
Можно заменить привязку (удобно для моков в тестовой сцене):
scene.RegisterType<IReplaceSample, ReplaceA>().AsSingle();
scene.Replace<IReplaceSample>(c => new ReplaceB()).AsSingle().NonLazy();
Утилиты DiUnity
— для сцепления контейнера и компонентов:
// Новый пустой GO с компонентом
scene.BindNewGo<IFoo, FooMono>("FooGo", tag: "foo_go");
// Инстанс из префаба (UI или обычный)
scene.BindPrefab<IFoo, FooMono>(widgetPrefab, parent, isUI: true, tag: "foo_widget");
// Только компонент (Self) без интерфейса:
scene.BindPrefabSelf<HUD>(hudPrefab, parent);
Важно: если биндите несколько экземпляров одного
MonoBehaviour
, давайте разные теги, иначе будет коллизия ключей.
- Если у класса один конструктор — он используется.
- Если конструкторов несколько — можете пометить нужный
[InjectionConstructor]
. - Для выбора по тегу используйте
[Tag("...")]
у параметра.
var nav = project.Resolve<IGameNavigation>();
var ok = scene.TryResolve<IFoo>(out var foo, tag: "foo_widget");
var has = scene.HasRegistration<IEnemySpawner>();
var all = scene.ResolveAll<IProcessor>(); // агрегирует через регистр/родителя
-
DiProvider.Project
— хранит проектный контейнер (вы создаёте его сами вBootstrapper
/ProjectContext
). -
При загрузке сцены
SceneContext
вызываетDiProvider.SceneContextBuilder(parent, sceneName, containerName)
— создаётся сценовой контейнер, кэшируется по handle сцены и используется родителем Project. -
На
SceneManager.sceneUnloaded
контейнер сцены Dispose()-ится и снимается с кэша. -
Получение контейнеров:
- активной сцены:
DiProvider.GetCurrentSceneContainer()
- по ссылке на сцену:
scene.GetSceneContainer()
- по имени:
DiProvider.GetSceneContainerBySceneName("Gameplay")
- активной сцены:
Можно собрать сценовой контейнер вручную (если строите своё последовательное управление загрузкой): вызовите DiProvider.SceneContextBuilder(projectContainer, sceneName)
в момент, когда сцена уже валидна (обычно после LoadSceneAsync
и перед первым кадром).
- Инспектор
SceneContext
в Play Mode показывает реестр биндингов контейнера сцены. - Инспектор инсталлеров (
SceneInstallerSO
) поддерживает предпросмотр черезPreviewHints()
. - Окно Tools → CuteDI → DI Debugger — быстрый просмотр содержимого контейнеров (проект/сцена/Prefab Stage), фильтр по типу/тегу.
В сцене Other
лежит AllBindingsInstaller
— демонстрация:
RegisterType / RegisterInstance / Register(factory)
- теги и
[Tag]
ResolveAll<T>()
Replace<T>()
BindNewGo / BindPrefab / BindPrefabSelf
Плюс в примере SceneHintUI
(большие подсказки на экране) с клавишами:
[1]
→ MainMenu[2]
→ Gameplay[3]
→ Other
- Ключ регистрации —
(tag, serviceType)
. Для нескольких реализаций одного интерфейса используйте теги. BindOnGo
должен получать сценовый экземпляр. Для ассетов и префабов используйтеBindPrefab*
.- Контейнер сцены удаляется при выгрузке сцены (
Dispose()
); проектный контейнер живёт до конца приложения. - Фреймворк не потокобезопасен; используйте в основном потоке Unity.
MIT. Подробности — в LICENSE
.