Что наша жизнь? Игра

в 15:13, , рубрики: 2d игры, java, openttd, Дизайн игр, ненормальное программирование, разработка игр, симулятор, симуляторы

Что делает в отпуске директор группы программерских компаний? То, что не может делать на работе. Программирует. Всласть. :)

Мне много лет, я выполз из тьмы и в Transport Tycoon Deluxe я играл ещё в то время, когда эта игра только появилась (это 94-й год).

Оригинальный Transport Tycoon
Оригинальный Transport Tycoon

Я считаю эту игру одним из лучших транспортно-экономических симуляторов всех времён и народов. И не я один. Когда игра устарела нашёлся человек, который её дизассемблировал (!) и переписал на Си. И получил игру с открытым кодом практически идентичную оригиналу. То есть - она могла поднимать game save и использовать спрайты оригинальной игры. Появилась OpenTTD.

OpenTTD
OpenTTD

OPenTTD развивается до сих пор и, в целом, это совершенно фантастический успех опенсорс сообщества. С ней всё хорошо. Кроме одного. Разработчики OpenTTD до сих пор удерживают внутри игры совместимость со старой игрой из 90-х. И это превращает код игры в изрядный треш. Ну то есть, например, состояние клетки игрового поля до сих пор хранится в виде группы бит определённого байта (с именем от m1 до m7, чтобы не было скучно). Не struct { ... int tileType : 4 ... } и type = tile.tileType, нет! - GB(_m[tile].m1, 4, 4), и только так. То есть, по сути, игра написана на ассемблере, обёрнутом в Си.

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

Ну и вот. Я решился взять этот вес.

Способ конвертирования кода был избран наиболее прямолинейный. Берём исходник на Си, кидаем его в файл ТоЖеИмя.java и открываем в эклипсе. Сначала купируем истерику компилятора снося все взятия адресов объектов, обращения через * и доступ к полям через ->, дальше код превращается из полностью красного в пятнистый. Идём по пятнам красного и исправляем.

Чтобы понимать масштабы бедствия - версия, от которой я стартовал - порядка 4 мб исходных текстов, порядка 150 тыс строк. Я сразу отрезал часть кода, которой решил точно не заниматься - музыка, звуки, save/load (логика реализации этой части невыносима из си) и ещё несколько некритичных и сделанных слишком по-сишному частей.

Читатель догадывается, что эксперимент был скорее успешен, чем нет. Иначе бы этой статьи не было. Но где-то на пятый день непрерывной войны я стал задумываться, реально ли вообще победить. Впрочем, малодушие было пресечено и безрассудство победило.

Несколько слов о том, каковы основные сложности конверсии. Я сознательно взял именно версию кода на Си (проект уже переписан на ++), полагая, что так будет проще. Думаю, что не прогадал.

Булевы и целые

Си не видит разницы между boolean и int. Java строга в этом плане, и пришлось пополнить код сотнями if( -> if( 0 != (... - впрочем, кое где это потом помогло выловить неприятности.

Беззнаковые целые разных размеров

В Яве нет unsigned. В основном это не проблема, потому что код тянулся ещё с 16-битных времён и оперировал больше байтами и 16-битными словами. Для него обычный int вполне беззнаков в рамках его потребностей. Но были неприятные места. Отмечу, что там где в оригинальном коде применялся uint8, применять явский байт нельзя - он знаковый. Ну или можно, но 0xFF & обязателен (при этом происходит преобразование в int и обрезание верха, так что результат верный). Я не осознал этой проблемы на старте и уже позже, во время отладки пришлось почти везде по коду искать и убивать переменные и поля типа byte и преобразования к нему. Впрочем, нашлись и обратные примеры - когда требовался именно знаковый байт. Например, смещение спрайта от точки отрисовки хранится в байте со знаком. Хранить можно и в int, но при чтении байта из оригинальной таблицы знак надо сохранить.

Использование оператора запятая

В Яве его тоже упразднили, и никто особо не заметил бы, но в коде OpenTTD пасся какой-то яростный фанат этого оператора. Применял он его не без изящества, оцените:

( bonus += 10, age > 10 ) || ( bonus += 20, age > 5 ) || ( bonus += 40, age > 2 ) || ( bonus += 100, true )

Красиво? Заменяется на switch с fall through. Но есть и идиотские примеры. Типа такого:

if (!tile.IsTileType(TileTypes.MP_RAILWAY) || ((dir = 0, tile.getMap().m5 != 1) && (dir = 1, _tile.getMap().m5 != 2)))

return Cmd.return_cmd_error(Str.STR_1005_NO_SUITABLE_RAILROAD_TRACK);

Их расшивать бывает трудно.

Пойнтеры в параметрах функций для возврата значений

Внезапно, лечатся очень легко.

modify( int[] x ) { x[0]++; }

Аллокация локальных объектов

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

NPFFindStationOrTileData fstd; ... NPFFillWithOrderData(&fstd, v);

Должно быть

NPFFindStationOrTileData fstd = new NPFFindStationOrTileData();
...
NPFFillWithOrderData(fstd, v);

На практике проблемы вообще нет, компилятор сообщает о том, что переменная не инициализирована.

Перечислимые типы

Тут у Явы всё отлично, но это отлично неприменимо по-сишному. В Си константы из enum - это целые. И можно flags & FLG_USED и будет всё как надо. В итоге почти все enum были заменены на просто целые константы.

Нет макросов

А в Си их применяют часто. Где-то (в инициализации массивов, например) за макросы сошли статические методы - если метод можно посчитать при компиляции, Ява принимает его при инициализации массивов. Но, к примеру, конструкцию BitOps.SB(tile.getMap().m2, 0, 4, new_ground); , которая позволяет модифицировать биты у произвольного поля, пришлось переделывать повсеместно.

Работа с памятью

Янус Полуэктович прошел к себе в кабинет, на ходу небрежно, одним универсальным движением брови ликвидировав всю сотворенную мною кунсткамеру.

Ну, понятно, что вызов free() мы просто удаляем. Но из-за отсутствия сборки мусора программы на Си местами вынуждены жутко извращаться в плане управления аллокацией и это местами заставляет программиста извращаться ещё жутче в коде, чтобы построить такие структуры, которые можно собрать при ручном управлении памятью. Любимый хит - указатель something **next, который смотрел на поле next последней собранной структуры, чтобы потом из более позднего вызова туда в прошлое впилить указатель на свежесозданный объект.

Все такие вещи я детально анализировал, сносил под корень и заменял на нормальные списки, хеш-таблицы и прочие человеческие инструменты работы со структурами данных. Иногда это упрощало код кардинально. Раз в 10.

В целом сам процесс доведения кода для безошибочного с точки зрения компилятора Ява был муторным, но на 90% - элементарным. Трудно было с goto. Применялись они редко, но иезуитски - код на 10 страниц с пятью вложенными циклами и switch/if, и три перехода. Куда-нибудь на вторую страницу внутри цикла. Местами это решалось выносом части кода в функции (там, где переход был применён для повторного использования кода), а местами требовалась очень серьёзная перереботка. Иногда приходилось просто добавлять булевы переменные для виртуального обхода части кода. Увы, в правильности всех подобных конверсий я пока не уверен.

Всё, устал писать. Продолжу позже. Репозиторий - https://github.com/dzavalishin/jdrive, если интересно - присоединяйтесь. Если вызовет интерес - напишу ещё. На сегодня код собирается, многое работает, но играть пока нельзя. Не только из-за отсутствия save/load. Из всех видов транспорта, пожалуй, ближе всех к завершению - корабли. Но об этом - в следующей статье.

Автор: Дмитрий Завалишин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js