Systems
Системы управляют логикой мира через определённый жизненный цикл
- Вложенный класс
World<TWorld>.Systems<SysType>— каждый типISystemsTypeсоздаёт изолированную группу систем внутри мира - Единый интерфейс
ISystemс четырьмя методами (все опциональны) - Системы выполняются в порядке, определённом параметром
order - Нереализованные методы не вызываются и не создают накладных расходов
- Системы могут быть структурами или классами
ISystemsType
Маркерный интерфейс для изоляции групп систем. Каждый тип получает собственное статическое хранилище:
public struct GameSystems : ISystemsType { }
public struct FixedSystems : ISystemsType { }
public struct LateSystems : ISystemsType { }
// Алиасы для удобного доступа
public abstract class GameSys : W.Systems<GameSystems> { }
public abstract class FixedSys : W.Systems<FixedSystems> { }
public abstract class LateSys : W.Systems<LateSystems> { }
ISystem
Единый интерфейс для всех систем. Реализуйте только нужные методы — остальные не будут вызываться:
public interface ISystem {
// Вызывается один раз при Systems.Initialize()
void Init() { }
// Вызывается каждый кадр при Systems.Update()
void Update() { }
// Вызывается перед каждым Update() — false пропускает обновление
bool UpdateIsActive() => true;
// Вызывается один раз при Systems.Destroy()
void Destroy() { }
// Хуки сериализации в снапшот — переопределите Guid(), чтобы подключить систему к снапшотам
Guid? Guid() => null;
byte Version() => 0;
void Write(ref BinaryPackWriter writer) {}
void Read(ref BinaryPackReader reader, byte version) {}
}
Не оставляйте пустые реализации методов. Если метод не нужен — не реализуйте его. Нереализованные методы обнаруживаются через рефлексию и не вызываются.
Примеры систем:
// Система только с Update
public struct MoveSystem : ISystem {
public void Update() {
W.Query().For(static (ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
});
}
}
// Система с инициализацией и уничтожением
public struct AudioSystem : ISystem {
public void Init() {
// загрузить аудио-ресурсы
}
public void Update() {
// обработать звуки
}
public void Destroy() {
// освободить ресурсы
}
}
// Система с условным выполнением
public struct PausableSystem : ISystem {
public void Update() {
// игровая логика
}
public bool UpdateIsActive() {
return !W.GetResource<GameState>().IsPaused;
}
}
Жизненный цикл
Create() → Add() → Initialize() → Update() цикл → Destroy()
// 1. Создать группу систем (baseSize — начальная ёмкость массива, snapshotGuid — идентификатор группы в снапшотах)
GameSys.Create(baseSize: 64);
// либо с явным Guid группы для стабильности снапшотов при переименовании:
// GameSys.Create(baseSize: 64, snapshotGuid: new("…stable-pipeline-guid…"));
// 2. Зарегистрировать системы (order определяет порядок выполнения)
GameSys.Add(new InputSystem(), order: -10)
.Add(new MoveSystem(), order: 0)
.Add(new RenderSystem(), order: 10);
// 3. Инициализировать — сортирует по order, вызывает Init() у всех систем
GameSys.Initialize();
// 4. Игровой цикл — вызывает Update() каждый кадр
while (gameIsRunning) {
GameSys.Update();
}
// 5. Уничтожить — вызывает Destroy() у всех систем, сбрасывает состояние
GameSys.Destroy();
Регистрация
Все системы регистрируются одним методом Add<T>():
// Базовая регистрация (order по умолчанию = 0)
GameSys.Add(new MoveSystem());
// С указанием порядка (меньше = раньше)
GameSys.Add(new InputSystem(), order: -10) // выполняется первой
.Add(new PhysicsSystem(), order: 0) // затем физика
.Add(new RenderSystem(), order: 10); // рендер последним
// Системы с одинаковым order выполняются в порядке регистрации
GameSys.Add(new SystemA(), order: 0) // первая среди order=0
.Add(new SystemB(), order: 0); // вторая среди order=0
Условное выполнение
Метод UpdateIsActive() позволяет пропускать обновление системы на текущем кадре:
public struct GameplaySystem : ISystem {
public void Update() {
// логика, выполняемая только когда игра не на паузе
}
public bool UpdateIsActive() {
return !W.GetResource<GameState>().IsPaused;
}
}
public struct TutorialSystem : ISystem {
public void Update() {
// логика обучения
}
public bool UpdateIsActive() {
return W.GetResource<PlayerProgress>().IsFirstPlay;
}
}
Несколько групп систем
Разные ISystemsType создают независимые группы с собственным жизненным циклом:
public struct GameSystems : ISystemsType { }
public struct FixedSystems : ISystemsType { }
public abstract class GameSys : W.Systems<GameSystems> { }
public abstract class FixedSys : W.Systems<FixedSystems> { }
// Настройка
GameSys.Create();
GameSys.Add(new InputSystem())
.Add(new RenderSystem());
GameSys.Initialize();
FixedSys.Create();
FixedSys.Add(new PhysicsSystem())
.Add(new CollisionSystem());
FixedSys.Initialize();
// Игровой цикл
while (gameIsRunning) {
GameSys.Update(); // каждый кадр
while (fixedTimeAccumulated) {
FixedSys.Update(); // с фиксированным шагом
}
}
GameSys.Destroy();
FixedSys.Destroy();
Полный пример
// Типы систем
public struct GameSystems : ISystemsType { }
// Системы
public struct InputSystem : ISystem {
public void Update() {
// чтение ввода
}
}
public struct MoveSystem : ISystem {
public void Update() {
W.Query().For(static (ref Position pos, in Velocity vel) => {
pos.Value += vel.Value;
});
}
}
public struct DamageSystem : ISystem {
private EventReceiver<WT, OnDamage> _receiver;
public void Init() {
_receiver = W.RegisterEventReceiver<OnDamage>();
}
public void Update() {
foreach (var e in _receiver) {
if (e.Value.Target.TryUnpack<WT>(out var target)) {
ref var health = ref target.Ref<Health>();
health.Current -= e.Value.Amount;
}
}
}
public void Destroy() {
W.DeleteEventReceiver(ref _receiver);
}
}
// Запуск
W.Create(WorldConfig.Default());
// ... регистрация типов ...
W.Initialize();
GameSys.Create();
GameSys.Add(new InputSystem(), order: -10)
.Add(new MoveSystem(), order: 0)
.Add(new DamageSystem(), order: 5);
GameSys.Initialize();
while (gameIsRunning) {
GameSys.Update();
}
GameSys.Destroy();
W.Destroy();
Сериализация в снапшот
ISystem имеет четыре опциональных метода с дефолтной реализацией (Guid?, Version, Write, Read) — та же форма, что и у IResource. Переопределите Guid(), чтобы подключить инстанс системы к сериализации:
public class SpawnerSystem : ISystem {
private int _nextId;
public Guid? Guid() => new("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
public byte Version() => 1;
public void Update() { /* ... */ }
public void Write(ref BinaryPackWriter writer) => writer.WriteInt(_nextId);
public void Read(ref BinaryPackReader reader, byte version) => _nextId = reader.ReadInt();
}
Валидация выполняется при Add<TSystem>:
- Системы без
Guidмолча не попадают в снапшот. - Любая система с
Guidобязана переопределить иWrite, иReadнезависимо от лэйаута (инстансы систем хранятся упакованными вSystemData, поэтому unmanaged fast-path неприменим). Их отсутствие выбрасываетStaticEcsException. - Дубликат
Guidвнутри одной группыSystems<TSystemsType>ассертится в DEBUG.
Каждый Systems<TSystemsType>.Create регистрирует свою группу в реестре снапшотов мира; Guid группы по умолчанию = typeof(TSystemsType).GuidFromAQN() и переопределяется опциональным параметром snapshotGuid. WorldSnapshot автоматически пишет одну секцию на группу (её scoped-ресурсы + все системы с Guid); при загрузке секции с незарегистрированным Guid группы молча пропускаются.
Отдельный API зеркалирует Create/LoadEventsSnapshot:
// Сохранить
byte[] snapshot = W.Serializer.CreateSystemsSnapshot();
W.Serializer.CreateSystemsSnapshot("systems.bin", gzip: true);
// Загрузить (gzip определяется автоматически)
W.Serializer.LoadSystemsSnapshot(snapshot);
W.Serializer.LoadSystemsSnapshot("systems.bin");
Полные детали формата и миграции: см. Сериализация → Сериализация систем.