Ранее я писал, что мне в руки попали исходники игры Crypt Quest:
Crypt Quest это игра более чем 20-летней давности от французского разработчика Пьера Лероя, которую я очень полюбил за нестандартный геймплей и высокую логическую сложность. Искать её в гугле сейчас бесполезно, так как есть уже другая игра с таким же названием, которая забила все результаты поиска.
Получив исходники, я занимался тем, что переосмысливал код игры и пытался его переписать по-своему. Но он изначально был написан грамотно и эффективно, поэтому мои правки оказались чисто косметическими, ну и также я использовал C вместо C++.
Основное время я потратил на то, чтобы перепройти все уровни. Их 101 штука. Так как автор отдал мне все версии, среди которых были и незаконченные, я должен был отобрать финальные версии уровней.
Проект представлял собой классическую версионную клоаку – несколько папок с файлами, в которых есть подпапки с такими же файлами, архивы, в которых тоже несколько папок с теми же файламии, готовые игровые сборки с упакованными уровнями, и т.д. Для начала я вытащил наверх все уровни из всех хранилищ, какие были, и приготовил папки для сравнения. Ситуация осложнялась тем, что некоторые уровни были закодированы сверху вниз, а некоторые снизу вверх, поэтому они могли быть идентичны, но бинарным сравнением это не определялось.
Поэтому я написал программу для "умного" сравнения уровней, и отсеял все дубликаты. Осталось около 120 штук. Их нужно было сравнить визуально. Для этого пришлось написать ещё одну программу, которая генерировала скриншоты из файлов уровней, и среди этих скриншотов я искал похожие.
В результате нашлись парные уровни, которые отличались какой-то деталью.
Чтобы понять, какую роль играет отличие, уровень нужно было пройти в обоих вариантах и оценить, насколько это сложно. Я исключал варианты, где нужно было делать что-то нештатное, например быстро-быстро бежать и успевать на последней доле секунды. Так как это не аркада, а логическая головоломка, такого быть не должно. Иногда даже была видна логика автора: вместо бомбы с коротким запалом он помещал бомбу с длинным запалом, и мне сразу становилось понятно, зачем. Поэтому в финал вышли уровни, которые проходятся абсолютно надёжно, без случайного везения, хотя при желании прохождение можно сократить с помощью скоростных трюков.
В итоге я прошёл все уровни, многие более 10 раз, и это было очень непросто. На некоторых я зависал по 2-3 дня.
Теперь у меня есть полностью переписанный на C и протестированный движок игры и протестированные уровни. Но осталось ещё много важных доделок: это, конечно же, пользовательский интерфейс.
В оригинале он напрочь отсутствовал. Выводился стартовый экран с текстом, нажималась любая клавиша, и начиналась игра. Я хочу сделать правильное игровое меню, настройки, выбор уровней и т.п.
И для этого я хочу привлечь другой свой проект:
Это позволит двум проектам развиваться одновременно.
Хотя GUI ещё не завершён, в текущем виде его более чем достаточно для такой игры, как Crypt Quest.
Но есть нюанс
Первоначально проект GUI я делал таким образом, чтобы все исходники включались в программу. Это удобно, так как даёт полный доступ ко всем внутренностям GUI во время разработки. Но в то же время не совсем удобно, если приходится включать эти исходники в какой-то совершенно другой проект, вроде CQuest.
Поэтому я немного переделал GUI так, чтобы его можно было скомпилировать и использовать в виде внешней подключаемой библиотеки.
Подробнее про библиотеки тут:
Это сразу обнажило некоторые тонкости, которые были незаметны изначально.
Рассмотрим требования к подключаемому GUI.
1. Отвязка от рендера
Диспетчер GUI должен работать исключительно с абстрактными представлениями элементов, которые для него являются по сути просто прямоугольниками. Отрисовкой элементов должен заниматься независимый компонент, который можно подключать в виде отдельной библиотеки.
Например, я использую для рисования библиотеку SDL2, но теоретически рендеринг может происходить вообще в HTML-код для отображения, допустим, на веб-странице.
Это требование легко выполняется. Рендерер в GUI и так отдельный, просто не нужно включать его в общую библиотеку. Таким образом, к программе нужно подключить отдельно библиотеку с диспетчером GUI и отдельно библиотеку с рендерером на выбор.
2. Шрифты
GUI имеет встроенный стандартный шрифт, но легко сделать внешний также в виде отдельной библиотеки.
3. Модульность
GUI поддерживает несколько типов элементов: окно, кнопка, чекбокс и т.п. Но в каком-то конкретном случае мне, например, абсолютно не нужен чекбокс.
Тогда хорошо бы просто исключить поддержку такого элемента из GUI.
Самому диспетчеру всё равно. Он различает только прямоугольники, а не типы элементов. Но при возникновении события над каким-то прямоугольником его надо обработать. Для этого обработка выносится в отдельный компонент – процессор.
Процессор смотрит на тип элемента и в зависимости от него вызывает соответствующую функцию обработки. Проблема в том, что если мы не используем элемент, но логика его обработки прописана в процессоре, и вызывается функция с определённым именем, то и библиотеку с этой функцией обязательно надо присоединить, иначе компилятор заругается.
Аналогичная ситуация и в рендерере: если элемент ни разу не рисуется, но прописан там, библиотеку с его функциями всё равно придётся присоединить.
Выход здесь такой. Либо можно оставить всё как есть, либо переписать отдельно процессор и отдельно рендерер, выбросив из них всё лишнее. Для этого потребуются уже не библиотеки, а исходный код. Что немного портит концепцию.
Более гибким решением было бы зарегистрировать для каждого типа элемента указатели на функции его обработки и рендеринга. Фактически это были бы привычные callback-и.
Тогда ни процессор, ни рендерер не содержали бы в себе явных ссылок на функции, а регистрация происходила бы в основной программе перед началом работы GUI. И тогда можно было бы присоединять только то, что явно указано в главной программе.
4. Расширяемость
Практически в любой игре могут быть нестандартные GUI-элементы. Например, само игровое поле с лабиринтом это тоже элемент GUI, куда можно тыкать мышкой и как-то обрабатывать события. Но естественно, такой элемент не прописан изначально в GUI.
Поэтому хотелось бы иметь возможность расширять процессор и рендерер своими дополнительными функциями для, соответственно, обработки события и рендеринга.
И в целом, учитывая предыдущий пункт про callback-и, эта проблема фактически решена. Добавляем новые типы и регистрируем для них функции, остальное автоматически подхватится процессором и рендерером.
Единственная загвоздка в том, как добавлять новые типы. В GUI типы элементов объявлены как перечисляемый тип GUI_ItemType. Куда входят: GUI_ITEM_WINDOW, GUI_ITEM_BUTTON и т.д.
Чтобы продолжить эту последовательность, можно просто влезть в GUI.h (который в любом случае будет вставляться в основную программу) и там дописать свои типы. Но это чревато тем, что GUI.h потом можно вставить в какой-то другой проект, забыв о том, что там изменено.
Перезадать перечисляемый тип GUI_ITemType мы тоже не можем, так как он уже задан в GUI. Значит, мы должны сделать какой-то новый перечисляемый тип, например GUI_MyItemType, и начать его с последнего значения из GUI_ITemType.
Что же тогда произойдёт с функциями, которые ожидают тип GUI_ItemType, а вместо этого получат GUI_MyItemType? А ничего. В языке C это допустимо. Фактически это просто int.
Так что особой проблемы нет, разве что "неаккуратненько".
В ближайшее время я подключу GUI каким-то способом (главное, чтобы работало), затем надо будет перерисовать графику.
Читайте дальше: