ESPNBriefcases, ladders and chaos: Your guide to WWE Money in the BankESPN Deportes¿Qué necesita tu equipo de NBA? El mayor hueco en cada plantillaThe Jerusalem PostMiddle Israel: Remember October 7 by building a stronger western Negev - opinionDaily MaverickLOCAL ELECTIONS 2026: High court orders ANC to reinstate Andile Lungisa after unlawful removalInquirerSenate takes down impeachment livestream after data privacy concernsRTP DesportoFC Porto com prejuízos de mais de 20 milhõesZDF heuteAktuelle Pressemitteilungen des ZDFConsequencePhoebe Bridgers Reunites with boygenius for A24’s Sound Check at Cherry Lane Theatre Series: WatchGolem.deCyberangriffe auf Banken: Hacker nutzt KI und hinterlässt AccountdatenSRF NewsLehrer, Vater, Sportler – Wenn andere aufgeben, fängt Giuseppe Palaia erst anBillboardFuerza Regida Talk Street Mob Records, A&R & Their Stadium Tour | Billboard Cover Interview7sur7L’épisode El Niño atteint son niveau record
The Daily Newsstand · Free, Always
Friday, October 9, 2026

Steam показал мою игру 480 тысяч раз. В ней было нечего делать — поэтому я переделал её под мультиплеер

Translate

У моей игры в Steam было 480 439 показов, 49 788 переходов на страницу и CTR 10,4%.

И восемь отзывов.

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

Он был прав.

Я выпустил Drift Desert Race как игру-мем: садишься в машину и восемь часов едешь через пустыню, чтобы в конце узнать смысл жизни. Идея объясняется одной фразой, хорошо выглядит в ролике и почти не отвечает на главный вопрос игры:

А что игрок должен делать на второй минуте?

Я мог заменить пару моделей, добавить ещё одну машину и назвать это большим обновлением. Вместо этого начал переделывать проект в кооперативный road trip на 2–4 человека: с короткими поездками, общей дорогой, случайными событиями, нормальной картой и миром, который постепенно сходит с ума.

Под катом — не история «я добавил мультиплеер за выходные». Здесь Steam Lobby, P2P, дорога с floating origin, детерминированная генерация и баг, из-за которого соединение между двумя компьютерами было идеальным, ping составлял 2 мс, а игра всё равно не могла передать даже кнопку Ready.

Новая витрина Drift Desert Race

Что именно было не так с первой версией

В оригинальной игре была одна сильная идея: восьмичасовая поездка.

Она же была её главной проблемой.

Человек покупал игру, выбирал машину, выезжал на дорогу и довольно быстро видел почти всё, что игра могла ему показать. Восемь часов не создавали глубину — они растягивали отсутствие событий. Это как сделать загрузочный экран длиной в полный рабочий день и назвать его endurance-механикой.

У меня уже были работающая физика автомобиля, таймер, Steam Achievements и лидерборды. Формально список возможностей выглядел нормально. Игрового цикла внутри него не было.

Особенно неприятно это выглядело рядом со статистикой страницы:

Метрика

Значение

Показы в Steam

480 439

Переходы на страницу

49 788

CTR

10,4%

Отзывы на момент разбора

8

CTR говорил, что обложка и сама идея заставляют людей кликнуть. Количество отзывов — что дальше я их не убеждаю.

Это не точная воронка продаж: показы, посещения и отзывы измеряют разные этапы и сами по себе не объясняют, где ушёл каждый человек. Но разрыв был достаточно большим, чтобы перестать обвинять алгоритмы Steam и посмотреть на игру.

Решение: не делать маленькую Forza

Первая реакция была предсказуемой: больше машин, лучше физика, больше трасс, тюнинг.

То есть попробовать в одиночку конкурировать с играми, у которых сотни разработчиков, лицензированные автомобили и годы производства.

Отличный план, если хочется гарантированно не закончить проект.

Я выбрал другое направление:

Сможете ли вы с друзьями восемь часов ехать через бесконечную пустыню, пока мир постепенно сходит с ума?

Восьмичасовой режим остался, но перестал быть единственным:

  • Quick Ride — 15 минут;

  • Desert Run — 45 минут;

  • Endurance — 2 часа;

  • Ultimate Journey — 8 часов;

  • Free Ride — без финиша.

Все режимы используют одну сцену и один таймер. Длительность — данные выбранной поездки, а не очередная ветка if (ultimate) duration = 28800.

Это кажется мелочью, но раньше вся игра была построена вокруг магического числа 28800f. Теперь короткий режим можно пройти, протестировать и дать человеку, который не планировал посвятить инди-игре смену на заводе.

Поездка конвоем

Поездка конвоем

Инцидент 1. Лобби работает, игроки видят друг друга, игра не начинается

Для мультиплеера я взял Steam Lobby и ISteamNetworkingMessages. Без выделенного сервера: один игрок создаёт комнату и становится host, остальные подключаются к нему через Steam P2P.

Host отвечает за:

  • состояние комнаты;

  • выбранную поездку и карту;

  • seed мира;

  • время сессии;

  • запуск и завершение игры.

Каждый игрок локально считает физику своей машины. Положение отправляется остальным 25 раз в секунду, а удалённая машина отображается с интерполяционной задержкой 120 мс. Старый snapshot не подтверждается и не пересылается: через 40 мс всё равно придёт новый.

На бумаге всё было готово. Unit-тесты проходили. С одним Steam-аккаунтом создавалось лобби, обновлялись metadata, работал поиск комнат.

На первом тесте с двумя настоящими аккаунтами игрок вошёл в комнату — и всё развалилось.

Статусы не обновлялись. Kick мигал. Host не мог начать игру. Сессия то принималась, то отваливалась по таймауту.

Причин оказалось несколько.

Первая: запрос P2P приходил раньше callback Steam о новом участнике. Host проверял список комнаты, не находил там отправителя и честно считал его посторонним.

Вторая: после десяти секунд тишины код удалял peer не только из сетевой сессии, но и из собственного whitelist. Человек всё ещё находился в Steam Lobby, однако игра уже решила, что его не существует.

Третья: heartbeat отправлялся с флагом UnreliableNoDelay. Steam имеет право выбросить такой пакет, пока маршрут ещё строится. Пакет, который должен был оживить соединение, не отправлялся именно потому, что соединение ещё не ожило.

Я исправил порядок допуска peer, разделил roster комнаты и состояние транспорта, добавил повторную отправку состояния игрока и автоперезапуск сломанной сессии.

Следующий тест показал Connected, ping 2 мс и входящие сообщения.

Кнопка Ready всё ещё не работала.

Инцидент 2. Я отправлял по сети только заголовки пакетов

У каждого сообщения был общий заголовок: версия протокола, тип, sequence, sender, tick и timestamp. Затем callback дописывал payload конкретного сообщения.

Упрощённо код выглядел так:

void Send(MessageType type, Action<NetworkWriter> writePayload)
{
    var writer = new NetworkWriter(buffer);
    WriteHeader(writer, type);
    writePayload(writer);
    transport.Send(writer.ToArray());
}

NetworkWriter был struct.

Action<NetworkWriter> получал его по значению.

Payload исправно записывался — в копию writer, которая сразу исчезала. В сеть уходил только header. Поэтому транспорт видел пакеты и считал соединение живым, а Ready, выбранная машина и GameStart оставались пустыми.

Снаружи это выглядело особенно убедительно:

Connecting -> FindingRoute -> Connected
Ping: 2 ms
Messages received: yes
Game state synchronized: no

Починил передачей writer по ссылке:

delegate void PayloadWriter(ref NetworkWriter writer);

void Send(MessageType type, PayloadWriter writePayload)
{
    var writer = new NetworkWriter(buffer);
    WriteHeader(ref writer, type);
    writePayload(ref writer);
    transport.Send(writer.ToArray());
}

И добавил тест, который проверяет не только то, что пакет «отправился», но и то, что после заголовка действительно существует payload и он читается обратно.

Вывод неприятный, но полезный: состояние Steam-сессии ничего не говорит о состоянии вашей игры. Connected подтверждает маршрут между компьютерами. Оно не подтверждает, что вы сериализовали хотя бы один байт полезных данных.

Чтобы экран комнаты не зависел от P2P, готовность, машина, journey и команда старта теперь дублируются через lobby metadata. P2P остаётся нужен для движения автомобилей, но потеря одного пакета больше не превращает кнопку Start в декоративный элемент.

Инцидент 3. Бесконечную дорогу нельзя хранить в координатах Unity

Старая карта была короткой и повторяющейся. Если просто вытянуть её на восемь часов, появляется другая проблема: чем дальше объект уезжает от начала координат, тем меньше точность float. Физика начинает дрожать, колёса живут своей жизнью, камера подтверждает существование четвёртого измерения.

Поэтому мир теперь состоит из индексированных сегментов:

segment -2, -1, 0, 1, 2, 3 ...

В памяти находится только окно вокруг машин. Сегменты берутся из пула и возвращаются туда же — без постоянного Instantiate/Destroy во время поездки.

Когда игрок уезжает примерно на несколько километров, FloatingOrigin переносит весь активный мир обратно ближе к нулю. Но постоянная позиция машины хранится не в Unity-координатах, а в логических:

logical position = local position + accumulated origin offset

Именно логическая позиция уходит по сети. Иначе два компьютера после разных origin shift считали бы, что одна машина одновременно находится в разных местах.

Тут нашёлся отдельный визуальный баг. Световые следы RCC хранили точки в мировых координатах. После переноса мира на пять километров старые точки оставались на месте, и от машины через половину карты тянулась красная линия.

Выглядело эффектно.

Особенно если не знать, что это баг.

Теперь trails очищаются при teleport и origin shift, причём несколько кадров подряд: интерполируемый rigidbody успевал ещё раз нарисоваться в старой позиции уже после первого сброса.

Одинаковый мир без синхронизации каждого кактуса

Отправлять по сети положение каждого камня, знака и заброшенной заправки бессмысленно. Мир строится как чистая функция:

content = Generate(worldSeed, segmentIndex)

Host передаёт seed один раз. Каждый peer независимо получает ту же геометрию дороги, landmark и декорации для конкретного индекса.

Сейчас на дороге есть rest stop, заправка, кладбище машин, разбитый самолёт, мотель, город-призрак и другие ориентиры. Соседние сегменты не выбирают один и тот же landmark, а число активных объектов остаётся ограниченным длиной окна, а не пройденным расстоянием.

Карта при этом стала палитрой. Та же система дороги и streaming работает для пустыни, зимы и Японии. В комнате карту выбирает host; её ID передаётся вместе с seed и командой старта.

Зимняя палитра

Зимняя палитра

Японская палитра

Японская палитра

Это дешевле, чем делать три отдельные игры внутри игры, и надёжнее для сети: меняются материалы, декорации и набор landmark, но не фундамент мира.

Дорога должна не только продолжаться, но и отвечать

Процедурная генерация сама по себе не создаёт контент. Если бесконечно переставлять одни и те же кактусы, получится очень длинная пустота.

Поэтому поверх дороги появился RandomEventSystem:

  • Sandstorm;

  • Wrong Way Driver;

  • Engine Trouble;

  • Radio Signal;

  • The Bus;

  • UFO.

После каждого события следующее предупреждение появляется через 40–120 секунд. Расписание зависит от seed и host-time, поэтому у всех игроков событие начинается в одной точке путешествия.

Сами события разделены на definition, scheduling и presentation. Планировщик можно тестировать без сцены, Steam и автомобиля. Визуальный слой уже решает, включить ли туман, создать встречную машину или показать предупреждение.

В короткой поездке игра старается сначала показать разные события, а не пять раз подряд сломать двигатель. В длинной растёт уровень странности: сначала это песчаная буря и странный сигнал, позже — автобус, НЛО и вещи, которые сложнее объяснить дорожными работами.

События на дороге

События на дороге

Главная цель не в том, чтобы каждое событие было отдельной AAA-миссией. Оно должно нарушить ритм, заставить друзей заговорить и создать момент, который можно вырезать в короткий ролик.

Четыре человека спокойно едут сорок минут. Затем сзади появляется автобус на скорости 180 км/ч.

Вот это уже игровой эпизод. Ещё один перекрашенный автомобиль — нет.

Что ещё пришлось переделать

Мультиплеер быстро вытянул за собой половину проекта.

Меню и комнаты. Появились создание и поиск Steam Lobby, Friends Only и Private, приглашения, выбор машины, карты и поездки, ready-state и понятные состояния CONNECTING / READY / NOT READY. Строки интерфейса больше не разбросаны литералами по MonoBehaviour.

Пауза. В одиночной игре она останавливает время. В мультиплеере — только снимает локальное управление. Иначе один человек нажал Escape, а пустыня замерла у всех четырёх.

Late join. Игрок, вошедший в уже начатую поездку, получает snapshot: journey, seed, прошедшее время, состояние комнаты и roster. Его локальное время присутствия начинается с нуля, поэтому он не получает часовое достижение за семь часов, которые проехал host.

Машины. Новые low-poly модели адаптируются через собственные компоненты поверх RCC. Vendor-код не переписывается под каждую идею проекта.

Локализация. Каталоги подготовлены для английского, русского, упрощённого китайского, немецкого, испанского, бразильского португальского, французского, японского, корейского и польского. Автоматическая проверка ловит пропущенный ключ и несовпавшие параметры форматирования.

Сборка и диагностика. Появились проверки сцен, journey-каталога, локализации, сетевой smoke test и отдельный UI/world tour, который сам проходит экраны и фотографирует карту. После случая с «исправил код, загрузил старый build» скрипт Steam upload отказывается отправлять сборку, которая старше исходников.

Последний пункт родился не из любви к инфраструктуре.

Я действительно один раз тестировал исправление, которого физически не было в загруженном exe.

Почему я не называю мультиплеер готовым

Код умеет создать комнату, синхронизировать настройки, загрузить общую карту, передавать snapshots автомобилей, восстанавливать состояние late joiner и переживать отключение клиента.

Но полный приёмочный прогон между двумя Steam-аккаунтами после последних исправлений ещё нужен. Также нужны тесты с задержкой и потерей пакетов, disconnect посреди заезда и восьмичасовой soak test.

Есть и игровые системы, которые пока существуют только как фундамент:

  • drift scoring посчитан и покрыт тестами, но ещё не подключён к машине и HUD;

  • Stay Together и Convoy имеют host-authoritative lifecycle, но не встроены в реальный заезд;

  • интерфейс leaderboards и offline fallback готовы, Steam adapter ещё не написан;

  • переводы требуют визуальной проверки шрифтов и ручной редакторской вычитки;

  • collision удалённых машин работает через запаздывающую позу и пока не имеет prediction.

Это важное различие. «Реализовано» означает, что код существует. «Проверено» — что два человека действительно прошли через него от кнопки Create Room до финиша и получили ожидаемый результат.

После истории с writer-копией я стараюсь не путать эти слова.

Что в итоге изменилось

До переработки Drift Desert Race была восьмичасовой шуткой с таймером.

Теперь это каркас кооперативного road trip:

Lobby
  -> выбор journey, карты и машин
  -> общий seed
  -> детерминированная дорога
  -> landmarks и события
  -> короткий финиш или восьмичасовое испытание

У проекта появилась не просто длина, а темп. Человек может получить законченную поездку за 15 минут. Восемь часов остаются экстремальным режимом, но больше не прикрывают собой отсутствие выбора.

А ещё я наконец понял разницу между продающей идеей и игровой петлёй.

«Ехать восемь часов ради смысла жизни» — продающая идея.

«Удержать конвой, пережить песчаную бурю, не дать автобусу разнести друзей, доехать до следующей странности и сравнить результат» — потенциальная игровая петля.

Первой достаточно для клика.

Для игры нужна вторая.

Выводы, которые я бы хотел знать до релиза

  1. Мем объясняет игру, но не заменяет её. Если концепт помещается в одну фразу, это хорошо для страницы Steam. На второй минуте всё равно нужна цель.

  2. Длинная сессия начинается с короткой. Пока 15 минут не работают, растягивать их до восьми часов бессмысленно.

  3. Connected — не тест протокола. Проверяйте round trip каждого payload, а не только состояние транспорта.

  4. Процедурный мир должен быть детерминированным. Особенно если завтра в него войдёт второй компьютер.

  5. Не синхронизируйте то, что можно одинаково вычислить. Seed и индекс сегмента дешевле тысячи сетевых объектов.

  6. Честный список незаконченного полезнее roadmap на двадцать обещаний. Он не даёт принять собранный фундамент за готовую игру.

Сейчас обновление уже гораздо ближе к той игре, которую обещает название и страница Steam. Но главный тест впереди: захотят ли два человека после первой Quick Ride нажать ещё раз — и решатся ли однажды на Ultimate Journey.

Если хотите посмотреть, как выглядела исходная восьмичасовая идея и во что она превращается, вот страница Drift Desert Race в Steam.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.