Обзор ECS-lite-архитектуры
Отлично! Ты чётко сформулировал суть архитектурного идеала, и он полностью согласуется с твоими ценностями: простота, ясность, переиспользуемость, масштабируемость — без излишеств.
Давай зафиксируем финальную модель взаимодействия, как ты её видишь, и убедимся, что она практична и архитектурно чиста.
Философия твоей архитектуры (итоговое видение)
Системы — чистые функции, работающие только с теми данными, которые им необходимы.
Все связи между системами — через события с минимальным контекстом (в идеале — толькоEntityId).
Адаптеры (например,VFXHandler) — получаютEntityId, находят сущность, читают нужные именно им данные и действуют.
Централизованный реестр (EntityRegistry) — обеспечивает быстрый доступ к любой сущности поEntityId, но не участвует в логике.
🔹 Как это работает шаг за шагом
1. Создание сущности
- На
GameObjectдобавляютсяHealthComponent,EntityTypeComponent,VFXComponentи т.д. - При
Awake()каждыйDataComponent<T>автоматически регистрируется вEntityRegistry. EntityRegistryсоздаёт/обновляетEntityViewдля этогоGameObject(ключ —GetInstanceID()).
✅ Никакой ручной работы.
✅ Любое количество компонентов — поддерживается автоматически.
2. Изменение состояния (например, урон)
-
Где-то (например, в
BulletCollisionHandler) вызывается:HealthSystem.TakeDamage(entityId, amount); -
HealthSystem:-
Получает
HealthDataизEntityRegistryпоentityId, -
Изменяет его,
-
Если здоровье ≤ 0 — отправляет событие:
GameEvents.Raise(new HealthDepletedEvent { entityId = entityId });
-
✅
HealthSystemне знает проTransform,EntityType,VFXComponent.
✅ Она работает только сHealthData.
3. Обработка события (например, VFX)
-
VFXHandler(MonoBehaviour, один на сцене) подписан наHealthDepletedEvent. -
При получении события:
void OnHealthDepleted(HealthDepletedEvent e) { if (!registry.TryGetView(e.entityId, out var view)) return; // Читает ТОЛЬКО то, что нужно ему var vfxData = view.Get<VFXData>(); var transform = view.transform; var entityType = view.Get<EntityTypeData>(); // Решает, какой VFX проиграть PlayVFX(vfxData, transform.position, entityType.type); }
✅
VFXHandler— адаптер к Unity, он может читать любые данные, потому что он знает о реестре.
✅ Он не нарушает чистоту систем.
4. Обработка другого события (например, прогресс)
-
ProgressSystem(статический класс) тоже подписан:void OnHealthDepleted(HealthDepletedEvent e) { var entityType = registry.GetView(e.entityId).Get<EntityTypeData>(); if (entityType.type == EnemyType.Boss) Stats.Increment("bossKills"); }
✅ Та же подписка — разные потребители.
✅ Каждый читает только то, что ему нужно.
🔑 Ключевые принципы, которые ты закладываешь
| Принцип | Как реализован |
|---|---|
| Разделение ответственности | Системы — логика, адаптеры — Unity-интеграция |
| Минимальный контекст в событиях | Только EntityId — максимум декуплинга |
| Автоматическая регистрация | DataComponent<T>.Awake() → EntityRegistry |
| Единый источник истины | EntityRegistry знает всё о всех сущностях |
| Масштабируемость | Работает для 3 и для 300 000 сущностей |
| Простота разработки | Добавил компонент → автоматически доступен везде |
🔍 Ответ на твой главный вопрос
«Нужно ли
HealthSystemзнать проTransform?»
Нет.
Она знает только проHealthDataиEntityId.
Всё остальное — задача обработчиков событий.
Это идеальный декуплинг:
- Системы не зависят друг от друга,
- Адаптеры не лезут в логику,
- Реестр — нейтральный справочник.
🎯 Почему это отлично подходит для тебя
Ты — один разработчик, который:
- Делает разные игры,
- Хочет накапливать кодовую базу,
- Ценит ясность и предсказуемость.
Эта архитектура даёт тебе:
- Шаблон:
DataComponent<T>→System→Event→Handler, - Повторное использование:
HealthSystemработает везде, - Быстрое прототипирование: добавил компонент — получил функционал,
- Безопасность: нельзя случайно создать зависимость,
- Гибкость: легко добавлять аналитику, квесты, VFX — просто подпишись на событие.