Я предполагаю, что это займет достаточно много времени и у меня не останется нигде ссылок на старую первую версию, так что я не получу невалидную ссылку. Даже такие короткоживущие сущности, как намерения обрабатываются довольно быстро, их не надо хранить в долгосроке, и на практике достаточно запаса в 30 сек для любой пост-обработки этих сущностей. 30 сек это примерно 10-20 версий. Так что запас есть
Какие тут минусы
Раньше, зная entityId я мог сделать просто Entity e = new Entity(entityId) и получить сущность.
Теперь так делать нельзя, тк массивы хранят компактный индекс, а entityId еще надо восстановить
Теперь надо вначале получить версию из EntityPool - у него должен быть метод, который по компактному индексу вернет актуальную версию сущности
А затем уже восстановить entityId. Это делается так
т.е добавляется 2 вызова вместо простого конструктора
Пока не могу сказать, насколько сильно это влияет на производительность, тк не закончил полный переезд на эту архитектуру, в конце уже сделаю тест на производительность и посмотрю, что там показывается в профайлере
Какие тут минусы
Раньше, зная entityId я мог сделать просто Entity e = new Entity(entityId) и получить сущность.
Теперь так делать нельзя, тк массивы хранят компактный индекс, а entityId еще надо восстановить
Теперь надо вначале получить версию из EntityPool - у него должен быть метод, который по компактному индексу вернет актуальную версию сущности
А затем уже восстановить entityId. Это делается так
public const int IndexMask = (1 << IndexBits) - 1;
public const int VersionMask = (1 << VersionBits) - 1;
public static Entity Make(int index, int version)
=> new((version & VersionMask) << IndexBits | (index & IndexMask));
т.е добавляется 2 вызова вместо простого конструктора
Пока не могу сказать, насколько сильно это влияет на производительность, тк не закончил полный переезд на эту архитектуру, в конце уже сделаю тест на производительность и посмотрю, что там показывается в профайлере
Media is too big
VIEW IN TELEGRAM
Вот так теперь выглядит перемещение
Перевел анимацию и поворот на ECS.
Сама идея тут довольно простая, но дрочево возникло с версиями сущностей - об этом написал выше.
Теперь на ECS вся базовая логика по работе с агентами. Осталась еще довольно большая фича по диалогу между NPC, потом можно двигаться дальше
В чем была тут суть рефакторинга
Раньше я вычислял поворот так
Для этого я для каждого агента хранил дополнительно 2 параметра - положение в текущем тике и предыдущем (зачем в предыдущем - хз, тк по идее агент должен смотреть на целевую точку, куда он идет)
Но это не просто хранение, это еще и обновление этих параметров -> плюс к накладным расходам
Как это выглядит сейчас
Есть компонент, который хранит позицию и поворот
И есть компонент, который хранит данные модели - в частности индекс текущей ячейки. Несколькими постами ранее я описывал логику интерполяции - как осуществляется плавное перемещение агента. Визуал перемещается от текущей позиции визуала к позиции модели (визуал запаздывает)
Поэтому поворот мы должны считать как вектор от текущей позиции визуала к целевой (текущей позиции модели)
Поэтому никаких доп. параметров не надо вводить, можно вычислить его из имеющихся данных
здесь CellToWorld - вспомогательная функция, которая переводит Vector3Int ячейку в Vector3
ToVector3Int - вспомогательная функция, которая переводит индекс ячейки грида в Vector3Int (да, грид должен хранится в целочисленных индексах для возможности оптимальных параллельных вычислений и производительности)
Перевел анимацию и поворот на ECS.
Сама идея тут довольно простая, но дрочево возникло с версиями сущностей - об этом написал выше.
Теперь на ECS вся базовая логика по работе с агентами. Осталась еще довольно большая фича по диалогу между NPC, потом можно двигаться дальше
В чем была тут суть рефакторинга
Раньше я вычислял поворот так
var visualContext = actionExecutionContextProvider.GetByIndex(index).VisualContext;
Vector3 dir = visualContext.CurrentTickPosition - visualContext.PreviousTickPosition;
Для этого я для каждого агента хранил дополнительно 2 параметра - положение в текущем тике и предыдущем (зачем в предыдущем - хз, тк по идее агент должен смотреть на целевую точку, куда он идет)
Но это не просто хранение, это еще и обновление этих параметров -> плюс к накладным расходам
Как это выглядит сейчас
Есть компонент, который хранит позицию и поворот
private float[] x;
private float[] y;
private float[] z;
private float4[] rot;
И есть компонент, который хранит данные модели - в частности индекс текущей ячейки. Несколькими постами ранее я описывал логику интерполяции - как осуществляется плавное перемещение агента. Визуал перемещается от текущей позиции визуала к позиции модели (визуал запаздывает)
Поэтому поворот мы должны считать как вектор от текущей позиции визуала к целевой (текущей позиции модели)
Поэтому никаких доп. параметров не надо вводить, можно вычислить его из имеющихся данных
Vector3 target = coordinateManager.CellToWorld(GridTopology.ToVector3Int(modelPositionReader.CellIndex(agentE)));
Vector3 curr = visualPositionReader.GetPos(agentE);
Vector3 dir = target - curr;
здесь CellToWorld - вспомогательная функция, которая переводит Vector3Int ячейку в Vector3
ToVector3Int - вспомогательная функция, которая переводит индекс ячейки грида в Vector3Int (да, грид должен хранится в целочисленных индексах для возможности оптимальных параллельных вычислений и производительности)
#ECS
Несколько дней потратил на ебаторию межгаллактического масштаба (как всегда с тупыми багами, правятся они в 2 строчки, а времени тратишь на их локализацию в 1000 раз больше, чем на исправление)
На первом ролике показал как выглядело перемещение агентов (да, анимация до конца еще не поправлена)
Тут есть сразу 2 проблемы:
А. Телепортация агентов (глобальная)
Б. Локальная телепортация визуалов (размазывание изображение в пределах пары клеток)
По ходу исправления вычистил еще несколько мелких багов, но суть в чем
Есть система перемещения агентов, она в цикле проходит все сущности агентов, у которых есть перемещение
И в этой системе есть кусок, который меняет позицию агентов
Приведу часть кода
здесь я из стора, в котором хранится перемещение получил индекс агента
Затем делаю так
тут я уже в другом сторе обновляю данные по ref, а индекс беру из первого стора
Пока агент один - все прекрасно, но, если их несколько - получается рассинхрон, тк когда один из агентов завершает перемещение, с него снимается компонент движения. Затем он получает новую задачу на перемещение в новую точку - перемещение вешается еще раз, но он уже в сторе movementComponentStore будет в другой позиции. В частном случае, когда агентов 2, они просто меняются местами.
Проблема Б имеет похожую суть
В системе интерполяции я получаю ячейки, между которыми осуществляю интерполяцию визуала
здесь d - индекс в сторе перемещений
а затем создавал джобу для Burst
здесь я из стора, хранящего прогресс интерполяции получаю значение по индексу из стора, хранящего данные для перемещения
исправление супер-быстрое - надо вначале было получить индекс в конкретном сторе типа
Мораль
1. Всегда убеждаться, что работаешь с нужными индексами
2. Писать минимальные проверки в режиме дебага
Не знаю, можно ли вообще избавиться от подобного рода ошибок, в ECS вообще отладка сложнее
Несколько дней потратил на ебаторию межгаллактического масштаба (как всегда с тупыми багами, правятся они в 2 строчки, а времени тратишь на их локализацию в 1000 раз больше, чем на исправление)
На первом ролике показал как выглядело перемещение агентов (да, анимация до конца еще не поправлена)
Тут есть сразу 2 проблемы:
А. Телепортация агентов (глобальная)
Б. Локальная телепортация визуалов (размазывание изображение в пределах пары клеток)
По ходу исправления вычистил еще несколько мелких багов, но суть в чем
Есть система перемещения агентов, она в цикле проходит все сущности агентов, у которых есть перемещение
И в этой системе есть кусок, который меняет позицию агентов
Приведу часть кода
Entity agentEntity = new Entity(movingAgents[i]);
int movingAgentIndex = movementComponentStore.IndexOf(movingAgents[i]);
здесь я из стора, в котором хранится перемещение получил индекс агента
Затем делаю так
modelsPositionRefAccessor.CellIndexRef(movingAgentIndex) = nextWaypoint;
modelsPositionRefAccessor.X(movingAgentIndex) = nextPos.x;
modelsPositionRefAccessor.Y(movingAgentIndex) = nextPos.y;
modelsPositionRefAccessor.Z(movingAgentIndex) = nextPos.z;
тут я уже в другом сторе обновляю данные по ref, а индекс беру из первого стора
Пока агент один - все прекрасно, но, если их несколько - получается рассинхрон, тк когда один из агентов завершает перемещение, с него снимается компонент движения. Затем он получает новую задачу на перемещение в новую точку - перемещение вешается еще раз, но он уже в сторе movementComponentStore будет в другой позиции. В частном случае, когда агентов 2, они просто меняются местами.
Проблема Б имеет похожую суть
В системе интерполяции я получаю ячейки, между которыми осуществляю интерполяцию визуала
if (!movements.TryGet(e, out int d)) continue;
int prevCell = movements.PrevCell(d);
int currCell = movements.CurrentCell(d);
float speed = movements.Speed(d);
здесь d - индекс в сторе перемещений
а затем создавал джобу для Burst
From = from,
To = to,
Speed = speed,
Progress = lerpsRef.Interp(d)
здесь я из стора, хранящего прогресс интерполяции получаю значение по индексу из стора, хранящего данные для перемещения
исправление супер-быстрое - надо вначале было получить индекс в конкретном сторе типа
int d_lerp = lerpsRef.IndexOf(agentE);Мораль
1. Всегда убеждаться, что работаешь с нужными индексами
2. Писать минимальные проверки в режиме дебага
Не знаю, можно ли вообще избавиться от подобного рода ошибок, в ECS вообще отладка сложнее
Media is too big
VIEW IN TELEGRAM
#ECS
Итак, вот первый результат оптимизации с переходом на ECS.
Сделал спавн 1000 NPC с HTN-задачами на перемещение в рандомную точку у каждого агента
В профайлере видно, что в целом FPS держится на уровне близком к 30-40 с некоторыми пиками просадок (еще есть потенциал для оптимизации)
Ранее у меня все фризилось до 10-15 fps уже на 64 агентах, сейчас вполне терпимо на 1000.
Плюс есть фриз-места чисто в дебаге из-за того, что я вынужден был сделать разные проверки вида
Какие мои ощущения от перехода
1. Ключевые оптимизационные проблемы лежат не в плоскости архитектуры ECS-ООП, а в алгоритмах и кэшах. Просто с ECS эти кэши проще делать. Условно говоря нам надо закэшировать состояния задач или действий, которые выполняют агенты. И ты по сути все равно делаешь набор массивов, в котором лежат "горячие данные".
2. Чистый выигрыш в производительности именно за счет ускорения вычислений из-за архитектуры на мой субъективный взгляд составил где-то 30%.
3. С переходом на ECS и если следовать концепции SoA - набор массивов, то сложность отладки возрастает многократно. Приходится делать какие-то обертки чисто для дебага, чтобы не умереть
4. Если движущихся агентов мало (меньше 500 условно), то профит от ECS не стоит геморроя в дизайне архитектуры и еще большего геморроя в отладке
Итак, вот первый результат оптимизации с переходом на ECS.
Сделал спавн 1000 NPC с HTN-задачами на перемещение в рандомную точку у каждого агента
В профайлере видно, что в целом FPS держится на уровне близком к 30-40 с некоторыми пиками просадок (еще есть потенциал для оптимизации)
Ранее у меня все фризилось до 10-15 fps уже на 64 агентах, сейчас вполне терпимо на 1000.
Плюс есть фриз-места чисто в дебаге из-за того, что я вынужден был сделать разные проверки вида
#if debug
Какие мои ощущения от перехода
1. Ключевые оптимизационные проблемы лежат не в плоскости архитектуры ECS-ООП, а в алгоритмах и кэшах. Просто с ECS эти кэши проще делать. Условно говоря нам надо закэшировать состояния задач или действий, которые выполняют агенты. И ты по сути все равно делаешь набор массивов, в котором лежат "горячие данные".
2. Чистый выигрыш в производительности именно за счет ускорения вычислений из-за архитектуры на мой субъективный взгляд составил где-то 30%.
3. С переходом на ECS и если следовать концепции SoA - набор массивов, то сложность отладки возрастает многократно. Приходится делать какие-то обертки чисто для дебага, чтобы не умереть
4. Если движущихся агентов мало (меньше 500 условно), то профит от ECS не стоит геморроя в дизайне архитектуры и еще большего геморроя в отладке
А и еще один момент, вся фишка в том, что в таком подходе, когда компоненты хранят чистые (не зависящие от Unity) данные, то супер-логичное решение - использовать параллельные вычисления
так вот, вынес я из нескольких тяжелых систем математику в Burst:
1. Для системы поворота вынес в Job расчет поворота модельки
2. Для перемещения вынес в Job прогресс (по аналогии как это сделано для системы интерполяции). В интерполяции мне важна плавность, там обновления каждый кадр, поэтому я там сразу додумался сделать расчет через Burst, а тут логические тики 10 раз в секунду и чет я не сразу до этого допер
Дальше по идее это можно расширять и параллелить поведенческую систему, которая "тикает" статусы задач
Ради интереса собрал релизный билд, чтобы протестить производительность
Вот картина на 5к агентах
Да, есть подлагивания при перемещении карты, но это вполне играбильно будет.
так вот, вынес я из нескольких тяжелых систем математику в Burst:
1. Для системы поворота вынес в Job расчет поворота модельки
2. Для перемещения вынес в Job прогресс (по аналогии как это сделано для системы интерполяции). В интерполяции мне важна плавность, там обновления каждый кадр, поэтому я там сразу додумался сделать расчет через Burst, а тут логические тики 10 раз в секунду и чет я не сразу до этого допер
Дальше по идее это можно расширять и параллелить поведенческую систему, которая "тикает" статусы задач
Ради интереса собрал релизный билд, чтобы протестить производительность
Вот картина на 5к агентах
Да, есть подлагивания при перемещении карты, но это вполне играбильно будет.
Никаких обновлений пока нет, я оказался очень "везучим".
В начале или середине октября занимался с так называемым "тренером" и усугубил свою межпозвоночную грыжу.
А 3 дня назад просто чихнул пару раз и она лопнула. Сейчас в больничке на реабилитации. Возможно, в течение месяца надо будет сделать лапароскопическую операцию по удалению остатков хряща из спинномозгового канала, тк они давят на корешок нерва.
Ноутбук в палату я взял, но лежать на спине пока не могу, поэтому проект +- дней 10 на паузе. Надеюсь вернуться в строй к концу след недели, но там будет уходить время на неврологов и нейрохирургов + продолжение реабилитации. Может удастся хотя бы 1-2 часа перед сном покодить
В начале или середине октября занимался с так называемым "тренером" и усугубил свою межпозвоночную грыжу.
А 3 дня назад просто чихнул пару раз и она лопнула. Сейчас в больничке на реабилитации. Возможно, в течение месяца надо будет сделать лапароскопическую операцию по удалению остатков хряща из спинномозгового канала, тк они давят на корешок нерва.
Ноутбук в палату я взял, но лежать на спине пока не могу, поэтому проект +- дней 10 на паузе. Надеюсь вернуться в строй к концу след недели, но там будет уходить время на неврологов и нейрохирургов + продолжение реабилитации. Может удастся хотя бы 1-2 часа перед сном покодить
Вот, что значит "развалиться с одного чиха" 😂
Хотя сама ситуация мягко говоря неприятная, но обстоятельство, конечно..... Мне даже смешно😂
Хотя сама ситуация мягко говоря неприятная, но обстоятельство, конечно..... Мне даже смешно
Please open Telegram to view this post
VIEW IN TELEGRAM
image_2025-12-10_23-57-12.png
187.8 KB
Такс, я понемногу возвращаюсь к проекту
В последний раз у меня был затык с тем, что у меня лагала система, которая выдает маршрут для агентов
Идея была в чем, я спавнил сразу 1000 NPC, у них одна задача - бродить по карте, соответственно, они получают рандомную точку и запрашивают сервис маршрутизации "Дай мне маршрут от моей позиции до желаемой точки"
И так для каждого агента.
В профайлере система тратила около 15-16мс для примерно 200 агентов. Мне пока сложно сказать много это или мало, просто факт.
Я увидел там поле для оптимизации. У меня уже сервис маршрутизации поддерживает как синхронные, так и асинхронные запросы. Асинхронные могут выполняться пачкой, т.е. я могу в течение тика класть в очередь запрос на маршрут, а потом в конце тика отправить всю пачку рассчитываться. Расчет идет на самописных Thread, тк все остальное блокирует основной поток. Предполагается, что это используется для расчета тяжелых "длительных" маршрутов.
Для коротких маршрутов мне не нужна асинхронщина, нужен результат здесь и сейчас. Но я сразу поленился делать норм архитектуру и сделал так, что сервис может принимать только один запрос.
Идея оптимизации заключается в том, чтобы также рассчитывать сразу пачку маршрутов. Следующая идея была в том, чтобы заюзать для этого Burst.
Плюсы
Это быстро, реально быстро. Сейчас система обработки очереди запросов обрабатывает те же 200-250 запросов за какие-то 3.2 мс, что как минимум в 5 раза быстрее, чем было
Минусы
1. Пришлось переписать ядро А*, чтобы оно было совместимо с Burst
2. Пришлось задублировать логику А* для Burst-джобы.
3. Я не уверен, что разница между Burst и моими Thread настолько существенна, чтобы пожертвовать архитектурой и сделать дублирующую логику А* чисто для Burst.
Пока я сделал это немного на коленке, надо еще избавиться от излишнего GC, сейчас из-за него просадки (см зеленые пики на скрине). При этом логика (синяя область) реально стала занимать меньше времени в тике
В последний раз у меня был затык с тем, что у меня лагала система, которая выдает маршрут для агентов
Идея была в чем, я спавнил сразу 1000 NPC, у них одна задача - бродить по карте, соответственно, они получают рандомную точку и запрашивают сервис маршрутизации "Дай мне маршрут от моей позиции до желаемой точки"
И так для каждого агента.
В профайлере система тратила около 15-16мс для примерно 200 агентов. Мне пока сложно сказать много это или мало, просто факт.
Я увидел там поле для оптимизации. У меня уже сервис маршрутизации поддерживает как синхронные, так и асинхронные запросы. Асинхронные могут выполняться пачкой, т.е. я могу в течение тика класть в очередь запрос на маршрут, а потом в конце тика отправить всю пачку рассчитываться. Расчет идет на самописных Thread, тк все остальное блокирует основной поток. Предполагается, что это используется для расчета тяжелых "длительных" маршрутов.
Для коротких маршрутов мне не нужна асинхронщина, нужен результат здесь и сейчас. Но я сразу поленился делать норм архитектуру и сделал так, что сервис может принимать только один запрос.
Идея оптимизации заключается в том, чтобы также рассчитывать сразу пачку маршрутов. Следующая идея была в том, чтобы заюзать для этого Burst.
Плюсы
Это быстро, реально быстро. Сейчас система обработки очереди запросов обрабатывает те же 200-250 запросов за какие-то 3.2 мс, что как минимум в 5 раза быстрее, чем было
Минусы
1. Пришлось переписать ядро А*, чтобы оно было совместимо с Burst
2. Пришлось задублировать логику А* для Burst-джобы.
3. Я не уверен, что разница между Burst и моими Thread настолько существенна, чтобы пожертвовать архитектурой и сделать дублирующую логику А* чисто для Burst.
Пока я сделал это немного на коленке, надо еще избавиться от излишнего GC, сейчас из-за него просадки (см зеленые пики на скрине). При этом логика (синяя область) реально стала занимать меньше времени в тике