Powrót do strony głównej

Uczę się C przez ASCII-RPG: kod i struktury

Artykuł analizuje kod prostej ASCII-RPG w C do nauki języka. Opisane struktury danych, funkcje walki, menu i typowe błędy z wejściem. Nadaje się dla middle/senior developerów.

ASCII-RPG w C: pełna analiza kodu gry
Advertisement 728x90

Tworzenie ASCII-RPG w C: od struktur po pętlę gry

Programiści często uczą się C poprzez praktyczne projekty. Stworzenie prostej gry ASCII-RPG pozwala opanować struktury danych, wskaźniki, funkcje oraz obsługę wejścia/wyjścia. W tym artykule przeanalizujemy kod gry terminalowej z walkami przeciwko losowym potworom, omawiając przy tym typowe pułapki, w które wpadają początkujący.

Podstawowe struktury danych

Projekt wykorzystuje typedef do definiowania struktur opisujących byty w grze:

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;
  • Player: imię (ograniczone do 14 znaków), HP, atak.
  • Monster: imię, HP, zakres obrażeń, typ służący do wyświetlania ASCII-artu.
  • Heal: zakres przywracania punktów życia.

Ograniczenie długości imienia za pomocą %14s w scanf zapobiega przepełnieniu bufora. Maksymalna liczba HP gracza jest na sztywno ustawiona na 20.

Google AdInline article slot

Inicjalizacja bytów

Funkcje inicjalizujące wypełniają struktury wartościami początkowymi:

void init_player(Player* player) {
    printf("Send Nickname: ");
    scanf("%14s", player->Name);
    player->Hp = 20;
}

W przypadku potworów wykorzystywana jest tablica wzorców z rand() do losowego wyboru:

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];
}

srand(time(NULL)) w main() zapewnia pseudolosowość przy każdym uruchomieniu programu.

Google AdInline article slot

Wyświetlanie stanu walki

Funkcja print_status rysuje ASCII-art potwora w zależności od jego typu i wyświetla aktualne HP:

void print_status(Player player, Monster monster){
    printf("\n--- MerRPG ---\n");
    if (monster.type == 0) {
        printf(" 0             (AMOGUS)\n");
        printf("/|\\             (00)\n");
        printf("/ \\             /___\\\n");
    } // ... inne typy
    printf("%s HP: %d\n", player.Name, player.Hp);
    printf("%s HP: %d\n", monster.Name, monster.Hp);
}

Argumenty są przekazywane przez wartość, ponieważ funkcja jedynie odczytuje dane.

Mechaniki walki

Atak gracza generuje obrażenia w zakresie 1–5 i wykorzystuje wskaźniki do modyfikacji HP potwora:

Google AdInline article slot
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;
    }
}

Atak potwora korzysta z zakresu zapisanego w strukturze:

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("Enemy caused %d damage\n", damage_monster);
    if (player->Hp < 0) {
        player->Hp = 0;
    }
}

Leczenie ogranicza HP do maksymalnej wartości 20:

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;
    }
}

Główna pętla gry

Funkcja start_game() zarządza przebiegiem walki w pętli while:

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);  
}

Pętla sprawdza HP obu stron. Po ruchu gracza potwór kontratakuje, o ile wciąż żyje.

Menu i modularność

Menu zostało wydzielone do osobnego pliku z nagłówkiem menu.h:

void menu_game(void) {
    while (1) {
        // ASCII-art banera
        printf("1 - Start game\n");
        printf("2 - Exit\n");
        scanf("%d", &choice);
        if (choice == 1) {
            start_game();
        } else if (choice == 2) {
            break;
        }
    }
}

Analogicznie wygląda plik game.h dla funkcji start_game(). main() inicjalizuje generator liczb losowych i uruchamia menu.

Typowe problemy i obejścia

  • Czyszczenie bufora wejścia: Po scanf("%d") w buforze pozostaje znak \n. getchar() pochłania go przed kolejnym scanf("%s", &tmp).
  • Cyrylica w danych wejściowych: %c odczytuje pojedynczy bajt, podczas gdy cyrylica to znaki wielobajtowe. Zastąpienie go %s rozwiązuje ten problem.
  • Sztywne limity HP: Struktura Heal jest inicjalizowana lokalnie jako {1,6}, bez wykorzystania osobnej struktury w pętli.

W przyszłości warto dodać dynamiczne tablice potworów, system zapisywania postępu oraz rozbudować mechaniki rozgrywki.

Co warto zapamiętać

  • Używaj wskaźników do modyfikowania struktur wewnątrz funkcji.
  • rand() wymaga srand(time(NULL)), aby generować nieprzewidywalne wartości przy każdym uruchomieniu.
  • Zawsze czyść bufor wejścia po scanf z %d.
  • Ograniczaj długość ciągów znaków w scanf (%14s), aby uniknąć przepełnienia bufora.
  • Wydzielaj funkcje do plików .h/.c dla zachowania modularności kodu.

— Editorial Team

Advertisement 728x90

Czytaj dalej