C 언어로 ASCII RPG 만들기: 구조체부터 게임 루프까지
많은 개발자가 직접 프로젝트를 만들어 보며 C 언어를 익힙니다. 간단한 ASCII 기반 RPG를 만드는 것은 데이터 구조, 포인터, 함수, 입출력 처리에 익숙해지는 데 더할 나위 없이 좋은 방법입니다. 이 가이드에서는 터미널 기반 전투 게임의 코드를 단계별로 살펴보며, 랜덤 몬스터 조우 시스템을 구현하는 과정에서 초보자가 자주 빠지는 함정들을 함께 짚어보겠습니다.
핵심 데이터 구조
이 프로젝트는 게임의 핵심 개체를 정의하기 위해 typedef를 활용합니다:
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(플레이어): 이름(최대 14자), HP, 공격력
- Monster(몬스터): 이름, HP, 데미지 범위, ASCII 아트용 타입 식별자
- Heal(회복): HP 회복 범위
scanf에서 %14s를 사용해 이름 입력 길이를 제한하면 버퍼 오버플로우를 방지할 수 있습니다. 플레이어의 최대 HP는 20으로 하드코딩되어 있습니다.
개체 초기화
초기화 함수는 구조체에 시작 값을 할당합니다:
void init_player(Player* player) {
printf("Send Nickname: ");
scanf("%14s", player->Name);
player->Hp = 20;
}
몬스터는 rand()를 사용해 템플릿 배열에서 무작위로 선택됩니다:
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];
}
main() 함수 내에서 srand(time(NULL))를 호출하면 프로그램이 실행될 때마다 새로운 난수 시드가 적용됩니다.
전투 상태 렌더링
print_status 함수는 몬스터의 타입에 따라 ASCII 아트를 출력하고 현재 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");
} // ... other types
printf("%s HP: %d\n", player.Name, player.Hp);
printf("%s HP: %d\n", monster.Name, monster.Hp);
}
이 함수는 데이터 읽기만 수행하므로 값이 값에 의한 전달(pass by value) 방식으로 전달됩니다.
전투 메커니즘
플레이어의 공격은 1~5의 데미지를 입히며, 포인터를 사용해 몬스터의 HP를 직접 수정합니다:
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("Enemy caused %d damage\n", damage_monster);
if (player->Hp < 0) {
player->Hp = 0;
}
}
회복 시 플레이어의 HP는 최대치인 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;
}
}
메인 게임 루프
start_game() 함수는 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);
}
루프는 두 개체의 HP를 지속적으로 확인합니다. 플레이어가 행동을 마치면 몬스터가 생존해 있는 경우 반격합니다.
메뉴 및 모듈화
메인 메뉴는 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;
}
}
}
마찬가지로 start_game()은 game.h에 선언됩니다. main() 함수는 난수 생성기(RNG)의 시드를 설정하고 메뉴를 실행합니다.
자주 발생하는 함정과 해결 방안
- 입력 버퍼 비우기:
scanf("%d")실행 후 개행 문자(\n)가 버퍼에 남습니다. 다음scanf("%s", &tmp)호출 전getchar()로 이를 제거해야 합니다. - 다중 바이트 문자 입력:
%c는 단일 바이트만 읽으므로 키릴 문자 같은 다중 바이트 문자에서 오류가 발생합니다.%s로 변경하면 이 문제를 우회할 수 있습니다. - 하드코딩된 HP 제한:
Heal구조체가 루프 외부에서{1,6}으로 로컬 초기화되어 유연성이 떨어집니다.
향후 개선 버전에서는 동적 몬스터 배열, 저장 시스템, 확장된 게임플레이 메커니즘을 도입하는 것이 좋습니다.
핵심 요약
- 함수 내에서 구조체를 수정할 때는 포인터를 활용하세요.
- 올바른 난수 생성을 위해
rand()는 항상srand(time(NULL))와 함께 사용하세요. scanf로 정수를 읽은 후에는 입력 버퍼를 반드시 비우세요.- 버퍼 오버플로우를 방지하려면
scanf에서 문자열 길이 제한(예:%14s)을 적용하세요. - 코드 모듈화를 위해
.h와.c파일로 분리해 관리하세요.
— Editorial Team
아직 댓글이 없습니다.