- PVSM.RU - https://www.pvsm.ru -

Апогей возможностей первокурсника — первая консольная игра на С++

Предисловие

Видеоигры на протяжении уже более 30 лет активно развиваются, начиная от геймплея и графических возможностей, вплоть до продвинутых систем искусственного интеллекта и восприятия пользователями подаваемого разработчиками материала. Киберспорт, в свою очередь, является полноценным видом спорта с многомиллионными призами и миллиардными оборотами. А сама индустрия видеоигр неустанно набирает обороты, являясь одной из наиболее кассовых и массовых ответвлений сферы развлечений. А ко всему этому человечество пришло от примитивнейшей реализации «тенниса», запускавшейся на осциллографе.

Я, как первокурсник, решил в рамках изучения базовых консольных возможностей C/C++ возможно запрограммировать "классическую" ASCII-игру, требующую от игрока скорости мышления [1], непрерывного взаимодействия с игрой, а также обладающую несложной графической частью, которую игрок мог бы интерпретировать в некое подобие трехмерного изображения.

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

Вдохновение

На заре появления на рынке мобильных телефонов с монохромными дисплеями выделилась игра «Space Impact», прошивавшаяся в аппаратах Nokia. Она отлично вписалась в ограниченные возможности тогдашних сотовых. После введения простых игр в мобильные телефоны – продажи аппаратов резко возросли, а мобильный гейминг постепенно стал популярным, и сейчас вполне конкурирует со индустрией «взрослых» ААА-проектов.

«Space Impact» и послужила основой для моей первой игры ввиду своей легкости исполнения и популярности в начале 2000-х.

Особенности «Space Invader»

В первую очередь – легковесность программы. Исполняемый файл занимает менее 100 кб и будет работать, как задумано, практически на любом компьютере под OC Windows с пакетом Visual C++.

Во-вторых, переносимость — исходный код можно за несколько минут переделать под POSIX-системы, тем самым обеспечив работоспособность на UNIX и Mac операционных системах, лишь заменив несколько функций и пересобрав программу на соответствующем компиляторе.

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

Наипростейший интуитивно понятный интерфейс, не требующий описания или инструкций по использованию.

Содержание игры «Space Invader»

  • Главное меню из 4-х пунктов, выбирать которые можно с помощью стрелок на клавиатуре.
  • Пункт «Игра» — открывает меню их двух пунктов:
    • Подпункт «Новая игра» — запускает новый игровой сеанс.
    • Подпункт «Продолжить» — загружает последнюю сохраненную игру из бинарного файла в директории исполняемого файла и запускает игровой сеанс с использованием полученных данных.

  • Пункт «Помощь» — инструкции по работе с приложением и его описание. Пролистываются по нажатию стрелок на клавиатуре.
  • Пункт «Зал славы» — список лидеров игровых сессий, загружается из бинарного файла в директории исполняемого файла и форматированно выводится в консоль.
  • Пункт «Выход» — выход из приложения.
  • Сдвигающийся влево «мир» с динамической скоростью. «Мир» содержит рандомные поля сверху и снизу толщиной в 1 или 2 символа. Также между полями рандомно появляется «космический мусор», отображаемый символом «¤», являющийся препятствием для игрока.
  • Прижатый к левому краю окна «космический корабль». Корабль смещается с помощью стрелок на клавиатуре вверх и вниз, по нажатию пробела выпускает снаряд, уничтожающий «космический мусор» по касанию.
  • «Приборная панель» вверху консоли, отображающая пройденный километраж, текущую скорость и количество оставшихся попыток.

Как работает «Space Invader»?

При запуске приложения возникает заставка-анимация. Она состоит из 6 заранее отформатированных символьных массивов, сменяющихся через каждые 200мс.

image
Начальная заставка – итоговый слайд

Далее идет обращение в функцию главного меню с параметром 1(целое число). Функция отображает меню с выделенным угловыми скобками пунктом меню, номер которого совпадает с входным параметром. Пунктов в меню 4, соответственно входной параметр может различаться от 1 до 4. При нажатии стрелки вниз происходит рекурсивное обращение с инкрементированным входным параметром в том случае, если входной параметр меньше 4, с параметром 1, если входной параметр равен 4. При нажатии Space или Enter происходит обращение к функции, соответствующей выделенному пункту меню.

void StartMenu(int switcher)
{
    system("cls");
    switch (switcher)
    {
    case 1:
        cout << "nnn                  <<  ИГРАТЬ!  >>nn                      ПОМОЩЬ!nn                     ЗАЛ СЛАВЫnn                       ВЫХОД";
        break;
    case 2:
        cout << "nnn                      ИГРАТЬ!nn                  <<  ПОМОЩЬ!  >>nn                     ЗАЛ СЛАВЫnn                       ВЫХОД";
        break;
    case 3:
        cout << "nnn                      ИГРАТЬ!nn                      ПОМОЩЬ!nn                  << ЗАЛ СЛАВЫ >>nn                       ВЫХОД";
        break;
    case 4:
        cout << "nnn                      ИГРАТЬ!nn                      ПОМОЩЬ!nn                     ЗАЛ СЛАВЫnn                  <<   ВЫХОД   >>";
        break;
    }
    int choice = _getch();
    if (choice == 224)
        choice = _getch();
    if (choice == 72)
        if (switcher != 1)
            StartMenu(switcher - 1);
        else
            StartMenu(4);
    if (choice == 80)
        if (switcher != 4)
            StartMenu(switcher + 1);
        else
            StartMenu(1);
    if (choice == 13 || choice == 32)
    {
        if (switcher == 1)
            GameMenu(1);
        if (switcher == 2)
            Help(0);
        if (switcher == 3)
            TopChart();
        if (switcher == 4)
            _exit(0);
    }
}

image
Главное меню (вход в функцию выполнен с параметром 1)

При обращении к функции, соответствующей пункту «Игра» запускается функция, аналогичная по функционалу, но выбор есть только из 2-х пунктов. Соответственно, входной параметр будет 1 или 2, и при нажатии любой из стрелок (вверх или вниз) нам необходимо лишь сменить цифру на «противоположную». Наиболее оптимизированным будет вариант отнимания входного параметра от 3 (3 – 1 = 2, 3 – 2 = 1).

void GameMenu(int switcher)
{
    system("cls");
    if (switcher == 1)
        cout << "nnnnn                <<  НОВАЯ ИГРА!  >>nn                    ПРОДОЛЖИТЬ!";
    else
        cout << "nnnnn                    НОВАЯ ИГРА!nn                <<  ПРОДОЛЖИТЬ!  >>";
    int choice = _getch();
    if (choice == 224)
        choice = _getch();
    if (choice == 72 || choice == 80)
        GameMenu(3 - switcher);
    if (choice == 27)
        StartMenu(1);
    if (choice == 13 || choice == 32)
        Game(switcher);
}

image
Дополнительное меню (вход в функцию выполнен с параметром 1)

Теперь к основному – процессу игры. При выборе подпункта «Новая игра» — запускается новый игровой сеанс. Создается двумерный массив, размерностью 14 строк на 50 столбцов. Первая строка выделяется под приборную панель. Первый прибор – количество пройденных километров, оно равно количеству обновлений консоли (изначально консоль обновляется раз в 80мс, с каждым обновлением этот параметр декрементируется, пока не достигнет значения 25).

int odometerBuf = odometer, odometerDigitLength;
        for (odometerDigitLength = 0; odometerBuf != 0; odometerBuf /= 10, odometerDigitLength++);//вычисление количества цифр на одометре
        for (int i = odometerDigitLength, odometerBuf = odometer; i >= 0; i--, scr[0][i] = odometerBuf % 10 + '0', odometerBuf /= 10);//прорисовка одометра на приборную панель
        scr[0][odometerDigitLength++] = 'К'; scr[0][odometerDigitLength++] = 'М';//дописывание "КМ"
        odometer++;//наращение одометра

Второй прибор, текущая скорость, являет собой формулу — 1000/скорость обновления консоли. Скорость измеряется в километрах в секунду. Таким образом, изначально корабль движется со скоростью 12км/с, и через некоторое время достигает отметки в 40 км/с.

speed = 1000 / timer;//обновление спидометра
        int speedBuf = speed;
        for (int i = 42; speed != 0; i--, scr[0][i] = speed % 10 + '0', speed /= 10);//прорисовка спидометра на приборную панель
        scr[0][42] = 'К'; scr[0][43] = 'М'; scr[0][44] = '/'; scr[0][45] = 'С';//дописывание "КМ/С"

Третий прибор отображает количество оставшихся попыток, изначально их 3. Попытки отображаются символом «&».

for (int i = 50; lifes > 0; i--, lifes--, scr[0][i] = '&');

image
Приборная панель в начале игрового сеанса

Следующие 2 строки, как и последние 2 – являются полями игры. Декорации полей выбираются случайным образом, могут состоять из 3-х символов или пробела. Крайние строки всегда полностью заполнены, а вторая и предпоследняя содержит символы, отличные от пробела лишь в тех местах, где эти символы «растут» из других.

char borderSymbols[] = { '†', '‡', '¤', ' ' };
    for (int aboveBelow = 0; aboveBelow < 50; aboveBelow++)//прорисовка верхнего и нижнего полей (2 + 2)
    {
        scr[1][aboveBelow] = borderSymbols[rand() % 3];
        if (scr[1][aboveBelow] == '‡')
            scr[2][aboveBelow] = '¤';

        scr[13][aboveBelow] = borderSymbols[rand() % 3];
        if (scr[13][aboveBelow] == '‡')
            scr[12][aboveBelow] = '¤';
    }

image
Приборная панель и поля в начале игрового сеанса

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

scr[6][0] = '\'; scr[6][1] = '\';//прорисовка корабля
    scr[7][0] = '3'; scr[7][1] = '='; scr[7][2] = '=';
    scr[8][0] = '/'; scr[8][1] = '/';

Управлять ним можно стрелками вверх и вниз.

if (_kbhit())//если клавиша была нажата
        {
            control = _getch();//переменная примет ее значение
            if (control == 224)
                control = _getch();
        }
        if (control == 72)//при движении корабля вверх
            if (scr[2][0] == '\' || scr[3][0] == '\' && scr[2][0] == '¤' || scr[3][1] == '\' && scr[2][1] == '¤')//если корабль врезался в верхнее поле - игра окончена
                if (lifes > 1)
                {
                    cout << 'a';
                    lifes--;
                    weaponPos = 7;
                    GameStart(scr, lifes, &timer);
                    Sleep(1000);
                }
                else
                    GameOver(odometer);
            else
            {
                for (int i = 2; i < 13; i++)//корабль смещается на элемент выше
                    for (int j = 0; j < 49; j++)
                        if (scr[i][j] == '3' || scr[i][j] == '\' || scr[i][j] == '=' || scr[i][j] == '/')
                        {
                            scr[i - 1][j] = scr[i][j];
                            scr[i][j] = ' ';
                        }
                weaponPos--;
            }
        if (control == 80)//при движении корабля вниз
            if (scr[12][0] == '/' || scr[11][0] == '/' && scr[12][0] == '¤' || scr[11][1] == '/' && scr[12][1] == '¤')//если корабль врезался в нижнее поле - игра окончена
                if (lifes > 1)
                {
                    cout << 'a';
                    lifes--;
                    weaponPos = 7;
                    GameStart(scr, lifes, &timer);
                    Sleep(1000);
                }
                else
                    GameOver(odometer);
            else
            {
                for (int i = 12; i >= 2; i--)//корабль смещается на элемент вниз
                    for (int j = 0; j < 49; j++)
                        if (scr[i][j] == '3' || scr[i][j] == '\' || scr[i][j] == '=' || scr[i][j] == '/')
                        {
                            scr[i + 1][j] = scr[i][j];
                            scr[i][j] = ' ';
                        }
                weaponPos++;
            }

image
Расположение корабля при двукратном нажатии стрелки вверх

Отойдя от визуализации, стоит отметить, что вместе с кораблем мы смещаем вверх или вниз элемент массива, отвечающий за «дуло» корабля. Соответственно, из которого можно выпускать снаряды по нажатию пробела.

Однако, ни поле, ни снаряды не двигаются, потому, чтобы «оживить» игровой процесс, по истечению таймера перерисовки экрана «мир» будет смещаться на один столбец влево, а снаряды на один столбец вправо. Корабль же остается на месте, пока игрок не захочет иного. Но после перерисовки теперь возникает один пустой столбец – заполним его случайным образом полями и «космическим мусором», который можно сбивать снарядами или боковыми отбойниками корабля.

for (int i = 1; i < 14; i++)//все "космические" элементы смещаются на элемент влево
            for (int j = 0; j < 49; j++)
            {
                if (scr[i][j] == '\' && scr[i][j + 1] == '¤' || scr[i][j] == '=' && scr[i][j + 1] == '¤' || scr[i][j] == '/' && scr[i][j + 1] == '¤')
                    if (lifes > 1)
                    {
                        cout << 'a';
                        lifes--;
                        weaponPos = 7;
                        GameStart(scr, lifes, &timer);
                        Sleep(1000);
                    }
                    else
                        GameOver(odometer);
                if (scr[i][j] != '3' && scr[i][j] != '\' && scr[i][j] != '=' && scr[i][j] != '/' && scr[i][j] != '-' && scr[i][j + 1] != '-')
                    scr[i][j] = scr[i][j + 1];
                if (scr[i][j] == '¤')
                    scr[i][j + 1] = ' ';
            }
        for (int i = 1; i < 14; i++)//все снаряды смещаются на элемент вправо
            for (int j = 48; j >= 0; j--)
                if (scr[i][j] == '-')
                    if (j != 48)
                    {
                        scr[i][j + 1] = '-';
                        scr[i][j] = ' ';
                    }
                    else
                        scr[i][j] = ' ';
        char borderSymbols[] = { '†', '‡', '¤', ' ' };
        scr[2][49] = ' ';//рандомное заполнение новых элементов краев
        scr[1][49] = borderSymbols[rand() % 3];
        if (scr[1][49] == '‡')
            scr[2][49] = '¤';
        scr[12][49] = ' ';
        scr[13][49] = borderSymbols[rand() % 3];
        if (scr[13][49] == '‡')
            scr[12][49] = '¤';
        for (int i = 3; i < 12; i++)//рандомное появление космического мусора
        {
            if (rand() % 10 == 1)
                scr[i][49] = '¤';
        }

При движении корабля вверх или вниз в этот момент космический мусор будет уничтожен без урона кораблю, все благодаря отбойникам

image
Если в данный момент не увернуться от мусора стрелкой вниз – корабль будет разбит

Снаряды, выпускаемые кораблем, сконструированы так, что сметают все на пути своего следования. Следовательно, выпустив один снаряд, расчищается коридор в одну строку.

Впрочем, хоть поля состоят из материалов, схожих на космический мусор, их нельзя разрушить отбойниками. Соприкосновение любой части корабля с полями приводит к неминуемому краху. Также, как и попадание космического мусора в лицевую часть корабля. Если в такие моменты на приборной панели есть обозначение еще хотя бы одной «попытки» — игра как будто начинается сначала, но сохранив набранный счет и потеряв одну «попытку». Скорость же сбрасывается до начального значения в 12км/с.

Если же игрок «разбился», а попыток не осталось – игровой сеанс заканчивается, а игроку предлагается ввести его имя, чтобы сохранить свой результат в «зале славы».

image
Предложение игроку ввести имя, чтобы сохранить свой результат в «зале славы»

Приложение обрабатывает 2 файла:

  • «TopChart.bin» — двоичный файл для хранения таблицы лидеров. Данные хранятся в структурах (ник игрока, его счет, дата завершения игрового сеанса). Данные дозаписываются в конец файла при окончании игры. При вызове пункта «Зал славы» файл открывается для чтения с возможностью редактирования. Далее объявляется динамический массив структур, в который переписываются данные из файла, после чего массив сортируется и форматированно выводится в консоль (максимально возможное число результатов – 12, если массив содержит 13 результатов – последний отбрасывается после сортировки). Далее файл перезаписывается массивом структур результатов, после чего массив уничтожается.
  • «CurrentSave.bin» — двоичный файл для хранения сохраненной игры. Вызывается для чтения при запуске подпункта «Продолжить» пункта «Игра». Может содержать данные для восстановления одного незавершенного игрового сеанса: номер строки, в которой содержится нос корабля, количество пройденных километров, количество оставшихся попыток, расположение обьектов на экране. С помощью этих данных формируется игровой сеанс, в точности повторяющий незавершенный. Во избежание нечестной игры, при загрузке сеанса из файла – файл удаляется. При нажатии Escape во время игры данный файл создается, и в него записываются все необходимые данные для успешного дальнейшего продолжения игрового сеанса.

Пункт главного меню «Помощь» — функция, принимающая параметр от 0 до 22, отображающая последующие 12 строк по 50 символов от входного параметра. Управление осуществляется рекурсивно с помощью стрелок вверх и вниз.

void Help(int switcher)
{
    system("cls");
    cout << "ПРОКРУТКА: СТРЕЛКИ ВВЕРХ/ВНИЗ | ВЕРНУТЬСЯ: ESCAPEn";
    char arr[1800] = { "                УПРАВЛЕНИЕ В МЕНЮ                 Передвигаться по пунктам – СТРЕЛКИ ВВЕРХ/ВНИЗ     Выбрать пункт – ПРОБЕЛ или ENTER                  Вернуться в предыдущее меню – ESCAPE                              УПРАВЛЕНИЕ В ИГРЕ                 Передвигаться вверх/вниз – СТРЕЛКИ ВВЕРХ/ВНИЗ     Сделать выстрел – ПРОБЕЛ                          Вернуться в меню, сохранив игру – ESCAPE                               БРИФИНГ                      Вы – пилот космического корабля, попавшего в      космическую бурю. Вам необходимо не разбиться и   пролететь как можно большее расстояние. Корабль   оборудован динамическим управлением. Чем быстрее  вы летите – тем острее поворачивает судно. Корабльавтоматически постепенно разгоняется до 40 км/с.  Вы можете сбивать космический мусор с помощью     магнитной пушки, встроенной в судно, а также      боковыми отбойниками.                             При управлении кораблем на щитке приборов         отображается пройденная дистанция, текущая        скорость и количество оставшихся «ячеек отката»   (отображаются символом  «&»), изначально их 3.    Если решите прекратить игру – просто нажмите      ESCAPE. Игра сохранится, и вы сможете ее          продолжить даже после перезапуска приложения с    помощью пункта «ПРОДОЛЖИТЬ!».                     В главном меню можно посмотреть таблицу почетных  пилотов. Добейтесь своего права там оказаться!                         АВТОРСТВО                                 Svjatoslav Laskov – AUTHOR                          Igor Marchenko – COACH                         National Technical University                    «Kharkiv Polytechnic Institute»                                                     2016" };
    for (int i = 0, buf = switcher; i < 13; i++)
    {
        for (int j = buf * 50; j < buf * 50 + 50; j++)
            cout << arr[j];
        if (i != 12)
            cout << endl;
        buf++;
    }
    int controller = _getch();//получить значение нажатой клавиши
    if (controller == 224)//если была нажата стрелка
        controller = _getch();//то определить какая именно
    if (controller == 72)//если стрелка вверх
        if (switcher > 0)
            Help(switcher - 1);
        else
            Help(0);
    if (controller == 80)//если стрелка вниз
        if (switcher < 22)
            Help(switcher + 1);
        else
            Help(22);
    if (controller == 27)//если Escape
        StartMenu(2);
}

Пункт главного меню «Выход» — осуществляет выход из приложения.

Руководство пользователя

• УПРАВЛЕНИЕ В МЕНЮ
o Передвигаться по пунктам – СТРЕЛКИ ВВЕРХ/ВНИЗ
o Выбрать пункт – ПРОБЕЛ или ENTER
o Вернуться в предыдущее меню – ESCAPE
• УПРАВЛЕНИЕ В ИГРЕ
o Передвигаться вверх/вниз – СТРЕЛКИ ВВЕРХ/ВНИЗ
o Сделать выстрел – ПРОБЕЛ
o Вернуться в меню, сохранив игру – ESCAPE
• БРИФИНГ
Вы – пилот космического корабля, попавшего в космическую бурю. Вам необходимо не разбиться и пролететь как можно большее расстояние.

Корабль оборудован динамическим управлением. Чем быстрее вы летите – тем острее поворачивает судно. Корабль автоматически постепенно разгоняется до 40 км/с.

Вы можете сбивать космический мусор с помощью магнитной пушки, встроенной в судно, а также боковыми отбойниками.
При управлении кораблем на щитке приборов отображается пройденная дистанция, текущая скорость и количество оставшихся «ячеек отката» (отображаются символом «&»), изначально их 3.

Если решите прекратить игру – просто нажмите ESCAPE. Игра сохранится, и вы сможете ее продолжить даже после перезапуска приложения с помощью пункта «ПРОДОЛЖИТЬ!».

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

Итог

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

Хотелось еще разделить приложение на несколько потоков для более корректной реакции на нажатие клавиш пользователем, но треды я реализую уже в следующем проекте на С++.

Исходный код

#include "conio.h"
#include "windows.h"
#include "ctime"
#include <iostream>
using namespace std;

struct player//определение структуры, хранящей данные о результатах какого-либо завершенного игрового сеанса
{
    char name[7];
    int score;
    int mday;
    int mon;
    int year;
};

struct save//определение структуры, хранящей данные о незавершенном игровом сеансе
{
    int weaponPos;
    int timer;
    int odometer;
    int lifes;
    char scr[14][50];
};

void ScreenOutput(char scr[14][50])//функция поэлементного вывода массива в консоль
{
    system("cls");
    for (int i = 0; i < 14; i++)
    {
        for (int j = 0; j < 50; j++)
            cout << scr[i][j];
        if (i != 13)
            cout << endl;
    }
}
//блок прототипов функций
void StartMenu(int switcher);//функция, вызывающаяся из главного меню, содержит пункты "ИГРА" и "ПРОДОЛЖИТЬ"
void GameMenu(int switcher);//функция главного меню
void GameStart(char scr[14][50], int lifes, int *timer);//функция, определяющая начальный символьный массив при запуске нового игрового сеанса
void Game(int var);//функция игровго сеанса
void GameOver(int score);//функция, спрашивающая имя игрока, и записывающая его результат в бинарный файл
void Help(int switcher);//функция помощи игроку
void TopChart();//функция "ЗАЛ СЛАВЫ" - отображает список лидеров

void Help(int switcher)
{
    system("cls");
    cout << "ПРОКРУТКА: СТРЕЛКИ ВВЕРХ/ВНИЗ | ВЕРНУТЬСЯ: ESCAPEn";
    char arr[1800] = { "                УПРАВЛЕНИЕ В МЕНЮ                 Передвигаться по пунктам – СТРЕЛКИ ВВЕРХ/ВНИЗ     Выбрать пункт – ПРОБЕЛ или ENTER                  Вернуться в предыдущее меню – ESCAPE                              УПРАВЛЕНИЕ В ИГРЕ                 Передвигаться вверх/вниз – СТРЕЛКИ ВВЕРХ/ВНИЗ     Сделать выстрел – ПРОБЕЛ                          Вернуться в меню, сохранив игру – ESCAPE                               БРИФИНГ                      Вы – пилот космического корабля, попавшего в      космическую бурю. Вам необходимо не разбиться и   пролететь как можно большее расстояние. Корабль   оборудован динамическим управлением. Чем быстрее  вы летите – тем острее поворачивает судно. Корабльавтоматически постепенно разгоняется до 40 км/с.  Вы можете сбивать космический мусор с помощью     магнитной пушки, встроенной в судно, а также      боковыми отбойниками.                             При управлении кораблем на щитке приборов         отображается пройденная дистанция, текущая        скорость и количество оставшихся «ячеек отката»   (отображаются символом  «&»), изначально их 3.    Если решите прекратить игру – просто нажмите      ESCAPE. Игра сохранится, и вы сможете ее          продолжить даже после перезапуска приложения с    помощью пункта «ПРОДОЛЖИТЬ!».                     В главном меню можно посмотреть таблицу почетных  пилотов. Добейтесь своего права там оказаться!                         АВТОРСТВО                                 Svjatoslav Laskov – AUTHOR                          Igor Marchenko – COACH                         National Technical University                    «Kharkiv Polytechnic Institute»                                                     2016" };
    for (int i = 0, buf = switcher; i < 13; i++)
    {
        for (int j = buf * 50; j < buf * 50 + 50; j++)
            cout << arr[j];
        if (i != 12)
            cout << endl;
        buf++;
    }
    int controller = _getch();//получить значение надатой клавиши
    if (controller == 224)//если была нажата стрелка
        controller = _getch();//то определить какая именно
    if (controller == 72)//если стрелка вверх
        if (switcher > 0)
            Help(switcher - 1);
        else
            Help(0);
    if (controller == 80)//если стрелка вниз
        if (switcher < 22)
            Help(switcher + 1);
        else
            Help(22);
    if (controller == 27)//если Escape
        StartMenu(2);
}

void StartMenu(int switcher)
{
    system("cls");
    switch (switcher)
    {
    case 1:
        cout << "nnn                  <<  ИГРАТЬ!  >>nn                      ПОМОЩЬ!nn                     ЗАЛ СЛАВЫnn                       ВЫХОД";
        break;
    case 2:
        cout << "nnn                      ИГРАТЬ!nn                  <<  ПОМОЩЬ!  >>nn                     ЗАЛ СЛАВЫnn                       ВЫХОД";
        break;
    case 3:
        cout << "nnn                      ИГРАТЬ!nn                      ПОМОЩЬ!nn                  << ЗАЛ СЛАВЫ >>nn                       ВЫХОД";
        break;
    case 4:
        cout << "nnn                      ИГРАТЬ!nn                      ПОМОЩЬ!nn                     ЗАЛ СЛАВЫnn                  <<   ВЫХОД   >>";
        break;
    }
    int choice = _getch();
    if (choice == 224)
        choice = _getch();
    if (choice == 72)
        if (switcher != 1)
            StartMenu(switcher - 1);
        else
            StartMenu(4);
    if (choice == 80)
        if (switcher != 4)
            StartMenu(switcher + 1);
        else
            StartMenu(1);
    if (choice == 13 || choice == 32)
    {
        if (switcher == 1)
            GameMenu(1);
        if (switcher == 2)
            Help(0);
        if (switcher == 3)
            TopChart();
        if (switcher == 4)
            _exit(0);
    }
}

void GameMenu(int switcher)
{
    system("cls");
    if (switcher == 1)
        cout << "nnnnn                <<  НОВАЯ ИГРА!  >>nn                    ПРОДОЛЖИТЬ!";
    else
        cout << "nnnnn                    НОВАЯ ИГРА!nn                <<  ПРОДОЛЖИТЬ!  >>";
    int choice = _getch();
    if (choice == 224)
        choice = _getch();
    if (choice == 72 || choice == 80)
        GameMenu(3 - switcher);
    if (choice == 27)
        StartMenu(1);
    if (choice == 13 || choice == 32)
        Game(switcher);
}

void GameStart(char scr[14][50], int lifes, int *timer)
{
    for (int i = 0; i < 14; i++)//очищение от мусора
        for (int j = 0; j < 50; j++)
            scr[i][j] = ' ';
    for (int i = 50; lifes > 0; i--, lifes--, scr[0][i] = '&');
    *timer = 80;
    char borderSymbols[] = { '†', '‡', '¤', ' ' };
    for (int aboveBelow = 0; aboveBelow < 50; aboveBelow++)//прорисовка верхнего и нижнего полей (2 + 2)
    {
        scr[1][aboveBelow] = borderSymbols[rand() % 3];
        if (scr[1][aboveBelow] == '‡')
            scr[2][aboveBelow] = '¤';

        scr[13][aboveBelow] = borderSymbols[rand() % 3];
        if (scr[13][aboveBelow] == '‡')
            scr[12][aboveBelow] = '¤';
    }
    scr[6][0] = '\'; scr[6][1] = '\';//прорисовка корабля
    scr[7][0] = '3'; scr[7][1] = '='; scr[7][2] = '=';
    scr[8][0] = '/'; scr[8][1] = '/';
}

void GameOver(int score)
{
    system("cls");
    player newPlayer;//объявляние структуры
    newPlayer.score = score;//инициализацие поля набранного счета
    cout << "Поздравляем Вас!nВы продержались " << score << " километров.nn(Пожалуйста, не используйте кириллические символы)n(Используйте не более 6 символов)nОставьте свое имя и станьте примеромnдля подражания будущим игрокам: ";
    cin.getline(newPlayer.name, 7);//инициализацие поля имени
    time_t timeCur;
    time(&timeCur);
    struct tm * timeCurStruct = localtime(&timeCur);
    newPlayer.mday = timeCurStruct->tm_mday;//инициализацие даты завершения игры
    newPlayer.mon = timeCurStruct->tm_mon;
    newPlayer.year = timeCurStruct->tm_year;
    FILE *topChart;
    fopen_s(&topChart, "TopChart.bin", "ab+");
    fwrite(&newPlayer, 1, sizeof(player), topChart);//дозапись результата в файл
    fclose(topChart);
    TopChart();
}

void TopChart()
{
    FILE *topChart;
    fopen_s(&topChart, "TopChart.bin", "rb+");
    system("cls");
    if (topChart == NULL)//если произошла ошибка при открытии файла
    {
        system("cls");
        cout << "Нет ни единого результата.";
        Sleep(1000);
        system("cls");
        cout << "Нет ни единого результата..";
        Sleep(1000);
        system("cls");
        cout << "Нет ни единого результата...";
        Sleep(1000);
        cout << "nНажмите любую клавишу, чтобы вернуться.";
        _getch();
        StartMenu(3);
    }
    fseek(topChart, 0L, SEEK_END);
    int playerAmount = ftell(topChart) / sizeof(player);
    player *temp = new player[playerAmount];
    fseek(topChart, 0L, SEEK_SET);
    for (int i = 0; i < playerAmount; i++)//копирование содержиомого файла в структкры
        fread(&temp[i], 1, sizeof(player), topChart);
    fclose(topChart);
    for (int i = 1; i < playerAmount; i++)//сортировка структур по спаданию итоговых счетов
        if (temp[i].score > temp[i - 1].score)
        {
            player tempAlone;
            strcpy(tempAlone.name, temp[i].name);
            tempAlone.score = temp[i].score;
            tempAlone.mday = temp[i].mday;
            tempAlone.mon = temp[i].mon;
            tempAlone.year = temp[i].year;

            strcpy(temp[i].name, temp[i - 1].name);
            temp[i].score = temp[i - 1].score;
            temp[i].mday = temp[i - 1].mday;
            temp[i].mon = temp[i - 1].mon;
            temp[i].year = temp[i - 1].year;

            strcpy(temp[i - 1].name, tempAlone.name);
            temp[i - 1].score = tempAlone.score;
            temp[i - 1].mday = tempAlone.mday;
            temp[i - 1].mon = tempAlone.mon;
            temp[i - 1].year = tempAlone.year;

            if (i > 1)
                i -= 2;
            else
                i = 0;
        }
    if (playerAmount > 12)
        playerAmount = 12;
    cout << "№       " << "Имя" << 't' << "Счет" << 't' << "Дата" << endl;//вывод таблицы лидеров в консоль
    for (int i = 0; i < playerAmount; i++)
    {
        cout << i + 1 << ')' << 't' << temp[i].name << 't' << temp[i].score << 't';
        if (temp[i].mday / 10 == 0)
            cout << '0' << temp[i].mday;
        else
            cout << temp[i].mday;
        cout << ' ';
        switch (temp[i].mon)
        {
        case 0:
            cout << "января";
            break;
        case 1:
            cout << "февраля";
            break;
        case 2:
            cout << "марта";
            break;
        case 3:
            cout << "апреля";
            break;
        case 4:
            cout << "мая";
            break;
        case 5:
            cout << "июня";
            break;
        case 6:
            cout << "июля";
            break;
        case 7:
            cout << "августа";
            break;
        case 8:
            cout << "сентября";
            break;
        case 9:
            cout << "октября";
            break;
        case 10:
            cout << "ноября";
            break;
        case 11:
            cout << "декабря";
            break;
        }
        cout << ' ' << 1900 + temp[i].year << endl;
    }
    fopen_s(&topChart, "TopChart.bin", "wb+");
    for (int i = 0; i < playerAmount; i++)//запись таблицы лидеров в бинарный файл
        fwrite(&temp[i], 1, sizeof(player), topChart);
    fclose(topChart);
    delete[] temp;
    _getch();
    StartMenu(3);
}

int main()
{
    setlocale(LC_ALL, "Rus");//задание кодировки
    system("mode con cols=51 lines=14");//задание размеров окна консоли
    system("title Space Invader");//задание описания окна консоли
    system("color 0A");//задание цвета консоли (0-задний фон; А-передний фон)
    HANDLE hCons = GetStdHandle(STD_OUTPUT_HANDLE);//получение хендла
    CONSOLE_CURSOR_INFO cursor = { 100, false };//число от 1 до 100 размер курсора в процентах; falsetrue - видимость
    SetConsoleCursorInfo(hCons, &cursor);//применение заданных параметров курсора
    int timer = 200;
    cout << "          (____/(__)  \_/\_/ \___)(____)nnnnnnnnnnn      __   __ _  _  _   __   ____  ____  ____n     (  )(  ( \/ )( \ / _\ (    \(  __)(  _ \";//вступительная заставка
    Sleep(timer);
    system("cls");
    cout << "          \___ \ ) __//    \( (__  ) _)n          (____/(__)  \_/\_/ \___)(____)nnnnnnnnn      __  __ _  _  _   __   ____  ____  ____n     (  )(  ( \/ )( \ / _\ (    \(  __)(  _ \n      )( /    /\ \/ //    \ ) D ( ) _)  )   /";//вступительная заставка
    Sleep(timer);
    system("cls");
    cout << "          / ___)(  _ \ / _\  / __)(  __)n          \___ \ ) __//    \( (__  ) _)n          (____/(__)  \_/\_/ \___)(____)nnnnnnn      __  __ _  _  _   __   ____  ____  ____n     (  )(  ( \/ )( \ / _\ (    \(  __)(  _ \n      )( /    /\ \/ //    \ ) D ( ) _)  )   /n     (__)\_)__) \__/ \_/\_/(____/(____)(__\_)";//вступительная заставка
    Sleep(timer);
    system("cls");
    cout << "           ____  ____   __    ___  ____n          / ___)(  _ \ / _\  / __)(  __)n          \___ \ ) __//    \( (__  ) _)n          (____/(__)  \_/\_/ \___)(____)nnnnn      __  __ _  _  _   __   ____  ____  ____n     (  )(  ( \/ )( \ / _\ (    \(  __)(  _ \n      )( /    /\ \/ //    \ ) D ( ) _)  )   /n     (__)\_)__) \__/ \_/\_/(____/(____)(__\_)";//вступительная заставка
    Sleep(timer);
    system("cls");
    cout << "n           ____  ____   __    ___  ____n          / ___)(  _ \ / _\  / __)(  __)n          \___ \ ) __//    \( (__  ) _)n          (____/(__)  \_/\_/ \___)(____)nnn      __  __ _  _  _   __   ____  ____  ____n     (  )(  ( \/ )( \ / _\ (    \(  __)(  _ \n      )( /    /\ \/ //    \ ) D ( ) _)  )   /n     (__)\_)__) \__/ \_/\_/(____/(____)(__\_)";//вступительная заставка
    Sleep(timer);
    system("cls");
    cout << "nn           ____  ____   __    ___  ____n          / ___)(  _ \ / _\  / __)(  __)n          \___ \ ) __//    \( (__  ) _)n          (____/(__)  \_/\_/ \___)(____)n      __  __ _  _  _   __   ____  ____  ____n     (  )(  ( \/ )( \ / _\ (    \(  __)(  _ \n      )( /    /\ \/ //    \ ) D ( ) _)  )   /n     (__)\_)__) \__/ \_/\_/(____/(____)(__\_)";//вступительная заставка
    cout << 'a';
    Sleep(10 * timer);//задержка заставки
    StartMenu(1);
    return 0;
}

void Game(int var)
{
    int weaponPos;//позиция строки дула в массиве
    int timer;//задержка между перерисовками экрана
    int odometer;//количество перерисовок экрана, они же итоговые очки
    int lifes;//количество жизней
    char control = '&';//переменная управления кораблем
    int shotPause = 4;//задержка между выстрелами (указывать на одну перерисовку больше)
    int speed;//скорость корабля
    char scr[14][50];
    if (var == 1)
    {
        weaponPos = 7;//позиция строки дула в массиве
        odometer = 1;//количество перерисовок экрана, они же итоговые очки
        lifes = 3;//количество жизней
        GameStart(scr, lifes, &timer);
    }
    else//при восстановлении игрового сеанса из сохранения
    {
        FILE *saveBin;
        fopen_s(&saveBin, "CurrentSave.bin", "rb");
        if (!saveBin)
        {
            system("cls");
            cout << "Нет сохранения.";
            Sleep(1000);
            system("cls");
            cout << "Нет сохранения..";
            Sleep(1000);
            system("cls");
            cout << "Нет сохранения...";
            Sleep(1000);
            Game(1);
        }
        fread(&weaponPos, 1, sizeof(int), saveBin);
        timer = 80;
        fread(&odometer, 1, sizeof(int), saveBin);
        fread(&lifes, 1, sizeof(int), saveBin);
        fread(&scr, 14 * 50, sizeof(char), saveBin);
        fclose(saveBin);
        remove("CurrentSave.bin");
    }
    while (true)
    {
        int odometerBuf = odometer, odometerDigitLength;
        for (odometerDigitLength = 0; odometerBuf != 0; odometerBuf /= 10, odometerDigitLength++);//вычисление количества цифр на одометре
        for (int i = odometerDigitLength, odometerBuf = odometer; i >= 0; i--, scr[0][i] = odometerBuf % 10 + '0', odometerBuf /= 10);//прорисовка одометра на приборную панель
        scr[0][odometerDigitLength++] = 'К'; scr[0][odometerDigitLength++] = 'М';//дописывание "КМ"
        odometer++;//наращение одометра
        speed = 1000 / timer;//обновление спидометра
        int speedBuf = speed;
        for (int i = 42; speed != 0; i--, scr[0][i] = speed % 10 + '0', speed /= 10);//прорисовка спидометра на приборную панель
        scr[0][42] = 'К'; scr[0][43] = 'М'; scr[0][44] = '/'; scr[0][45] = 'С';//дописывание "КМ/С"
        if (_kbhit())//если клавиша была нажата
        {
            control = _getch();//переменная примет ее значение
            if (control == 224)
                control = _getch();
        }
        if (control == 13 && shotPause == 4 || control == 32 && shotPause == 4)//при нажатии на курок если пушка перезаряжена
        {
            scr[weaponPos][3] = '-';
            shotPause = 0;
        }
        if (shotPause < 4)//перезарядка
            shotPause++;
        if (control == 27)//при выходе
        {
            FILE *saveBin;
            fopen_s(&saveBin, "CurrentSave.bin", "wb");
            fwrite(&weaponPos, 1, sizeof(int), saveBin);
            fwrite(&odometer, 1, sizeof(int), saveBin);
            fwrite(&lifes, 1, sizeof(int), saveBin);
            fwrite(&scr, 14 * 50, sizeof(char), saveBin);
            fclose(saveBin);
            GameMenu(2);
        }
        if (control == 72)//при движении корабля вверх
            if (scr[2][0] == '\' || scr[3][0] == '\' && scr[2][0] == '¤' || scr[3][1] == '\' && scr[2][1] == '¤')//если корабль врезался в верхнее поле - игра окончена
                if (lifes > 1)
                {
                    cout << 'a';
                    lifes--;
                    weaponPos = 7;
                    GameStart(scr, lifes, &timer);
                    Sleep(1000);
                }
                else
                    GameOver(odometer);
            else
            {
                for (int i = 2; i < 13; i++)//корабль смещается на элемент выше
                    for (int j = 0; j < 49; j++)
                        if (scr[i][j] == '3' || scr[i][j] == '\' || scr[i][j] == '=' || scr[i][j] == '/')
                        {
                            scr[i - 1][j] = scr[i][j];
                            scr[i][j] = ' ';
                        }
                weaponPos--;
            }
        if (control == 80)//при движении корабля вниз
            if (scr[12][0] == '/' || scr[11][0] == '/' && scr[12][0] == '¤' || scr[11][1] == '/' && scr[12][1] == '¤')//если корабль врезался в нижнее поле - игра окончена
                if (lifes > 1)
                {
                    cout << 'a';
                    lifes--;
                    weaponPos = 7;
                    GameStart(scr, lifes, &timer);
                    Sleep(1000);
                }
                else
                    GameOver(odometer);
            else
            {
                for (int i = 12; i >= 2; i--)//корабль смещается на элемент вниз
                    for (int j = 0; j < 49; j++)
                        if (scr[i][j] == '3' || scr[i][j] == '\' || scr[i][j] == '=' || scr[i][j] == '/')
                        {
                            scr[i + 1][j] = scr[i][j];
                            scr[i][j] = ' ';
                        }
                weaponPos++;
            }
        for (int i = 1; i < 14; i++)//все "космические" элементы смещаются на элемент влево
            for (int j = 0; j < 49; j++)
            {
                if (scr[i][j] == '\' && scr[i][j + 1] == '¤' || scr[i][j] == '=' && scr[i][j + 1] == '¤' || scr[i][j] == '/' && scr[i][j + 1] == '¤')
                    if (lifes > 1)
                    {
                        cout << 'a';
                        lifes--;
                        weaponPos = 7;
                        GameStart(scr, lifes, &timer);
                        Sleep(1000);
                    }
                    else
                        GameOver(odometer);
                if (scr[i][j] != '3' && scr[i][j] != '\' && scr[i][j] != '=' && scr[i][j] != '/' && scr[i][j] != '-' && scr[i][j + 1] != '-')
                    scr[i][j] = scr[i][j + 1];
                if (scr[i][j] == '¤')
                    scr[i][j + 1] = ' ';
            }
        for (int i = 1; i < 14; i++)//все снаряды смещаются на элемент вправо
            for (int j = 48; j >= 0; j--)
                if (scr[i][j] == '-')
                    if (j != 48)
                    {
                        scr[i][j + 1] = '-';
                        scr[i][j] = ' ';
                    }
                    else
                        scr[i][j] = ' ';
        char borderSymbols[] = { '†', '‡', '¤', ' ' };
        scr[2][49] = ' ';//рандомное заполнение новых элементов краев
        scr[1][49] = borderSymbols[rand() % 3];
        if (scr[1][49] == '‡')
            scr[2][49] = '¤';
        scr[12][49] = ' ';
        scr[13][49] = borderSymbols[rand() % 3];
        if (scr[13][49] == '‡')
            scr[12][49] = '¤';
        for (int i = 3; i < 12; i++)//рандомное появление космического мусора
        {
            if (rand() % 10 == 1)
                scr[i][49] = '¤';
        }
        ScreenOutput(scr);//вывод экрана
        if (control != '&')//"обнуление" управляющей переменной
            control = '&';
        if (timer > 25)//ускорение корабля
            timer--;
        Sleep(timer);//задержка перерисовки
    }
}

Загрузить исполняемый файл .exe можно тут: Space Invader [2].

P.S.: Это мой первый проект и статья на Хабре, жду комментариев.

Автор: ZEN_UA

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/igra/147064

Ссылки в тексте:

[1] мышления: http://www.braintools.ru

[2] Space Invader: https://1drv.ms/u/s!AslVydZlbUmZgzY8OdF67vpSDaWw

[3] Источник: https://habrahabr.ru/post/304448/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox