У меня много скриптов. Я хочу иметь возможность управлять ими все в 1 скрипте. Я хочу, чтобы основной скрипт активировал определенный скрипт, а затем, когда вторичный скрипт завершен, он возвращает значение основному скрипту. После этого главный скрипт вызывает другой вторичный скрипт и т. Д. ...

Есть ли правильный способ сделать это?

Более точный вопрос:

    Можно ли активировать скрипт AHK из другого скрипта AHK?

    В настоящий момент, чтобы обнаружить, что на вторичном скрипте завершен, способ, которым я в настоящее время пользуюсь, заключается в том, что прямо перед завершением вторичного скрипта я нажимаю комбинацию ключей, которые обнаружит основной скрипт. И как только он будет обнаружен, он увеличит основную переменную сценария на один, и это вызовет активацию следующего скрипта. Есть ли лучший способ достичь этого?

0

3 ответы

Ниже приведен рабочий пример из документации:

; Example: Send a string of any length from one script to another. This is a working example. ; To use it, save and run both of the following scripts then press Win+Space to show an ; InputBox that will prompt you to type in a string. ; Save the following script as "Receiver.ahk" then launch it: #SingleInstance OnMessage(0x4a, "Receive_WM_COPYDATA") ; 0x4a is WM_COPYDATA return Receive_WM_COPYDATA(wParam, lParam) { StringAddress:= NumGet(lParam + 2*A_PtrSize) ; Retrieves the CopyDataStruct"s lpData member. CopyOfData:= StrGet(StringAddress) ; Copy the string out of the structure. ; Show it with ToolTip vs. MsgBox so we can return in a timely fashion: ToolTip %A_ScriptName%`nReceived the following string:`n%CopyOfData% return true ; Returning 1 (true) is the traditional way to acknowledge this message. } ; Save the following script as "Sender.ahk" then launch it. After that, press the Win+Space hotkey. TargetScriptTitle = Receiver.ahk ahk_class AutoHotkey #space:: ; Win+Space hotkey. Press it to show an InputBox for entry of a message string. InputBox, StringToSend, Send text via WM_COPYDATA, Enter some text to Send: if ErrorLevel ; User pressed the Cancel button. return result:= Send_WM_COPYDATA(StringToSend, TargetScriptTitle) if result = FAIL MsgBox SendMessage failed. Does the following WinTitle exist?:`n%TargetScriptTitle% else if result = 0 MsgBox Message sent but the target window responded with 0, which may mean it ignored it. return Send_WM_COPYDATA(ByRef StringToSend, ByRef TargetScriptTitle) ; ByRef saves a little memory in this case. ; This function sends the specified string to the specified window and returns the reply. ; The reply is 1 if the target window processed the message, or 0 if it ignored it. { VarSetCapacity(CopyDataStruct, 3*A_PtrSize, 0) ; Set up the structure"s memory area. ; First set the structure"s cbData member to the size of the string, including its zero terminator: SizeInBytes:= (StrLen(StringToSend) + 1) * (A_IsUnicode ? 2: 1) NumPut(SizeInBytes, CopyDataStruct, A_PtrSize) ; OS requires that this be done. NumPut(&StringToSend, CopyDataStruct, 2*A_PtrSize) ; Set lpData to point to the string itself. Prev_DetectHiddenWindows:= A_DetectHiddenWindows Prev_TitleMatchMode:= A_TitleMatchMode DetectHiddenWindows On SetTitleMatchMode 2 SendMessage, 0x4a, 0, &CopyDataStruct, %TargetScriptTitle% ; 0x4a is WM_COPYDATA. Must use Send not Post. DetectHiddenWindows %Prev_DetectHiddenWindows% ; Restore original setting for the caller. SetTitleMatchMode %Prev_TitleMatchMode% ; Same. return ErrorLevel ; Return SendMessage"s reply back to our caller. }

теоретически вы также можете использовать файловый объект между сценариями как метод stdin/stdout, так как при открытии файла с файловым объектом вы можете установить его как общий.

Вы также можете установить переменную окружения и передать имя переменной скрипту, учитывая, что у вас есть настройка аргумента в целевом скрипте, который затем устанавливает значение переменной среды при закрытии. используя RunWait, и вы можете узнать, что результат возврата скрипта после запуска.

Наконец, изучите использование функции, поскольку это, вероятно, «лучшая практика» для того, что вы пытаетесь сделать. Поскольку функция может делать все, что может сделать сценарий, и вы можете передать ей массив для работы с или с помощью ByRef в параметре массива. Это означало бы, что вам не нужно писать в кучу параметров при записи функции, и переменные будут освобождать память после завершения функции автоматически. Вы даже можете записать свои функции в отдельный файл и использовать #Include, чтобы использовать их в своем скрипте.

2018-06-22T12:41:05+00:00

Проблема корректного создания и освобождения COM-объектов в любом managed языке (со сборщиком мусора) сложна и многогранна - столько всего уже написано на эту тему и всё равно возникают постоянные споры и недопонимания на форумах.

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

Я лишь опишу свой опыт применительно к использованию OneScript для общения с базами 1С через внешнее соединение при запуске из Обновлятора (хотя способ запуска на самом деле не имеет значения).

При этом я не буду останавливаться на самом понятии COM-объекта (в этом смысле я всех отсылаю к замечательной книге "Основы COM" Дейла Роджерсона).

Также я не буду останавливаться на том, как COM-объекты уживаются в языках с автоматическим управлением памятью, к которым относится в том числе OneScript.

В этой статье будут лишь практические выводы.

Суть проблемы

А проблема состоит в том, что при выполнении кода через внешнее соединение с базой (которое само по себе является COM-объектом) порождается большое количество как явных (которые мы сами объявили), так и неявных COM-объектов.

И если мы не уничтожаем эти объекты напрямую, то они уничтожаются автоматически в порядке и в момент, когда это сочтёт нужным сделать среда выполнения.

В целом в идеальном мире это не должно быть проблемой и COM-библиотеки должны учитывать этот момент. И если бы это было всегда так - мне не пришлось бы вообще писать эту статью.

К сожалению, практика в целом и применительно к COM-библиотеке для внешнего подключения к базам 1С в частности показывает, что порядок уничтожения всех COM-объектов должен быть задан явно и он должен быть обратным порядку их создания.

И если этого не делать, то наш скрипт будет отлично работать на одних компьютерах (или с одной платформой 1с) и при этом валиться с ошибкой на других компьютерах (других платформах 1с).

Ошибка будет возникать в самом конце работы скрипта при уничтожении COM-объектов сборщиком мусора. Такая ошибка будет нестабильной и в лучшем случае будет просто приводить к тому, что не будет корректно завершаться соединение с базой. То есть скрипт уже отработает, а консоль сервера 1с будет показывать, что соединение с базой ещё есть.

При этом сам скрипт отработает замечательно и выполнит всё, что мы от него хотим, но вот само соединение с базой будет завершено некорректно и код ошибки от OneScript чаще всего будет -1073741819.

При этом в самих примерах на OneScript в Обновляторе я изначально не буду делать явное освобождение ресурсов, чтобы не отпугивать пользователей. Вместо этого я буду давать ссылку на эту статью с разбором простейших примеров.

Первый пример

Рассмотрим простейший скрипт по выводу списка пользователей:

Какие здесь COM-объекты мы видим:

  1. v8 - этот объект был создан обновлятором явно и уничтожается он в процедуре ПриОкончанииРаботы.
  2. v8.ПользователиИнформационнойБазы - здесь мы обратились через точку к менеджеру пользователей информационной базы и новый COM-объект был создан неявно средой выполнения OneScript. Это недопустимая для нас ситуация, так как мы не сможем освободить такой объект в нужный нам момент. Ниже я покажу как избавиться от такого неявного создания объекта.
  3. СписокПользователей - этот COM-объект нам вернул метод ПолучитьПользователей.
  4. Пользователь - этот COM-объект создаётся на каждой итерации цикла.

Вроде бы всё? А вот и нет. Здесь присутствует ещё один неявно создаваемый COM-объект внутри среды выполнения. И причина его создания - использование цикла Для Каждого. При использовании такого цикла создаётся итератор для СписокПользователей и этот итератор содержит внутренний COM-объект, который мы также не сможем освободить. Отсюда сразу правило - следует избегать циклов Для Каждого при обходе COM-коллекций.

А вот как следует переписать этот код, чтобы после его выполнения были явно и в нужном порядке освобождены все созданные в нём COM-объекты:

ПользователиИнформационнойБазы = Неопределено ; СписокПользователей = Неопределено ; Попытка ПользователиИнформационнойБазы = v8. ПользователиИнформационнойБазы; СписокПользователей = ПользователиИнформационнойБазы. ПолучитьПользователей() ; Сообщить("Выводим всех пользователей базы:" ) ; Для Индекс = 0 По СписокПользователей. Количество() - 1 Цикл Пользователь = СписокПользователей. Получить(Индекс) ; Сообщить(Пользователь. Имя) ; ОсвободитьОбъект(Пользователь) ; КонецЦикла ; Исключение КонецПопытки; Если СписокПользователей <> Неопределено Тогда ОсвободитьОбъект(СписокПользователей) ; КонецЕсли ; Если ПользователиИнформационнойБазы <> Неопределено Тогда ОсвободитьОбъект(ПользователиИнформационнойБазы) ; КонецЕсли ;

Обратите внимание, что здесь мы:

  1. Сохранили обращение к менеджеру информационных баз в отдельную переменную, чтобы затем явно вызвать его освобождение.
  2. Избавились от цикла Для Каждого.
  3. На каждом шаге цикла освобождаем объект Пользователь.
  4. Обернули весь код в блок Попытка Исключение, чтобы после его выполнения (целиком или частично в случае ошибок) гарантированно освободить все созданные COM-объекты. При этом мы опустили обработку ошибок (ничего не написали внутри блока Исключение КонецПопытки).

Второй пример

Предположим, что мы программно создаём обработку из конфигурации базы, чтобы запустить её выполнение из нашего кода.

Код создания обработки будет таким:

  1. Неявно создаётся COM-объект v8.Обработки
  2. Неявно создаётся COM-объект v8.Обработки.ИмпортКейса
  3. Явно создаётся COM-объект обработки и сохраняется в переменной МодульЗагрузки.

При таком коде мы сможем явно освободить только МодульЗагрузки, а вот с двумя неявно созданными COM-объектами мы ничего поделать не сможем.

Поэтому такой код должен быть переписан вот так:

Обработки = Неопределено ; ИмпортКейса = Неопределено ; МодульЗагрузки = Неопределено ; Попытка Обработки = v8. Обработки; ИмпортКейса = Обработки. ИмпортКейса; МодульЗагрузки = ИмпортКейса. Создать() ; // остальной код... Исключение КонецПопытки; Если МодульЗагрузки <> Неопределено Тогда ОсвободитьОбъект(МодульЗагрузки) ; КонецЕсли ; Если ИмпортКейса <> Неопределено Тогда ОсвободитьОбъект(ИмпортКейса) ; КонецЕсли ; Если Обработки <> Неопределено Тогда ОсвободитьОбъект(Обработки) ; КонецЕсли ;

Третий пример

А что будет, если мы в нашем скрипте выполним вот такой код (выдержка из предыдущего примера):

Обработки = v8. Обработки; ИмпортКейса = Обработки. ИмпортКейса; ИмпортКейса. Создать() ;

Обратите внимание на то, что мы вызвали метод Создать(), который вернул нам COM-объект, но мы его никуда не сохранили.

Такой код будет ошибкой, так как если метод возвращает COM-объект, то этот объект остаётся висеть в памяти, даже если мы его не сохранили и не работаем с ним в коде.

Да, в этом случае такой код не имел бы смысла (зачем создавать экземпляр обработки и не использовать его), но могут быть ситуации, когда мы вызываем некоторый метод у COM-объекта и не обрабатываем результат этого метода, так как он нам не важен. И вот если в этой ситуации окажется, что результат метода тоже COM-объект, который мы не сохранили и соотв. не освободили явно - нас ждут проблемы.

Большой пример скрипта

В качестве реального примера скрипта, который написан по всем правилам освобождения COM-объектов я предлагаю рассмотреть код загрузки комплектов отчётности в формате Repx. Его можно найти на github .

И это всё?

К сожалению, не всё и есть более сложные ситуации связанные с подсчётом ссылок на COM-объекты, которые могут приводить к проблемам. Я не буду приводить их в статье, чтобы не запутать вас окончательно.

Вы можете присылать ([email protected]) мне примеры кода, когда вам так и не удалось добиться корректного освобождения COM-объектов, и я постараюсь вам помочь в меру своих сил.

А можно не заморачиваться?

Я согласен, что написание реального кода, в котором явно и в нужном порядке освобождаются все COM-объекты задача не из лёгких, так как способов "выстрелить себе в ногу" при этом предостаточно.

Вы можете писать код точно также, как будто он выполняется прямо в базе, и игнорировать код ошибки, который вам возвращает OneScript.

При такой стратегии я рекомендую процедуру ПриОкончанииРаботы переписать с достаточно большой паузой в конце - как показывает практика это несколько повышает шансы на то, что оставшиеся в памяти COM-объекты будут завершены без ошибок.

Вот этот код:

Процедура ПриОкончанииРаботы() Если v8 <> Неопределено Тогда Попытка ОсвободитьОбъект(v8) ; v8 = Неопределено ; Исключение КонецПопытки; КонецЕсли ; Если connector <> Неопределено Тогда Попытка ОсвободитьОбъект(connector) ; connector = Неопределено ; Исключение КонецПопытки; КонецЕсли ; Если updater <> Неопределено Тогда Попытка ОсвободитьОбъект(updater) ; updater = Неопределено ; Исключение КонецПопытки; КонецЕсли ; // Ожидание в конце выполнения программы // магическим образом помогает избежать // проблем с освобождением ресурсов, если // мы использовали внешнее подключение к // базе. Приостановить(10000 ) ; // 10 секунд Если errors Тогда ЗавершитьРаботу(1 ) ; КонецЕсли ; КонецПроцедуры

А есть ли альтернатива?

Есть альтернативный способ пакетного выполнения программного кода в базах.

Даже средний Unity3D проект очень быстро наполняется большим количеством разнообразных скриптов и возникает вопрос взаимодействия этих скриптов друг с другом.
Данная статья предлагает несколько различных подходов к организации таких взаимодействий от простого до продвинутого и описывает к каким проблемам может привести каждый из подходов, а так же предложит способы решения этих проблем.

Подход 1. Назначение через редактор Unity3D

Пусть у нас в проекте есть два скрипта. Первый скрип отвечает за начисление очков в игре, а второй за пользовательский интерфейс, который, отображает количество набранных очков на экране игры.
Назовем оба скрипта менеджерами: ScoresManager и HUDManager.
Каким же образом менеджеру, отвечающему за меню экрана можно получить текущее количество очков от менеджера, отвечающего за начисление очков?
Предполагается, что в иерархии объектов(Hierarchy) сцены существуют два объекта, на один из которых назначен скрипт ScoresManager, а на другой скрипт HUDManager.
Один из подходов, содержит следующий принцип:
В скрипте UIManager определяем переменную типа ScoresManager:

Public class HUDManager: MonoBehaviour { public ScoresManager ScoresManager; }
Но переменную ScoresManager необходимо еще инициализировать экземпляром класса. Для этого выберем в иерархии объектов объект, на который назначен скрипт HUDManager и в настройках объекта увидим переменную ScoresManager со значением None.

После чего, у нас появляется возможность из кода HUDManager обращаться к скрипту ScoresManager, таким образом:

Public class HUDManager: MonoBehaviour { public ScoresManager ScoresManager; public void Update () { ShowScores(ScoresManager.Scores); } }
Все просто, но игра, не ограничивается одними набранными очками, HUD может отображать текущие жизни игрока, меню доступных действия игрока, информацию о уровне и многое другое. Игра может насчитывать в себе десятки и сотни различных скриптов, которым нужно получать информацию друг от друга.
Чтобы получить в одном скрипте данные из другого скрипта нам каждый раз придется описывать переменную в одном скрипте и назначать (перетаскивать вручную) ее с помощью редактора, что само по себе нудная работа, которую легко можно забыть сделать и потом долго искать какая из переменных не инициализирована.
Если мы захотим что-то отрефакторить, переименовать скрипт, то все старые инициализации в иерархии объектов, связанные с переименованным скриптом, сбросятся и придется их назначать снова.
В то же время, такой механизм не работает для префабов (prefab) - динамического создания объектов из шаблона. Если какому-либо префабу нужно обращаться к менеджеру, расположенному в иерархии объектов, то вы не сможете назначить самому префабу элемент из иерархии, а придется сначала создать объект из префаба и после этого программно присвоить экземпляр менеджера переменной только что созданного объекта. Не нужная работа, не нужный код, дополнительная связанность.
Следующий подход решает все эти проблемы.

Подход 2. «Синглтоны»

Применим упрощенную классификацию возможных скриптов, которые используются при создании игры. Первый тип скриптов: «скрипты-менеджеры», второй: «скрипты-игровые-объекты».
Основное отличие одних от других в том, что «скрипты-менеджеры» всегда имеют единственный экземпляр в игре, в то время как «скрипты-игровые-объекты» могут иметь количество экземпляров больше единицы.

Примеры

Как правило, в единственном экземпляре существуют скрипты, отвечающие за общую логику пользовательского интерфейса, за проигрывание музыки, за отслеживание условий завершения уровня, за управление системой заданий, за отображение спецэффектов и так далее.
В то же время, скрипты игровых объектов существуют в большом количестве экземпляров: каждая птичка из «Angry Birds» управляется экземпляром скрипта птички со своим уникальным состоянием; для любого юнита в стратегии создается экземпляр скрипта юнита, содержащий его текущее количество жизней, позицию на поле и личную цель; поведение пяти разных иконок обеспечивается различными экземплярами одних и тех же скриптов, отвечающих за это поведение.
В примере из предыдущего шага скрипты HUDManager и ScoresManager всегда существуют в единственном экземпляре. Для их взаимодействия друг с другом применим паттерн «синглтон» (Singleton, он же одиночка).
В классе ScoresManager опишем статическое свойство типа ScoresManager, в котором будет храниться единственный экземпляр менеджера очков:

Public class ScoresManager: MonoBehaviour { public static ScoresManager Instance { get; private set; } public int Scores; }
Осталось инициализировать свойство Instance экземпляром класса, который создает среда Unity3D. Так как ScoresManager наследник MonoBehaviour, то он участвует в жизненном цикле всех активных скриптов в сцене и во время инициализации скрипта у него вызывается метод Awake. В этот метод мы и поместить код инициализации свойства Instance:

Public class ScoresManager: MonoBehaviour { public static ScoresManager Instance { get; private set; } public int Scores; public void Awake() { Instance = this; } }
После чего, использовать ScoresManager из других скриптов можно следующим образом:

Public class HUDManager: MonoBehaviour { public void Update () { ShowScores(ScoresManager.Instance.Scores); } }
Теперь нет необходимости в HUDManager описывать поле типа ScoresManager и назначать его в редакторе Unity3D, любой «скрипт-менеджер» может предоставлять доступ к себе через статическое свойство Instance, которое будет инициализировать в функции Awake.

Плюсы

- нет необходимости описывать поле скрипта и назначать его через редактор Unity3D.
- можно смело рефакторить код, если что и отвалится, то компилятор даст знать.
- к другим «скриптам-менеджерам» теперь можно обращаться из префабов, через свойство Instance.

Минусы

- подход обеспечивает доступ только к «скриптам-менеджерам», существующим в единственном экземпляре.
- сильная связанность.
На последнем «минусе» остановимся подробнее.
Пусть мы разрабатываем игру, в которой есть персонажи (unit) и эти персонажи могут погибать (die).
Где-то находится участок кода, который проверяет не погиб ли наш персонаж:

Public class Unit: MonoBehaviour { public int LifePoints; public void TakeDamage(int damage) { LifePoints -= damage; if (LifePoints <= 0) Die(); } }
Каким образом игра может отреагировать на смерть персонажа? Множеством разнообразных реакций! Приведу несколько вариантов:
- надо удалить персонажа из сцены игры, чтобы он больше не отображался на ней.
- в игре начисляются очки за каждого погибшего персонажа, нужно их начислить и обновить значение на экране.
- на специальной панели отображаются все персонажи в игре, где мы можем выбрать конкретного персонажа. При смерти персонажа, нам нужно обновить панель, либо убрать персонажа с нее, либо отобразить что он мертв.
- нужно проиграть звуковой эффект смерти персонажа.
- нужно проиграть визуальный эффект смерти персонажа (взрыв, брызги крови).
- система достижений игры имеет достижение, которое считает общее число убитых персонажей за все время. Нужно добавить к счетчику только что умершего персонажа.
- система аналитики игры отправляет на внешний сервер факт смерти персонажа, нам этот факт важен для отслеживания прогресса игрока.
Учитывая все вышеперечисленное, функция Die может выглядеть следующим образом:

Private void Die() { DeleteFromScene(); ScoresManager.Instance.OnUnitDied(this); LevelConditionManager.Instance.OnUnitDied(this); UnitsPanel.Instance.RemoveUnit(this); SoundsManager.Instance.PlayUnitDieSound(); EffectsManager.Instance.PlaySmallExplosion(); AchivementsManager.Instance.OnUnitDied(this); AnaliticsManager.Instance.SendUnitDiedEvent(this); }
Получается, что персонаж после совей смерти должен разослать всем компонентам, которые в ней заинтересованы этот печальный факт, он должен знать о существовании этих компонентов и должен знать, что они им интересуются. Не слишком ли много знаний, для маленького юнита?
Так как игра, по логике, очень связанная структура, то и события происходящие в других компонентах интересуют третьи, юнит тут ничем не особенный.
Примеры таких событий (далеко не все):
- Условие прохождение уровня зависит от количества набранных очков, набрали 1000 очков – прошли уровень (LevelConditionManager связан с ScoresManager).
- Когда набираем 500 очков, достигаем важную стадию прохождения уровня, нужно проиграть веселую мелодию и визуальный эффект (ScoresManager связан с EffectsManager и SoundsManager).
- Когда персонаж восстанавливает здоровье, нужно проиграть эффект лечения над картинкой персонажа в панели персонажа (UnitsPanel связан с EffectsManager).
- и так далее.
В результате таких связей мы приходим к картине похожей на следующую, где все про всех все знают:

Пример со смертью персонажа немного преувеличен, сообщать о смерти (или другом событии) шести разным компонентам не так часто приходится. Но варианты, когда при каком-то событии в игре, функция, в которой произошло событие, сообщает об этом 2-3 другим компонентам встречается сплошь и рядом по всему коду.
Следующий подход пытается решает эту проблему.

Подход 3. Мировой эфир (Event Aggregator)

Введем специальный компонент «EventAggregator», основная функция которого хранить список событий, происходящих в игре.
Событие в игре - это функционал, предоставляющий любому другому компоненту возможность как подписаться на себя, так и опубликовать факт совершения этого события. Реализация функционала события может быть любой на вкус разработчика, можно использовать стандартные решения языка или написать свою реализацию.
Пример простой реализации события из прошлого примера (о смерти юнита):

Public class UnitDiedEvent { private readonly List> _callbacks = new List>(); public void Subscribe(Action callback) { _callbacks.Add(callback); } public void Publish(Unit unit) { foreach (Action callback in _callbacks) callback(unit); } }
Добавляем это событие в «EventAggregator»:

Public class EventAggregator { public static UnitDiedEvent UnitDied; }
Теперь, функция Die из предыдущего примера с восемью строчками преобразуется в функцию с одной строчкой кода. Нам нет необходимости сообщать о том, что юнит умер всем заинтересованным компонентам и знать о этих заинтересованных. Мы просто публикуем факт свершения события:

Private void Die() { EventAggregator.UnitDied.Publish(this); }
А любой компонент, которому интересно это событие, может отреагировать на него следующим образом (на примере менеджера отвечающего за количество набранных очков):

Public class ScoresManager: MonoBehaviour { public int Scores; public void Awake() { EventAggregator.UnitDied.Subscribe(OnUnitDied); } private void OnUnitDied(Unit unit) { Scores += CalculateScores(unit); } }
В функции Awake менеджер подписывается на событие и передает делегат, отвечающий за обработку этого события. Сам же обработчик события, принимает в качестве параметра экземпляр умершего юнита и добавляет количество очков в зависимости от типа этого юнита.
Таким же образом, все другие компоненты, кому интересно событие смерти юнита, могут подписаться на него и обработать, когда событие произойдет.
В результате, диаграмма связей между компонентами, когда каждая компонента знала друг о друге, превращается в диаграмму, когда компоненты знают только о событиях, которые происходят в игре (только о интересующих их событиях), но им все равно, от куда эти события пришли. Новая диаграмма будет выглядеть следующим образом:

Я же люблю другую интерпретацию: представьте, что прямоугольник «EventAggregator» растянулся во все стороны и захватил внутрь себя все остальные прямоугольники, превратившись в границы мира. В моей голове, на этой диаграмме «EventAggregator» вообще отсутствует. «EventAggregator» это просто мир игры, некий «игровой эфир», куда различные части игры кричат «Эй, народ! Юнит такой-то умер!», и все прослушивают эфир и если какое-то из услышанных событий их заинтересует, они на него отреагируют. Таким образом - связей нет, каждый компонент независим.
Если я компонент и отвечаю за публикацию какого-то события, то я кричу в эфир мол этот умер, этот получил уровень, снаряд врезался в танк. И мне наплевать интересно кому-нибудь об этом. Возможно, никто не слушает это событие сейчас, а может на него подписана сотня других объектов. Меня, как автора события, это ни грамма не волнует, я про них ничего не знаю и знать не хочу.
Такой подход позволяет легко вводить новый функционал без изменения старого. Допустим, в готовую игру мы решили добавить систему достижений. Мы создаем новую компоненту системы достижений и подписываемся на все интересующие нас события. Никакой другой код не меняется. Не надо ходить по другим компонентам и из них вызывать систему достижений и говорить ей мол и мое событие посчитай пожалуйста. К тому же, все кто публикуют события в мире ничего не знают о системе достижений, даже о факте ее существования.

Замечание

Говоря, что никакой другой код не меняется, я конечно немножко лукавлю. Может оказаться так, что систему достижений интересуют события, которые ранее просто не публиковались в игре, потому как ни одну другую систему до этого не интересовали. И в этом случае, нам нужно будет решить какие новые события добавить в игру и кто будет их публиковать. Но в идеальной игре уже все возможные события есть и эфир наполнен ими по полной.

Плюсы

- не связанность компонентов, мне достаточно просто опубликовать событие, а кого оно интересует не имеет значение.
- не связанность компонентов, я просто подписываюсь на нужные мне события.
- можно добавлять отдельные модули без изменения в существующем функционале.

Минусы

- нужно постоянно описывать новые события и добавлять их в мир.
- нарушение функциональной атомарности.

Последний минус рассмотрим более детально

Представим, что у нас есть объект «ObjectA», в котором вызывается метод «MethodA». Метод «MethodA», состоит из трех шагов и вызывает внутри себя три других метода, которые выполняют эти шаги последовательно («MethodA1», «MethodA2» и «MethodA3»). Во втором методе «MethodA2» происходит публикация какого-то события. И тут происходит следующее: все кто подписан на это событие начнут его обрабатывать, выполняя какую-то свою логику. В этой логике тоже может произойти публикация других событий, обработка которых также может привести к публикации новых событий и так далее. Дерево публикаций и реакции в отдельных случаях может очень сильно разрастись. Такие длинные цепочки крайне тяжело отлаживать.
Но самая страшная проблема, которая тут может произойти, это когда одна из веток цепочки приводит обратно в «ObjectA» и начинает обрабатывать событие путем вызова какого-то другого метода «MethodB». Получается, что метод «MethodA» у нас еще не выполнил все шаги, так как был прерван на втором шаге, и содержит сейчас в себе не валидное состояние (в шаге 1 и 2 мы изменили состояние объекта, но последнее изменение из шага 3 еще не сделали) и при этом начинается выполняться «MethodB» в этом же объекте, имея это не валидное состояние. Такие ситуации порождают ошибки, очень сложно отлавливаются, приводят к тому, что надо контролировать порядок вызова методов и публикации событий, когда по логике этого делать нет необходимости и вводят дополнительную сложность, которую хотелось бы избежать.

Решение

Решить описанную проблему не сложно, достаточно добавить функционал отложенной реакции на событие. В качестве простой реализации такого функционала мы можем завести хранилище, в которое будем складывать произошедшие события. Когда событие произошло, мы не выполняем его немедленно, а просто сохраняем где-то у себя. И в момент наступления очереди выполнения функционала какой-то компоненты в игре (в методе Update, например) мы проверяем на наличие произошедших событий и выполняем обработку, если есть такие события.
Таким образом, при выполнении метода «MethodA» не происходит его прерывание, а опубликованное событие все заинтересованные записывают себе в специальное хранилище. И только после того как к заинтересованным подписчикам дойдет очередь, они достанут из хранилища событие и обработают его. В этот момент весь «MethodA» будет завершен и «ObjectA» будет иметь валидное состояние.

Заключение

Компьютерная игра это сложная структура с большим количеством компонентов, которые тесно взаимодействуют друг с другом. Можно придумать множество механизмов организации этого взаимодействия, я же предпочитаю механизм, описанный мною, основанный на событиях и к которому я пришел эволюционным путем прохода по всевозможным граблям. Надеюсь кому-нибудь он тоже понравится и моя статья внесет ясность и будет полезной.