Идея выучить C появилась у меня довольно давно. Я пробовал писать в Arduino IDE, но мне не хватало чего-то более масштабного — такого проекта, где можно наделать кучу ошибок, но при этом видеть результат и двигаться дальше.

Я наткнулся в Play Market на ASCII-RPG под названием Stone Story. Сам формат меня зацепил: минимализм, но при этом ощущение полноценной игры. Поэтому я решил сделать нечто похожее, но со своими механиками.

Мне показалось, что сочетание моего ника и RPG звучит вполне нормально. Так и появилось название MerRPG.

Структуры

Первым делом я сразу решил использовать структуры, а именно:

typedef struct
{
    char Name[15];
    int Hp;
    int Attack;
} Player;

typedef struct
{
    char Name[15];
    int Hp;
    int min_attack;
    int max_attack;
    int type;
} Monster;

typedef struct 
{
    int min_heal;
    int max_heal;
} Heal;
  • Структура игрока

  • Структура монстров

  • Структура лечения

Функции

С этими аспектами мне понравилось рабоать больше всего. Я использовал void функции (за исключением главной функции main)

Разделю на несколько аспектов:

  1. Инициализация игрока и монстров:

void init_player(Player* player) {
    printf("Send Nickname: ");
    scanf("%14s", player->Name);
    player->Hp = 20;
}
  • Сделал ограничение для имени персонажа

  • Сделал ограничение хп игрока

void init_monster(Monster* monster) {
    Monster monster_list[3] = {
        {"Amogus",30,3,6,0},
        {"Slime",20,1,3,1},
        {"Spider",25,2,4,2}
    };
    int id = rand() %3;
    *monster = monster_list[id];

}
  • Добавил несколько монстров

  • Сделал случайный выбор одного из трех

  1. 2. Вывод модельки игрока и монстра:

    void print_status(Player player,Monster monster){
        printf("\n--- MerRPG ---\n");
        if (monster.type == 0) {
        printf(" 0             (AMOGUS)\n");
        printf("/|\\             (00)\n");
        printf("/ \\             /__\\\n");
        } else if (monster.type == 1) {
            printf(" 0            (SLIME)\n");
            printf("/|\\            (00)\n");
            printf("/ \\           /~~~\\\n");
        } else if (monster.type == 2) {
            printf(" 0             (SPIDER)\n");
            printf("/|\\           //\\(oo)/\\\\\n");
            printf("/ \\          //        \\\\\n");
        }
    
        printf("%s HP: %d\n",player.Name, player.Hp);
        printf("%s HP: %d\n", monster.Name,monster.Hp);
    
    }

Почему здесь я не использовал указатель? Т.к программа лишь выводит значения, но не изменяет их.

  1. 3 Атака игрока и монстра:

    void player_attack(Player* player,Monster* monster){
        int damage_player= rand() % 5 +1;
        monster ->Hp -= damage_player;
        printf("You caused %d damage\n",damage_player);
        if (monster ->Hp < 0) {
            monster ->Hp =0;
        }
    }

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

void monster_attack(Player* player,Monster* monster){
    int damage_monster = rand() % (monster ->max_attack - monster ->min_attack +1) + monster ->min_attack;
    player ->Hp -= damage_monster;
    printf("Emeny caused %d damage\n",damage_monster);
        if (player ->Hp < 0) {
            player ->Hp =0;
    }
}

По сути две эти функции не отличаются функционалом, однако специально для монстров я сделал и использовал понятия минимального и максимального урона.

  1. 4 Функция лечения игрока

void player_heal(Player* player, Heal* heal) {
    int heal_player = rand() % (heal ->max_heal - heal ->min_heal + 1) + heal->min_heal;
    player ->Hp += heal_player;
    printf("You have restored %d Hp\n", heal_player);
    if (player ->Hp > 20) {
        player ->Hp = 20;
    }
}

Здесь я добавил ограничение, чтобы лечение не превышало максимальное HP.

  1. 5 Самая большая функция - функция старта игры:

void start_game() {    
Player player;
Monster monster;
Heal heal = {1,6};
init_player(&player);
init_monster(&monster);

while(player.Hp > 0 && monster.Hp > 0 ) {
    print_status(player,monster);
    printf("[0] - Exit\n");
    printf("[1] - Attack\n");
    printf("[2] - Healing\n");
    scanf("%d", &choice);
    if (choice == 1) {
        player_attack(&player,&monster);
        if ( monster.Hp > 0) {
            monster_attack(&player,&monster);
        }
    } else if (choice == 2) {
        player_heal(&player,&heal);
        monster_attack(&player,&monster);
    }
    else if (choice == 0) {
        printf("You are out of the game\n");
        break;
    } else {
        break;
    }
}
if (player.Hp == 0) {
    printf("You Lose...\n");
} else if (monster.Hp == 0){
    printf("You Win!\n");
}
printf("Press any button to return to menu\n");
char tmp;
getchar();
scanf("%s",&tmp);  
    }
  • Основной цикл боя

  • Обработка действий игрока

  • Проверка победы или поражения

  1. 6 Уже в отдельном файле была перенесена функция начального меню игры:

void menu_game(void) {
      while (1) {
printf(" __  __           ____  ____   ____ \n");
printf("|  \\/  | ___ _ __|  _ \\|  _ \\ / ___| \n");
printf("| |\\/| |/ _ \\ '__| |_) | |_) | |  _ \n");
printf("| |  | |  __/ |  |  _ <|  __/| |_| |\n");
printf("|_|  |_|\\___|_|  |_| \\_\\_|    \\____|\n");
    printf("=== MerRPG ===\n");
    printf("1 - Start game\n");
    printf("2 - Exit\n");

    scanf("%d", &choice);

    if (choice == 1) {
        start_game();
    }
    else if (choice == 2) {
        break;
    }
}
}

В будущем планирую сюда добавить настройки.

  1. 7 Ну и последняя, но немало важная функция - главная функция main:

    int main() {
    srand(time(NULL));
    menu_game();
    
    return 0;
    }

srand() в данном случае делает так, что бы при каждом запуске игры:

  • Монстры

  • Урон

  • Лечение

    Были случайными.

Костыли:

Хочу вынести парочку интересных для меня костлкй.. А именно:

printf("Press any button to return to menu\n");
char tmp;
getchar();
scanf("%s",&tmp);  
    }

Я хотел, чтобы игрок мог нажать любую кнопку для возврата в меню. Но при использовании кириллицы программа начинала вести себя странно и могла зациклиться. Насколько я понял, это связано с тем, что %c читает один байт, а кириллица занимает больше. В итоге я заменил %c на %s.

Еще один костыль в этом же куске... Без getchar() игра просто не даст пользователю нажать на любую кнопку и сразу же перекинет его в главное меню. Поэтому пришлось вручную очищать буфер ;)

Костыль с ограничением количество хп, которое дает лечение:

void start_game() {    
Player player;
Monster monster;
Heal heal = {1,6};

Мне не удалось сделать это по аналогии с минимальным и максимальным уроном монстров. Поэтому я нашел более "легкий" способ.

Файлы с расширением .h

Как только мне понадобилось вынести меню в отдельный файл, так сразу этот файл мне помог это сделать.. А точнее 2 файла. Т.к в меню используется данный кусок, вызывающий функцию start_game():

    if (choice == 1) {
        start_game();
    }

Нужно было создать два файла .h - game.h и menu.h

#ifndef GAME_H
#define GAME_H

void start_game(void);

#endif
#ifndef MENU_H
#define MENU_H

void menu_game(void);

#endif

В каждом из которых нужно было вызывать требуемые функции.

В целом мне нравится заниматься данными вещами. Игра будет развиваться, будут появляться новые фичи ( и костыли).

Я буду вам очень признателен за помощь в исправлении моих ошибок. Выслушаю любую критику.

Комментарии (10)


  1. Mausglov
    29.03.2026 11:07

    Хочу обратить ваше внимание на то, что подобный подход не научит вас писать код на C правильно. Максимум, чему вы научитесь - писать как-нибудь, и результат будет работать. А иногда не работать. У вас в профиле не указано никаких других языков, кроме C/C++ - откуда я делаю вывод, что их нет.
    Чтобы как-то улучшить результат, я предлагаю вам либо спрашивать ревью у ИИ, либо, для чистоты процесса, после написания какого-то законченного небольшого куска кода предлагать решить ту же задачу ИИ, не показывая ваш код - как я понимаю принцип работы LLM, она покажет самые ходовые практики для данного случая.


    1. Meronto Автор
      29.03.2026 11:07

      Хорошо, спасибо :)


  1. Serpentine
    29.03.2026 11:07

    Я буду вам очень признателен за помощь в исправлении моих ошибок. Выслушаю любую критику.

    Критики не будет, как и исправления ошибок. Просто наподумать/допилить.

    Почему-то игра странно себя ведет при вводе следующих никнеймов:

    • Fluggegecheimen

    • Nikita Dzhigurda

    В итоге я заменил %c на %s.

    ... но она упорно продолжает зацикливаться на кириллице.

    Ну и ждем патч с добавлением обработки некорректных команд от игрока (например, когда он вместо '1' или '2' ввёл '3' или 'q').


    1. Meronto Автор
      29.03.2026 11:07

      У меня стоит ограничение по длине никнейма в 14 (а не в 15, как в выделенном массиве) символов. Но спасибо :)


  1. ecolog_veteran
    29.03.2026 11:07

    Почему здесь я не использовал указатель? Т.к программа лишь выводит значения, но не изменяет их.

    В таком случае можно было бы объявить указатель на константную структуру, т.е.:

    void print_status(const Player* player, const Monster* monster) {
    ...
    }
    

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


    1. Meronto Автор
      29.03.2026 11:07

      Хорошо, спасибо :)


  1. Jijiki
    29.03.2026 11:07

    начинайте сразу с квадратных 2д обьектов, консольные игры хороши, как наброски, спросите у ИИ роадмап, но попытайтесь точно сформулировать пункт начальный базовый 2Д и пункт куда вы хотите придти, роадмап сохраните и отрабатывайте как раньше без ИИ - эту карту, первые шаги не так важны, важно понять суть и то что конкретно вы хотите получить, почему с 2д, потомучто сам терминал это 2д на квадратиках, кароче как не крути база это 2д с какими-то моментами

    2д еще проще доделать, как концептуально так и до полного рабочего прототипа, а там уже будет видно ваше приложение вам самому

    сосредоточтесь на модульности, нужно 2д, звук, менеджер ресурсов(с ростом проекта персистентность добавляйте), сосредоточтесь на таком принципе каждый новый подход использует старый ваш материал(можно добиться примерно 50%), и тогда материал будет расти вы будете получать знания

    есть некоторые техники сокращения пути, или упрощения как в 3д или 2д, но база это 2д, как емуляция процессора в какой-то мере, по-сути и там и там всё одно и тоже, просто в играх упор на игровую механику, поэтому база важна


    1. Meronto Автор
      29.03.2026 11:07

      Большое спасибо)


  1. domix32
    29.03.2026 11:07

    В Си можно склеивать строковые литералы просто рисуя их рядом без разделения запятой.

    printf("ASDqwe\n");
    // будет эквивалентно
    //          вот тут запятой не нужно
    //          v 
    printf("ASD" "qwe\n");
    printf( // и так тоже можно
      "ASD"
      "qwe\n"
    );
      

    Так что как минимум количество printf в вашей программе можно радикально сохранить. А уж если вынести литералы в дефайны, то и подавно.

    Наверное имеет смысл немножко почитать про ANSII ecape sequences. Попробуйте попользоваться каким-нибудь gitui/tig или тот же fzf и сравните насколько они интерактивны не выходя из терминала.

    Чтобы посмотреть как довольно неплохо писать код на C можно подглядеть код и подход raylib, например.

    С точки зрения разработки игр - если игра не будет сильно усложняться и останется на этапе попеременного обменивания тумаками, то вопросов нет. Если же вы действительно хотите сделать полноценную RPG, то имеет сразу задумываться о написании ECS, чтобы обработка логики происходила системами, а не юнитами. Ну а сами юниты превращаются в набор систем и пару пропертей, типа того же имени и ID.


    1. Meronto Автор
      29.03.2026 11:07

      Ооо, спасибо)