Версия изменения блока работает на уровне блока. Это видно из названия, я знаю! Но я споткнулся. Что такое кусок? Пожалуйста, подумайте.

У меня есть система, которая работает с компонентом A. Я хочу, чтобы она работала только при изменении A. Я также хочу использовать итерацию фрагментов, потому что API итерации фрагментов должен быть абсолютной базой, и у меня там будет наибольшая свобода (внутри того, что возможно с ECS).

Измененная версия Chunk может стать вашим лучшим дизайнерским инструментом при создании умной / оптимизированной системы. Тебе лучше знать это наизнанку! «Тупая» система - это система, которая использует ваш драгоценный цикл процессора для выполнения работы, не имеющей никакого значения. В версии изменения блока Chunk главное - пропускать работы. Мы все любим пропускать работы, верно?

Поэтому я вставляю DidChange во время итерации кусков. Я тестировал его с 1 Entity, и он работает! Система всегда обновляется, но преждевременно выходит, если ничего не изменилось.

Концепции управления версиями

Он начинается с этих двух методов, которые находятся в основном потоке вашей системы. (Учитывая, что ваша система прошла проверку ShouldRunSystem, то есть есть что ввести и т. Д.)

Что внутри?

Глобальная версия системы

Общий глобальный int, глобальный, как в «ECS World» (буквально «глобус»), потому что EntityManager хранит его, и у вас может быть только один EntityManager на мир.

Нам нужны 2 стороны версии для обнаружения изменений

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

Сторона A: Версия в системе

При каждом обновлении вашей системы

  • Перед OnUpdate это глобальное число увеличивается на 1.
  • Версия системы еще не обновлена ​​до этого недавно увеличенного глобального номера. Этот еще не обновленный номер версии системы отображается в защищенном свойстве LastSystemVersion.
  • После OnUpdate ваша система запомнит эту увеличенную глобальную версию как свой собственный номер версии.
  • Все фильтры вашей группы автоматически получат версию этой системы, которая еще не обновлена. Если вы не используете фильтр, вы собираетесь использовать это свойство вручную с итерацией фрагментов.

Теперь у нас есть одна сторона, это глобальная версия, распространяемая по системам. Другая сторона - это глобальная версия, распространяемая по данным. (Система - это не данные!)

Сторона B: версия чанка.

Вы знаете, что глобальная версия влияет на 2 номера версии (текущая версия системы + LastSystemVersion), запоминаемые для каждой системы, теперь давайте посмотрим, как номер версии запекается в обновлениях данных. Итак, наконец, когда мы получаем 2 стороны, мы можем сравнить «это изменилось?».

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

  • Очевидно, что это делают такие методы, как EntityManager.SetComponentData ‹T›.
  • Простое получение ComponentDataArray ‹T› с доступом на запись еще не обновляет версию, пока вы действительно не измените данные с помощью этого индексатора int. (Но CDA в любом случае устарел)
  • Позже мы поговорим о том, чтобы не обновлять версию блока.

Один блок может содержать измененную версию для КАЖДОГО типа компонента.

Вы знаете, что один кусок строго принадлежит одному Archetype. Архетип состоит из нескольких типов. У каждого из этих типов есть версии!

Вот почему, когда вы делаете chunk.DidChange(___), вы должны сказать, какой тип, когда вы в первую очередь извлекаете кусок из ArchetypeChunkComponentType <T>.

Ориентир

Вы заметили, что он запрашивает LastSystemVersion. Это защищенное свойство каждой вашей системы.

Для каждого фрагмента уже отслеживается версия изменения для каждого компонента этого фрагмента. Изменить версию - это еще не состояние «истина / ложь», а всего лишь текущий номер. Чтобы получить логическое состояние, нам нужна точка отсчета. (из чего изменилось?) Какой другой номер версии.

DidChange - это метод фрагмента, который означает, что «этот фрагмент изменился относительно некоторого номера версии системы?» Вы уже знаете, что номер версии системы обновлен до globalVersion + 1 перед OnUpdate. НО, используя последнюю версию системы, вы сравниваете ее с моментом времени на предыдущем обновлении этой системы.

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

Версия 0

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

Сейчас в игре 3 версии:

  1. Глобальная версия системы: повышена на любой системе до обновления. Непрерывно распределяется на 2 стороны: система и чанки.
  2. Версия системы: обновляется после обновления системы. Также следите за его предыдущей версией. Распространяется на измененные фильтры автоматически. Используется при итерации фрагментов вручную.
  3. Версия в каждом блоке: обновляется до новейшей при доступе на запись.

Вы не одиноки в куске: «измененная» причина другой сущностью.

Но также, когда у меня есть несколько объектов, когда любой из них изменяется, флаг устанавливается для всего фрагмента. Chunk - это наиболее детализированный и эффективный уровень для Unity, позволяющий реализовать эту функцию. Обнаружение изменений для отдельных объектов должно осуществляться другими (более дорогостоящими) способами.

Я разрабатывал библиотеку пользовательского интерфейса ECS под названием «Izumi», которая работает наподобие Flux / Redux, где изменения данных транслируются для просмотра через версию изменения блока ECS. Вместо того, чтобы пытаться обнаружить изменение по отдельности, я ограничил себя другим ограничением дизайна и принял «изменение всего фрагмента».

Движение фрагмента: «изменено», вызвано изменением архетипа.

Позже я хочу добавить функциональность Hidden, поэтому я создал компонент тега. Привязка Hidden к некоторому объекту с помощью A вызывает перемещение фрагмента. Я вообще не трогал компонент A, но объект, который был присоединен только с A, теперь находится в новом чанке, который имеет версию 0 и будет обнаружен DidChange.

Поначалу это может быть не интуитивно понятно, когда то, что вы говорите в коде, было chunk.DidChange(typeofA), и вы не трогали A, вы также не «добавляли» A, но изменение было инициировано из-за перемещения фрагмента путем добавления / удаления другого несвязанный компонент, который теперь A считается «добавленным». (Помните, что версия 0 = добавлена ​​= изменена = DidChange)

Случай 1: Первопроходец

Я стреляю в себя, когда система, которая работает на измененном A, использовала DidChange и эта работа прикрепляет Hidden к этому объекту, когда происходит изменение. Представьте, что у нас есть только одна сущность с A, но ни у кого в Мире пока нет и A, и Hidden. Этот A - первый, который получит Hidden компонент. Затем я касаюсь некоторых данных в A, чтобы вызвать присоединение Hidden.

После присоединения это заставляет систему снова рекурсивно обрабатывать A, немедленно! Почему, потому что этот новый кусок (с архетипом A + Hidden) свежий. (В случае, если этот A будет первым, который будет присоединен к Hidden) версия по-прежнему равна 0 для обоих типов, что соответствует критериям DidChange.

Случай 2: больше иммигрантов

Что произойдет, если целевой блок содержит несколько других объектов, то есть A, на которых уже было Hidden. Позже еще один A был присоединен к Hidden и объединится с остальными.

Давайте начнем с основного: что происходит, когда сущность меняет архетип? Придется посмотреть на EntityDataManager.AddComponent. Добавление блока 100% изменения компонента, в противном случае это приведет к классической ошибке noob, в которой говорится, что компонент дублирован.

Все сводится к этому SetArchetype. Изменение архетипа одного Entity

Код гласит: «ChunkDataUtility преобразует одну вещь из старого фрагмента в новый фрагмент. Затем у нас есть дыра в старом фрагменте, в которую мы просто переместим последнюю вещь в старом фрагменте, чтобы заполнить этот пробел ».

ChunkDataUtility.Convert вот изюминка. Что происходит в целевом чанке?

… Похоже, ничего об изменении версии места назначения здесь нет. Тогда мы знаем, что ваша организация полностью отбрасывает свою старую версию блока и теперь использует версию целевого блока. Версия целевого чанка никоим образом не изменяется при добавлении / удалении компонента. (Но если вы затем используете SetComponent, чтобы присвоить ему значение, тогда, конечно, версия будет обновлена)

Представьте себе этот возможный сценарий. Чанк X: A, Чанк Y: A и Hidden

  1. Вы создали объект с A, вы изменили некоторые данные, и теперь его версия равна 4. Обновление системы с предыдущей версией ниже 4 обнаружит изменение DidChange
  2. Вы добавляете Hidden к этому одному объекту, он перемещается в блок Y. Блок Y был создан заново, поэтому версии для A и Hidden равны 0. Система, выполняющая итерацию блока с DidChange(A), видит, что это «Добавить» (= 0) и бежит бесконечно.
  3. Вы создали еще 1000 сущностей, используя только A. Вы так много раз меняли значение на них, но потом решили добавить Hidden ко всем сразу. Теперь ваша система видит, что все они «новые», потому что целевой блок по-прежнему имеет версию 0 как для A, так и для Hidden.

Здесь стоит отметить статус добавления DidChange (версия = 0). Статус «добавить» не будет удален, пока вы не коснетесь фрагмента. Но если вы не касаетесь этого фрагмента, и другие объекты решили перейти в этот фрагмент, все они получат статус «добавления» в целом. Как использовать это поведение зависит от вас.

"Измененный фильтр" ComponentGroup

Вы можете применить измененный фильтр к своей группе компонентов до 2-х типов. Это другой зверь, чем версия чанка. ComponentGroup содержит несколько фрагментов, которых на самом деле еще нет, пока вы не выполните GetComponentDataArray (или не выполните итерацию фрагментов с CreateArchetypeChunkArray), так как можно фильтровать группу еще до того, как мы что-то сделаем?

Не влияет на активацию системы

Я думаю, что фильтр для группы компонентов влияет только на итераторы, сделанные из них (например, ComponentDataArray), что позволяет уменьшить их длину до 0. Но даже если он равен 0, система будет работать, как если бы фильтра нет.

Разъяснение ситуации на примере

Обладая этими знаниями, мы уже можем моделировать некоторые ситуации. SystemA и SystemB имеют ComponentGroup Z. Таким образом, он работает с данными с ComponentDataArray ‹Z›. Обратите внимание, что CDA не рекомендуется, но вы можете представить себе запись данных с итерацией фрагментов.

  • SystemA запускает OnUpdate с глобальной версией 1. Текущая версия SystemA - 0. SystemA безоговорочно вносит некоторые изменения в ComponentDataArray ‹Z›, в результате чего все блоки, содержащие Z, имеют версию 1. После этого SystemA теперь также имеет версию 1.
  • SystemB запускает OnUpdate с глобальной версией 2. Все фильтры SystemB получают версию 0, потому что это номер версии SystemB. Может ли SystemB обнаруживать изменения? ДА, потому что у блока есть версия 1, он новее, чем у фильтра 0. После этого SystemB теперь хранит версию 2. SystemB только читает данные, эти блоки все еще имеют версию 1 .
  • SystemA снова обновляется, но ничего не делает. SystemA сейчас находится в версии 3. Эти блоки все еще находятся в версии 1.
  • SystemB снова запускается, обнаружит ли SystemB изменения? НЕТ, потому что в настоящее время фильтр SystemB имеет версию 2, но эти фрагменты все еще имеют версию 1. Этот фрагмент старше фильтра.
  • Затем SystemA решает прикрепить измененный фильтр к своему OnUpdate. Обнаружит ли SystemA «отсутствие изменений», поскольку ранее SystemB только считывала? НЕТ, SystemA видит, что «все изменилось», потому что версия фильтра по-прежнему равна нулю. Любой фрагмент новее версии 0.

Урок выучен

  • «Измененное» определение действует только в течение одного раунда обновления. «Раунд» относится к циклу обновления системы.
  • Представьте, что у вас есть система A B C D A B C D A B C D… работает
  • Если B прямо здесь A [B] CDABCDABCD вносит изменения, а все остальные обновления только читаются, тогда A [B] CDA B C D A B C D, система полужирным шрифтом может обнаружить это изменение, но все highlighted системы не могут обнаружить изменения.
  • B не может обнаружить собственное изменение в предыдущем раунде.
  • Присоединение измененного фильтра невозможно сразу использовать прямо в этом OnUpdate, вам нужно подождать еще один раунд.

CalculateLength ошибается? (превью 21)

Я заметил, что иногда CalculateLength использовал устаревший номер версии, давая «без изменений», даже если на самом деле он изменился. Лучше всего пока их избегать?

Обратите внимание, что есть этот комментарий, предполагающий, что итератор фрагментов компонентов не работает с измененной версией?

/*
 Can't be enabled yet because a bunch of tests rely on it, need to make some decisions on this...
#if ENABLE_UNITY_COLLECTIONS_CHECKS
                if ((filter.Type & FilterType.Changed) != 0)
                {
                    throw new System.ArgumentException("CalculateLength() can't be used with change filtering since it requires all component data to have been processed");
                }
#endif
*/

Номер версии итерации фрагмента дает ожидаемый результат одновременно с ошибкой CG.

Когда CG возвращает длину 0 из-за фильтра, я попытался создать массив ArchetypeChunk из того же CG, и DidChange показывает, что он фактически изменен (правильно). Ну что ж…

Проблема «Отсутствует изменение» - когда задания смешиваются с основным потоком

  • Вы планируете некоторые задания из JobComponentSystem, который изменяет значение.
  • Второй JobComponentSystem запускается после этого JobComponentSystem. Вы проверяете наличие измененного фрагмента или версии CG в OnUpdate, чтобы вообще не планировать задание, если данные не изменяются.
  • Вы ожидаете, что вторая система запустится, потому что вы изменили значение, но нет! Система заданий Unity на самом деле не завершается или даже не запускается, прежде чем кто-то запросит зависимость от нее! Когда вы проверяете версию, она все еще не изменилась.
  • Правильный способ проверить, не изменились ли работы, - это также проверить работу. Таким образом, вы не сможете избежать запланированных затрат, но сможете досрочно завершить работу.
  • Или использовать JobHandle.ScheduleBatchedJobs ()? Не уверен, что это хорошая идея .. или даже требуемые изменения версии произойдут немедленно?

IJobProcessComponentData

Добавьте [ChangedFilter] в свой ref, который влияет на ComponentGroup, как объяснялось ранее. IJPCD работает параллельно для каждого фрагмента, вы можете пропустить некоторые фрагменты или даже все фрагменты.

IJobChunk

Вы получаете по одному ArchetypeChunk на рабочий поток. Просто сделай .DidChange на нем!

Решив НЕ писать

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

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

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

Как избежать записи с помощью ComponentDataArray

  • Если вы используете ComponentDataArray, GetComponentDataArray с доступом на запись еще не считается записью. Когда вы используете индексатор для записи в задание, именно в этот момент увеличивается номер блока. Очень просто не вызвать писем! Но когда я действительно хочу вызвать запись, иногда новый номер версии не применяется правильно? (превью 21)

Избегайте записи с итерацией фрагментов

  • Если вы используете итерацию фрагментов, запись определяется, когда вы используете isReadOnly : false ArchetypeChunkComponentType для получения NativeArray вашего типа. Это испачкает кусок. Вы можете проверить условия, прежде чем принять решение GetNativeArray(ACCT)
  • Это означает, что вы не можете решить, записывать или не записывать в итерации фрагмента на основе собственных данных, если вы ввели ACCT с доступом на запись, просто «просмотр» данных требует получения NativeArray, а это уже запись. Это отличается от CDA, вы можете читать без записи на записываемый CDA.
  • Кроме того, вы не можете добавить в задание ACCT, доступный только для чтения и доступный для записи, будет сказано: InvalidOperationException: NativeArray с возможностью записи… это тот же NativeArray, что и…, два NativeArrays могут не совпадать (псевдонимы). (ACCT - это собственный массив ??)
  • К сожалению, часто приходится смотреть на текущие данные, чтобы решить, нужна ли запись или нет.
  • Однако вы можете взглянуть с версией только для чтения вне задания, если вы хотите написать, запланируйте задание с версией, доступной для записи.
  • Или, если вы не хотите разрывать цепочку dep, выполняя что-то в основном потоке, что может потребовать завершения некоторых заданий, создайте новое задание только для просмотра данных с читаемой версией и отправьте результат просмотра через NativeArray для второго задания, которое использует первое задание. JobHandle задания с доступной для записи версией для раннего выхода без записи, в то же время избегая проблемы с псевдонимом, потому что массивы псевдонимов теперь находятся в отдельных заданиях. Ебена мать! Все эти неприятности только для того, чтобы избежать записи.
  • В некотором смысле управление версией фрагмента с помощью ComponentDataArray кажется самым простым, поскольку версия не работает или не очень интуитивно понятна с помощью индексатора. Но CDA не рекомендуется ...

Слабость IJobProcessComponentData

Если вы используете IJobProcessComponentData, вы можете подготовить место назначения записи как ref без [ReadOnly]. Это уже считается записью и немедленно увеличивает версию блока. Вы не можете проверить другие вещи, а потом решить не писать. (Невозможно НЕ писать)

ref использует указатель, у него нет механизма, чтобы узнать, действительно ли вы назначаете ему что-то новое или нет.

Это работа ComponentChunkIterator, содержащаяся в вашем IJPCD, метод UpdateCacheToCurrentChunk. При выполнении каждого фрагмента любой компонент без [ReadOnly] будет немедленно увеличивать версию.

Также с IJPCD вы не можете предотвратить планирование задания (и, следовательно, изменение) с помощью одного предыдущего задания, которое проверяет условия, поскольку Unity не позволяет планировать задание из задания. Обе работы уже на подходе. Если вы не используете IJPCD, этот трюк возможен, и вы просто отказываетесь от второго задания, прежде чем оно сможет записывать данные.

Остерегайтесь изменений обратной связи!

Представьте себе систему A B C, работающую в таком порядке в кадре.

  • A: Используйте IComponentData X, чтобы обновить положение пользовательского интерфейса некоторых гибридных объектов в ComponentArray. Вы думаете, что не существует всегда работающей предыдущей системы, которая могла бы изменить X, поэтому она должна быть эффективной.
  • B: отсканируйте эти объекты пользовательского интерфейса и возьмите их вычисленный RectTransform обратно, сохраните его как Rect, надеясь затем выполнить некоторую трассировку лучей в ECS. Вы используете то же измененное условие, что и A, думая, что если A не произойдет, то эта система не должна работать.
  • C: вычисленные прямоугольники сохраняются обратно в X.

Первый цикл DidChange вводит специальный случай, когда все, что противоречит версии 0, возвращает истину. A B C запускается, затем в следующем кадре A обнаружит изменение C по сравнению с предыдущим кадром, что, в свою очередь, заставит C снова выполнить запись. Достаточно только первой активации, чтобы запустить этот «цикл обратной связи». При нормальном дизайне системы без уловки с измененной версией это приведет к нормальному поведению, но вы перегрузили.

В некоторых случаях вам необходимо тщательно разработать другие условия остановки. В этом случае условие A не должно основываться на X или чисто X.

ГДЕ изменение? Помощь!

Вы разбросали Debug.Log под каждым DidChange и обнаружили, что ваша система все еще работает, даже если вы ничего не делаете с игрой. Вы ничего не помните, что писали!

Базовый: ищите контейнеры, не предназначенные только для чтения

Поскольку собственные контейнеры должны быть украшены [ReadOnly], когда вы не собираетесь писать или иметь какой-то флаг на нем (CDA использует [ReadOnly], ACCT использует флаг, поле ACCT в задании также использует [ReadOnly] и т. Д.) и поскольку библиотека ECS основана на строго типизированных универсальных работах, вы можете использовать свою среду IDE для поиска проблемного типа, которые неосознанно меняются (помните, что версия изменения для каждого компонента для каждого фрагмента).

Эксперт: посмотрите номер версии изменения и выясните, что происходит

Вы можете перейти к исходному коду в файле Library / PackageCache ChangeVersionUtility и поместить что-нибудь в метод DidOrChange.

А также попробуйте отладить защищенное поле LastSystemVersion вокруг проверки изменений.

Задача: отладка. Зарегистрируйте источник изменения версии.

Трудно найти аргумент IJobProcessComponentData ref без [ReadOnly], и в этом случае он вызывает запись прямо в расписании задания.

В этом случае вы можете перейти к ComponentChunkIterator.cs и в методах UpdateCacheToCurrentChunk и UpdateChangeVersion поместить что-то вроде Debug.Log ($ ”Wow! Изменено на {m_GlobalSystemVersion}”); и вы увидите это, когда IJPCD запланирован. ! Затем просто следуйте трассировке стека.

Другие источники включают ArchetypeChunkArray.cs, который связан с генерацией собственного массива итерации фрагментов (который, как я сказал, он записывается сразу после получения массива), или ChunkDataUtility.cs, который находится на более высоком уровне.

Стратегия одноэлементной сущности с измененным фильтром

У меня столько IComponentData, которые просто держат общее состояние игры. Теперь вопрос, должен ли я создавать 1 Entity со всеми этими данными или по 1 Entity на данные?

Один синглтон

Вы можете использовать IJobProcessComponentData для изменения формы ВСЕХ данных общих состояний друг от друга, поскольку IJPCD работает с одним и тем же объектом. Если вы сделаете несколько синглтонов, это будет невозможно.

Несколько синглтонов + измененный фильтр

Это я хочу представить. Представьте себе синглтон GameTime : IComponentData, содержащий различные порядковые номера, полученные из дельта-времени. Затем еще один синглтон Weather : IComponentData, содержащий условие этапа. В любой момент времени существует только 1 этап, отсюда и статус singleton.

Если бы я объединил Weather и GameTime, было бы невозможно разработать систему IJPCD, которая «что-то делает, только если Weather изменяет значения», потому что GameTime всегда меняет каждый кадр, версия фрагмента будет постоянно обновляться. Использование [ChangedFilter] на Weather не будет иметь никакого эффекта, поскольку изменения проверяются на уровне фрагментов, а не для отдельных компонентов.

Если я делаю отдельные синглтоны, в IJPCD можно использовать [ChangedFilter] с Weather, а также с итерацией фрагментов можно сделать .DidChange на фрагменте Weather (который в любом случае содержит только 1 объект, но вы можете пропустить много работы автоматически)

Эффект бабочки!

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

Резюме

Зная все это, вы точно знаете, как не увеличивать версию блока без необходимости. И тогда ваш тщательно спланированный DidChange или [ChangedFilter] должен творить чудеса, пропуская работы!

Ошибки

Превью 19

См. Комментарий к этой статье.

Превью 20

Они изменили DidAddOrChange, чтобы принять одну произвольную версию для сравнения с версией фрагмента, в которой вы должны использовать недавно открытый LastSystemVersion. Но они не добавили этот новый аргумент для DidChange, поэтому вам придется добавить его вручную. (TransformSystem интенсивно использует DidAddOrChange, и никакой другой внутренний код не использует DidChange, поэтому я думаю, они исправят это достаточно просто)

Превью 23

DidAddOrChange пропал, остался только DidChange, который теперь определяет то же, что и DidAddOrChange (Версия = 0 теперь запускает DidChange)