본문 바로가기
개발 일지

C언어 콘솔로 간단한 RPG 게임 만들기 - 04. 공격과 물리엔진

by PrintedLove 2020. 2. 8.

 

안녕하세요 Printed Love입니다.

거의 일주일만에 계발글을 올리는데요, 중간중간 모르는것들을 찾아보고 자잘한 버그를 잡느라 꽤 오래 걸렸습니다.

이번 글에서는 캐릭터의 공격 모션과 가속도를 이용한 물리엔진을 추가했습니다.

GitHub에 코드를 올리니 수정사항이 궁금하시다면 한번 들려보세요 ^^

 

 

 

 

좀 더 깔끔해진 UI와 좀더 스무스 해진 이동동작, 그리고 대망의 공격 모션이 추가되었습니다.

기본공격은 총 3타로, 3번째 공격에서는 3연속 찌르기가 나갑니다.

좀더 멋지게 구현해보고 싶었는데 ASCII 그래픽의 한계에 부딛혀... 이게 최선이었습니다ㅠ

(1, 2, 3 키로 무기를 변경할 수 있습니다)

 

 다음은 소스코드인데... 거의 500줄에 육박하는 어머어머한 덩치가 되었군요. 제 코딩 실력의 한계인듯 합니다...ㄷㄷ

 

// [C Game] Simple RPG
// made by "PrintedLove"
// https://printed.tistory.com/
#include <stdio.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 22

typedef struct _Character {
    short x = MAP_X_MAX / 2 + 1, y = MAP_Y_MAX / 2;
    float x_accel = 0, y_accel = 0;
    short size_x = 3, size_y = 3;
    float flyTime = 0;
    bool direction = TRUE;	//true=right, false=left
    char sprite[10] = " 0 (|)_^_";
    			//character stat
    char name[16];
    int lv = 1;
    unsigned long int exp = 0, expi = 100;
    int hpm = 100, hp = hpm;
    int mpm = 50, mp = mpm;
    int power = 10;
    short weapon = 0;
    			//animation control
    short leg_m = 1, atk_m[3] = {FALSE, 0, 1};	//motion
    unsigned int gen_tick, leg_tick = 0, atk_tick = 0;
}Character;

Character character;

void SetConsole();
void ControlUI();
void ControlCharacter();
void ControlMove(short *x, short *y, float *x_accel, float *y_accel, short size_x, short size_y, float *flyTime);

void FillMap(char str[], char str_s, int max_value);
void EditMap(int x, int y, char str);
void DrawSprite(int x, int y, int size_x, int size_y, char spr[]);
void DrawNumber(int x, int y, int num);
void DrawBox(int x, int y, int size_x, int size_y);

int NumLen(int num);
int StrLen(char str[]);

char wall_floor[MAP_X_MAX];
char sprite_invenWeapon[][11] = {"   /   /  ", "   /  '+. ", "  |   \"+\" "};
char sprite_rightWeapon[][4] = {"---", "+--", "+=>"};
char sprite_leftWeapon[][4] = {"---", "--+", "<=+"};
short weapon_stat[] = {5, 10, 15};
char mapData[MAP_X_MAX * MAP_Y_MAX];

int main() {
	srand((unsigned int)time(NULL));
	SetConsole();
	
	printf("Enter your name: ");
	scanf("%[^\n]s", character.name);
	
	FillMap(wall_floor, '=', MAP_X_MAX);
	
	unsigned int system_tick = GetTickCount();
	
	while (TRUE) {
		if (system_tick + 30 < GetTickCount()) {
			system_tick = GetTickCount();
			
			FillMap(mapData, ' ', MAP_X_MAX * MAP_Y_MAX);
		
			ControlCharacter();
			ControlUI();
			
			printf("%s", mapData);				//update mapdata
		}
	}
	
	return 0;
}

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

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

void ControlCharacter() {
	unsigned int tick = GetTickCount();
	bool move = FALSE, attack = FALSE;
				//LV up
	if (character.exp >= character.expi) {
		character.exp = 0;
		character.lv ++; character.hpm += 10; character.mpm += 5; character.power ++;
		character.expi += character.lv * 10;
	}
				//hp, mp
	if (character.gen_tick + 900 < tick) {
		character.gen_tick = tick;
		character.hp += roundf(character.hpm * 0.01);
		character.mp += roundf(character.mpm * 0.05);
	}
	if (character.hp > character.hpm)
		character.hp = character.hpm;
	if (character.mp > character.mpm)
		character.mp = character.mpm;
				//keyboard
	if (GetAsyncKeyState(0x5A) & 0x8000 && character.flyTime == 0) {
		attack = TRUE;
		character.atk_m[0] = TRUE;
	}
	
	if (character.atk_m[0]) {
		if (tick > character.atk_tick + 150) {	//attack
			character.atk_tick = tick;
			character.atk_m[1]++;
		}
		
		if (character.atk_m[1] > 3) {
			if (attack) {
				character.atk_m[1] = 1;
				character.atk_m[2]++;	
			} else {
				character.atk_m[0] = FALSE;
				character.atk_m[1] = 0;
				character.atk_m[2] = 1;
			}
			
			if (character.atk_m[2] > 3) {
				character.atk_m[2] = 1;
				if (character.direction) {
					if (character.x < MAP_X_MAX - 2)
						character.x++;
				} else {
					if (character.x > 1)
						character.x--;
				}
			}	
		}
	} else {
		if (GetAsyncKeyState(VK_LEFT) & 0x8000 && character.x > 1) {
			character.x_accel = -1;
			character.direction = FALSE;
			move = TRUE;
		}
		
		if (GetAsyncKeyState(VK_RIGHT) & 0x8000 && character.x < MAP_X_MAX - 2) {
			character.x_accel = 1;
			character.direction = TRUE;
			move = TRUE;
		}
	}
	
	if (GetAsyncKeyState(VK_UP) & 0x8000 && character.y + 3 == FLOOR_Y)
			character.y_accel = -1.75;
	
	if (GetAsyncKeyState(0x31) & 0x8000)	//1
		character.weapon = 0;
		
	if (GetAsyncKeyState(0x32) & 0x8000)	//2
		character.weapon = 1;
	
	if (GetAsyncKeyState(0x33) & 0x8000)	//3
		character.weapon = 2;
	
	ControlMove(&character.x, &character.y, &character.x_accel, &character.y_accel, character.size_x, character.size_y, &character.flyTime);
	
	if (tick > character.leg_tick + 90) {	//leg tick	
		character.leg_tick = tick;
		
		if (move == TRUE)
			character.leg_m++;
		else
			character.leg_m = 0;
			 
		if (character.leg_m > 3)
			character.leg_m = 1;
	}
				//sprite
	character.sprite[4] = '|'; character.sprite[6] = '_'; character.sprite[8] = '_';
	
	if (character.direction) {
		character.sprite[3] = '(';
		
		if (character.atk_m[0]) {
			switch (character.atk_m[2]) {
				case 1:
					character.sprite[5] = ' ';
					
					switch (character.atk_m[1]) {
						case 1:
							character.sprite[6] = '_'; character.sprite[8] = '_';
							DrawSprite(character.x + 3, character.y, 4, 1, "o -.");
							break;
						case 2:
							character.leg_m = 1;
							DrawSprite(character.x + 3, character.y, 5, 3, "   . o   )   \' ");
							break;
						case 3:
							character.sprite[6] = '_'; character.sprite[8] = '_';
							DrawSprite(character.x + 3, character.y + 1, 4, 2, "o     -\'");
							break;
						default:
							break;
					} break;
				case 2:
					switch (character.atk_m[1]) {
						case 1:
							character.sprite[5] = 'o'; character.sprite[6] = '_'; character.sprite[8] = '_';
							DrawSprite(character.x + 3, character.y + 1, 3, 1, sprite_rightWeapon[character.weapon]);
							break;
						case 2:
							character.sprite[5] = ' ';
							character.leg_m = 1;
							EditMap(character.x + 4, character.y + 1, 'o');
							DrawSprite(character.x + 5, character.y + 1, 3, 1, sprite_rightWeapon[character.weapon]);
							break;
						case 3:
							character.sprite[5] = 'o'; character.sprite[6] = '_'; character.sprite[8] = '_';
							DrawSprite(character.x + 3, character.y + 1, 3, 1, sprite_rightWeapon[character.weapon]);
							break;
						default:
							break;
					} break;
				case 3:
					character.sprite[5] = ' ';
					
					switch (character.atk_m[1]) {
						case 1:
							character.leg_m = 1;
							EditMap(character.x + 3, character.y + 2, 'o');
							DrawSprite(character.x + 4, character.y + 2, 3, 1, sprite_rightWeapon[character.weapon]);
							DrawSprite(character.x + 4, character.y, 3, 1, sprite_rightWeapon[character.weapon]);
							
							break;
						case 2:
							character.leg_m = 1;
							EditMap(character.x + 4, character.y + 1, 'o');
							DrawSprite(character.x + 5, character.y + 1, 3, 1, sprite_rightWeapon[character.weapon]);
							break;
						case 3:
							character.sprite[6] = '_'; character.sprite[8] = '_';
							EditMap(character.x + 3, character.y, 'o');
							DrawSprite(character.x + 4, character.y + 2, 3, 1, sprite_rightWeapon[character.weapon]);
							DrawSprite(character.x + 4, character.y, 3, 1, sprite_rightWeapon[character.weapon]);
							break;
						default:
							break;
					} break;
				default:
					break;
			}
		} else {
			character.sprite[5] = 'o';
			DrawSprite(character.x + 3, character.y + 1, 3, 1, sprite_rightWeapon[character.weapon]);
		}
		
		switch (character.leg_m) {
			case 1:
				character.sprite[4] = 'l'; character.sprite[6] = '.'; character.sprite[8] = '-';
				break;
			case 2:
				character.sprite[6] = '\''; character.sprite[8] = '_';
				break;
			case 3:
				character.sprite[6] = '.'; character.sprite[8] = '_';
				break;
			default:
				break;
		}
	} else {
		character.sprite[5] = ')';
		
		if (character.atk_m[0]) {
			switch (character.atk_m[2]) {
				case 1:
					character.sprite[3] = ' ';
					
					switch (character.atk_m[1]) {
						case 1:
							character.sprite[6] = '_'; character.sprite[8] = '_';
							DrawSprite(character.x - 4, character.y, 4, 1, ".- o");
							break;
						case 2:
							character.leg_m = 1;
							DrawSprite(character.x - 5, character.y, 5, 3, " .   (   o \'   ");
							break;
						case 3:
							character.sprite[6] = '_'; character.sprite[8] = '_';
							DrawSprite(character.x - 4, character.y + 1, 4, 2, "   o\'-  ");
							break;
						default:
							break;
					} break;
				case 2:
					switch (character.atk_m[1]) {
						case 1:
							character.sprite[3] = 'o'; character.sprite[6] = '_'; character.sprite[8] = '_';
							DrawSprite(character.x - 3, character.y + 1, 3, 1, sprite_leftWeapon[character.weapon]);
							break;
						case 2:
							character.sprite[3] = ' ';
							character.leg_m = 1;
							EditMap(character.x - 2, character.y + 1, 'o');
							DrawSprite(character.x - 5, character.y + 1, 3, 1, sprite_leftWeapon[character.weapon]);
							break;
						case 3:
							character.sprite[3] = 'o'; character.sprite[6] = '_'; character.sprite[8] = '_';
							DrawSprite(character.x - 3, character.y + 1, 3, 1, sprite_leftWeapon[character.weapon]);
							break;
						default:
							break;
					} break;
				case 3:
					character.sprite[3] = ' ';
					
					switch (character.atk_m[1]) {
						case 1:
							character.leg_m = 1;
							EditMap(character.x - 1, character.y + 2, 'o');
							DrawSprite(character.x - 4, character.y + 2, 3, 1, sprite_leftWeapon[character.weapon]);
							DrawSprite(character.x - 4, character.y, 3, 1, sprite_leftWeapon[character.weapon]);
							
							break;
						case 2:
							character.leg_m = 1;
							EditMap(character.x - 2, character.y + 1, 'o');
							DrawSprite(character.x - 5, character.y + 1, 3, 1, sprite_leftWeapon[character.weapon]);
							break;
						case 3:
							character.sprite[6] = '_'; character.sprite[8] = '_';
							EditMap(character.x - 1, character.y, 'o');
							DrawSprite(character.x - 4, character.y + 2, 3, 1, sprite_leftWeapon[character.weapon]);
							DrawSprite(character.x - 4, character.y, 3, 1, sprite_leftWeapon[character.weapon]);
							break;
						default:
							break;
					} break;
				default:
					break;
			}
		} else {
			character.sprite[3] = 'o'; 
			DrawSprite(character.x - 3, character.y + 1, 3, 1, sprite_leftWeapon[character.weapon]);
		}
		
		switch (character.leg_m) {
			case 1:
				character.sprite[4] = 'l'; character.sprite[6] = '-'; character.sprite[8] = '.';
				break;
			case 2:
				character.sprite[6] = '_'; character.sprite[8] = '\'';
				break;
			case 3:
				character.sprite[6] = '_'; character.sprite[8] = '.';
				break;
			default:
				break;
		}
	}
	DrawSprite(character.x, character.y, character.size_x, character.size_y, character.sprite);
}

void ControlMove(short *x, short *y, float *x_accel, float *y_accel, short size_x, short size_y, float *flyTime) {
	
	float x_value = *x_accel, y_value = *y_accel;
	
	if (*y + size_y < FLOOR_Y) {
		*flyTime += 0.05;
		*y_accel += *flyTime;
	} else {
		*flyTime = 0;
	}
	
	if (x_value != 0 || y_value != 0) {
		if (*x + x_value < 1)
			x_value = 1 - *x;
		if (*x + size_x + x_value > MAP_X_MAX)
			x_value = MAP_X_MAX - size_x - *x;
		if (*y + size_y + y_value > FLOOR_Y)
			y_value = FLOOR_Y - *y - size_y;
	}
	
	*x += floor(x_value + 0.5); *y += floor(y_value + 0.5);
	
	if (*x_accel > 0) *x_accel -= 0.1; if (*x_accel < 0) *x_accel += 0.1;
	if (*y_accel > 0) *y_accel -= 0.1; if (*y_accel < 0) *y_accel += 0.1;
}

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

void EditMap(int x, int 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;
}

void DrawSprite(int x, int y, int size_x, int size_y, 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 DrawNumber(int x, int y, int num) {
	int tmp = num;
	short 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 DrawBox(int x, int y, int size_x, int 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, '|');
	}
}

int NumLen(int num) {
	int tmp = num;
	short len = 0;
	
	while(tmp != 0) {
        tmp /= 10;
        len++;
    }
    
    return len;
}

int StrLen(char str[]) {
	short len = 0;
	
	for(int i = 0; str[i]; i++)
        len ++;
    
    return len;
}

 

으악 너무 길어;

다음부턴 그냥 CCP 첨부파일로 올려야 될듯 하네여

 

 일단 바뀐점은 앞에서 대충 설명했죠?

 캐릭터 공격 모션은 그야말로 노가다였으니 넘어가구요.

 거기에 Character 구조체의 멤버가 조금 늘고 박스를 드로우 해주는 함수가 추가되었는데 그것들도 별 중요한건 아니니 또 넘어가구요.

 

void ControlMove(short *x, short *y, float *x_accel, float *y_accel, short size_x, short size_y, float *flyTime) {
	
	float x_value = *x_accel, y_value = *y_accel;
	
	if (*y + size_y < FLOOR_Y) {
		*flyTime += 0.05;
		*y_accel += *flyTime;
	} else {
		*flyTime = 0;
	}
	
	if (x_value != 0 || y_value != 0) {
		if (*x + x_value < 1)
			x_value = 1 - *x;
		if (*x + size_x + x_value > MAP_X_MAX)
			x_value = MAP_X_MAX - size_x - *x;
		if (*y + size_y + y_value > FLOOR_Y)
			y_value = FLOOR_Y - *y - size_y;
	}
	
	*x += floor(x_value + 0.5); *y += floor(y_value + 0.5);
	
	if (*x_accel > 0) *x_accel -= 0.1; if (*x_accel < 0) *x_accel += 0.1;
	if (*y_accel > 0) *y_accel -= 0.1; if (*y_accel < 0) *y_accel += 0.1;
}

 

 그럼 남는건 이 함수입니다. 물리엔진을 구현해주는 이번 글의 핵심이죠.

 구조체(캐릭터, 아직 안 만들었지만 적이나 투사체)의 x, y좌표값, 가속도 값, 그리고 크기와 공중에 뜬 시간을 입력 받아 중력 처리와 가속도 계산을 해주는 함수입니다.

 ASCII를 그래픽으로 쓰기 떄문에 픽셀 기준 중력공식을 쓰면 이상해집니다. 노가다로 만든 야매 중력공식을 사용해 임의로 중력이 받는것 처럼 보이도록 해봤습니다.

 이 함수는 대부분의 구조체(게임 내의 오브젝트)들의 이동을 처리할때 사용될 예정입니다. 캐릭터의 넉백이나, 적 몬스터의 공격등을 구현할때 유용하겠죠.

 

 다음 글에서는... 뭘할까요

 

댓글