Сетевой слой.
Какой способ получения данных выбрать?
Обычно игры все ресурсы тянут с локального жесткого диска, а по сети получают только координаты объектов и других игроков. В моём же случае весь игровой мир приходит из интернета. Вопрос в том как организовать эффективный стримминг.
Начал эксперименты я с хорошо мне знакомого 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