본문 바로가기
개발 일지

C언어 콘솔로 간단한 RPG 게임 만들기 - 05. 몬스터

by PrintedLove 2020. 2. 22.

 

오랫만에 글을 쓰네요. 이번 글에서는 드디어 RPG에서 빠질 수 없는 몬스터를 추가 했습니다.

영상 먼저 보시죠.

 

 

 저 슬라임 3마리 구현한다고 위해 2주일에 가까운 시간을.... ㅠ

 제가 c언어 메모리쪽으로 공부한 적이 없어서 맨땅에 헤딩한다고 오래 걸렸습니다. 뭐... 덕분에 많이 배우기도 했지만 에러 고친다고 고생한 거 떠올리면... 아오!

 아래는 소스코드 입니다.

 

// [C Game] Simple RPG
// made by "PrintedLove"
// https://printed.tistory.com/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <memory.h>
#include <math.h>
#include <time.h>
#include <windows.h>
#define FALSE 0
#define TRUE 1
#define MAP_X_MAX 96
#define MAP_Y_MAX 32
#define FLOOR_Y 26
#define OBJECT_MAX 32

typedef struct _Character {
    short coord[2], size[2];	//coordinate value and size
    float accel[2], flyTime;	//acceleration value and flotation time
    bool direction;	//true=right, false=left
    			//stat
    char name[16];
    int lv;
    unsigned int exp[2];	//0=exp required, 1=current exp
    int hp[2], mp[2];	//0=max value, 1=current value
    short power, weapon;
    unsigned int score;
    			//animation control
    short motion[4];	//motion value		//leg_motion, attack_motion(1, 2, 3)
    unsigned int tick[4];	//tick 		//gen_tick, leg_tick, atk_tick, dash_tick
}Character;

typedef struct _Object {	//enemies, projectiles, particles, etc.
    short coord[2], size[2];
    float accel[2], flyTime;
    bool direction;

    short kind;	//1~99: items, 100~199: enemies, 200~: projectiles, particles
    int hp[2];
    int exp;
    short dam;
    

    short motion[3];	//motion
    unsigned int tick[4];	//0: hpshow time(enemy) or active time(projecticles, particles)
}Object;

Character character;
Object **objects;

const short size_enemy[2][2] = {{3, 3}, {4, 2}};
const short stat_weapon[3] = {5, 10, 15};
unsigned int tick = 0;

const char sprite_character[10] = " 0  | _^_";
const char sprite_character_leg[2][3][4] = 
{{"-^.", "_^\'", "_^."},
 {".^-", "\'^_", ".^_"}};
const char sprite_enemy1[2][13] = {" __ (  )----", " __ [  ]\'--\'"};
const char sprite_normalAttack[2][3][16] = 
{{" .- o          ", " .   (   o \'   ", "         o \'-  "},
 {"o -.           ", "   . o   )   \' ", "     o      -\' "}};
 const char sprite_weapon[2][3][4] = 
{{"---", "--+", "<=+"},
 {"---", "+--", "+=>"}};
const char sprite_invenWeapon[3][11] = {"   /   /  ", "   /  '*. ", "  |   \"+\" "};

char sprite_floor[MAP_X_MAX];
char mapData[MAP_X_MAX * MAP_Y_MAX];	//array for graphics

void StartGame();	//=initialize
void UpdateGame();
void ExitGame();
void SetConsole();
void ControlUI();
void ControlCharacter();
void ControlObject();
void ControlEnemy(int index);
void CreateObject(short x, short y, short kind);
void RemoveObject(int index);
bool EnemyPosition(short x, short size_x);	//direction the enemy looks at the character
bool CollisionCheck(short coord1[], short coord2[], short size1[], short size2[]);	//check collision
void MoveControl(short coord[], float accel[], short size[], float *flyTime);	// motion control
void DrawBox(short x, short y, short size_x, short size_y);	//draw box of size_x, size_y at x, y coordinates
void DrawNumber(short x, short y, int num);	//draw numbers at x, y coordinates (align left)
void DrawSprite(short x, short y, short size_x, short size_y, const char spr[]);	//draw sprite of size_x, size_y at x, y coordinates
void FillMap(char str[], char str_s, int max_value);	//array initialization
void EditMap(short x, short y, char str);	// edit x, y coordinate mapdata
int NumLen(int num);	//return length of number

int main() {
	StartGame();
	
	while (TRUE) {
		if (tick + 30 < GetTickCount()) {
			tick = GetTickCount();
			UpdateGame();
		}
	}
	
	ExitGame();
	return 0;
}

void StartGame() {
	SetConsole();
	
	printf("Enter your name: ");
	scanf("%[^\n]s", character.name);
	
	FillMap(sprite_floor, '=', MAP_X_MAX);
	
	character.coord[0] = MAP_X_MAX / 2; character.coord[1] = MAP_Y_MAX / 2;	//character initialize
	character.size[0] = 3; character.size[1] = 3;
	character.lv = 1;
	character.exp[0] = 100;
	character.hp[0] = 100; character.hp[1] = 100;
	character.mp[0] = 50; character.mp[1] = 50;
	character.power = 10;
	
	objects = (Object **)malloc(sizeof(Object *) * OBJECT_MAX);
	memset(objects, 0, sizeof(Object *) * OBJECT_MAX);
	
	CreateObject(76, 10, 100);
	CreateObject(70, 10, 100);
	CreateObject(64, 10, 100);
}

void UpdateGame() {
	FillMap(mapData, ' ', MAP_X_MAX * MAP_Y_MAX);	//initialize mapdata
	
	ControlUI();
	ControlCharacter();
	ControlObject();
	
	puts(mapData);	//update mapdata
}

void ExitGame() {
    for (int i = 0; i < OBJECT_MAX; i++) {
        if (objects[i])
            free(objects[i]);
    }
    
    free(objects);
}

void SetConsole() {
	system("mode con:cols=96 lines=32");
	system("title RPG test");
	
	HANDLE hConsole;
    CONSOLE_CURSOR_INFO ConsoleCursor;
    hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    
    ConsoleCursor.bVisible = 0;
    ConsoleCursor.dwSize = 1;
    SetConsoleCursorInfo(hConsole , &ConsoleCursor);
    
    srand((unsigned int)time(NULL));
}

void ControlUI() {
	int expPer = roundf(character.exp[1] * 100 / character.exp[0]);
	int len;	//length of previous sprite
	
	DrawSprite(1, FLOOR_Y, MAP_X_MAX, 1, sprite_floor);	//draw floor
	
	DrawBox(1, 2, 35, 8); DrawBox(27, 5, 7, 4);	//draw weaponinven
	DrawSprite(28, 6, 5, 2, sprite_invenWeapon[character.weapon]);
	DrawSprite(28, 4, 6, 1, "Weapon");
	
	EditMap(3, 3, '\"');	//draw name, lv, exp
	DrawSprite(4, 3, strlen(character.name), 1, character.name);	len = 4 + strlen(character.name);
	DrawSprite(len, 3, 7, 1, "\" LV.");	len += 5;
	DrawNumber(len, 3, character.lv);	len += NumLen(character.lv);
	DrawSprite(len, 3, 2, 1, " (");	len += 2;
	if (!expPer) {
		EditMap(len, 3, '0');	len ++;
	} else {
		DrawNumber(len, 3, expPer);	len += NumLen(expPer);
	}
	DrawSprite(len, 3, 2, 1, "%)");
	
	DrawSprite(MAP_X_MAX - NumLen(character.score) - 7, 3, 6, 1, "SCORE:");	//draw score
	DrawNumber(MAP_X_MAX - NumLen(character.score), 3, character.score);
	
	DrawSprite(4, 5, 3, 1, "HP:");	//draw HP
	DrawNumber(8, 5, character.hp[1]);
	EditMap(9 + NumLen(character.hp[1]), 5, '/');
	DrawNumber(11 + NumLen(character.hp[1]), 5, character.hp[0]);
	
	DrawSprite(4, 6, 3, 1, "MP:");	//draw MP
	DrawNumber(8, 6, character.mp[1]);
	EditMap(9 + NumLen(character.mp[1]), 6, '/');
	DrawNumber(11 + NumLen(character.mp[1]), 6, character.mp[0]);
	
	DrawSprite(4, 8, 6, 1, "Power:");	//draw power
	DrawNumber(11, 8, character.power);
}

void ControlCharacter() {
	bool move = FALSE, attack = FALSE;
	int x = character.coord[0], y = character.coord[1];
	
	if (character.exp[1] >= character.exp[0]) {	//LV up
		character.hp[0] += 10;
		character.mp[0] += 5;
		character.power ++;
		character.lv ++;
		character.exp[1] = 0;
		character.exp[0] += character.lv * 10;
	}
	
	if (character.tick[0] + 900 < tick) {	//hp,mp gen & control
		character.tick[0] = tick;
		character.hp[1] += roundf(character.hp[0] * 0.01);
		character.mp[1] += roundf(character.mp[0] * 0.05);
	}
	if (character.hp[1] > character.hp[0])
		character.hp[1] = character.hp[0];
	if (character.mp[1] > character.mp[0])
		character.mp[1] = character.mp[0];
	
	if (GetAsyncKeyState(0x5A) & 0x8000 && character.flyTime == 0) {	//attack
		attack = TRUE;
		character.motion[1] = TRUE;
	}
	
	if (character.motion[1]) {
		if (tick > character.tick[2] + 150) {	//attack motion calculation
			character.tick[2] = tick;
			character.motion[2]++;
		}
		
		if (character.motion[2] > 3) {
			if (attack) {
				character.motion[2] = 1; character.motion[3]++;
			} else {
				character.motion[1] = FALSE; character.motion[2] = 0; character.motion[3] = 1;
			}
			
			if (character.motion[3] > 3)
				character.motion[3] = 1;	
		}
	} else {
		if (GetAsyncKeyState(VK_LEFT) & 0x8000 && x > 1) {	//move left
			if (character.accel[0] > -1)
				character.accel[0] = -1;
				
			character.direction = FALSE;
			move = TRUE;
		}
		
		if (GetAsyncKeyState(VK_RIGHT) & 0x8000 && x < MAP_X_MAX - 2) {	//move right
			if (character.accel[0] < 1)
				character.accel[0] = 1;
				
			character.direction = TRUE;
			move = TRUE;
		}
			
		if (GetAsyncKeyState(0x58) & 0x8000 && character.tick[3] + 1200 <= tick) {	//dash
			character.accel[0] = character.direction * 6 - 3;
			character.tick[3] = tick;
		}
	}
	
	if (GetAsyncKeyState(VK_UP) & 0x8000 && y + 3 == FLOOR_Y)	//jump
			character.accel[1] = -1.75;
	
	if (tick > character.tick[1] + 90) {	//leg tick	
		character.tick[1] = tick;
		
		if (move == TRUE)
			character.motion[0]++;
		else
			character.motion[0] = 0;
			 
		if (character.motion[0] > 3)
			character.motion[0] = 1;
	}
	
	DrawSprite(x, y, character.size[0], character.size[1], sprite_character);	//draw character sprite
	MoveControl(character.coord, character.accel, character.size, &character.flyTime);	// control character movement
	
	if (character.direction) {
		EditMap(x, y + 1, '(');
	} else {
		EditMap(x + 2, y + 1, ')');
	}
	
	if (character.accel[0] > 1)
		DrawSprite(x - 2, y, 1, 3, "===");
	if (character.accel[0] < -1)
		DrawSprite(x + 4, y, 1, 3, "===");
		
	if (character.motion[1] && character.motion[2] > 0) {	//draw attack motion
		if (character.motion[3] == 3) {
			DrawSprite(x - 5 + 8 * character.direction, y, 5, 3, sprite_normalAttack[character.direction][character.motion[2] - 1]);
		} else {
			if (character.motion[2] == 2) {
				EditMap(x - 2 + 6 * character.direction, y + 1, 'o');
				DrawSprite(x - 5 + 10 * character.direction, y + 1, 3, 1, sprite_weapon[character.direction][character.weapon]);
			} else {
				EditMap(x + 2 * character.direction, y + 1, 'o');
				DrawSprite(x - 3 + 6 * character.direction, y + 1, 3, 1, sprite_weapon[character.direction][character.weapon]);
			}
		}
	} else {
		EditMap(x + character.direction * 2, y + 1, 'o');
		DrawSprite(x - 3 + 6 * character.direction, y + 1, 3, 1, sprite_weapon[character.direction][character.weapon]);
		
		if (character.motion[0] == 3)
			EditMap(x + 1, y + 1, 'l');
	}
	
	if (character.motion[0] > 0)
		DrawSprite(x, y + 2, 3, 1, sprite_character_leg[character.direction][character.motion[0] - 1]);	//draw leg motion
}

void ControlEnemy(int index) {
	bool move = FALSE, attack = FALSE;
	short x = objects[index]->coord[0], y = objects[index]->coord[1];
	short character_attack_coord[2] = {character.coord[0] - 4 + 8 * character.direction, character.coord[1]};
	
	if (objects[index]->hp[1] < 1) {
		character.exp[1] += objects[index]->exp;
		RemoveObject(index);
		return;
	}
	
	if (objects[index]->tick[0] + 2000 > tick)
		DrawNumber(x + objects[index]->size[0] / 2 - NumLen(character.mp[1]) / 2 - 1, y - 1, objects[index]->hp[1]);
	
	if (character.motion[2] == 1 && CollisionCheck(objects[index]->coord, character_attack_coord, objects[index]->size, character.size)) {
		objects[index]->tick[0] = tick;
		objects[index]->hp[1] -= character.power;
		if (character.motion[3] == 3)
			objects[index]->hp[1] -= character.power;
			
		objects[index]->accel[1] = - 0.55;
		if (EnemyPosition(x,  objects[index]->size[0]))
			objects[index]->accel[0] = -0.75;
		else
			objects[index]->accel[0] = 0.75;
	}
	
	switch (objects[index]->kind) {
		case 100:
			if (y + objects[index]->size[1] == FLOOR_Y)
				objects[index]->motion[0] = 0;
			else 
				objects[index]->motion[0] = 1;

			if (objects[index]->tick[1] + objects[index]->tick[2] < tick) {
				objects[index]->tick[1] = tick;
				objects[index]->tick[2] = 1000 + rand() % 1000;
				objects[index]->accel[1] = rand() / (float)RAND_MAX / 2 - 1.2;
				
				if (EnemyPosition(x,  objects[index]->size[0]))
					objects[index]->accel[0] = 2.4 - rand() / (float)RAND_MAX;
				else
					objects[index]->accel[0] = rand() / (float)RAND_MAX - 2.4;
			}
			
			DrawSprite(x, y, objects[index]->size[0], objects[index]->size[1], sprite_enemy1[objects[index]->motion[0]]);
			break;
	}
	
	MoveControl(objects[index]->coord, objects[index]->accel, objects[index]->size, &objects[index]->flyTime);
}

void ControlObject() {
	for(int i = 0; i < OBJECT_MAX; i++) {
		if (objects[i]) {
			if (objects[i]->kind < 100)
				return;
			else if (objects[i]->kind > 99 && objects[i]->kind < 200)
				ControlEnemy(i);
			else
				return;
		}
	}
}

void CreateObject(short x, short y, short kind) {
	int index = 0;
	Object *obj = 0;
	
	while(TRUE) {
		if (! objects[index])
			break;
			
		if (index == OBJECT_MAX)
			return;

    	index ++;
	}
	
	obj = (Object *)malloc(sizeof(Object));
    objects[index] = obj;
    obj->kind = kind;
    obj->coord[0] = x; obj->coord[1] = y;
    obj->tick[0] = 0; obj->tick[1] = 0; obj->tick[2] = 0; obj->tick[3] = 0;
    
    switch (kind) {
		case 100:
			obj->hp[0] = 300; obj->hp[1] = 300;
			obj->exp = 30;
			obj->size[0] = 4; obj->size[1] = 3;
			obj->tick[2] = 1000 + rand() % 1000;
			break;
	}
}

void RemoveObject(int index) {
	free(objects[index]);
    objects[index] = 0;
}

bool CollisionCheck(short coord1[], short coord2[], short size1[], short size2[]) {
	if (coord1[0] > coord2[0] - size1[0] && coord1[0] < coord2[0] + size2[0]
	 && coord1[1] > coord2[1] - size1[1] && coord1[1] < coord2[1] + size2[1])
		return TRUE;
	else
		return FALSE;
}

bool EnemyPosition(short x, short size_x) {
	if (character.coord[0] + 1 < x + floor(size_x / 2 + 0.5))
		return FALSE;
	else
		return TRUE;
}

void MoveControl(short coord[], float accel[], short size[], float *flyTime) {
	float x_value = accel[0], y_value = accel[1];
	
	if (coord[1] + size[1] == FLOOR_Y) {
		*flyTime = 0;
	} else {
		*flyTime += 0.05;
		accel[1] += *flyTime;
	}
	
	if (x_value != 0 || y_value != 0) {
		if (coord[0] + x_value < 1)
			x_value = 1 - coord[0];
		if (coord[0] + size[0] + x_value > MAP_X_MAX)
			x_value = MAP_X_MAX - size[0] - coord[0];
		if (coord[1] + size[1] + y_value > FLOOR_Y)
			y_value = FLOOR_Y - coord[1] - size[1];
	}
	
	coord[0] += floor(x_value + 0.5);
	coord[1] += floor(y_value + 0.5);
	
	if (accel[0] > 0) accel[0] -= 0.2; if (accel[0] < 0) accel[0] += 0.2;
	if (accel[1] > 0) accel[1] -= 0.1; if (accel[1] < 0) accel[1] += 0.1;
}

void DrawBox(short x, short y, short size_x, short size_y) {
	EditMap(x, y, '.'); EditMap(x + size_x - 1, y, '.');
	EditMap(x, y + size_y - 1, '\''); EditMap(x + size_x - 1, y + size_y - 1, '\'');
	
	for (int i = 1; i < size_x - 1; i++) {
		EditMap(x + i, y, '-'); EditMap(x + i, y + size_y - 1, '-');
	}
	for (int i = 1; i < size_y - 1; i++) {
		EditMap(x, y + i, '|'); EditMap(x + size_x - 1, y + i, '|');
	}
}

void DrawNumber(short x, short y, int num) {
	int tmp = num, len = NumLen(tmp), cnt = len;
    char str[len];
    
    do {
        cnt--;
        str[cnt] = (char)(tmp % 10 + 48);
        tmp /= 10;
    } while(tmp != 0);
    
    DrawSprite(x, y, len, 1, str);
}

void DrawSprite(short x, short y, short size_x, short size_y, const char spr[]) {
	for (int i = 0; i < size_y; i++) {
		for (int n = 0; n < size_x; n++)
			EditMap(x + n, y + i, spr[i * size_x + n]);
	}
}

void FillMap(char str[], char str_s, int max_value) {
	for (int i = 0; i < max_value; i++)
		str[i] = str_s;
}

void EditMap(short x, short y, char str) {
	if (x > 0 && y > 0 && x - 1 < MAP_X_MAX && y - 1 < MAP_Y_MAX)
		mapData[(y - 1) * MAP_X_MAX + x - 1] = str;
}

int NumLen(int num) {
	int tmp = num, len = 0;
	
	if (num == 0) {
		return 1;
	} else {
		while(tmp != 0) {
        	tmp /= 10;
        	len++;
    	}
	}
	
    return len;
}

 

 cpp로 첨부할까 했는데 그냥 끝까지 코드 블럭으로 올리려구요 ㅎㅎ

 코드 줄로 따지면 저번과 크게 차이가 없습니다. 옆으로 길게 늘린 건 아니구요 이전에 캐릭터 애니메이션 부분에서 200줄이나 잡아먹던 걸 최대한 단순화 시켰기 때문입니다.

 함수들을 최적화 시킨 것도 크고요.

 

void CreateObject(short x, short y, short kind) {
	int index = 0;
	Object *obj = 0;
	
	while(TRUE) {
		if (! objects[index])
			break;
			
		if (index == OBJECT_MAX)
			return;

    	index ++;
	}
	
	obj = (Object *)malloc(sizeof(Object));
    objects[index] = obj;
    obj->kind = kind;
    obj->coord[0] = x; obj->coord[1] = y;
    obj->tick[0] = 0; obj->tick[1] = 0; obj->tick[2] = 0; obj->tick[3] = 0;
    
    switch (kind) {
		case 100:
			obj->hp[0] = 300; obj->hp[1] = 300;
			obj->exp = 30;
			obj->size[0] = 4; obj->size[1] = 3;
			obj->tick[2] = 1000 + rand() % 1000;
			break;
	}
}
void ControlObject() {
	for(int i = 0; i < OBJECT_MAX; i++) {
		if (objects[i]) {
			if (objects[i]->kind < 100)
				return;
			else if (objects[i]->kind > 99 && objects[i]->kind < 200)
				ControlEnemy(i);
			else
				return;
		}
	}
}
void RemoveObject(int index) {
	free(objects[index]);
    objects[index] = 0;
}

 

 위의 3개 함수는 이번 글에서 가장 중요한 부분들입니다.

 아이템, 몬스터, 투사체 그리고 파티클을 처리하기 위해 구조체 Object를 만들었습니다. Object 구조체는 3종류(혹은 이상)의 오브젝트들의 정보를 관리 하기 위한 틀로, 오브젝트 생성과 함꼐 메모리를 할당 받습니다.

 구분은 구조체 멤버인 kind로 합니다. 1~99값의 Object는 아이템, 100 ~ 199값은 몬스터, 이런식으로요.

 오브젝트의 최대 숫자는 32개로 메인 루프에서 for문으로 처리되어 관리됩니다. kind 값이 0이 아닐 시에만 처리되고 작은 수 기준으로 생성, 제거 됩니다. 나름대로 효율성을 추구해서 코드를 짜 보았는데... 음. 렉 안걸리는거 보니 괜찮겠죠 뭐.

 그 이외에도 게임(Game) 으로 시작하는 함수들을 추가하여 코드리더빌리티를 높여보려 했습니다. 충돌 판정 함수, 몬스터 방향 계산함수 등 잡다한 함수들도 추가했구요.

 

 다음 글에서는 캐릭터 피격을 마저 처리하고 유아이를 좀 손보려 합니다.

댓글