Вы можете думать о EntityCommandBuffer / EntityCommandBuffer.Concurrent как об инструменте, позволяющем избежать ограничений на то, что вы можете делать на работе. В основном, в работе вы не можете использовать EntityManager, потому что это вызывает структурные изменения, поэтому вы откладываете действие на потом, которое произойдет в BarrierSystem. Но я только что открыл с ними крутые трюки.

Представьте, что у вас есть 6 * почти * не связанных заданий в одной системе, и все они читают из ComponentDataArray<T>, но только одно из них записывает в нее. Теперь в сообщении об ошибке говорится, что один из них читает, когда кто-то пишет, пожалуйста, добавьте зависимости и т. Д. Невозможность сказать [ReadOnly] в одном из них вынуждает вас добавлять зависимости друг от друга.

Но! Если вы используете commandBuffer.SetComponentData, чтобы написать «позже», теперь вы действительно можете написать туда, где вы сказали [ReadOnly]! Если немедленное написание не важно, я думаю, что это очень хорошая оптимизация. Вы также должны взвесить стоимость воспроизведения команд, но я думаю, что выгода от включенного параллелизма, вероятно, будет намного больше. (как правило?)

Только не ставьте случайно неверные команды в очередь! Например, добавление двух повторяющихся компонентов, установка компонента, когда его нет, и т. Д. Это сложно в командном буфере, поскольку у нас нет возможности проверить, допустимо действие или нет во время выполнения. Вы можете выделить NativeHashMap<Entity,bool> внутри задания, чтобы помнить, для какого объекта вы уже выполнили команду.

Некоторые примечания о зависимостях барьеров и внедрении барьеров

Напомню, что BarrierSystem работает, когда вы вводите их в JobComponentSystem. В AfterOnUpdate в JCS он будет уведомлять все введенные барьеры для команд воспроизведения. Перед воспроизведением все задания будут .Complete для создания определенной точки синхронизации.

Если я правильно помнил из исходного кода, дело не в том, что каждое текущее выполняемое задание будет .Complete (это EntityManager.CompleteAllJobs), а только заданиями из систем, которые вводят барьер.

Когда вы указываете зависимость, ясно дайте понять, что вы действительно хотите дождаться системы или ТАКЖЕ воспроизведения команды от внедренного барьера? Здесь у меня есть система, которая ставит команду в очередь на свой барьер.

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

Но в отладчике сущностей он скажет просто «Барьер», что может быть неудобно, если у вас их так много.

НО любой класс на самом деле не владеет классом барьера, я просто помещаю определение внутри класса для более аккуратного кода, но, пожалуйста, относитесь к нему как к отдельной системе. Любая система использует барьер путем «внедрения системы» (вы [вводите] имя системы, которое не влияет на критерии активации, вы можете сделать это с любой системой, а не только с барьером)

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

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

Также он может действовать как UpdateGroup, потому что любой, кому нужны данные, может указать UpdateAfter на этот барьер, и они получат то, что хотят.

О порядке обновления барьера

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

Барьер был бы бесполезен, если бы он активировался раньше, чем система, которая будет отдавать ему команды. (На самом деле не бесполезно, думаю, в следующем кадре команда все равно будет выполняться)

Например, если система A вводит барьер B и использует его, необходимо ли ставить [UpdateBefore (B)] на A или [UpdateAfter (A)] на барьер B?

Я выяснил, что в основном порядок соответствует назначению без явного порядка обновления, поскольку наша UpdateBefore/After цепочка заставляет это делать, но это не гарантия. Обычно это так, если у вас есть более поздняя система, указывающая UpdateAfter барьер в цепочке, и у барьера нет другого выбора, кроме как запускать что-то, что команды очереди, но когда вы выключаете некоторые системы, порядок может пойти не так, и вы получите ошибки потому что любая система (включая барьерную «систему») хочет работать как можно раньше. Поэтому рекомендуется правильно навести порядок обновления на барьере.

Стоимость воспроизведения EntityCommandBuffer / BarrierSystem

Это график с глубоким профилем барьера, заполненный множеством команд компонента «Добавить» и «Установить».

Я обнаружил, что Set очень дешев, а Add стоит дорого (около 20-30 раз от набора?), Поскольку он должен перемещать объект в кусок. Но в первом добавлении в такой барьерной системе всегда есть это «BeforeStructuralChange», которого не происходит с более поздними добавлениями. Более поздние добавления, которые вы видите здесь, на самом деле чередуются между некоторыми наборами, но они настолько малы, что вы не можете их увидеть.

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

RemoveComponent, кстати, то же самое, что и Add, поскольку меняет структуру размещения данных. Вы видите, что первое удаление нужно BeforeStructuralChange

Но! Что это за 2-е более длинное удаление, у которого длиннее CopyManagedObjects? Я думаю…

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

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

PostUpdateCommands сейчас не так хорош!

Это также относится к PostUpdateCommands из ComponentSystem. Вы можете думать об этом как о барьере, который запускается сразу после OnUpdate, и иногда лучше НЕ его использовать, а использовать барьер. Раньше я думал, что ComponentSystem это привилегия - иметь то, чего JobComponentSystem нет, но нет ... это просто удобство. Если вы используете это везде только потому, что не хотите аннулировать промежуточное обновление, это как если бы у вас повсюду были препятствия!

Тактика оптимизации барьеров

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

Первое красное поле - это барьер, на котором некоторые JobComponentSystem системы ранее ставили в очередь команды. Как видите, BeforeStructuralChange означает, что он содержит add-remove. Затем второе красное поле - это ComponentSystem со своим собственным барьером привилегий PostUpdateCommands, вы снова видите, что он, вероятно, содержит добавление-удаление. Вы также видите, что он воспроизводится сразу после OnUpdate, как следует из названия.

Желтая линия показывает мою [UpdateAfter] зависимость, которую я установил. Поэтому разумнее не использовать PostUpdateCommands, а затем поместить все команды в этот первый барьер красного поля, а затем вся желтая линия должна ссылаться на этот барьер. Первое красное поле должно автоматически переместиться немного позже, когда вы снова протестируете игру. Вы можете сэкономить один кусок BeforeStructuralChange !! Когда вы видите два места воспроизведения ECB в нескольких шагах друг от друга, это возможность их объединить.

(В реальном устройстве BeforeStructuralChange не такой большой, здесь он большой, потому что система безопасности помечает массив как недействительный, поэтому при нарушении правила это может выдать вам ошибки. Но все же объединение барьеров лучше, чем нет)

Тогда вас может раздражать то, что вы создали барьер для одной цели (если посмотреть на имя MissCheckingBarrier, это означает, что команды, которые у него есть, будут касаться проверки пропусков… и так далее), и теперь он будет «испорчен» набором других команд. из системы подсчета очков… но просто переименуйте его в MissCheckAndScoringBarrier и получите результат.

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

Вы не можете выполнить пакетную компиляцию Entity Command Buffer

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

Https://forum.unity.com/threads/anyone-got-burst-working-with-entitycommanuffer.543789/

Единственное, что работает сейчас (8.05.2018), - это ECB.DestroyEntity! Эта работа компилируется и работает нормально.

Бонус: EndFrameBarrier

Кстати, Unity также предоставила специальную версию: EndFrameBarrier. Так что вам не нужно вводить этот порядок обновления каждый раз, когда вы хотите, чтобы действие вступило в силу не сразу после задания, а в самом конце этого кадра. Initialization, которое вы видите здесь, на самом деле означает в начале следующего кадра, но это почти то же значение, что и конечный кадр.

Так что вспомнили, что PostUpdateCommand это плохо? Если вы думаете, что ваши команды больше никому не понадобятся в этом кадре, тогда лучшим местом назначения будет EndFrameBarrier! Просто [Inject] EndFrameBarrier efb; сделайте из этого ECB и используйте его вместо PostUpdateCommand. Структурное изменение только один раз за раз. Здорово!

Бонус 2: Модульное тестирование

При модульном тестировании не забудьте вызвать .Update на барьере, чтобы он запустился. Или поставленные в очередь команды из вашей системы не будут выполняться. Кроме того, когда вы world.CreateManager<SystemThatInjectsABarrier>, вы также автоматически создаете барьер. (согласно правилу «системного внедрения»). Если в следующей строке написано world.CreateManager<BarrierName>, это будет ошибкой.

В любом случае при тестировании лучше всего использовать world.GetOrCreateManager, так как без вашего ведома трудно узнать, какое создание какой системы будет производить, сколько других систем.

Бонус 3: Ручной барьер

Вы можете ввести барьер, создать из него ECB, указать ECB не воспроизводить, а затем вручную .PlayBack() введенный барьер самостоятельно.