Привет, Хабр! Меня зовут Вова, я разработчик в Selectel. В прошлом году серии TrackMania исполнилось 20 лет. Это игра моего детства и мне захотелось «размять свои юные олдскулы», посмотреть, что изменилось, и применить новые навыки.

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

Используйте навигацию, если не хотите читать текст полностью:

История
Кубок дня
Официальные модификации
OpenPlanet
Написание плагина
Демонстрация

История


Всего лишь одна карта демонстрирует особенности TrackMania.

При упоминании «гоночек» быстрее всего вспоминается серия Need for Speed, которая на девять лет старше первой TrackMania (2003). Тем не менее, именно TrackMania показала мне жанр гонок с новой стороны.

  • Гонки проходят на треках, где можно проехаться по стене, подпрыгнуть на трамплине или даже сделать мертвую петлю. Это вам не городская среда!
  • У всех одинаковые машины — решает умение, а не валюты на тюнинг.
  • Заезд на время: вы сражаетесь с призраком, который поставил рекорд, а в мультиплеере машины не сталкиваются друг с другом. Снова решает умение, а не грамотная или удачная подстава для соперника.
  • Есть редактор карт.
  • Помимо привычных гонок на время есть режим Puzzle, в котором нужно сначала достроить трек.
  • В первой части есть три окружения: снега, пустыня и ралли. Каждое отличается скоростью и управлением.

Вскоре вышли продолжения в виде TrackMania Sunrise (2005) и TrackMania Nations EWSC (2006), которые добавляют суммарно еще четыре новых окружения.

TrackMania — это первая на моей памяти игра, которая помимо классического набора из «бронзы», «серебра» и «золота» ввела «золото авторов». Об особенностях авторского золота размышлял в своем Telegram-канале. Подписывайтесь, там можно увидеть заметки по темам статей, над которыми я работаю, и небольшие познавательные посты.

Семь окружений в трех разных играх… Напрашивалось объединение, которым стала TrackMania United (2006). Спустя два года вышло улучшение TrackMania United Forever (TMUF, 2008) и бесплатная TrackMania Nations Forever (TMNF). Последняя имела несколько ограничений:

  • доступно всего одно окружение — стадион;
  • в мультиплеере отображаются только скины по умолчанию, раскрашенные в цвета флагов стран.


Ключ TMUF был на шесть месяцев, но если купить TMU, то лицензия TMUF была безлимитной. Узнал этот секрет я только с третьего раза. TrackMania.

Я «перескочил» из первой части сразу в TMUF и был приятно впечатлен. Игра пропитана духом состязаний в онлайне. После получения медали в одиночной игре можно было сравнить свой рекорд с друзьями. Или пойти на выделенный сервер и соревноваться за лучшее прохождение в рамках игровой сессии.

TMUF и TMNF все еще пользуются популярностью в 2024, хоть и требуют дополнительных действий и включения небезопасного TLS 1.0.

В 2011 году выходит продолжение серии под названием TrackMania 2. Всего было выпущено четыре игры: Canyon (2011), Stadium (2013), Valley (2016) и Lagoon (2017). Если вам кажется, что история повторяется, то вы правы. Есть TrackMania: Turbo (2016), которая объединяет все четыре окружения в одной игре. Окружение Lagoon сперва появилось в TM: Turbo, а потом было «бэкпортировано» в TrackMania 2.

Актуальная сейчас версия игры называется TrackMania, но для различия с первой частью используют название TrackMania 2020. Несмотря на десяток окружений, которые были придуманы за 17 лет существования серии, TrackMania 2020 предлагает лишь одно, трижды проверенное: стадион. Однако в рамках него доступны различные гоночные поверхности и три дополнительных вида машин из самой первой игры.

TrackMania 2020 является бесплатной игрой с «клубным доступом» за €20/год. Бесплатная версия ограничена официальными режимами игры: рейтинговым режимом, режимом Royal на коротких трассах и одиночным режимом на сезонной кампании, которая обновляется четыре раза в год.


Кубок дня


Я прошел кампанию «Весна 2024» и решил, что время купить клубную подписку. Эта подписка открывает доступ к пользовательским картам, в том числе к «карте дня» (Track of the Day, totd) и режиму «кубок дня» (Cup of the Day, cotd).

Кубок дня — это ежедневное мероприятие, собирающее несколько тысяч игроков со всего мира. В 20:00 по московскому времени публикуется новая карта дня и начинается этап квалификации длиной в 15 минут. Она проходит на официальных серверах — в зачет идет лучшее время заезда (Personal Best, PB).

Основной кубок дня проходит в 20:00, но есть дополнительные в 5:00 и 12:00 следующего дня. На них людей меньше, а условия уже не так равны: карта доступна с 20:00.

Игроки сортируются по лучшему личному времени и объединяются в дивизионы по 64 человека и начинается чемпионат на выбывание.

  • Все игроки начинают заезд одновременно.
  • Первый раунд ознакомительный, никто не выбывает.
  • Каждый следующий раунд выбывают четыре худших игрока, пока количество не сократится до 16.
  • С 16 до 8 игроков каждый раунд выбывают два игрока.
  • С 8 до 2 игроков каждый раунд выбывает один игрок.
  • Последний игрок объявляется победителем.

Очевидно, что это соревнование на выносливость, ведь в борьбе за первое место придется проехать 23 раунда без ошибок. Иногда лучше выбирать наиболее стабильный и безопасный маршрут, а не вылететь с трассы в желании приехать первым каждый раунд.

Низшие дивизионы называют bonky div, а игроков — bonkers. Слово bonk используют в значении «врезаться». Google-переводчик дает достаточно грубые формы перевода, но мне все равно нравятся эти слова, описывающие новичков, к которым я отношу себя.


Спидометр на заднем бампере, а текущее место в гонке — на крыше машины. TrackMania.

Разработчики TrackMania сделали приятный интерфейс: на машине отображается текущее место в гонке (14), скорость (202) и три буквы ника. При прохождении контрольной точки чуть выше центра экрана отображается время и разница с лучшим заездом для этой контрольной точки. Это удобно для гонки на время, но, на мой взгляд, неудобно для гонки на выбывание.


Интерфейс гонки на выбывание. Да, игра не адаптирована к экранам с соотношением сторон 21:9. TrackMania.

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

Игра, конечно, вежливо намекает, если вы в зоне риска. При прохождении контрольной точки верхняя и нижняя рамки экрана начинают пульсировать красным. Это неплохо, но из-за концентрации на треке может быть незаметно. По крайней мере мне.

Забавы ради я решил, что хочу сделать предупреждение более заметным. Дополнительно захотел сделать уведомление о «безопасном» заезде. Это ситуация, когда все места на выбывание занимают игроки, которые нажали кнопку рестарта. В таблице они обозначаются НФ — «Не Финишировал». В этом случае нужно просто доехать до финиша.

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

Официальные модификации


Редактор карт был в TrackMania с самого начала. Неудивительно, что его развивали все время существования серии. В 2009 году с тизером TrackMania 2 анонсировали цифровую платформу ManiaPlanet, зачатки которой были в TMUF, — внутриигровую валюту и встроенные «сайты», продающие треки и скины.

Сразу после релиза в 2011 амбиции ManiaPlanet были по-настоящему велики.

  • На одном игровом движке и редакторе сделать гонки (TrackMania), шутер (ShootMania) и РПГ (QuestMania).
  • ManiaPlanet — user powered площадка обмена контентом для всех игр.
  • ManiaLink — веб-сайты внутри ManiaPlanet.
  • ManiaScript — скриптовой язык для программирования внутриигровых объектов. Как частей карт, так и игровых режимов.

Однако проект не взлетел. Последнее обновление цифровой площадки ManiaPlanet было в 2017. На смену TrackMania 2 пришла TrackMania 2020 без внутреннего «магазина», ShootMania как-то существует, но онлайн там минимален, а QuestMania и вовсе сохранилась только в веб-архиве и на форуме в теме «Мы ждем уже 9 лет!».

Наследие ManiaPlanet все же попало в TrackMania 2020.

  • Редактор карт стал более мощным. Сохранилась блочная система, в которую можно импортировать собственные 3D-модели. Это, в частности, демонстрирует чемпионат Формула Е, который воссоздает в игре настоящие треки из Токио, Берлина и Лондона.
  • К редактору карт прилагается редактор повторов Mediatracker. Этот редактор позволяет монтировать кинематографическую составляющую карт или записанных заездов.
  • Сохранился скриптовый язык ManiaScript для программирования внутри игры.

В TM2020 нет официальной документации по ManiaScript. Есть только заголовочный файл doc.h, по которому генерируют дерево классов и форум с примерами для предыдущих версий. В общем, официально остается только делать карты и скины. Выглядит очень грустно.

К счастью, TrackMania собрала удивительно целеустремленное коммьюнити. В 2016 году появился альтернативный инструмент для моддинга — OpenPlanet. Исходный код загрузчика модов закрыт, но инструмент официально признан разработчиками игры. На это указывает раздел OpenPlanet на форуме ManiaPlanet и упоминание, что некоторые функции OpenPlanet заблокированы без клубной подписки в игре. В частности, неподписанные плагины не будут запускаться в бесплатной версии игры. Это ограничение связано с борьбой против пиратства.

У меня уже есть клубный доступ, поэтому установим OpenPlanet.

OpenPlanet



Загрузчик OpenPlanet очень дружелюбный.

Установка OpenPlanet тривиальна. Загружаем exe-файл с официального сайта openplanet.dev, указываем каталог с TrackMania.exe и все. В случае, если игра не запускается, то в документации есть секция по устранению проблем. В случае удачного запуска OpenPlanet выведет уведомление, что панель модов открывается клавишей F3.


Магазин плагинов OpenPlanet.

В выпадающем меню Plugin Manager можно открыть магазин плагинов. Это подписанные плагины, которые будут работать в бесплатной версии игры. Многие из них можно встретить у стримеров по TrackMania. Вот краткий список полезных плагинов.

  • Split Speeds — на контрольных точках показывает не только дельту времени, но и скорости.
  • Ultimate Medals — маленькое окно, показывающее время, необходимое для получения каждой медали.
  • Dashboard — выводит различные элементы игры в интерфейс. Стримеры используют для отображения чувствительности устройств ввода, но можно также выводить внутреннее состояние машины: обороты в минуты, состояние колес и номер передачи.
  • Checkpoint Counter — выводит количество контрольных точек.
  • No-Respawn Timer — выводит идеальное время заезда, как будто вы не перезапускались с контрольных точек.

Эти плагины отображают доступную информацию, что позволяет оттачивать свои навыки. OpenPlanet и подписанные плагины не запрещены в том числе на онлайн-мероприятиях. На крупных мероприятиях, может быть, правила другие, но этого я не знаю.

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

  • MLFeed: Race Data — предоставляет информацию о текущем заезде: таблицу лидеров, время игроков на каждой контрольной точке, состояние гонки на выживание.
  • COTD Delta Time — выводит на экран дельту между игроком и первым местом на выбывание. Исходный код открыт, поэтому можно подсматривать в реализацию.

Этого хватит. Приступаем к разработке плагина.

Написание плагина



Включение режима разработчика.

В качестве скриптового языка в OpenPlanet используется язык С-подобный AngelScript со строгой типизацией. Также скрипты проходят через простой препроцессор ccpp, что обеспечивает совместимость скриптов с разными версиями TrackMania.

Плагины располагаются в каталоге C:\Users\%имя_пользователя%\Openplanet%версия_игры%\Plugins и могут быть представлены в двух форматах:

  • Zip-архив с расширением .op;
  • Каталог.

Для разработки, очевидно, удобнее использовать каталог. Поэтому создаем каталог GameStateExporter и переходим в него. Для описания плагина нужен манифест info.toml. Создаем его.

[meta]
name        = "GameState Exporter"
author      = "f1remoon"
category    = "Testing"
version     = "0.0.1"
[script]
dependencies = ["MLHook", "MLFeedRaceData"]


Время писать скрипт. Все файлы с кодом должны быть с расширением .as. При этом в пределах одного плагина вся исходная программа собирается одновременно. Например, опишем некоторые настройки в файле Settings.as.

[Setting category="Network" name="Enabled"]
bool GSE_Enabled = false;
[Setting category="Network" name="HTTP Endpoint" description="Only HTTP endpoints supported"]
string GSE_HTTPEndpoint = "http://127.0.0.1/";
[Setting category="Network" name="Delay between requests, ms" min=100 max=10000]
uint GSE_NetworkDelay = 1000;

А затем используем эти переменные в файле Main.as:

void Main()
{
        while (true) {
                sleep(GSE_NetworkDelay);
                if(!GSE_Enabled) {
                        continue;
                }
                print("Hello, world!");
        }
}

Файлы Settings.as и Main.as компилируются в один модуль и просто начинают работать. Дополнение: так как в info.toml указаны зависимости, то некоторые export-файлы зависимостей также компилируются в наш модуль. Подробнее — в документации.


Hello, World TrackMania!

Отлично. Теперь нужен архитектурный минимум. Попробуем отправить JSON из игры куда-нибудь. В документации OpenPlanet можно найти сразу все необходимые инструменты.

  • Пространство имен Json — сериализация и десериализация данных в JSON.
  • Пространство имен Net — сокеты.

Быстро изменяемые данные из игры правильнее всего отправлять через UDP, но, к сожалению, Net содержит только TCP-сокеты и HTTP-обертку. Не будем усложнять себе жизнь и воспользуемся HTTP.

Код ↓
void Main() {        
        while(true) {
                sleep(GSE_NetworkDelay > 100 ? GSE_NetworkDelay : 100);
                if(!GSE_Enabled) {
                        continue;
                }
                // Выходные данные
                auto data = Json::Object();
                // Собираем данные из плагинов
                const MLFeed::HookRaceStatsEventsBase_V2@ RaceData = MLFeed::GetRaceData_V2();
                const MLFeed::KoDataProxy@ KoData = MLFeed::GetKoData();
                // Игровой режим
                auto gamemode = KoData.GameMode;
                data["gamemode"] = gamemode;
                // Для игры на выбывание собираем условия
                if(gamemode == "TM_KnockoutDaily_Online" or gamemode == "TM_Knockout_Online") {
                        data["ko"] = Json::Object();
                        // Сколько игроков выбывают за раунд
                        data["ko"]["KOperRound"] = KoData.KOsNumber; 
                        // Когда останется столько игроков, количество выбывающих изменится
                        data["ko"]["milestone"] = KoData.KOsMilestone; 
                        data["ko"]["players"] = Json::Array();
                        for(uint i = 0; i < KoData.Players.Length; i++) {
                                auto p = Json::Object();
                                // Имя игрока
                                p["name"] = KoData.Players[i];
                                auto KoState = KoData.GetPlayerState(KoData.Players[i]);
                                // В игре?
                                p["isAlive"] = KoState.isAlive;
                                // Нажал рестарт? = выбыл досрочно
                                p["isDNF"] = KoState.isDNF;
                                data["ko"]["players"].Add(p);
                        }
                        data["ko"]["round"] = Json::Object();
                        // Номер текущего раунда
                        data["ko"]["round"]["current"] = KoData.MapRoundNb;
                        // Всего раундов
                        data["ko"]["round"]["total"] = KoData.MapRoundTotal;        
                }
                // Для любых гонок
                if(gamemode != "") {                        
                        data["live"] = Json::Array();
                        // SortedPlayers_Race - игроки сортированы по количеству пройденных
                        // контрольных точек
                        for(uint i = 0; i < RaceData.SortedPlayers_Race.Length; i++) {
                                auto player_data = Json::Object();
                                auto player = RaceData.SortedPlayers_Race[i];
                                // Имя игрока
                                player_data["name"] = player.name;
                                // Массив времён прохождения контрольных точек в мс.
                                // Нулевой элемент - старт, всегда 0
                                player_data["cp"] = player.CpTimes;
                                // Номер в рейтинге по мнению игры
                                player_data["rank"] = player.RaceRank;
                                // Количество рестартов
                                player_data["respawns"] = player.NbRespawnsRequested;
                                data["live"].Add(player_data);
                        }
                }
                // Пакуем в JSON и отправляем
                auto payload = Json::Write(data);
                Net::HttpPost(GSE_HTTPEndpoint, payload, "application/json");
        }
}


Этот код не идеален, но его можно улучшить.

  • Можно собирать данные не через заданный интервал, а через функцию обратного вызова (callback) Update, которая вызывается каждый игровой кадр.
  • Отправлять данные в отдельной корутине при изменении, а не через фиксированный интервал.

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


Бывшая гирлянда послужит в этом проекте.

В качестве светильника я взял пять метров WS2812B и Wemos D1 Mini, в качестве прошивки — WLED. Внутри WLED создал три пресета: красный (id=1), желтый (id=2) и зеленый свет (id=3). В локальной сети можно менять пресет, отправив JSON на эндпоинт /json/state такого содержания:

{“ps”: номер}

Компонент, принимающий решения, напишу на Python и FastAPI. Общая логика такова:

  • если игроков с isDNF=true больше, чем выбывает за раунд, то включает зеленый пресет;
  • если положение в рейтинге больше, чем количество игроков минус количество выбывающих за раунд, то включаем красный пресет;
  • в остальных случаях включаем желтый пресет.

Исходный код доступен на Github Gist.

Режим на выбывание не популярен на серверах, а кубок дня проходит в 20:00 и потом повторяется в 12:00. Поэтому писать код приходится в надежде, что все заработает с первого раза. Отлаживать во время соревнования затруднительно — можно быстро вылететь.

Возможно, эти тексты тоже вас заинтересуют:

Оцениваем алгоритмы планирования процессов в операционных системах
Как проектируют дата-центры? Разбираемся на практике
Встречаем Orange Pi CM5: альтернативу Raspberry Pi CM4 с 16 ГБ ОЗУ и 256 ГБ eMMC

Демонстрация


Очень кустарная запись. Подстать гирлянде в коробке. ?

К счастью, все заработало с первого раза. Ну, вернее, почти все. На видео нужно приглядываться, чтобы увидеть, что в момент перед переключением игра пропускает пару кадров. Во время гонки эти пропуски кадров выглядят вечностью. Я догадываюсь, в чем проблема, но посмотрим, что говорит OpenPlanet.


Слева — отладчик в режиме профилировщика, справа внизу — местный анализатор пакетов. TrackMania.

В начале игры я говорил, что инструментарий неофициального моддинга прекрасен. Здесь даже есть профилировщик плагинов, который обнаруживает медленное выполнение. Профилировщику, очевидно, не нравятся наши сетевые функции. Дело в том, что Net::HttpPost ожидает ответа от веб-сервера, а у нас медленный Python, который в момент обработки запроса отправляет запрос к медленному контроллеру и дожидается ответа.

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

Отправка данных в Python на локальном адресе не выбивается за 15 мс, а потому незаметна. Но взаимодействие с контроллером замедляет выполнение запроса на несколько сотен миллисекунд, что приводит к пропуску кадров.

Какие выводы из этого можно сделать? Можно внести изменения в плагин, как я упоминал в предыдущих разделах, а в Python вынести обращение к контроллеру в background_task. В идеале — добавить в OpenPlanet поддержку UDP.

Исследовать витиеватую историю любимой гоночной серии было непросто, но интересно. Делитесь своими впечатлениями в комментариях. И, конечно, используйте мой мод!

Комментарии (1)


  1. FFxSquall
    26.07.2024 15:25
    +2

    Спасибо за чувство ностальгии. Очень много времени проведено в TrackMania Nations Forever в свое время. Даже немного пытался в киберспорт, в то время некоторые организации открывали свои составы по TMNF. Прекрасное было время.