LeoECS - реализация событий в Entity Component System

Быстрая обработка данных в LeoECS - это хорошо, но что насчет оповещения других систем о каких-либо событиях? В ECS-подходе это решается довольно просто.

Так как “все есть данные”, то мы можем представить любое событие как блок этих самых данных (компонент в терминах ECS), который в дальнейшем можем удалить после обработки всеми нужными системами. Удалять можно как отдельной системой, которая будет выполняться последней, так и автоматически с использованием специального атрибута.
Пример создания события:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class DamageEvent {
public int DamageAmount;
}
class Unit {
public int Health;
}

[EcsInject]
class AddDamageToUnitsSystem : IEcsRunSystem {
// Auto-injected fields.
EcsWorld _world = null;
EcsFilter<Unit> _units = null;

void IEcsRunSystem.Run() {
foreach (var idx in _units) {
var dmgEvt = _world.AddComponent<DamageEvent>();
dmgEvt.DamageAmount = 10;
}
}
}
[EcsInject]
class ProcessDamageOnUnitsSystem : IEcsRunSystem {
// Auto-injected fields.
EcsWorld _world = null;
EcsFilter<Unit, DamageEvent> _damagedUnits = null;

void IEcsRunSystem.Run() {
foreach (var idx in _damagedUnits) {
_damagedUnits[idx].Health -= _damagedUnits[idx].DamageAmount;
_world.RemoveComponent<DamageEvent>(_damagedUnits.Entities[idx]);
}
}
}

Если мы подключим обе системы в порядке их описания, то получим постоянно уменьшающийся запас здоровья у юнитов.

В ProcessDamageOnUnitsSystem-системе видно, что мы удаляем компонент события руками - это вполне нормальное решение, но что если нам нужно обработать данное событие несколькими системами и только после этого удалить? Для этого мы можем вынести удаление в отдельную систему, либо воспользоваться специальным атрибутом [EcsOneFrame]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[EcsOneFrame]
class DamageEvent {
public int DamageAmount;
}
class Unit {
public int Health;
}
[EcsInject]
class AddDamageToUnitsSystem : IEcsRunSystem {
// Auto-injected fields.
EcsWorld _world = null;
EcsFilter<Unit> _units = null;

void IEcsRunSystem.Run() {
foreach (var idx in _units) {
var dmgEvt = _world.AddComponent<DamageEvent>();
dmgEvt.DamageAmount = 10;
}
}
}
[EcsInject]
class ProcessDamageOnUnitsSystem : IEcsRunSystem {
// Auto-injected fields.
EcsWorld _world = null;
EcsFilter<Unit, DamageEvent> _damagedUnits = null;

void IEcsRunSystem.Run() {
foreach (var idx in _damagedUnits) {
_damagedUnits[idx].Health -= _damagedUnits[idx].DamageAmount;
}
}
}

Код идентичный за исключением атрибута на компоненте DamageEvent и отсутствия явного удаления компонентов этого типа руками. Ядро ECS это сделает автоматически при вызове метода EcsWorld.RemoveOneFrameComponents:

1
2
3
4
5
6
7
8
9
10
11
class EcsStartup : MonoBehaviour {
EcsWorld _world;
EcsSystems _systems;
// initialization, etc.

// processing
void Update () {
_systems.Run ();
_world.RemoveOneFrameComponents ();
}
}

Событие может быть не только частью уже существующих сущностей (entity), но и быть независимым от них, например, пользовательский ввод:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[EcsOneFrame]
class UserInput {
public Vector2 Direction;
}
class Unit {
public Vector2 Position;
}
[EcsInject]
class GetUserInputSystem : IEcsRunSystem {
// Auto-injected fields.
EcsWorld _world = null;

void IEcsRunSystem.Run() {
var dir = new Vector2 (Input.GetAxis ("Horizontal"), Input.GetAxis ("Vertical"));
if (dir.sqrMagnitude > 0.1f) {
var input = _world.CreateEntityWith<UserInput>();
input.Direction = dir;
}
}
}
[EcsInject]
class ProcessUserInputSystem : IEcsRunSystem {
// Auto-injected fields.
EcsWorld _world = null;
EcsFilter<UserUnit> _unputs = null;
EcsFilter<Unit> _units = null;

const float UnitSpeed = 10f;

void IEcsRunSystem.Run() {
var speed = Time.deltaTime * UnitSpeed;
foreach (var idx in _inputs) {
var input = _inputs.Components1[idx];
foreach (var idx2 in _units) {
_units.Components1[idx2].Position += input.Direction * speed;
}
}
}
}

По сути данный подход напоминает “шину событий” (EventBus) - события могут обработаться всей последовательностью систем, а так же обработка может быть прекращена любой системой в момент ее выполнения.

Оформить подписку можно здесь: