Dwarf Fortress — один из тех странных проектов, ставших любимыми в Интернете. Это бесплатная игра, в которой можно быть или авантюристом, или управлять крепостью дворфов в случайно сгенерированном фэнтези-мире. Симуляция очень подробна, каждая новая игра создает множество цивилизаций со своей историей, мифологиями и артефактами.
Она стала знаменитой, и вполне справедливо. Каждый дворф уникален, он имеет эмоциональное состояние, предпочтения в драгоценных камнях и причины для недовольства. И всё это происходит в ASCII-интерфейсе, кажущемся новичкам каким-то обманом, но опытные игроки способны ориентироваться в нём как в бегущем тексте из фильма «Матрица»: они видят в этих символах дворфов, реки, легендарных огромных чудовищ.
Вся игра стала творением одного разработчика — Тарна Адамса с ником Toady One, который работал над Dwarf Fortress с 2002 года. Первые четыре года это был хобби-проект, но с 2006 года он стал его основной работой. Адамс пишет весь код сам, а его брат помогает с дизайном и создаёт истории на основе игры. До недавнего времени он собирал пожертвования, чтобы продолжать работу, но сейчас создаёт версию с пиксельной графикой и переработанным UI, которую можно будет купить в Steam.
Я связался с Тарном Адамсом, чтобы узнать у него о том, как ему удавалось в течение пятнадцати с лишним лет справляться с единой увеличивающейся кодовой базой, сложностями поиска путей и отладкой мёртвых кошек.
Вопрос: Какие языки программирования и технологии вы используете? Каков ваш стек? Менялся ли он за 15-20 лет вашей работы?
Ответ: DF — это некая комбинация из C и C++, но не в каком-то подчиняющемся стандартам формате, а скорее, накопившийся со временем хаос. Я пользовался Microsoft Visual Studio, начиная с MSVC 6, а сейчас работаю в версии Visual Studio Community.
Для решения задач движка я использую OpenGL и SDL. Мы выбрали их, потому что их проще портировать на OSX и Linux, хотя я даже сейчас не смог бы сделать всё это самостоятельно. Не знаю, будь у меня выбор, использовал ли бы я сегодня что-то вроде Unity или Unreal, потому что не знаком ни с тем, ни с другим. Однако поддержка собственного движка — это настоящая боль, особенно теперь, когда я работаю с пиксельной графикой. Для создания звуков я использую FMOD.
Всё это оставалось неизменным на протяжении многих лет развития проекта, за исключением SDL, который был добавлен спустя несколько лет после начала, чтобы можно было создавать порты. Что касается механик игры, то я не пользуюсь большим количеством внешних библиотек, но иногда подбираю библиотеки для генерации случайных чисел — очень давно я добавил Mersenne Twister, а последним дополнением стал SplitMix64, о котором рассказывали в докладе на последнем Roguelike Celebration.
В: Какие сложности возникают при такой длительной разработке одного проекта? Считаете ли вы, что проще делать его самому? Ведь если каждую строку написал ты, его проще поддерживать и изменять?
О: Всё очень быстро забывается! Если посчитать количество символов ";", чтобы приблизительно оценить объём, то получится, что в коде примерно 711 тысяч строк, поэтому я уже просто не могу держать всё в голове. Я пытаюсь давать своим переменным и объектам стандартизированные и легко запоминающиеся имена, и оставляю достаточно много комментариев, чтобы напоминать себе, что происходит, когда мы доходим до конкретного участка кода. Иногда требуется несколько операций поиска, чтобы найти именно тот поток, над которым мне нужно поработать, когда я возвращаюсь к какому-то фрагменту кода, десяток лет остававшемуся неизменным, что бывает достаточно часто. Можно сказать, что большинство изменений происходит в отдельных частях игры, поэтому есть некое активное расплавленное ядро, которое знакомо мне больше всего. Но есть и сильно закостеневшие части кода, которые я не трогал с момента первого релиза игры в 2006 году.
Что касается простоты работы в одиночку, то это идеальный вариант, особенно для меня, не имевшего опыта работы над крупным коллективным проектом! Очевидно, что другие люди выполняют задачи иначе, например, в контексте AAA-игр, и чтобы справиться вовремя, в этой сфере нужно много инженеров. Не могу с уверенностью сказать, что способен вносить изменения быстрее, чем они, потому что раньше не работал в этом контексте, и если нужно внести изменения, передо мной не встаёт никаких бюрократических преград. Я могу просто браться и делать. Но делать приходится в одиночку.
В: Самый крупный рефакторинг/изменение, которое вам приходилось вносить?
О: Было несколько рефакторингов, которые длились месяцами, я переделывал некоторые структуры данных и тому подобное, однако я не считаю, что это можно назвать строго рефакторингом, потому что всегда есть возможности параллельно совершенствовать механики, и это логично делать, пока твои знания кода ещё свежи.
Добавление координаты Z, чтобы механически игра вышла в 3D (оставаясь при этом текстовой), было одним из таких изменений, оказавшимся почти самой сложной задачей за всю мою жизнь. Мне потребовались долгие недели на изучение логики и вызовов функций, в которых использовались X и Y, и на анализ того, как в них встроится ось Z.
Внедрение полиморфизма в систему предметов в конечном итоге оказалось ошибкой, и очень крупной.
В: Почему это было ошибкой?
О: Когда объявляешь класс, являющийся типом предмета, то ты оказываешься привязанным к этой структуре сильнее, чем если бы это были элементы. Возможность использования виртуальных функций и тому подобного удобна, но плата за это слишком высока. Я стал использовать в иерархии предмет «tool», который начал получать различные функции, и теперь может поддерживать всё, от приставной лестницы до улья или ступы (и отдельно песта для неё, ха-ха), и такая система кажется более гибкой, поэтому я хочу, чтобы каждый создаваемый в игре предмет относился к этой иерархии.
У нас много процедурной генерации, и если бы мы хотели, допустим, сгенерировать предмет, который частично действует как один предмет, и частично как другой, то это было бы намного сложнее сделать, если бы мы были ограничены иерархией классов. Добавление таких вещей, как ромбовидных зависимостей и тому подобного в результате запутывает тебя в узлах. При этом существует более чистый способ реализации такой схемы. Если различные компоненты можно просто включать и отключать, то это проще и даёт больше возможностей.
Кажется, некоторые разработчики называют это entity component system, однако те, кто делает сильный упор на оптимизацию, воспринимают её как нечто иное — как систему, в которой ты разбиваешь элементы на отдельные поля. Использование единого объекта с различными распределёнными подобъектами почти всегда хуже сказывается на промахах кэша, однако преимущества для упорядочивания, гибкости и расширяемости такой системы нельзя игнорировать, а различные подполя в предмете «tool» используются не настолько часто, чтобы это превратилось в проблему оптимизации.
В: Столкнулись ли вы с какими-нибудь проблемами при переходе с 32 на 64 бита? Кажется, это один из тех аспектов, которые в своё время были очень важны, но со временем стали достаточно привычны.
О: Вообще никаких проблем! Я с трудом могу припомнить какие-то сложности. К счастью для нас, мы уже и раньше хорошо контролировали байтовые размеры, потому что это важно для сохранения и загрузки миров; с форматом нужно было определиться ещё в начале работы, особенно потому что нам приходится иметь дело с различиями в endian между разными операционками и тому подобным. Поэтому мы избегаем любых хитрых операций с указателями и других вещей, которые могли бы вызвать проблемы. В конечном итоге наш код оказался очень подходящим для перехода на 64 бита только благодаря другим нашим практикам, это было полной случайностью. Основная проблема заключалась в том, чтобы выделить время на внесение изменений, но в конце концов это заняло значительно меньше времени, чем я рассчитывал.
В: Я видел другие игры, похожие на DF, которые тормозили из-за алгоритмов поиска пути. Какой алгоритм вы используете и как обеспечиваете его эффективность?
О: Да, базовый алгоритм — это только часть дела. Мы используем A*, который, разумеется, быстр, но сам по себе недостаточно хорош. Мы не можем воспользоваться некоторыми его инновациями (например, поиском точки перехода), потому что наша карта сильно меняется. Обычно разработчики для упрощения используют решения, добавляющие поверх карты различные крупные структуры, а из-за меняющейся карты их поддержка занимает слишком много времени или слишком трудоёмка. Поэтому мы решили просто следить за соединёнными компонентами, которых можно достичь пешком. Такую систему довольно легко обновлять даже при быстром изменении карты. однако в ней используется заливка. Например, если вода разделяет крепость пополам, то она должна вытечь с одной стороны и присвоить всей половине крепости новый индекс, но после всего этого проблем обычно не бывает. Это позволяет нам вырезать из игры почти все неудачные вызовы A* — нашим агентам достаточно опрашивать числа компонентов, и если они одинаковы, агенты будут знать, что вызов окажется успешным.
Эта система быстра в поддержке, но её недостаток заключается в том, что индексы компонентов хранятся только для ходьбы. Это значит, что летающие существа, например, не имеют знания о глобальном поиске путей, кроме пешеходных. Однако чтобы дать им некоторое преимущество, в боях и некоторых других ситуациях мы используем заливки на короткие расстояния, применяя их обычную логику. Но такое решение для них неидеально.
Не уверен, будем ли мы пробовать другие структуры, чтобы улучшить работу системы. При наших размерах карт все попытки заканчивались провалом, даже когда за них брались сторонние разработчики. Разумеется, если сконцентрировать усилия, решить проблему можно, и я видел, что в некоторых других играх использовались, например, прямоугольные оверлеи и другие решения, которые выглядят многообещающе, но я не знаю, насколько неизменны и крупны их карты.
Самой простой идеей было бы добавление для летающих существ нового индекса, но это сильно повлияет на память и скорость, и поэтому мы не хотим хранить одновременно два индекса; даже один — это уже серьёзная нагрузка. Более специфические оверлеи способны отслеживать свойства прокладывания пути (после чего они прокладывают путь через оверлеи, а не тайлы), но при изменении карты их поддержка сложна и медленна. У нас есть и множество других идей, например, отслеживание лестниц или выполнение ограниченного кэширования путей, и возможно, они могут дать определённые преимущества. Мы определённо находимся на пределе того, что можем на данный момент поддерживать с точки зрения агентов и сложности карты, поэтому чтобы выжать больше, нам необходимо что-то придумать.
В: Кстати, об этом: вы одновременно симулируете множество элементов — как вам удаётся управляться с таким количеством асинхронно (и действительно ли вы используете асинхронность)?
О: Если под асинхронностью понимать многопоточность, то нет, её мы не реализуем, если не считать самого графического отображения. Это многообещающая тема, даже при микропоточности, с которой мне сильно помогло сообщество, но у меня не было времени на глубокое изучение. У меня нет никакого опыта в этом, и такая система сильно подвержена багам.
В: Пробовали ли вы наряду с DF работать над другими проектами/технологиями?
О: Разумеется! Папка побочных проектов, которая мигрировала между компьютерами на протяжении примерно десятка лет, содержит в себе примерно 90 проектов. Некоторые из них длились несколько дней, другие — несколько лет. В основном это другие игры, почти всегда в других жанрах, но есть и несколько вспомогательных проектов для DF, например, прототип генератора мифов. Ничто из этого не близко к релизу, но экспериментировать с ними интересно.
В: Имея около 90 побочных проектов, исследовали ли вы другие языки программирования? Если да, есть ли у вас любимчики?
О: Ха-ха, нет! Мне больше нравится разбираться с архитектурой, чем с технологиями. Однако я уверен, что некоторые вещи сильно бы ускорили реализацию моих архитектур, поэтому мне, вероятно, стоит изучить скриптинг и глубже разбираться с потоками. Другие разработчики любезно предоставили мне в помощь библиотеки и другие вещи, но сложно уделить время работы над побочными проектами для изучения технологий, если моё время для побочных проектов предназначено для расслабления.
В: У вас очень интересные описания изменений в версиях игры. Какой ваш любимый баг и что его вызывало?
О: Наверно, это покажется скучным, но для меня на первом месте баг с пьяными кошками. О нём уже сняли несколько видео. Кошки валяются на полу таверны и кажутся мёртвыми, но оказывается, что они налакались пролитого алкоголя, пока облизывали свои лапы. В коде проглатывания при чистке одно число было не на том месте, и это вызывало у них все симптомы алкогольного отравления (которое мы добавили, когда совершенствовали ядовитых существ.)