Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.4. Перенос игры Doom на новую платформу

1.4.1. Адаптация Doom для ОС Linux с TUI

В этом разделе предполагается, что отладка и тестирование кода ведётся на устройстве под управлением ОС Ubuntu Linux x86-64.

Написанную в переносимом стиле программу с GUI несложно адаптировать для вывода графики в консоль. Приложение командной строки, или CLI-приложение (Command Line Interface, CLI), использующее терминал для вывода графики, позволяет обеспечить поддержку не только распространённых ОС, таких как Windows, Linux или MacOS, но и серверных ОС без поддержки GUI, а также устройств, обладающих ограниченными вычислительными ресурсами, сессий протокола SSH (Secure Shell Protocol).

Более того, передача GUI с использованием протокола удаленного доступа к рабочему столу (Remote Desktop Protocol, RDP), например, протокола RBF (Remote FrameBuffer), требует высокой пропускной способности канала связи, в то время как передача простого текста по протоколу SSH таких ограничений на канал связи не накладывает.

Рассмотрим процесс портирования программы с GUI на новую платформу с TUI (Text-based User Interface) на примере версии игры Doom doomgeneric [15] с улучшенной переносимостью.

Для добавления поддержки новой платформы для doomgeneric необходимо реализовать всего 6 платформозависимых функций, названия которых приведены в файле с документацией по проекту README.md и в табл. 2.

Таблица 2. Функции, которые следует реализовать для портирования doomgeneric

Функция

Описание

DG_Init

Инициализация платформозависимых функций.

DG_DrawFrame

Вывод кадрового буфера DG_ScreenBuffer на экран.

DG_SleepMs

Задержка в миллисекундах.

DG_GetTicksMs

Время в миллисекундах.

DG_GetKey

Обработка нажатий клавиатуры.

DG_SetWindowTitle

Обновление заголовка окна.

Адаптируем игру для поддержки вывода ASCII-графики в терминал, реализовав TUI для Doom вместо GUI.

Для портирования Doom с GUI на новую платформу – терминал с выводом графики в виде ASCII-символов – переместимся на ОС Ubuntu Linux в загруженный ранее репозиторий с кодом переносимой версии игры Doom doomgeneric [15], в папку doomgeneric.

Создадим новый файл doomgeneric_tty.c при помощи команды touch. Также создадим новый сценарий сборки на основе напечатанного командой cat в стандартный вывод содержимого файла Makefile. Заменим в стандартном выводе cat при помощи утилиты sed подстроку с именем объектного файла doomgeneric_xlib.o на подстроку с именем нового объектного файла doomgeneric_tty.o, этот файл будет создан в процессе компиляции файла с исходным кодом doomgeneric_tty.c. Для перенаправления стандартного вывода утилиты cat в стандартный ввод утилиты sed воспользуемся оператором конвейера Unix | [7], а для перенаправления вывода утилиты sed в файл с именем Makefile.tty воспользуемся оператором >:

~/doomgeneric/doomgeneric$ touch doomgeneric_tty.c
~/doomgeneric/doomgeneric$ cat Makefile | sed s/doomgeneric_xlib.o/doomgeneric_tty.o/ | sed s/-lX11// > Makefile.tty

Запустим сборку проекта, используя созданный сценарий Makefile.tty:

~/doomgeneric/doomgeneric$ make -f Makefile.tty
mkdir -p build
[Compiling dummy.c]
[Compiling am_map.c]
[Compiling doomdef.c]
...
(.text+0x24): undefined reference to `main'
collect2: error: ld returned 1 exit status

Сборка проекта завершилась неудачей, поскольку созданный файл doomgeneric_tty.c пока не содержит функции main и платформозависимых функций.

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

Для поиска сигнатур функций в заголовочных файлах с расширением .h воспользуемся утилитой grep:

~/doomgeneric/doomgeneric$ grep DG_Init *.h
doomgeneric.h:void DG_Init();
~/doomgeneric/doomgeneric$ grep DG_DrawFrame *.h
doomgeneric.h:void DG_DrawFrame();
~/doomgeneric/doomgeneric$ grep DG_SleepMs *.h
doomgeneric.h:void DG_SleepMs(uint32_t ms);
~/doomgeneric/doomgeneric$ grep DG_GetTicksMs *.h
doomgeneric.h:uint32_t DG_GetTicksMs();
~/doomgeneric/doomgeneric$ grep DG_GetKey *.h
doomgeneric.h:int DG_GetKey(int* pressed, unsigned char* key);
~/doomgeneric/doomgeneric$ grep DG_SetWindowTitle *.h
doomgeneric.h:void DG_SetWindowTitle(const char * title);

Поместим найденные сигнатуры и тривиальные реализации всех функций из табл. 2 в файл doomgeneric_tty.c. Добавим, как указано в README.md, главный цикл программы в функцию с именем main в том же файле, подключим заголовочный файл с сигнатурами функций doomgeneric.h в doomgeneric_tty.c:

#include <stdio.h>
#include <stdint.h>
#include "doomgeneric.h"

void DG_Init() { printf("Doom launched!\n"); }
void DG_SleepMs(uint32_t ms) {}
void DG_SetWindowTitle(const char *title) {}
void DG_DrawFrame() {}

uint32_t DG_GetTicksMs() { return 0; }
int DG_GetKey(int *pressed, unsigned char *key) { return 0; }

int main(int argc, char **argv) {
    doomgeneric_Create(argc, argv);
    while (1) doomgeneric_Tick();
    return 0;
}

Вновь запустим сборку проекта:

~/doomgeneric/doomgeneric$ make -f Makefile.tty
[Compiling doomgeneric_tty.c]
[Linking doomgeneric]
[Size]
size doomgeneric
  text  data    bss    dec   hex filename
322570 82200 271240 676010 a50aa doomgeneric

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

Проверим работу скомпилированной программы:

~/doomgeneric/doomgeneric$ ./doomgeneric doom1.wad
Doom launched!
Doom Generic 0.1
Z_Init: Init zone memory allocation daemon.
zone memory: 0x7f2fba975010, 600000 allocated for zone
Using . for configuration and saves
...
player 1 of 1 (1 nodes)
Emulating the behavior of the 'Doom 1.9' executable.
HU_Init: Setting up heads up display.
ST_Init: Init status bar.

Версия Doom с функциями-заглушками, размещёнными в файле doomgeneric_tty.c вместо платформозависимых реализаций функций, перечисленных в табл. 2, запустилась – на экран было выведено приветственное сообщение Doom launched, размещённое в функции-заглушке DG_Init, а также отладочные сообщения. Однако, затем выполнение программы остановилось. Это ожидаемое проведение программы, поскольку не все из платформозависимых функций (см. табл. 2) в файле doomgeneric_tty.c реализованы корректно.

Исправим реализацию функции DS_GetTicksMs – как указано в табл. 2, эта функция должна возвращать время, прошедшее с начала игры, в миллисекундах.

Для реализации этой функции создадим глобальную переменную counter, значение которой будет увеличиваться и возвращаться при каждом вызове функции DG_GetTicksMs. При помощи стандартной функции usleep, для использования которой необходимо подключить стандартный заголовочный файл unistd.h, добавим задержку, снижающую частоту смены кадров для упрощения отладки:

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include "doomgeneric.h"

void DG_Init() { printf("Doom launched!\n"); }
void DG_SleepMs(uint32_t ms) {} 
void DG_SetWindowTitle(const char *title) {}
void DG_DrawFrame() {}
 
uint32_t DG_GetTicksMs() {
    static uint32_t counter = 0;
    usleep(1000);
    printf("Tick: %d\n", counter);
    return counter++;
}

int DG_GetKey(int *pressed, unsigned char *key) { return 0; } 

int main(int argc, char **argv) { 
    doomgeneric_Create(argc, argv); 
    while (1) doomgeneric_Tick();
    return 0; 
}

Выполним сборку и проверим работу программы:

~/doomgeneric/doomgeneric$ make -f Makefile.tty
~/doomgeneric/doomgeneric$ ./doomgeneric doom1.wad
Doom launched!
Doom Generic 0.1
Z_Init: Init zone memory allocation daemon.
zone memory: 0x7fd914c28010, 600000 allocated for zone
Using . for configuration and saves
...
player 1 of 1 (1 nodes)
Emulating the behavior of the 'Doom 1.9' executable.
HU_Init: Setting up heads up display.
ST_Init: Init status bar.
Tick: 0
Tick: 1
Tick: 2
Tick: 3
Tick: 4
Tick: 5
...

Теперь после инициализации Doom не прекращает работу, а продолжает выводить в терминал отладочные сообщения с текущим значением переменной counter.

Для реализации функции DS_DrawFrame, задача которой – вывести кадровый буфер DG_ScreenBuffer на экран (см. табл. 2), необходимо определить тип глобальной переменной DG_ScreenBuffer:

~/doomgeneric/doomgeneric$ grep DG_ScreenBuffer *.h
doomgeneric.h:extern pixel_t* DG_ScreenBuffer;
~/doomgeneric/doomgeneric$ grep pixel_t *.h
doomgeneric.h:typedef uint32_t pixel_t;
doomgeneric.h:extern pixel_t* DG_ScreenBuffer;
~/doomgeneric/doomgeneric$ grep RES doomgeneric.h
#define DOOMGENERIC_RESX 640
#define DOOMGENERIC_RESY 400

Из вывода утилиты grep можно сделать вывод о том, что глобальная переменная DG_ScreenBuffer – это массив чисел типа uint32_t, поскольку pixel_t – это псевдоним типа uint32_t. Число пикселей в массиве пикселей оценивается как произведение ширины экрана DOOMGENERIC_RESX на его высоту DOOMGENERIC_RESY. Таким образом, для размера экрана 640 на 400 точек число пикселей в массиве DG_ScreenBuffer равно 256 000, пиксели в массиве сохраняются ядром Doom (см. рис. 6) построчно.

Каждое число в массиве DG_ScreenBuffer кодирует цвет пикселя и занимает 4 байта, причём 3 младших байта содержат цвет в формате RGB (Red, Green, Blue). Обновим реализацию функции DG_DrawFrame, при помощи побитовых операций в цикле извлечём компоненты цвета каждого 2-го пикселя по горизонтали и каждого 4-го пикселя по вертикали и выведем их на экран:

void DG_DrawFrame() {
    for (int y = 0; y < DOOMGENERIC_RESY; y += 5) {
        for (int x = 0; x < DOOMGENERIC_RESX; x += 3) {
            int i = y * DOOMGENERIC_RESX + x;
            uint32_t pixel = DG_ScreenBuffer[i];
            unsigned char r = (pixel >> 16) & 255;
            unsigned char g = (pixel >>  8) & 255;
            unsigned char b = (pixel >>  0) & 255;
            printf("%d %d %d; ", r, g, b);
        }
        printf("\n");
    }
}

Номер пикселя i в массиве DG_ScreenBuffer вычисляется как номер строки y умноженный на ширину экрана DOOMGENERIC_RESX, к которому прибавляется номер пикселя в строке x. Для извлечения байта красной компоненты цвета r, зелёной компоненты цвета g и синей компоненты цвета b используется побитовый сдвиг вправо >> и побитовое «и» &. Для извлечения красной компоненты значение pixel сдвигается вправо на 16 бит (на 2 байта), для извлечения зелёной компоненты – на 8 бит (на 1 байт). Побитовое «и» используется для наложение маски из восьми единиц для исключения из числа-результата старших разрядов.

Удалим инструкцию, выводящую отладочные сообщения, из функции DG_GetTicksMs. Скорость вывода цветов пикселей в консоль можно регулировать, меняя число миллисекунд, подаваемое на вход функции usleep, вызываемой из функции DG_GetTicksMs. Проверим работу обновлённой версии программы:

~/doomgeneric/doomgeneric$ make -f Makefile.tty
~/doomgeneric/doomgeneric$ ./doomgeneric doom1.wad
...
0 0 0; 0 0 0; 0 0 0; 0 0 0; 0 0 0; 0 0 0; 0 0 0; 0 0 0; 0 0 0; 0 0 0; ...
...
128 1 1; 116 1 1; 116 1 1; 104 1 1; 128 1 1; 155 1 1; 155 1 1; 167 1 1; 255 255 72; 155 92 20; ...
...

Через некоторое время после вывода чёрных пикселей на экран начинают выводиться и другие цвета, например, 255 255 72 (ярко-жёлтый цвет).

Для поддержки вывода графики Doom в виде ASCII-символов в терминале на следующем шаге необходимо преобразовать цвет каждого пикселя в формате RGB в ASCII-символ. Выбор ASCII-символа, соответствующего пикселю, может осуществляться, например, на основе относительной яркости (relative luminance) цвета пикселя, определённой в [19].

Воспользуемся следующей упрощённой формулой на основе [19] для вычисления относительной яркости цвета:

l(r, g, b) = 255−1(0.2126r + 0.7152g + 0.0722b) (3)

где r, g и b – численные значения красной, зелёной и синей компонент цвета в формате RGB, значения функции l принадлежат вещественному промежутку от 0 до 1 включительно.

Реализуем формулу (3) на языке C и используем вычисленное по ней округлённое вниз вещественное значение в функции DG_DrawFrame для выбора ASCII-символа пикселя из строковой глобальной переменной chars. Номер символа в строке chars соответствует его яркости – пробел будем считать самым тусклым, а # – самым ярким символом. Округление вниз для вещественных значений произведём при помощи оператора приведения типа данных (int).

Перед отрисовкой каждого кадра в терминале воспользуемся управляющей последовательностью ANSI (ANSI escape code) \033[1;1H – эта последовательность позволяет переместить курсор на позицию 1;1, в левый верхний угол терминала, и продолжить консольный вывод с этой позиции:

void DG_DrawFrame() {
    printf("\033[1;1H");
    for (int y = 0; y < DOOMGENERIC_RESY; y += 5) {
        for (int x = 0; x < DOOMGENERIC_RESX; x += 3) {
            int i = y * DOOMGENERIC_RESX + x;
            uint32_t pixel = DG_ScreenBuffer[i];
            unsigned char r = (pixel >> 16) & 255;
            unsigned char g = (pixel >>  8) & 255;
            unsigned char b = (pixel >>  0) & 255;
            float lum = (0.2126f * r + 0.7152f * g + 0.0722f * b) / 255;
            char *chars = " .:-+=*0#";
            int index = (int) (lum * sizeof(chars));
            printf("%c", chars[index]);
        }
        printf("\n");
    }
}

При помощи команды make -f Makefile.tty выполним сборку новой версии программы и проверим её работу, запустив программу при помощи команды ./doomgeneric doom1.wad. Экран заставки игры Doom, выведенный в терминал ОС Linux в виде ASCII-символов, показан на рис. 10.

Рисунок 10. ASCII-графика экрана заставки игры Doom в терминале ОС Linux

1.4.2. Адаптация Doom для ОС Windows с TUI

В этом разделе предполагается, что отладка и тестирование кода ведётся на устройстве под управлением ОС Windows x86-64.

Попробуем запустить реализованную в предыдущем разделе версию игры Doom с поддержкой вывода графики в терминал в виде ASCII-символов на ОС Windows.

Для этого в терминале PowerShell [6] переместимся в загруженный ранее репозиторий с кодом переносимой версии игры Doom doomgeneric [15], в папку doomgeneric. После этого создадим новый файл doomgeneric_tty.c и на основе созданного ранее сценария сборки Doom с GUI для ОС Windows build.ps1 создадим новый сценарий build-term.ps1 для сборки Doom с TUI для ОС Windows:

PS C:\doomgeneric\doomgeneric> New-Item doomgeneric_tty.c
PS C:\doomgeneric\doomgeneric> Get-Content build.ps1 | ForEach-Object { $_ -replace 'doomgeneric_win.c', 'doomgeneric_tty.c' } | Out-File -FilePath build-term.ps1

Содержимое файла build-term.ps1 отличается от содержимого файла build.ps1 тем, что подстрока с именем файла платформы с GUI doomgeneric_win.c в нём заменена подстрокой с именем файла платформы с TUI doomgeneric_tty.c.

Поместим в созданный файл doomgeneric_tty.c функции, перечисленные в табл. 2 и реализованные в предыдущем разделе для ОС Ubuntu Linux x86-64:

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include "doomgeneric.h"

void DG_Init() { printf("Doom launched!\n"); }
void DG_SleepMs(uint32_t ms) {}
void DG_SetWindowTitle(const char *title) {}
 int DG_GetKey(int *pressed, unsigned char *key) { return 0; }

void DG_DrawFrame() {
    printf("\033[1;1H");
    for (int y = 0; y < DOOMGENERIC_RESY; y += 5) {
        for (int x = 0; x < DOOMGENERIC_RESX; x += 3) {
            int i = y * DOOMGENERIC_RESX + x;
            uint32_t pixel = DG_ScreenBuffer[i];
            unsigned char r = (pixel >> 16) & 255;
            unsigned char g = (pixel >>  8) & 255;
            unsigned char b = (pixel >>  0) & 255;
            float lum = (0.2126f * r + 0.7152f * g + 0.0722f * b) / 255;
            char *chars = " .:-+=*0#";
            int index = (int) (lum * sizeof(chars));
            printf("%c", chars[index]);
        }
        printf("\n");
    }
}

uint32_t DG_GetTicksMs() {
    static uint32_t counter = 0;
    usleep(1000);
    return counter++;
}

int main(int argc, char **argv) {
    doomgeneric_Create(argc, argv);
    while (1) doomgeneric_Tick();
    return 0;
}

Выполним сборку проекта и запустим Doom в PowerShell-терминале:

PS C:\doomgeneric\doomgeneric> .\build-term.ps1
PS C:\doomgeneric\doomgeneric> .\doomgeneric.exe doom1.wad

После запуска программы в терминал будут выведены кадры, сформированные функцией DG_DrawFrame, показанные на рис. 11.

Рисунок 11. ASCII-графика экрана заставки игры Doom в терминале ОС Windows без поддержки управляющих последовательностей ANSI

По умолчанию в некоторых версиях Windows отключены управляющие последовательности ANSI, из-за чего выполнение инструкции printf("\033[1;1H"), перемещающей курсор в левый верхний угол экрана, не приведёт к ожидаемому результату. Вместо перемещения курсора управляющая последовательность будет выведена на экран как обычный текст. В результате в терминал кадры будут выводиться последовательно, как показано на рис. 11.

Для включения поддержки управляющих последовательностей ANSI на ОС Windows воспользуемся Windows-специфичными функциями GetStdHandle, GetConsoleMode и SetConsoleMode.

В функции инициализации DG_Init (см. табл. 2) в файле doomgeneric_tty.c получим дескриптор HANDLE для стандартного вывода при помощи функции GetStdHandle, после чего получим текущие настройки консоли – для этого применим функцию GetConsoleMode. Добавим к настройкам консоли dwMode флаг ENABLE_VIRTUAL_TERMINAL_PROCESSING и сохраним настройки функцией SetConsoleMode:

#ifdef _WIN32
#include <windows.h>
#endif

void DG_Init() {
#ifdef _WIN32
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
    DWORD dwMode;
    GetConsoleMode(handle, &dwMode);
    dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
    SetConsoleMode(handle, dwMode);
#endif
}

Для использования функций GetStdHandle, GetConsoleMode, SetConsoleMode необходимо подключить заголовочный файл windows.h. Этот заголовочный файл доступен только на ОС Windows и содержит специфичные для ОС Windows сигнатуры функций и типы данных.

В связи с этим для обеспечения переносимости реализации ASCII-графики Doom в файле doomgeneric_tty.c воспользуемся директивой компилятора #ifdef для проверки, компилируется ли программа для ОС Windows – в случае, если константа _WIN32 не определена, компилятор удалит добавленный нами Windows-специфичный код внутри директив #ifdef, и код в файле doomgeneric_tty.c продолжит работать на ОС Linux без изменений.

Скомпилируем и запустим обновлённую версию Doom:

PS C:\doomgeneric\doomgeneric> .\build-term.ps1
PS C:\doomgeneric\doomgeneric> .\doomgeneric.exe doom1.wad

После включения управляющих последовательностей ANSI поведение программы на ОС Windows (см. рис. 12) совпадает с поведением на ОС Linux (см. рис. 10) в части формата вывода графики.

Рисунок 12. ASCII-графика экрана заставки игры Doom в терминале ОС Windows

Однако, частота смены кадров терминальной версии Doom на ОС Windows существенно ниже частоты смены кадров на ОС Linux – из-за различий в деталях реализации консольного вывода посимвольный вывод, интенсивно используемый для отрисовки графики в виде ASCII-символов в терминале, работает намного медленнее на ОС Windows по сравнению с ОС Linux.

Для устранения этого различия исправим функцию DG_DrawFrame – вместо вызова функции printf для каждого выводимого символа в обновлённой функции DG_DrawFrame будем сохранять в массив line все символы очередной строки, которую необходимо вывести на экран, после чего выведем сформированную строку line при помощи вызова стандартной функции puts. Кроме того, переместим задержку из функции DG_GetTicksMs в функцию DG_DrawFrame:

void DG_DrawFrame() {
    char line[DOOMGENERIC_RESX];
    printf("\033[1;1H");
    for (int y = 0; y < DOOMGENERIC_RESY; y += 5) {
        int pos = 0;
        for (int x = 0; x < DOOMGENERIC_RESX; x += 3) {
            int i = y * DOOMGENERIC_RESX + x;
            uint32_t pixel = DG_ScreenBuffer[i];
            unsigned char r = (pixel >> 16) & 255;
            unsigned char g = (pixel >>  8) & 255;
            unsigned char b = (pixel >>  0) & 255;
            float lum = (0.2126f * r + 0.7152f * g + 0.0722f * b) / 255;
            char *chars = " .:-+=*0#";
            int index = (int) (lum * sizeof(chars));
            line[pos++] = chars[index];
        }
        line[pos] = 0;
        puts(line);
    }
    usleep(1000);
}

uint32_t DG_GetTicksMs() {
    static uint32_t counter = 0;
    return counter++;
}

Скомпилируем и запустим Doom с исправленными функциями:

PS C:\doomgeneric\doomgeneric> .\build-term.ps1
PS C:\doomgeneric\doomgeneric> .\doomgeneric.exe doom1.wad

Обновлённая версия Doom теперь характеризуется приемлемой частотой смены кадров – приветственная анимация игрового процесса, один из кадров которой показан на рис. 13, теперь выводится на экран плавно.

Рисунок 13. Кадр из приветственной анимации игры Doom на ОС Windows с TUI

1.4.3. Кроссплатформенный сценарий сборки для Doom с TUI

Полученная в предыдущем разделе реализация ASCII-графики Doom для платформ с терминалом doomgeneric_tty.c отличается кроссплатформенностью – рассмотренные реализации платформозависимых функций (см. табл. 2) в файле doomgeneric_tty.c позволяют обеспечить корректную работу игры Doom как в терминалах на ОС Linux, так и в терминалах на ОС Windows.

Кроссплатформенность doomgeneric_tty.c достигается за счёт использования директивы компилятора #ifdef для включения специфичного для Windows кода только на ОС Windows, а также за счёт ускоренного построчного вывода на экран изображения, состоящего из символов, функцией puts вместо посимвольного вывода изображения функцией printf, медленного на ОС Windows.

Однако, сборка проекта Doom для платформ с терминалом из файлов с исходным кодом выполнялась зависящим от целевой ОС способом – так, на ОС Linux использовалась система сборки GNU Make и адаптированный для ОС Linux файл Makefile.tty, созданный при помощи команд в терминале с языком оболочки Bash, а на ОС Windows использовался сценарий сборки build.ps1, созданный при помощи команд PowerShell [6].

Для реализации сценария сборки, совместимого и с ОС Windows, и с ОС Linux, воспользуемся кроссплатформенной системой сборки GNU Make [7]. В папке с исходным кодом doomgeneric и с файлом doomgeneric_tty.c создадим файл Makefile.tty со следующим содержимым:

CC = gcc -std=gnu99
OBJS = dummy.o am_map.o doomdef.o doomstat.o dstrings.o d_event.o d_items.o d_iwad.o d_loop.o d_main.o d_mode.o d_net.o f_finale.o f_wipe.o g_game.o hu_lib.o hu_stuff.o info.o i_cdmus.o i_endoom.o i_joystick.o i_scale.o i_sound.o i_system.o i_timer.o memio.o m_argv.o m_bbox.o m_cheat.o m_config.o m_controls.o m_fixed.o m_menu.o m_misc.o m_random.o p_ceilng.o p_doors.o p_enemy.o p_floor.o p_inter.o p_lights.o p_map.o p_maputl.o p_mobj.o p_plats.o p_pspr.o p_saveg.o p_setup.o p_sight.o p_spec.o p_switch.o p_telept.o p_tick.o p_user.o r_bsp.o r_data.o r_draw.o r_main.o r_plane.o r_segs.o r_sky.o r_things.o sha1.o sounds.o statdump.o st_lib.o st_stuff.o s_sound.o tables.o v_video.o wi_stuff.o w_checksum.o w_file.o w_main.o w_wad.o z_zone.o w_file_stdc.o i_input.o i_video.o doomgeneric.o doomgeneric_tty.o

%.o: %.c
    $(CC) -c $< -o $@

doom.exe: $(OBJS)
    $(CC) $(OBJS) -o doom.exe

clean:
ifeq ($(OS), Windows_NT)
    if exist *.o del /q *.o
else
    rm -f *.o
endif

В переменную CC поместим имя компилятора gcc с опцией -std=gnu99, указывающей на необходимость использования стандарта C99 и нестандартных GNU-расширений при компиляции Doom. В переменную OBJS поместим перечень файлов с расширением .o, полученный из файла с именем Makefile, уже присутствовавшего в репозитории doomgeneric.

За переменными в Makefile.tty следуют 2 цели сборки.

Первая цель сборки %.o: %.c описывает правило преобразования файлов с кодом на языке C в объектные файлы с расширением .o. При этом для сборки любого .o-файла требуется наличие на диске .c-файла с тем же именем. Например, для успешного выполнения цели doomgeneric_tty.o, которая может быть запущена командой make -f Makefile.tty doomgeneric_tty.o, требуется наличие на диске файла doomgeneric_tty.c.

Выражение $(CC) позволяет подставить в сборочную команду значение переменной CC, а выражение $< позволяет получить имя первой зависимости %.c цели %.o – то есть, имя конкретного файла с расширением .c. Выражение $@, в свою очередь, позволяет получить имя цели %.o – то есть, имя конкретного файла с расширением .o.

Таким образом, в результате выполнения команды make -f Makefile.tty doomgeneric_tty.o выражение $(CC) -c $< -o $@ преобразуется в команду gcc -std=gnu99 -c doomgeneric_tty.c -o doomgeneric_tty.o, после чего команда выполнится.

Вторая цель сборки doom.exe: $(OBJS) зависит от всех файлов с расширением .o, перечисленных в переменной OBJS. Перед выполнением цели doom.exe GNU Make выполнит цель %.o: %.c для каждого файла из OBJS, и в результате на диске будут созданы все объектные файлы, перечисленные в OBJS, из соответствующих им файлов с кодом на языке C. В результате выполнения цели doom.exe на диске будет создан исполняемый файл с именем doom.exe путём компоновки всех объектных файлов из OBJS.

Для удаления с диска артефактов сборки с расширениями .o в конец файла с именем Makefile.tty добавлена цель clean. На ОС Linux для удаления объектных файлов используется команда rm, а на ОС Windows – команда del:

Проверим работу Makefile.tty на ОС Windows x86-64:

PS C:\doomgeneric\doomgeneric> make -f Makefile.tty
gcc -std=gnu99 -c dummy.c -o dummy.o
gcc -std=gnu99 -c am_map.c -o am_map.o
gcc -std=gnu99 -c doomdef.c -o doomdef.o
...
PS C:\doomgeneric\doomgeneric> .\doom.exe doom1.wad

Проверим работу Makefile.tty на ОС Ubuntu Linux x86-64:

~/doomgeneric/doomgeneric$ make -f Makefile.tty
gcc -std=gnu99 -c dummy.c -o dummy.o
gcc -std=gnu99 -c am_map.c -o am_map.o
gcc -std=gnu99 -c doomdef.c -o doomdef.o
...
~/doomgeneric/doomgeneric$ ./doom.exe doom1.wad

При помощи кроссплатформенной реализации ASCII-графики для Doom doomgeneric_tty.c и кроссплатформенного сценария сборки Makefile.tty становится возможным собрать и запустить игру в рамках одной SSH-сессии, как показано на рис. 14.

Рисунок 14. Кадр из приветственной анимации игры Doom в SSH-сессии

Для лучшего визуального эффекта необходимо уменьшить размер выводимых в терминал символов до минимально возможного при помощи сочетания клавиш Ctrl и - после запуска программы в SSH-сессии, открытой в терминале на ОС Windows.

1.4.4. Упражнения

Задача 1. Добавьте поддержку пользовательского ввода в консольную версию Doom. Реализуйте в файле doomgeneric_tty.c платформозависимую функцию DG_GetKey, которая проверяет, была ли нажата клавиша, без блокировки выполнения программы. Проверьте корректность работы функции DG_GetKey на ОС Linux и на ОС Windows.

Задача 2. Добавьте в консольную версию Doom doomgeneric_tty.c опциональную поддержку ASCII-графики с использованием других цветов кроме чёрного и белого. Воспользуйтесь управляющими последовательностями ANSI для задания цвета выводимого в консоль текста.

Задача 3. Добавьте в консольную версию Doom doomgeneric_tty.c опциональную поддержку вывода в терминал символов геометрических фигур Unicode с различающейся прозрачностью вместо ASCII-символов.

Задача 4. Портируйте Doom с ASCII-графикой doomgeneric_tty.c в терминал веб-браузера при помощи Emscripten. Для очистки экрана вместо ANSI-кодов воспользуйтесь вызовом console.clear().

Задача 5. Добавьте в консольную версию Doom doomgeneric_tty.c поддержку вывода звука на ОС Linux и ОС Windows.