programming

C언어 프로젝트 - 콘솔 3d 게임 만들기

leesu0605 2022. 11. 21. 00:25

학교에서 C언어 프로젝트로 수행평가를 한다길래 타워 디펜스 게임을 만들기로 했고, 뭔가 특별한 점을 주고 싶어 3d로 제작하게 되었다.
아마 이번 블로그글에서 3d 게임 제작 과정 등을 설명하고, 다음 블로그글부터 이 프로그램을 직접 IDA로 분석하면서 리버싱 공부도 해볼 참이다.
일단 시연 영상부터 보자.

 

시연 영상(https://www.youtube.com/watch?v=pNkv3bDInOk&ab_channel=GGJ)

그렇겐 안 보이지만 어쨌든 타워 디펜스 게임이다.

일단 이 프로그램을 다운받고 readme.txt를 읽고 한 번 플레이하고 오자.
거기에 이 게임의 룰, 조작법 등이 적혀 있다.

mazer.zip
17.18MB

그럼 이제 대략적인 코드 구현 방법을 설명하도록 하겠다.


플레이어


플레이어에 관한 정보를 저장하기 위해 구조체를 만들었다.
이 구조체에는 플레이어의 y좌표, x좌표, 방향(각도로 표시), 체력, 공격력이 들어있다.
나중에 렌더링 할 때 이 플레이어 관련 정보와 맵 정보를 이용한다.



맵은 사용자가 맵 번호를 입력했을 때 맵 파일을 열어 읽어온다.
이 맵 파일엔 말 그대로 맵 그 자체가 들어있으며, 숫자가 의미하는 건 그 부분의 색깔이다.

map1.txt


특이하게도, resources 폴더 안에 들어가 보면 mapn_bfs.txt 파일도 들어있을 것이다.
이는 이 맵의 빈 공간 안에서 플레이어의 기지부터 시작해 bfs를 한 결과를 저장해 놓은 파일이다.
이렇게 저장해 놓은 이유는, 적이 생성되고, 플레이어 기지까지 최적의 경로로 갈 수 있게 하고 싶은데 매번 bfs를 돌리기엔 시간 낭비가 너무 심할 것 같아 적이 맵의 특정 위치에 있을 때 어느 방향으로 움직여야 하는지 저장해 놓은 배열이라고 보면 된다.
이 bfs.txt 파일은 C++로 map1에서 bfs를 돌려 구한 결과이다.

map1_bfs.txt


시야 회전, 움직이기


여기서부터 좀 복잡한 개념이 나온다.
아까 말했듯이, 플레이어의 위치 정보는, x좌표, y좌표, 보는 각도의 세 가지로 표현이 되는데, 이 때 좌표평면에서 원점을 맵의 맨 위, 맨 왼쪽으로 정하고, (플레이어의 x좌표, 플레이어의 y좌표) 지점을 지나며, 기울기가 tan (플레이어가 보는 각도)로 일차함수를 구할 수 있다.

y={tan(d%90)*(x-x1)+y1}

위 식에서 d%90이라 나와있지만, 시선 각도가 1, 3사분면을 지나는 각도일 때는 90-d%90을 써야한다는 점에 유의하자.

이 일차함수가 바로 플레이어 시선의 방정식이며, 시야를 오른쪽으로 회전한다하면, 플레이어가 보는 각도에 양수를 더하고, 왼쪽으로 회전한다하면 플레이어가 보는 각도에 음수를 더해 구현할 수 있다.

움직이는 기능은 시선과 달리 플레이어의 x좌표와 y좌표를 직접 움직여야 한다.
그냥 x좌표와 y좌표를 움직이면 될 것 같지만, 자연스럽게 매번 똑같은 길이만큼의 움직임을 만들어내기 위해선 플레이어가 보는 각도를 이용해야 한다.
그 이유는 플레이어가 앞으로 움직일 때는 플레이어가 보는 시선쪽으로 움직인다는 말이고, 옆으로 움직일 때는 거기에 90도를 더한 방향으로 움직이고, 뒤쪽으로 움직일 때는 플레이어 시선 각도에 180도를 더한 방향으로 움직인다는 이야기이기 때문이다.
따라서 플레이어의 시선 각도만이 주어졌을 때 x좌표로 얼마나 가야할지, y좌표로 얼마나 가야할지 구해야 한다.

이는 다음 두 가지 정보를 이용해 구할 수 있다.(플레이어 시선 각도는 편하게 d로 한다.)
- y`/x`=tan(d)
- x`^2+y`^2=움직일 거리^2

두 번째 식의 y값이나 x값에 첫번째 식으로 값을 치환해 넣으면 다음과 같은 식이 나온다.
- x`^2+(tan(d)*x`)^2=움직일 거리^2
- (x`^2)*(1+tan(d)^2)=움직일 거리^2
- x`^2=(움직일 거리^2)/(1+tan(d%90)^2)
- x`=sqrt((움직일 거리^2)/(1+tan(d%90)^2)

이렇게 x`를 구하면, 첫번째 식에 대입해 y`값도 구할 수 있다.
- y`/sqrt((움직일 거리^2)/(1+tan(d%90)^2)=tan(d%90)
- y`=tan(d)*sqrt((움직일 거리^2)/(1+tan(d%90)^2)

이렇게 x`, y`를 구할 수 있고, 이를 현재 플레이어의 x좌표, y좌표에 각각 더해주면 된다.


렌더링


이 프로그램에선 렌더링할 때 (현재 플레이어의 시선-40) ~ (현재 플레이어의 시선+40) 사이 범위의 각도들로 일직선의 광선을 쏴 맵 상에서 처음 만나는 지점을 좌표로 구하고, 그 좌표와 다음 일차방정식 사이의 거리를 이용해 화면에서 y축으로 몇 칸 정도를 표시해야할지 구했다.

y=tan(현재 플레이어의 시선+90)*(x-현재 플레이어의 x좌표)+현재 플레이어의 y좌표

그냥 처음 만나는 점과 플레이어가 위치한 점의 거리를 구하면 되는 거 아니냐고 물어볼 수 있다.
그러나, 필자도 처음엔 그런 식으로 구현을 해보았다.
단지 어안렌즈 효과를 받고 6시간을 삽질했을 뿐.
그런 식으로 구현하면, 두 지점 사이의 거리가 길어질 수록, 실제로 계산되는 거리는 기하급수적으로 늘어나므로 멀리 있는 물체는 실제보다 더 극적으로 작아 보이고, 가까이 있는 물체는 더 극적으로 커보인다.
그러나 플레이어 시선에 수직인 직선과의 거리는 벽에서 플레이어와의 거리가 멀어져도 항상 일정하게 늘어나므로 이런 어안렌즈 효과를 피할 수 있다. 

위 방정식과 광선과 벽이 처음 만나는 지점 사이의 거리는 점과 직선 사이의 거리 공식으로 구할 수 있다.
- abs(tan(현재 플레이어의 시선 각도+90)*(x-현재 플레이어의 x좌표)+현재 플레이어의 y좌표)/sqrt(tan(현재 플레이어의 시선 각도+90)^2+1)

계산량이 많지만, 아직 벡터를 공부하지 않은 상태라 이런 수식으로 구현할 수 밖에 없었다.


이런 식으로 벡터를 사용하지 않고 오직 좌표평면과 탄젠트만으로 3d 게임을 구현해보았다.
(코드 길이는 820줄..)
아직 위, 아래로 움직일 수는 없어 완벽한 3d라고는 할 수 없으나, 초등학교 1학년 ~ 고등학교 1학년 까지 배웠던 내용들만을 응용해 이런 복잡한 프로그램을 만들어보니 뿌듯했고, 보람찼다.
내년 중으로는 벡터와 미적분 등 응용 가능성이 뛰어난 기하학을 배워 위, 아래, 텍스처까지 보이는 3d 프로그램을 구현해볼 예정이다.