Dependencies: NaugntyAttribute -> assetstore
Boot цепочка:
AppEntryсоздаётGlobalRootи вешаетSceneOrchestrator(DontDestroyOnLoad)SceneOrchestratorподнимаетProjectRootConnector(из Resources, если надо)ProjectRootConnector.Awake()создаётProjectContextи вызываетExecute(ProjectContext, sender: this)- На каждый
sceneLoadedSceneOrchestratorвызываетSceneConnector.Execute(projectRoot.ProjectContext)для сцены
- Хост:
ProjectRootConnector.ProjectContext - Доступ откуда угодно:
ProjectRootRegistry.GetContext()ProjectServices.ContextProjectServices.Get<T>() / TryGet<T>() / Add<T>()
- Хост:
SceneConnector.SceneContext - Создаётся внутри
SceneConnector.Execute(projectContext)какnew ServiceRegistry(parentContainer: projectContext) - Внутри сцены любой нод получает
ServiceRegistry registryвBind/Construct/...— это и есть SceneContext
- У
ServiceRegistryнет публичного доступа к parent — и не надо:TryGet/Getавтоматически поднимаются вверх. - Если ты в SceneContext вызываешь
registry.Get<SomeGlobalService>(), он спокойно найдёт его в ProjectContext.
public sealed class AudioBootstrapNode : ConnectorNode
{
public override void Bind(ServiceRegistry registry)
{
registry.Add(new AudioService());
}
protected override void DisposeInternal()
{
// если надо гарантированно убрать именно свой инстанс:
// registry.RemoveIfSame(expected: audioService);
}
}var lifecycle = ProjectServices.Get<AppLifecycleService>();
ProjectServices.Add(new AnalyticsService());
var analytics = ProjectServices.Get<AnalyticsService>();TryGet— если сервис опциональный / модульныйGet— если сервис “обязан существовать” (например аналитика в сборке с аналитикой)
Bind(registry)Construct(registry)BeforeInit()Init()AfterInit()- затем циклы:
Tick / FixedTick / LateTick
Bind— сохранить ссылки на registry, добавить/получить сервисы, базовые подпискиConstruct— создать “тяжёлые” штуки, которые не зависят от других нодовBeforeInit/Init/AfterInit— логика инициализации в 3 фазы (аналог Awake/Start/после прогрева)DisposeInternal— отписки и чистка (всё что подписывал — отписать)
public sealed class MyNode : ConnectorNode
{
private ServiceRegistry registry;
public override void Bind(ServiceRegistry registry)
{
this.registry = registry;
}
public override void Construct(ServiceRegistry registry) { }
public override void BeforeInit() { }
public override void Init() { }
public override void AfterInit() { }
public override void Tick(float deltaTime) { }
public override void FixedTick(float fixedDeltaTime) { }
public override void LateTick(float deltaTime) { }
protected override void DisposeInternal()
{
// отписки
}
}В нодах держи логику в Bind/Init/Tick.... (В дебаге у тебя есть валидация, которая ругается на Awake/Start/Update у нодов.)
-
Сортировка:
Order(IOrder), затем по имени типа -
Где задавать:
- в инспекторе у нода (
ConnectorNodeуже имеет поле Order) - или в
OnValidateконкретного нода (Editor-only)
- в инспекторе у нода (
- Сортировка:
Order, затем по имени объекта
connector.OnPauseRequest(sender: this); // выключит EnabledTicks и вызовет OnPauseRequest у нодов
connector.OnResumeRequest(sender: this); // включит обратно и вызовет OnResumeRequest- Если нод отключён (
isActiveAndEnabled == false), он всё равно может тикать, если этоConnectorNodeи у него включёнRunWhenDisabled.
-
LocalConnector.Dispose():- отписывается от сцены
- вызывает
Dispose()у нодов сIDispose - чистит кэши tick интерфейсов
using UnityEngine.SceneManagement;
var scene = SceneManager.GetActiveScene();
if (SceneConnectorRegistry.TryGet(scene, out var sceneConnector))
{
var sceneContext = sceneConnector.SceneContext;
var sceneIndex = sceneContext.Get<SceneEntityIndex>();
}SceneConnectorпри Execute регает все статические коннекторы- Динамические коннекторы регаются через
LocalConnector.OnEnable()если сцена уже инициализирована - Для id/tag нужен
EntityKeyBehaviourна том же объекте, что иLocalConnector
var sceneIndex = registry.Get<SceneEntityIndex>();
if (sceneIndex.TryGetById(42, out var connector))
{
// ок
}
var mustExist = sceneIndex.GetByIdOrThrow(42);if (sceneIndex.TryGetFirstByTag("Chest", out var chest))
{
// первый попавшийся
}
var all = sceneIndex.GetAllByTag("Chest"); // IReadOnlyList<LocalConnector>
var mustExist = sceneIndex.GetFirstByTagOrThrow("Chest");if (sceneIndex.TryGetFirstNode<MyNode>(out var node, includeDerived: true))
{
// найден
}
var mustExist = sceneIndex.GetFirstNodeOrThrow<MyNode>(includeDerived: true);var buffer = new List<MyNode>(64);
var count = sceneIndex.GetNodes(buffer, includeDerived: true);if (sceneIndex.TryGetNodeInConnector<MyNode>(connector, out var node))
{
}if (sceneIndex.TryGetNodeInFirstByTag<MyNode>("Chest", out var node))
{
}-
Поля:
Id(int)Tag(string)AutoAssignId(bool)
-
Editor: кнопка Assign Unique Id (для статических id в сценах)
-
Runtime: если
Id <= 0иAutoAssignId == true, индекс может назначить id при регистрации
-
Если ты заспавнил объект с
LocalConnector:- при
OnEnable()он попробует зарегаться вSceneConnectorи выполниться (если сцена уже initialized)
- при
-
При
OnDisable()— разрегистрируется -
Execute()уLocalConnectorвызывается один раз (есть флаг executed)
Используй утилиту:
ConnectorDestroyUtils.DisposeAndDestroy(gameObject);В проекте есть SceneTransitionService:
-
Go(targetSceneName, doCleanup) -
грузит
transitionSceneName -
опционально делает:
Resources.UnloadUnusedAssets()GC.Collect()+WaitForPendingFinalizers()+GC.Collect()
-
потом грузит целевую сцену
Сделай нод на ProjectRootConnector (он DontDestroy) и зарегай сервис в Bind:
public sealed class SceneTransitionsBootstrapNode : ConnectorNode
{
[SerializeField] private string transitionSceneName = "EmptySceneTransition";
public override void Bind(ServiceRegistry registry)
{
// runner = этот нод (MonoBehaviour), он живёт вместе с ProjectRootConnector
ProjectServices.Add(new SceneTransitionService(runner: this, transitionSceneName: transitionSceneName));
}
}ProjectServices.Get<SceneTransitionService>().Go("Level_2", doCleanup: true);- В
EmptySceneTransitionможно не иметьSceneConnector. - В Editor у тебя может быть warn, если
SceneConnectorне найден — либо игнорируй, либо добавь пустойSceneConnectorв transition-сцену.
SceneOrchestrator гарантирует наличие AppLifecycleService в ProjectContext.
public sealed class LifecycleListenerNode : ConnectorNode
{
private AppLifecycleService lifecycle;
public override void Bind(ServiceRegistry registry)
{
lifecycle = ProjectServices.Get<AppLifecycleService>();
lifecycle.FocusChanged += OnFocusChanged;
lifecycle.PauseChanged += OnPauseChanged;
lifecycle.Quit += OnQuit;
}
protected override void DisposeInternal()
{
if (lifecycle == null)
return;
lifecycle.FocusChanged -= OnFocusChanged;
lifecycle.PauseChanged -= OnPauseChanged;
lifecycle.Quit -= OnQuit;
}
private void OnFocusChanged(bool hasFocus, Object sender) { }
private void OnPauseChanged(bool paused, Object sender) { }
private void OnQuit(Object sender) { }
}-
Получить ProjectContext:
var project = ProjectServices.Context;
-
Получить SceneContext из MonoBehaviour:
SceneConnectorRegistry.TryGet(gameObject.scene, out var sc); var ctx = sc.SceneContext;
-
Получить SceneEntityIndex из SceneContext:
var index = ctx.Get<SceneEntityIndex>();
-
Найти объект по тегу:
var door = index.GetFirstByTagOrThrow("Door");
-
Найти нод по типу:
var ui = index.GetFirstNodeOrThrow<MyUiNode>();
-
Усыпить тики у конкретного LocalConnector:
connector.OnPauseRequest(sender: this);
-
Удалить объект “правильно”:
ConnectorDestroyUtils.DisposeAndDestroy(go);