Физическое лицо
55 000
Развлечение и спорт
Россия
Декабрь 2024
Заказчик пришёл с чётким ТЗ: нужен прототип сетевой игры на Unity с NGO, поддержкой Lobby, Relay и Authentication. Ни графики, ни дизайна, ни фишек — только логика и функционал. Суть игры — простая "ходилка": бросаешь кубик, двигаешься по точкам, выполняешь действие. Но всё должно работать по сети, с синхронизацией и управлением сессией.
На старте мы детально разобрали ТЗ вместе с заказчиком. Главное, что выяснилось:
• Ему не нужна игра как продукт, ему нужна структура сетевого проекта, которую он может развивать или показывать команде.
• UX может быть грубым, но логика должна быть чёткой — всё, что написано в ТЗ, должно выполняться, даже если оно визуально выглядит как интерфейс из 2008 года.
• Он сам хотел понимать, что где происходит в проекте, поэтому — комментарии в коде, документация и чистый код.
С этого момента проект начал восприниматься как максимально честный прототип, без "украшательств", но с полной логической базой. Цель — чтобы любой разработчик потом мог зайти в проект и сразу понять структуру.
Прежде чем что-то делать визуально, была поставлена задача — собрать каркас, который потом не нужно будет выкидывать, если проект вдруг решат масштабировать. Всё начинается не с UI и не с кубика, а с архитектуры управления сценами, состоянием и сервисами.
Почему решили использовать DI (Dependency Injection)?
С заказчиком обсуждали, что:
• Он сам будет дальше работать с проектом или подключать других разработчиков.
• Важно, чтобы логика не была раскидана по случайным MonoBehaviour на сценах.
• Компоненты должны быть включаемыми и заменяемыми, без переписывания префабов.
• Не хочется танцев с синглтонами, статикой и прочими утечками памяти, которые потом чинятся слезами.
Поэтому встал вопрос: что использовать как DI-контейнер?
• VContainer
Почему не Zenject:
Zenject — перегружен, и у него периодически странные баги с Unity 2022+.
VContainer — компактнее, быстрее стартует, имеет хорошую интеграцию с Unity Jobs/Addressables, и не требует вороха аттрибутов ради простых вещей.
Заказчик хотел простую структуру, но при этом — чтобы сервисы можно было переиспользовать. Так и родилось ядро на VContainer.
Что конкретно сделали на этапе подготовки:
1. SceneManager (не Unity’s, а наш)
Создан отдельный ISceneLoaderService, реализующий логику переключения сцен через SceneManager.LoadSceneAsync, с передачей нужных параметров. Обёртка нужна, чтобы:
• Управлять сценами централизованно;
• Подключать middleware (например, прогресс-бары, preloader, async data injection);
• Не пихать SceneManager.LoadScene() в каждый UI Button.
• Загружать и проверять по сети состояние сцену у игроков.
Почему так: чтобы в будущем, при расширении, не пришлось пересобирать всё через ScriptableObject события или вручную прокидывать ссылки. Всё контролируется DI, всё централизовано.
2. GameState / AppState менеджер
Создана стейт-машина и базовые состояния
Это простая стейт-машина, чтобы понимать, где находится игрок, и какие сервисы должны быть активны. Например, Relay стартует только в состоянии LobbyHosting, а PlayerSpawner — только в Game.
Сделано это потому что Unity сама по себе не управляет состоянием. Без этого в сетевой игре легко словить ошибку, когда компонент активен не в то время и делает что-то лишнее (спавнит кубик в лобби, подключает Lobby в сцене Game и т.п.)
3. UI Navigation — через DI, а не через FindObjectOfType
Вместо того, чтобы искать UI объекты через Find или SerializedField, UI-канвасы и панели регистрируются в VContainer и вызываются из сервисов.
Примитивный IUIService управляет открытием окон, переключением страниц (типа лобби, подключения, создания сервера и т.д.), так UI становится управляемым, тестируемым и не завязанным на конкретную сцену. Панель можно заменить, отложить инициализацию, подключать поздно (например, если игрок перезашёл).
Как это обсуждалось с заказчиком:
Показывал, что можно сделать «по-старому» — навесить всю логику прямо в LobbyController, но тогда при любом добавлении новой фичи всё плывёт. Он согласился, что проект, скорее всего, будет жить долго, и лучше один раз сделать систему, чем потом "фиксить баги с появлением кнопок".
Он хотел видеть максимум прозрачности: где что находится, как что работает. Поэтому вся DI-интеграция была построена по именованным интерфейсам, с комментариями в LifetimeScope-конфигурации. Указано, что регистрируется, зачем, и как это потом используется.
На этом этапе фокус был на сетевом подключении: создание игры, поиск серверов, присоединение, выбор слота, все это должно работать через Lobby + Relay и параллельно по локалке, если интернет отключён.
Почему именно NGO + Lobby + Relay
С заказчиком обсудили два варианта:
• Photon (старый добрый вариант, но не встроен в Unity и платный);
• Netcode for GameObjects (NGO) с Relay и Lobby — встроенное, официальное, и главное — бесплатное в пределах лимитов.
Выбрали NGO. Он проще в интеграции, хорошо стыкуется с Unity Services. Relay — нужен для обхода NAT (иначе соединения тупо не проходят за роутерами), а Lobby — чтобы игроки могли искать сервера без внешней БД.
Далее приступили к системе слотов.
Каждому игроку нужно выбрать слот. Потому что дальше всё будет строиться на этих слотах — от очереди хода до отображения статуса. Сделали максимально просто:
• Слоты фиксируются после нажатия "Готов".
• Только свободные слоты активны.
• При выходе игрока слот освобождается.
• Хост может кикнуть любого игрока из слота.
У клиентов кнопка — "Готов", у хоста — "Старт".
Хост может запустить игру только если есть хотя бы один готовый клиент.
С заказчиком договорились, что важна прозрачность: у всех игроков отображаются имена, слоты, статус (Готов/Не готов), кто хост. Чтобы не было недопонимания “кто сейчас ждёт кого”.
На этом этапе — сама игра, то есть та самая "ходилка". Сцена Game, на ней происходит основное действие. У каждого игрока есть персонаж, очки, текущая точка. Всё должно быть синхронизировано, всё должно быть видно всем.
? Очередность и управление ходом
Самая важная логика: ходит только один игрок за раз, по очереди. Остальные просто наблюдают.
Как реализовано:
Используем клиент-серверную логику, все хранится на стороне хоста, после чего отправляется на клиента при каких-то событиях. Клиент же в свою очередь отправляет команды на сервер, сам ничего не изменяя в игровой логике. Т.е Host Authority архитектура.
Порядок ходов строится по слотам, выбранным в Lobby.
Когда игрок завершает ход — вызывается NextTurn() у сервера (хоста), и активным становится следующий игрок по списку.
Если игрок выходит — очередь пересчитывается.
Если игрок AFK > 3 минуты — исключается, вызывается SkipTurn().
? Кубик и движение
Бросок кубика — это физический объект, спавнится и кидается только у активного игрока, остальные видят через RPC позицию/значение.
После броска — движение по List Points.
После движения — событие (появляется UI с выбором действий).
У каждого есть PlayerData, синхронизированный через NetworkBehaviour.
Когда один игрок получает очки, все видят это обновление сразу (через ClientRpc).
Всё делалось с прицелом на максимальную читаемость — чтобы любой мог в любой момент понять, что происходит.
Использовали только прямое взаимодействие по сети, с явной синхронизацией состояний, без NetworkVariable и так далее.
Архитектура позволяла легко изменить условия (например, вместо очков — деньги, вместо клеток — зоны ивентов).
Всё, что здесь есть — результат последовательного диалога. Мы каждый этап проговаривали:
• Что точно нужно
• Что можно упростить
• Что должно остаться масштабируемым
Базовый принцип был такой: игра должна быть максимально "простой" — и в логике, и в коде. То есть — не просто «работает», а понятно, как работает. Никаких лишних абстракций, никаких custom SDK, никакой дикой архитектуры. Только простые, чистые инструменты Unity, чтобы заказчик или другой разработчик мог спокойно в это войти.
![]()
Алексей Кострыкин
Не всегда разработка - это разработка полноценного продукта.