А и еще один момент, вся фишка в том, что в таком подходе, когда компоненты хранят чистые (не зависящие от 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, сейчас из-за него просадки (см зеленые пики на скрине). При этом логика (синяя область) реально стала занимать меньше времени в тике
Так, грыжа, кажется, обойдется без операции, но реабилитация будет долгой. Очень долгой (и дорогой)
Тратится на это где-то часа 3 в день с учетом дороги. Так что проект все равно просядет. Не знаю, может буду мысленно придумывать решения в дороге и на процедурах, а потом приходить и кодить, хз
Сейчас есть идея оптимизации поведенческой системы. Раз уже взялся за рефакторинг, надо пройтись и пофиксить некоторые костыли, которые были сделаны.
Сейчас суть идеи в чем
Поведенческая система определяет задачу, которую будет выполнять агент. Я реализовал какое-то подобие HTN-подхода с иерархической вложенностью задач.
Есть ScriptableObject, в котором хранится:
1. конфиг шага-кирпичика
2. Метод, который определяет способ выполнения задачи. Метод состоит из набора таких кирпичиков
3. Ну и собственно говоря задача, в которой есть список методов, которыми она может быть выполнена + доп. параметры
Сейчас я в поведенческой системе каждый раз на лету высчитываю коэффициенты стоимости и определяю оптимальную задачу чисто утилитарным подходом.
Идея оптимизации заключается в том, чтобы сделать runtime-слой и собрать все возможные задачи с методами. Сейчас я каждый тик создаю список и через фабрики это собираю
При этом, такая система пока что жрет только 5-6 мс в тик.
Посмотрим, что будет после оптимизации. Вычисление стоимости пока все равно оставлю как есть, хотя его можно тоже попробовать закешировать, тк коэффициенты зависят от характеристик NPC, которые меняются не каждый тик. Но пока оставлю как есть и посмотрю на результат
Тратится на это где-то часа 3 в день с учетом дороги. Так что проект все равно просядет. Не знаю, может буду мысленно придумывать решения в дороге и на процедурах, а потом приходить и кодить, хз
Сейчас есть идея оптимизации поведенческой системы. Раз уже взялся за рефакторинг, надо пройтись и пофиксить некоторые костыли, которые были сделаны.
Сейчас суть идеи в чем
Поведенческая система определяет задачу, которую будет выполнять агент. Я реализовал какое-то подобие HTN-подхода с иерархической вложенностью задач.
Есть ScriptableObject, в котором хранится:
1. конфиг шага-кирпичика
2. Метод, который определяет способ выполнения задачи. Метод состоит из набора таких кирпичиков
3. Ну и собственно говоря задача, в которой есть список методов, которыми она может быть выполнена + доп. параметры
Сейчас я в поведенческой системе каждый раз на лету высчитываю коэффициенты стоимости и определяю оптимальную задачу чисто утилитарным подходом.
Идея оптимизации заключается в том, чтобы сделать runtime-слой и собрать все возможные задачи с методами. Сейчас я каждый тик создаю список и через фабрики это собираю
При этом, такая система пока что жрет только 5-6 мс в тик.
Посмотрим, что будет после оптимизации. Вычисление стоимости пока все равно оставлю как есть, хотя его можно тоже попробовать закешировать, тк коэффициенты зависят от характеристик NPC, которые меняются не каждый тик. Но пока оставлю как есть и посмотрю на результат
Идея оказалась неверной. Ну т.е. я, конечно, выиграл время, где-то 0.1-0.2 мс на тик в среднем😂
Там просто был вот такой кусок кода
Тут закомменченный фрагмент с созданием фабрик. Эта шняга выполнялась каждый тик. Фабрики - для создания задач. Оказалось, что это не критично, тк задач в целом не так много, способов их реализации тоже. Да и в полноценной игре будет там пусть даже 20-30 задач (задача - то, что умеет делать NPC - ходить, бегать, лежать, сидеть, учиться, драться и так далее)
Крч проблема не здесь
Проблема оказалась в двух других местах
1. Для каждого агента я определяю наилучший метод выполнения задачи. Это выглядит так
тут я передаю decisionContext - интерфейс, через который я могу достать все необходимые данные для принятия решений - статы NPC, модификаторы, поведение и так далее
и в коде функции по выбору наилучшего метода есть вот такой вызов
float score = evaluatorRegistry.Get(method).Evaluate(index, decisionContext, profile);
Сами действия рассчитываются довольно просто
вот такой оценщик перемещения у меня сейчас
2. Все оценки осуществляются каждый тик, вне зависимости от того, меняются характеристики NPC или нет. Сейчас у меня статы статичны, но я все равно каждый тик вычисляю оценки для действий
Направлений, для попыток это все оптимизировать я вижу несколько
1. Делать кэш для оценок и обновлять его по событиям, в случае, если характеристики NPC поменяются. Чем-то похожая идея была при оптимизации А*, там я тоже делал кеш для стоимости перемещения по ребрам между соседними клеточками грида.
Если менять характеристики (голод, усталость и т.д.) NPC где-то раз в 30 логических тиков (примерно 1 раз в 3 секунды, то смысл в этом есть.
2. Сейчас я последовательно прохожу в цикле для каждого NPC. Можно даже ничего не менять и попробовать впилить тут параллельные вычисления. Не уверен на счет Burst, тк меня напрягает необходимость дрочева с типами данных, я могу попробовать сделать это на Thread's.
3. Можно попробовать на стороне движка запилить какой-то ECS-стор для хранения статов и значений модификаторов, а игровой слой будет запрашивать сразу массив данных по всем агентам, а не по-одному.
Но думаю, что самый мощный эффект тут даст распараллеливание, тк я все тестирую на 1000 NPC, думаю, чисто за счет него можно примерно в 3-4 раза сократить время, а если еще и кеш сделать...
Там просто был вот такой кусок кода
if (planContext.CurrentStep == null)
{
Profiler.BeginSample("planContext.CurrentStep");
//var method = planContext.CurrentMethod;
var runtimeMethod = planContext.RuntimeMethod;
/*
var factories = new List<IAgentBehaviourStepFactory>(method.Steps.Count);
foreach (var step in method.Steps)
{
factories.Add(compositeTaskResolver.Resolve(step));
}
*/
var task = new ConfiguredCompoundTask(runtimeMethod.StepFactories);
planContext.SetStep(task);
Profiler.EndSample();
}
Тут закомменченный фрагмент с созданием фабрик. Эта шняга выполнялась каждый тик. Фабрики - для создания задач. Оказалось, что это не критично, тк задач в целом не так много, способов их реализации тоже. Да и в полноценной игре будет там пусть даже 20-30 задач (задача - то, что умеет делать NPC - ходить, бегать, лежать, сидеть, учиться, драться и так далее)
Крч проблема не здесь
Проблема оказалась в двух других местах
1. Для каждого агента я определяю наилучший метод выполнения задачи. Это выглядит так
var method = methodSelector.SelectBestMethod(
i,
planContext.CurrentTask,
decisionContext
);
тут я передаю decisionContext - интерфейс, через который я могу достать все необходимые данные для принятия решений - статы NPC, модификаторы, поведение и так далее
и в коде функции по выбору наилучшего метода есть вот такой вызов
float score = evaluatorRegistry.Get(method).Evaluate(index, decisionContext, profile);
Сами действия рассчитываются довольно просто
int energizerTag = personalityProfile.GetModifier(PersonalityModifierTag.Energizer);
int lazyAssTag = personalityProfile.GetModifier(PersonalityModifierTag.LazyAss);
float cost = +0.4f * energizerTag - 0.2f * lazyAssTag;
вот такой оценщик перемещения у меня сейчас
2. Все оценки осуществляются каждый тик, вне зависимости от того, меняются характеристики NPC или нет. Сейчас у меня статы статичны, но я все равно каждый тик вычисляю оценки для действий
Направлений, для попыток это все оптимизировать я вижу несколько
1. Делать кэш для оценок и обновлять его по событиям, в случае, если характеристики NPC поменяются. Чем-то похожая идея была при оптимизации А*, там я тоже делал кеш для стоимости перемещения по ребрам между соседними клеточками грида.
Если менять характеристики (голод, усталость и т.д.) NPC где-то раз в 30 логических тиков (примерно 1 раз в 3 секунды, то смысл в этом есть.
2. Сейчас я последовательно прохожу в цикле для каждого NPC. Можно даже ничего не менять и попробовать впилить тут параллельные вычисления. Не уверен на счет Burst, тк меня напрягает необходимость дрочева с типами данных, я могу попробовать сделать это на Thread's.
3. Можно попробовать на стороне движка запилить какой-то ECS-стор для хранения статов и значений модификаторов, а игровой слой будет запрашивать сразу массив данных по всем агентам, а не по-одному.
Но думаю, что самый мощный эффект тут даст распараллеливание, тк я все тестирую на 1000 NPC, думаю, чисто за счет него можно примерно в 3-4 раза сократить время, а если еще и кеш сделать...