Ein ASCII-RPG in C entwickeln: Von Structs bis zum Game Loop
Viele Entwickler lernen C durch praktische Projekte. Ein einfaches ASCII-basiertes RPG zu programmieren ist eine hervorragende Methode, um sich mit Datenstrukturen, Zeigern, Funktionen und der Ein-/Ausgabeverwaltung vertraut zu machen. In dieser Anleitung gehen wir Schritt für Schritt den Code für ein terminalbasiertes Kampfspiel mit zufälligen Monsterbegegnungen durch und beleuchten dabei typische Anfängerfehler.
Kern-Datenstrukturen
Das Projekt nutzt typedef, um die zentralen Spielobjekte zu definieren:
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: Name (auf 14 Zeichen begrenzt), LP (Lebenspunkte) und Angriffskraft.
- Monster: Name, LP, Schadensbereich und ein Typ-Bezeichner für die ASCII-Grafik.
- Heal: Bereich für die Wiederherstellung von LP.
Die Begrenzung der Namenseingabe mit %14s in scanf verhindert Pufferüberläufe. Die maximalen LP des Spielers sind fest auf 20 codiert.
Initialisierung der Entitäten
Initialisierungsfunktionen füllen die Strukturen mit Startwerten:
void init_player(Player* player) {
printf("Send Nickname: ");
scanf("%14s", player->Name);
player->Hp = 20;
}
Monster werden mithilfe von rand() aus einem Template-Array ausgewählt:
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];
}
Der Aufruf von srand(time(NULL)) in main() stellt sicher, dass bei jedem Programmstart ein neuer Seed verwendet wird.
Darstellung des Kampfstatus
Die Funktion print_status zeichnet die ASCII-Grafik des Monsters basierend auf seinem Typ und zeigt die aktuellen LP an:
void print_status(Player player, Monster monster){
printf("\n--- MerRPG ---\n");
if (monster.type == 0) {
printf(" 0 (AMOGUS)\n");
printf("/|\\ (00)\n");
printf("/ \\ /___\\\n");
} // ... other types
printf("%s HP: %d\n", player.Name, player.Hp);
printf("%s HP: %d\n", monster.Name, monster.Hp);
}
Hier werden die Werte als Kopie (Call-by-Value) übergeben, da die Funktion die Daten nur liest.
Kampfmechaniken
Der Angriff des Spielers verursacht 1–5 Schaden und nutzt Zeiger, um die LP des Monsters zu modifizieren:
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;
}
}
Der Angriff des Monsters basiert auf seinem definierten Schadensbereich:
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;
}
}
Die Heilung begrenzt die LP des Spielers auf das Maximum von 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;
}
}
Die Hauptschleife des Spiels
Die Funktion start_game() steuert die Kampfabfolge innerhalb einer while-Schleife:
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);
}
Die Schleife prüft kontinuierlich die LP beider Entitäten. Nach der Aktion des Spielers kontert das Monster, sofern es noch am Leben ist.
Menü und Modularität
Das Hauptmenü befindet sich in einer separaten Datei zusammen mit menu.h:
void menu_game(void) {
while (1) {
// ASCII banner art
printf("1 - Start game\n");
printf("2 - Exit\n");
scanf("%d", &choice);
if (choice == 1) {
start_game();
} else if (choice == 2) {
break;
}
}
}
Ebenso wird start_game() in game.h deklariert. Die main()-Funktion initialisiert den Zufallsgenerator und startet das Menü.
Häufige Fallstricke und Lösungen
- Leeren des Eingabepuffers: Nach
scanf("%d")verbleibt ein Zeilenumbruch (\n) im Puffer.getchar()entfernt diesen vor dem nächstenscanf("%s", &tmp). - Kyrillische/Multibyte-Eingaben:
%cliest nur ein einzelnes Byte, was bei Multibyte-Zeichen wie kyrillischen Buchstaben zu Problemen führt. Die Umstellung auf%sumgeht dieses Problem. - Fest codierte LP-Grenzen: Die
Heal-Struktur wird lokal als{1,6}initialisiert, ohne durch die Schleife übergeben zu werden.
Zukünftige Versionen sollten dynamische Monster-Arrays, ein Speichersystem und erweiterte Spielmechaniken einführen.
Wichtige Erkenntnisse
- Verwenden Sie Zeiger, um Strukturen innerhalb von Funktionen zu ändern.
- Kombinieren Sie
rand()immer mitsrand(time(NULL))für eine echte Zufallsverteilung. - Leeren Sie den Eingabepuffer nach dem Lesen von Ganzzahlen mit
scanf. - Begrenzen Sie die String-Länge in
scanf(z. B.%14s), um Pufferüberläufe zu vermeiden. - Teilen Sie Ihren Code in
.h- und.c-Dateien auf, um die Modularität zu verbessern.
— Editorial Team
Noch keine Kommentare.