AI-Evolution #2. От "одного агента" к популяции: Оптимизация рендеринга Canvas и баг, который морил ИИ голодом

Доброго утра!

Мне всё-таки удалось достичь прогресса. Я решил остановиться на использовании LERP-фактора. Поначалу думал, что будет глаза резать то, как агенты передвигаются рывками, но в итоге даже привык. В некотором роде это выглядит даже красиво — сейчас они перемещаются вприпрыжку, как водомерки на поверхности пруда.

Синие - серверные объекты. Зеленые - клиентские.
Синие - серверные объекты. Зеленые - клиентские.

Расскажу немного о том, как в целом у меня сейчас устроена «жизнь». На представленном выше рисунке можно увидеть, из чего состоит обычный агент и обычный куст. Поле id появилось самым последним, когда я решил перейти от концепции «один куст и один агент» к концепции «много кустов и много агентов». Что делает то или иное поле, в целом понятно из названия:

  • x и y — позиция на поле.
  • vx, vy — скорость по горизонтали и вертикали соответственно.
  • size — размер. Пока у всех агентов он одинаковый, но потом можно будет поиграть с масштабами. Теоретически его можно привязать к параметру «здоровье» или «выносливость» (которых пока что нет), а их, в свою очередь, привязать к метаболизму и выживаемости. Но это я забегаю вперед :)
  • energy, maxEnergy — текущая и максимальная энергия. Пока что максимальная энергия у всех равна 100. Когда агент кушает — энергия пополняется, в остальное время она потихоньку убывает. В будущем планирую ввести параметр возраста: чем агент старше, тем быстрее у него будет работать метаболизм, то есть быстрее убывать энергия. Также планирую в ближайшее время внедрить возможность размножения митозом. Причем здесь энергия? В теории размножение будет работать так: агент наедается до уровня energy = 200, и происходит деление. Агент и его потомок делят энергию поровну и идут добывать пропитание дальше.
  • isEating — находится ли сейчас агент в процессе поглощения еды.
  • target — текущая цель (куст).
  • viewRadius — радиус обзора агента. Пока что этот параметр тоже скорее декоративный, но свою роль всё равно исполняет: если куст находится в зоне обзора, агент бежит к нему. Декоративный он потому, что работает на все 360 градусов. Позже планирую сделать конус обзора перед агентом, чтобы он не мог видеть спиной.
  • targetX, targetY — позиция объекта на поле Canvas.

Для кустов поля практически те же самые и даже проще:

  • stage — текущий статус куста (растет / спелый / поедается / увядает). В текущей версии агент объедает куст целиком, даже когда у него полная энергия, плюс сам куст съедается без остатка. Его «жизнь» привязана к размеру: когда размер достигает нуля, куст исчезает и спавнится новый.

Конечно, это не финальный вариант, будет добавляться еще много полей и характеристик. Но на первое время для простенькой симуляции хватает и этого. По-хорошему, необходимо разработать экономику энергии как для агентов, так и для кустов. Это не так весело, как программировать, но без нормальной экономики хорошей симуляции не выйдет. Текущая ситуация хороша для тестирования, но слишком далека от идеальной. Сейчас при относительно непродолжительном наблюдении агенты быстро приходят к +/- единому поведению, а бесконечно нарождающиеся кусты не дают никому испытать нехватку пищи. Нужна драма :D

Один агент, один куст. Довольно пустынно пока
Один агент, один куст. Довольно пустынно пока

Но вернемся к разработке. Когда я смог настроить плавное передвижение, я взялся за добавление «жизни» на поле и переделал бэкенд и фронтенд под работу с большим количеством элементов. Создавать и передавать агентов и кусты я решил через объект {}. Альтернативой было использовать массивы [], но объекты оказались удобнее. Сложность алгоритма выбора элемента в объекте по ключу — O(1). Это гораздо эффективнее, чем каждый раз искать нужный элемент в массиве.

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

Реализовав эту мысль, я сразу наткнулся на несколько багов:

Во-первых, кусты зависали в одном состоянии и не обновлялись. Кусты на фронтенде хоть и берут данные с сервера, но это другие объекты. Сначала с сервера приходят данные о кустах, затем на странице создается новый объект на основе этих данных. В итоге мы, по сути, имеем две копии одного объекта (к слову, агенты создаются по тому же принципу). Для создания отображаемого куста я прохожусь циклом по всем кустам, пришедшим с сервера, а затем запускается логика отрисовки. И в цикле отрисовки я опечатался: вместо clientBush написал serverBush. В итоге все кусты на экране принимали вид последнего серверного куста в списке. Этот баг оказался некритичным и быстро исправился.

Во-вторых, и это было гораздо более неприятно — картинка ужасно дергалась.Я уж было подумал, что мой новенький комп просто не тянет такое «суперприложение» :D Но немного покопавшись, я нашел причину. Неприятно оставлять такие баги нерешенными — нужно хотя бы понять причину их появления, прежде чем откладывать на потом. Но насиловать и без того сонный, уставший мозг тоже не стоило. К счастью, на следующий день решение нашлось достаточно быстро.

Оказалось, что функция requestAnimationFrame запускалась прямо внутри ws.onMessage, то есть при каждом тике сервера. Представьте: данные с сервера приходят два раза в секунду, и дважды в секунду запускается цикл отрисовки анимации. В итоге через уже через несколько секунд компьютер натужно пытался отрисовать сотни кадров одновременно. Но помимо этого, отрисовка Canvas происходила через id, а не через ref. Из-за этого React перерисовывал виртуальное дерево для всего проекта, а не только точечные изменения в самом Canvas. Для двух объектов на заре создания приложения это не было критичной проблемой, но когда объектов стало больше — баг предсказуемо всплыл.

"Водомерки в пруду"

В итоге обе проблемы были решены. Но следом вылезла еще одна...

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

Следующим шагом необходимо решить этот баг и провести общий «улучшайзинг» проекта. До следующей встречи!🛠

Photo by Stefan Lehner on Unsplash  
Photo by Stefan Lehner on Unsplash  
2
1