Сетевой слой.
Какой способ получения данных выбрать?
Обычно игры все ресурсы тянут с локального жесткого диска, а по сети получают только координаты объектов и других игроков. В моём же случае весь игровой мир приходит из интернета. Вопрос в том как организовать эффективный стримминг.
Начал эксперименты я с хорошо мне знакомого HTTP + JSON.
Комбинация проверенная временем, позволяет удобно дебажить запросы, есть возможность использовать oauth, роутить ендпоинты через nginx. Готовый инструментарий на все случаи жизни.
Однако, в моём случае этот подход хранил много неприятных сюрпризов.
Во-первых, сам HTTP несет с собой много оверхеда — открытие соединения, обмен заголовками, закрытие соединения — и так на каждый запрос. А тайлы мира мы запрашиваем постоянно, особенно при движении на машине. И вместо того чтобы иметь постоянный канал нам приходится постоянно открывать/закрывать соединение.
Во-вторых, JSON очень громоздкий формат. Он безусловно очень удобен для ручного написания структур, для отладки, просмотра что там пришло с сервера, но проблема в том что для 1000 однотипных элементов сами имена свойств могут занимать места больше чем полезные данные! А значит мы гоняем по сети бесполезный балласт, расходуя ценный bandwidth.
В-третьих, JSON это строка, а значит принимающая сторона должна её распарсить превращая в понятные движку структуры. В худшем случае придется создавать managed-сущности, а это тот самый ненавистный GC-pressure.
Пришлось выкидывать этот подход на помойку и искать новый, более эффективный способ.
Начнем с самого формата данных. Вербозный JSON можно заменить на байт-код, либо кастомный свой формат, либо какой-нибудь всем известный protobuf или FlatBuffers. Так мы избавимся от парсинга строк, сократим до 60% объем передаваемых данных и сможем наладить unmanaged-пайплайн для обработки на стороне unity-клиента.
Осталось что-то придумать с HTTP. Как я уже говорил, у него есть несомненное преимущество в виде возможности роутить ендпоинты с помощью nginx и использовать oauth-авторизацию. Если мы возьмем голые сокеты, TCP или UDP — мы потеряем эти возможности.
На помощь пришел WebSocket — он умеет жить в среде http-роутов, его можно закрывать с помощью oauth. Какого-то значимого оверхеда в нашем случае он не несет. Вопрос в том как организовать unmanaged-подход для работы с ним на стороне Unity.
Обычный класс Socket является managed-сущностью, однако работу с ним можно сильно оптимизировать используя пулы и буферы. Но у меня уже большая часть проекта была на unmanaged-ECS и я искал способ ПОЛНОСТЬЮ избавиться от любых классов и использования GC.
Способ нашелся: в ECS-мире есть пакет com.unity.transport. Он полностью unmanaged, создан для использования в ECS, поддерживает WebSocket. Берем в работу и собираем прототип. Прототип собран, но что-то лыжи не едут (
Я вижу в логах nginx что соединение устанавливается, однако далее оно рвется ещё до первых сообщений. Пара часов отладки, логирования всего чего только можно и вдруг я натыкаюсь на информацию что не смотря на WebSocket-природу протокола, он является проприетарным и шлет свои handshake-сообщения. Да, он поддерживает WebSocket, но только для того чтобы работать из браузера при подключении к бэкенду на юнити где также используется com.unity.transport…
Как-то реверс-инженирить этот протокол смысла я не увидел — ненадежно это — и решил двигаться дальше.
К сожалению out-of-the-box решения для unmanaged сетевого взаимодействия я не нашел. Разве что вручную собирать сетевую библиотеку на C++ под все поддерживаемые платформы и подкладывать в сборку. Да, это в целом решение, но на текущей стадии разработки это большой оверхед. Поэтому я вернулся к managed-классу Socket, благо если обложить его пулами и буферами, в рантайме он никакой нагрузки на GC не создаёт.
В итоге мы имеем WebSocket канал взаимодействия с бэкендом, этот канал роутится на отдельный ендпоинт с помощью nginx, по нему мы гоняем бинарные данные и разбираем на клиенте без парсинга строк. PROFIT! )
Продолжение следует…
#devblog
Какой способ получения данных выбрать?
Обычно игры все ресурсы тянут с локального жесткого диска, а по сети получают только координаты объектов и других игроков. В моём же случае весь игровой мир приходит из интернета. Вопрос в том как организовать эффективный стримминг.
Начал эксперименты я с хорошо мне знакомого HTTP + JSON.
Комбинация проверенная временем, позволяет удобно дебажить запросы, есть возможность использовать oauth, роутить ендпоинты через nginx. Готовый инструментарий на все случаи жизни.
Однако, в моём случае этот подход хранил много неприятных сюрпризов.
Во-первых, сам HTTP несет с собой много оверхеда — открытие соединения, обмен заголовками, закрытие соединения — и так на каждый запрос. А тайлы мира мы запрашиваем постоянно, особенно при движении на машине. И вместо того чтобы иметь постоянный канал нам приходится постоянно открывать/закрывать соединение.
Во-вторых, JSON очень громоздкий формат. Он безусловно очень удобен для ручного написания структур, для отладки, просмотра что там пришло с сервера, но проблема в том что для 1000 однотипных элементов сами имена свойств могут занимать места больше чем полезные данные! А значит мы гоняем по сети бесполезный балласт, расходуя ценный bandwidth.
В-третьих, JSON это строка, а значит принимающая сторона должна её распарсить превращая в понятные движку структуры. В худшем случае придется создавать managed-сущности, а это тот самый ненавистный GC-pressure.
Пришлось выкидывать этот подход на помойку и искать новый, более эффективный способ.
Начнем с самого формата данных. Вербозный JSON можно заменить на байт-код, либо кастомный свой формат, либо какой-нибудь всем известный protobuf или FlatBuffers. Так мы избавимся от парсинга строк, сократим до 60% объем передаваемых данных и сможем наладить unmanaged-пайплайн для обработки на стороне unity-клиента.
Осталось что-то придумать с HTTP. Как я уже говорил, у него есть несомненное преимущество в виде возможности роутить ендпоинты с помощью nginx и использовать oauth-авторизацию. Если мы возьмем голые сокеты, TCP или UDP — мы потеряем эти возможности.
На помощь пришел WebSocket — он умеет жить в среде http-роутов, его можно закрывать с помощью oauth. Какого-то значимого оверхеда в нашем случае он не несет. Вопрос в том как организовать unmanaged-подход для работы с ним на стороне Unity.
Обычный класс Socket является managed-сущностью, однако работу с ним можно сильно оптимизировать используя пулы и буферы. Но у меня уже большая часть проекта была на unmanaged-ECS и я искал способ ПОЛНОСТЬЮ избавиться от любых классов и использования GC.
Способ нашелся: в ECS-мире есть пакет com.unity.transport. Он полностью unmanaged, создан для использования в ECS, поддерживает WebSocket. Берем в работу и собираем прототип. Прототип собран, но что-то лыжи не едут (
Я вижу в логах nginx что соединение устанавливается, однако далее оно рвется ещё до первых сообщений. Пара часов отладки, логирования всего чего только можно и вдруг я натыкаюсь на информацию что не смотря на WebSocket-природу протокола, он является проприетарным и шлет свои handshake-сообщения. Да, он поддерживает WebSocket, но только для того чтобы работать из браузера при подключении к бэкенду на юнити где также используется com.unity.transport…
Как-то реверс-инженирить этот протокол смысла я не увидел — ненадежно это — и решил двигаться дальше.
К сожалению out-of-the-box решения для unmanaged сетевого взаимодействия я не нашел. Разве что вручную собирать сетевую библиотеку на C++ под все поддерживаемые платформы и подкладывать в сборку. Да, это в целом решение, но на текущей стадии разработки это большой оверхед. Поэтому я вернулся к managed-классу Socket, благо если обложить его пулами и буферами, в рантайме он никакой нагрузки на GC не создаёт.
В итоге мы имеем WebSocket канал взаимодействия с бэкендом, этот канал роутится на отдельный ендпоинт с помощью nginx, по нему мы гоняем бинарные данные и разбираем на клиенте без парсинга строк. PROFIT! )
Продолжение следует…
#devblog
🔥2
С получением игровых локаций разобрались. Здесь важен каждый пакет, потому TCP-природа WebSocket приходится кстати.
Но TCP также несет определенный оверхед, который может быть вреден для получения данных перемещения NPC и других игроков.
Нам нет смысла дожидаться каждого пакета — ведь через долю секунды придет следующий, а значит нам важнее быстрее получать свежие данные чем гарантия доставки каждой крупицы информации.
С «голым» UDP работать сложновато, слишком много всего нужно реализовывать самому, начиная с формата данных, заканчивая handshake и прочими системными сообщениями. Есть готовые open-source-протоколы которые можно использовать.
Я обратил внимание на KCP. Он также умеет работать в режиме гарантии доставки (как TCP), что может пригодится для важных игровых событий вроде обновления статуса игровых объектов, применения поверапов или «попадания в цель».
К сожалению, unmanaged-реализации KCP под юнити я не нашел, но это не беда — я написал свою.
В качестве формата данных я взял FlatBuffers. Он поддерживает delta-сообщения а значит мы можем атомарно обновлять конкретные значения и не гонять по сети весь статус целиком.
В итоге любые изменения состояния шлются быстрыми короткими delta-пакетами, а раз в несколько секунд проводится полная синхронизация.
Мы получили молниеносный и стабильный способ синхронизации состояния с бэкендом, но к сожалению потеряли http-удобняшки вроде роутинга и oauth. Придется с этим как-то жить )
#devblog
Но TCP также несет определенный оверхед, который может быть вреден для получения данных перемещения NPC и других игроков.
Нам нет смысла дожидаться каждого пакета — ведь через долю секунды придет следующий, а значит нам важнее быстрее получать свежие данные чем гарантия доставки каждой крупицы информации.
С «голым» UDP работать сложновато, слишком много всего нужно реализовывать самому, начиная с формата данных, заканчивая handshake и прочими системными сообщениями. Есть готовые open-source-протоколы которые можно использовать.
Я обратил внимание на KCP. Он также умеет работать в режиме гарантии доставки (как TCP), что может пригодится для важных игровых событий вроде обновления статуса игровых объектов, применения поверапов или «попадания в цель».
К сожалению, unmanaged-реализации KCP под юнити я не нашел, но это не беда — я написал свою.
В качестве формата данных я взял FlatBuffers. Он поддерживает delta-сообщения а значит мы можем атомарно обновлять конкретные значения и не гонять по сети весь статус целиком.
В итоге любые изменения состояния шлются быстрыми короткими delta-пакетами, а раз в несколько секунд проводится полная синхронизация.
Мы получили молниеносный и стабильный способ синхронизации состояния с бэкендом, но к сожалению потеряли http-удобняшки вроде роутинга и oauth. Придется с этим как-то жить )
#devblog
🔥4
Наполнение биомов через детерменированный рандом используя принципы гильдий
Цикл приемки через визуальный анализ
На скриншотах LightForest
4 итерации, последнюю оставляем и переходим к следующему биому
#kitchensink
Цикл приемки через визуальный анализ
На скриншотах LightForest
4 итерации, последнюю оставляем и переходим к следующему биому
#kitchensink
🔥3👻1