written by yechoi

[42서울] cub3d 스텝 바이 스텝 본문

Born 2 Code/C, C++

[42서울] cub3d 스텝 바이 스텝

yechoi 2020. 8. 5. 18:18
반응형

 

cub3d는 레이캐스팅을 이용해 원시적인 일종의 3D 게임을 구현하는 과제다. 여기서 레이캐스팅이란 단어에서 짐작해보자면, 광선(ray)을 던져(cast) 현재 위치에서 벽(또는 장애물)이 충돌하는 지점을 찾아 거리를 구하는 방식이다. 원시적인 3D게임인 울펜스타인은 이 방식으로 만들어졌다.

 

 

wolf3d 게임 장면

 

 

처음 과제를 마주했을 때 든 생각은 '내가 이걸 한다고?'였다. 앞선 과제인 printf 구현도 각종 플래그 때문에 까다로웠지만, 그건 그래도 '어떤 방식으로 해야겠다'는 짐작은 드는 정도였다. 그런데 cub3d는 뭐부터 해야 하는지 감이 전혀 안왔다. 아무 것도 없는 상탠데 어떤 코드를 쓰면 저런 그림이 나오는지요...

 

레이캐스팅을 이해하고 구현하는 것도 어렵지만, 이 밖에도 cub3d에선 해줘야 할 부가적인 것들이 많다. 그러니까 레이캐스팅이 전부가 아닌 다소 복잡한 프로젝트다. 그래서 약 두달간 cub3d를 붙잡고 고군분투해 본 입장에서, 어떤 방식을 밟아가면 좋을지 정리해봤다. 

 

minilibx와 친해지기

 

가장 추천하고 싶은 것은 minilibx와 친해지는 것이다. minilibx(mlx)는 42에서 그래픽을 구현하기 위해 만든 라이브러리다. 창을 띄운다거나, 점을 찍는다거나, 선을 그린다거나(점을 여러개 연속적으로 그리기), 면을 그린다거나(선을 연속적으로 그리기), 텍스쳐를 넣는(실은 픽셀마다 일일히 다른 색의 점을 찍는 과정) 이 모든 그래픽 활동을 가능하게 한다.

 

intra 동영상 강의에 mlx에 대한 설명이 있기는 하지만, 그닥 추천하지 않는다. 처음봤을 때도 그렇고 과제를 거의 끝내가는 지금도 '짜게' 알려줬다는 생각이 든다. 앞으로 해나갈 것에 비해 너무나도 간단한 내용만을 알려주고 있다.

 

대신, 42 동료 @taelee님이 만든 mlx_example을 보면 좋다. 앞으로 과제를 하면서 사용할 mlx의 거의 모든 기능을 익혀볼 수 있다. 구체적으로 익혀볼 수 있는 기능은 다음과 같다. 

 

  • 창 띄우기
  • 키 입력 받기
  • xpm 파일 이미지로 변환하기
  • 직접 픽셀을 찍어 이미지 만들어보기
  • 선 그리기, 네모 그리기 etc
 

taelee42/mlx_example

Contribute to taelee42/mlx_example development by creating an account on GitHub.

github.com

 

 

(옵션) 맵 그리고  그 안에서 플레이어 이동시키기

 

 

플레이어의 위치와 레이를 시각화한 맵 

 

 

이런 식으로 맵을 그려보고 플레이어의 위치를 표시해 볼 수 있다. 여기서는 빨간색 점이 플레이어다.

 

그리고 나중에 레이케스팅을 익히고 나면, 레이를 시각화해볼 수도 있다. 레이캐스팅에서 벽에 부딫힌 지점을 찾아 플레이어 위치에서부터 선으로 그리면 된다. (피쿠마의 튜토리얼이 이 같은 과정을 거친다.)

 

이 부분을 옵션이라고 표시한 건 과제와 직접적으로 연결되는 부분은 아니라서다. 다만 이 과정을 추천하는 이유는 아래와 같은 점에 도움을 줄 수 있다.

  • 키가 입력된 대로 (의도한 대로) 플레이어를 움직여볼 수 있음
  • 벽 충돌 함수를 제대로 구현했는지 확인할 수 있음(플레이어가 벽을 뚫고 들어가지 말아야함)
  • 자신의 레이캐스팅 엔진이 잘 작동하는지 확인할 수 있음
  • 이걸 축소해서 보여주면 미니맵 구현임

 

나의 경우 레이캐스팅 엔진을 구현한 다음에 레이를 시각화했는데, 이 과정에서 내 레이캐스팅 엔진의 오류를 찾을 수 있었다.  가령 이런 것들.

 

 

왼) 텍스쳐를 올리지 않은 벽 오) 이 텍스쳐가 보일 때 레이캐스팅 상태

 

왼쪽 그림처럼 코너 부분이 짤뚱하게 보이는 이유가, 오른쪽을 보면 코너에서 레이캐스팅이 잘 안되고 있어서라는 걸 알 수 있었고(double 이나 float이 아닌 int 변수를 썼기 때문), 

 

 

 

저 깊은 안쪽 벽이 울고 있는 이유가 벽 체크를 못하고 초기화 된 위치인 (0, 0)을 찍고 있어서란 걸 알 수 있었다. 

 

 

맵 파싱하기

R 1920 1080
NO ./path_to_the_north_texture
SO ./path_to_the_south_texture
WE ./path_to_the_west_texture
EA ./path_to_the_east_texture
S ./path_to_the_sprite_texture
F 220,100,0
C 225,30,0
        1111111111111111111111111
        1000000000110000000000001
        1011000001110000002000001
        1001000000000000000000001
111111111011000001110000000000001
100000000011000001110111111111111
11110111111111011100000010001
11110111111111011101010010001
11000000110101011100000010001
10002000000000001100000010001
10000000000000001101010010001
11000001110101011111011110N0111
11110111 1110101 101111010001
11111111 1111111 111111111111

 

이런 식으로 생긴 맵을 받아, 정보를 저장해줘야 한다. 맵 파싱은 처음부터 시작해도 할 수 있는 부분이긴 하지만, mlx를 다뤄보고 하는 걸 추천한다. cub3d에선 각종 정보를 저장해야 해서 구조체가 굉장히 많이 쓰인다. mlx를 다뤄보고 나면, 어떤 구조로 구조체를 구성해야 효율적일지 감이 온다.  

 

맵의 조건에 따라 유효성을 검사하는 함수도 따로 만들어줘야 한다. 여기서 확인해줘야 할 부분은 과제를 번역해 옮겨왔다. 그래도 서브젝트를 다시 확인해보시길...  

 

  • 맵에 0(빈공간), 1(벽), 2(스프라이트), NSEW(플레이어 위치와 시선 각도) 말고 다른 요소가 있는지
  • 맵이 벽으로 둘러싸여 있는지
  • map content 외 다른 요소는 하나 또는 그 이상의 개행으로 구분될 수 있음
  • map은 마지막에 와야하지만, 다른 요소의 순서는 상관이 없음
  • map을 제외하고 다른 요소들의 정보들은 하나 또는 그 이상의 스페이스로 구분될 수 있음
  • map은 파일에서 보이는 것처럼 파싱돼야 한다. 스페이스는 맵에서 유효한 부분이며 어떻게 다룰지는 당신에게 달려있음. map 규칙을 지키는 어떤 맵이라도 파싱할 수 있어야 한다.
  • map을 제외하고 각 요소는 type identifier가 먼저 제시되고(하나 또는 두개의 글자로 이뤄짐), 엄격한 순서를 따르는 정보가 뒤따른다. 
  • 파일에서 설정이 잘못된 부분이 있다면, 프로그램은 올바르게 exit해야 하고 명확한 에러 상황을 담은 에러메세지 ("Error\n"로 시작)을 뱉어야 한다.

 

 

레이캐스팅 익히기

이제는 본격적으로 레이캐스팅을 익힐 차례다. 레이캐스팅 방법에는 크게 1) 벡터 기반 2) 각도 기반이 있다. 

 

벡터 기반 레이캐스팅

벡터 기반의 레이캐스팅은 비교적 양질의 자료가 많이 정리돼 있다. 가장 많이 보는 lodev's tutorial에선 이 방법이 각도 기반 방법보다 간편하다고 한다. 또한 스프라이트 구현도 좀더 명료한 듯하다. 스프라이트는 벽과 다른 방식으로 그려야 하는데, 이 방식이 벡터 기반에서 더 직관적이라고 느껴졌다.  

 

다만 벡터를 알지 못하는 문과 출신(그건 나...)은 튜토리얼을 이해하기 매우 어렵다. 벡터 덧셈 뺄셈 곱셈 강의를 들으면서 이해해보려고 했으나 그럼에도 이해하지 못했다. 교육과정을 꽤나 원망함 ^^,,,

 

다들 많이 참고하는 lodev's tutorial은 안그래도 어려운데 영어라 잘 안읽힌다. 그러나 다행히도 42 동료 @mihykim님이 우리말로 번역해 정리해두었다. 

 

365kim/raycasting_tutorial

(한글ver) 레이캐스팅 튜토리얼 . Contribute to 365kim/raycasting_tutorial development by creating an account on GitHub.

github.com

 

Raycasting

#define screenWidth 640 #define screenHeight 480 #define texWidth 64 #define texHeight 64 #define mapWidth 24 #define mapHeight 24 int worldMap[mapWidth][mapHeight]= { {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,7,7,7,7,7,7,7,7}, {4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,

lodev.org

 

 

각도 기반 레이캐스팅

각도 기반 레이캐스팅의 장점은 벡터를 몰라도 이해할 수 있다는 것이다.

 

아래 튜토리얼은 삼각함수 사용부터 시작해서 단계적으로 차근차근 레이캐스팅을 설명하고 있다. 자바스크립트로 돼있으나, 강의를 들으면서 c언어로 충분히 변환이 가능한 수준이었다. 나중에 c언어로 된 강좌를 사기도 했는데, 그건 사실 별로 도움이 안됐다. 자바스크립트 코드를 c로 바꾸는 과정만 더해진 것이었다. 

 

단점은 자료가 부족하다. cub3d에서 레이캐스팅 말고 진행해야 할 것은 크게 벽에 텍스쳐 올리기, 스프라이트 그리기가 있는데. 이 둘에 대한 자료를 찾기 힘들다. 텍스쳐를 올리는 것은 스스로 고민해서 구현했고, 스프라이트 그리기는 도저히 혼자 힘으로 어려워 이미 각도를 기반으로 스프라이트 구현에 성공한 동료의 도움을 많이 받았다. 나 혼자서는 생각하기 어려운 아이디어라 감탄하면서 설명 듣고 구현 ! 

 

Raycasting Basics with JavaScript

Learn the mathematics behind the ray casting technique used in the Wolfenstein 3D source code and implement a 3D projected scene using JavaScript

courses.pikuma.com

 

어떤 방법이든 레이캐스팅 엔진을 구현했다면, 민무늬 벽으로 이뤄진 화면을 볼 수 있을 것이다. 

 

 

 

 

남은 여정들...

레이캐스팅 엔진을 구현했다면, 이 과제의 가장 큰 산을 넘은 것이다! 남은 언덕들은 다음과 같다.

  • 벽에 텍스쳐 올리기
  • 스프라이트 구현하기 ★만만치 않음
  • 메모리 누수 체크
  • 노미네트 잡기
  • 보너스 기능 더해 게임으로 만들기

앞서 말했지만, 각도 기반 레이캐스팅에서는 텍스쳐나 스프라이트 구현에 참고할만한 자료가 별로 없다. 나중에 정리해볼까도 함.  그래서 자료를 정리중이고, 텍스쳐 구현은 아래 글을 참고하면 되겠다. (스프라이트 구현은 작성 중)

 

[cub3d] 각도 기반 레이케스팅 - 텍스쳐 올리기

각도 기반 레이케스팅에서 텍스쳐 올리는 법을 알아본다. 이 튜토리얼은 레이케스팅 엔진을 구현해, 이미 위 그림처럼 민무늬의 벽을 그릴 수 있다는 것을 가정한다. 아직 민무늬 벽을 그려보지

yechoi.tistory.com

 

보너스! 게임화 해보기

 

 

 

과제에 투자하는 시간이 워낙 많았다보니, 기본만 끝내자니 아쉬움이 남았다. 지금까지 한 것에 비해 보너스 항목의 난도는 낮아보였다. 그래서 보너스 항목을 살피며, 게임으로 할 만한 요소를 몇 가지 더 넣어보았다. 그래서 나온 결과물.

 

컨셉은 '펭수' ㅎㅇㅎ  맵 상의 참치 캔을 다 먹으면 게임이 끝난다. 미니맵 상의 노란색이 참치가 위치한 지점. 보라색 지점은 펭수가 서있다. 게임을 다하면 game clear라는 이미지가 뜬다. bgm도 나온다. 

 

 

 

다만 과제 마지막엔 '시간을 낭비하지 않도록 조심하라'는 문구가 있으니, 참고하시길...!

반응형