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.
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.
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:
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 kolejnymscanf("%s", &tmp). - Cyrylica w danych wejściowych:
%codczytuje pojedynczy bajt, podczas gdy cyrylica to znaki wielobajtowe. Zastąpienie go%srozwiązuje ten problem. - Sztywne limity HP: Struktura
Healjest 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()wymagasrand(time(NULL)), aby generować nieprzewidywalne wartości przy każdym uruchomieniu.- Zawsze czyść bufor wejścia po
scanfz%d. - Ograniczaj długość ciągów znaków w
scanf(%14s), aby uniknąć przepełnienia bufora. - Wydzielaj funkcje do plików
.h/.cdla zachowania modularności kodu.
— Editorial Team
Brak komentarzy.