Search
Duplicate
♟️

cub3D 내 맘대로 정리하기

간단소개
cub3D를 구현하며 제가 정리한 글을 소개드립니다.
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
cub3d
Scrap
태그
9 more properties

CUB3D Structure

CUB3D의 전체적인 구조를 먼저 그리고 시작하려 합니다. CUB3D의 대략적인 구조는 다음과 같습니다.
프로그램이 인자로 받은 .cub 파일을 이용해 map_path를 생성합니다.
map_path로 연 파일을 검사하고 사용할 수 있도록 메모리에 올립니다.
맵 정보를 활용해 그래픽을 그려냅니다.
PDF에 인자를 하나 받는 것으로 명시되어 있으니 인자가 하나가 아닐 때를 다루어야 합니다. 저는 이를 막아야 한다고 생각했습니다. 그래서 인자가 하나가 아닐 때 에러 메시지를 보여주고 프로그램이 종료되도록 했습니다.
#include "cub3d.h" #include "mlx/mlx.h" void show_error(char *error_message) { write(2, "\033[0;35mError\n", ft_strlen("\033[0;35mError\n")); write(2, error_message, ft_strlen(error_message)); write(2, "\033[0;0m", ft_strlen("\033[0;0m")); return ; } int main(int argc, char **argv) { char *map_path; t_map *map_info; if (argc != 2) { show_error("need one map file\n"); return (1); } map_path = parse_input(argv[1]); map_info = load_file(map_path); execute_window(map_info); return (0); }
C
복사

Parse input

프로그램이 인자로 받은 .cub 파일 이름을 사용해 map_path를 만들어야 합니다. 그리고 그 map_path로 파일을 open 할 수 있는지를 확인할 차례입니다. 다음과 같은 단계를 거쳐야합니다.
file_name을 확인합니다. 이름은 ‘.cub’로 끝나야 합니다.
map_path를 만듭니다. 이 때 ft_strjoin(”map directory path”, file_name)를 활용합니다.
만들어진 map_path로 파일을 open() 할 수 있는지를 확인합니다.
위의 단계가 끝나면 return (map_path) 합니다.
#include "../../cub3d.h" char *make_map_path(char *map_name) { char *map_path; map_path = "map/"; map_path = ft_strjoin(map_path, map_name); return (map_path); } void check_map_name(char *map_name) { int len; len = ft_strlen(map_name); if (len < 4) { show_error("check map name\n"); exit(1); } if (map_name[len - 1] != 'b' || \ map_name[len - 2] != 'u' || \ map_name[len - 3] != 'c' || \ map_name[len - 4] != '.') { show_error("check map extension\n"); exit(1); } return ; } void check_map_file(char *map_path) { int fd; fd = open_file(map_path); close (fd); return ; } char *parse_input(char *input) { char *map_path; printf("Making map path...\n"); check_map_name(input); // 이름이 .cub로 끝났는지를 확인합니다. map_path = make_map_path(input); // map_path를 생성합니다. check_map_file(map_path); // map_path를 통해 파일을 open 할 수 있는지 확인합니다. printf("map_path: %s\n", map_path); return (map_path); }
C
복사

Load file

자 이제 ‘.cub’ 파일로부터 정보를 얻어 맵에 관련한 정보를 메모리에 올릴 시간입니다. 지금부터는 이 맵 파일이 유효한지 아닌지를 먼저 확인해야 합니다. 만약 유효하지 않다면 이를 ERROR로 처리해야 합니다. 반대로 유효하다면 이를 이용해 3D 그래픽을 그려야 합니다.
파일이 유효한지 아닌지를 확인하려면 근거가 있어야 합니다. 우리는 CUB3D의 PDF를 기준으로 과제를 합니다. 그러므로 PDF에서 제시하는 바가 무엇인지 알아야 합니다. 그 내용은 다음과 같습니다.
.cub file은 scene description이라고 부릅니다. 줄여서 ‘sd’라고 하겠습니다.
sd에서 개행으로 구분되어 있는 한 줄을 element라고 부릅니다.
element안에서 스페이스로 구분되는 하나의 문자열을 informaiton이라고 합니다.
map componet
0 : 빈 공간을 의미합니다. 플레이어가 갈 수 있는 공간으로 해석합니다.
1 : 벽을 의미합니다.
N, S, E, W : 플레이어가 생성되는 자리이자 어느 방향을 보고 있는지를 의미합니다.
space: 맵의 유효한 일부분입니다. 벽은 아니지만 플레이어가 갈 수 없는 공간으로 해석합니다.
map은 반드시 벽으로 가두어져 있어야 합니다.
맵은 보이는대로 파싱되어야 합니다.
map element 를 제외하고, 각 element는 하나 이상의 empty line으로 구분될 수 있습니다.
map element 의 순서는 다른 모든 element들 뒤에 있어야 합니다.
map element 를 제외하고, 각 element의 순서는 상관없습니다.
map element 를 제외하고, information의 각 타입은 하나 이상의 스페이스로 구분될 수 있습니다.
map element 를 제외하고, 각 elemen의 첫 번째 informaion은 identifier type 입니다
identifer type은 하나 혹은 두 개의 문자로 이루어져있습니다.
각 element는 다음 두 개 중 하나의 형식을 따릅니다.
NO ./path_to_the_north_texture
F 220,100,0
위의 정보를 토대로 이제 SD가 유효한지 아닌지를 알 수 있습니다. PDF에 따르면 element는 map element 혹은 not element로 해석할 수 있습니다. 이것을 더 알아보기 쉽게 다음과 같이 명명하겠습니다.
map element → map element
그리고 이는 다음과 같은 문자를 첫 번째로 가집니다.
space
‘0’
‘1’
‘N’
‘S’
‘E’
‘W’
not map element → texture element
not map element 라고 해도 올바른 texutre element가 아닐 수 있습니다. 이는 다음과 같은 상황입니다.
first information 이 올바른 identifer 가 아니다.
first information 이 ‘\n’ 이다.
위의 과정을 탑다운 방식의 코드로 한번 구현해보겠습니다. 우리가 거쳐야할 단계는 다음과 같습니다.
open(map_path);
맵 정보를 담을 구조체 map_info 를 초기화 한다.
map_info 에 texture path를 담는다.
map_info 에 map element를 담는다.
이는 다음과 같은 코드 구조로 흘러갑니다.
t_map *load_file(char *map_path) { t_map *map_info; int fd; fd = open_file(map_path); map_info = init_map_info(); get_texture(map_info, fd); get_map(map_info, fd); return (map_info); }
C
복사

get_texture

자 이제 SD의 element 중 texture element에서 texture path 를 얻을 시간입니다. texture element는 두 개의 information으로 이루어져있으며 첫 번째 info에는 type identifier가 들어 있습니다. 그리고 두 번째 info에는 해당하는 texture가 있는 path가 있습니다.
texture path를 얻기 위해 지켜야하는 규칙을 다시 한번 나열하겠습니다. 이는 다음과 같습니다.
element 의 첫 번째 문자열이 texture identifier 이면 texture element로 인식한다.
texture identifier 는 “NO”, “SO”, “EA”, “WE”, “F”, “C” 중 하나이다.
texture identifier 는 중복될 수 없다.
texture path 는 중복될 수 없다.
Texture element 의 두 번째 문자열은 path 이다.
Texture element 의 세 번째 문자열은 NULL 이다.
중복되지 않은 6개의 texture element 를 얻기 전에 map element가 오면 안된다.
위 모든 항목을 만족하는 element이면 identifier에 따라 path를 등록할 수 있습니다. 만약 이 중 하나라도 규칙에 위반되면 error를 뱉고 종료하도록 합시다. element들을 검사하고 texture를 얻으려면 ‘.cub’ file dls scenedescriptor를 열어야 합니다. 그리고 sd를 읽어내리다가 모든 texture path를 얻었다면 fd를 close를 할 필요가 없습니다! 왜냐하면 또 map element를 읽어야 하니까요! 이 점을 생각하고 코드를 작성해봅시다.
먼저 우리는 map_info 라는 맵에 관련된 정보를 담는 구조체만들어야 합니다. map_info에는 map과 관련된 모든 정보들을 가지고 있도록 할 것입니다. 다음과 같이 이를 구성할 수 있습니다.
typedef struct s_player { double pos_x; double pos_y; double dir_x; double dir_y; double plane_x; double plane_y; } t_player; typedef struct s_texture { int count_texture; char *no_path; char *so_path; char *we_path; char *ea_path; char *c_color; char *f_color; } t_texture; typedef struct s_map { char **map; int height; int width; t_texture *texture; t_player *player; } t_map;
C
복사
그리고 t_map 구조체에 map texture를 담아야 하고, t_map 변수를 초기화 해야하므로 이를 초기화하는 함수를 만듭시다. 이는 다음과 같습니다.
t_map *init_map_info(void) { t_map *map_info; map_info = (t_map *)malloc(sizeof(t_map)); if (map_info == NULL) ctrl_error("failed to malloc\n"); else { map_info->height = 0; map_info->width = 0; map_info->map = NULL; map_info->player = NULL; map_info->texture = NULL; } return (map_info); }
C
복사
자 이제 드디어 get_texture 함수를 만들 차례입니다. 우리는 sd를 한번만 열어서 texture와 map을 연속으로 얻을 생각입니다. 그렇다면 fd는 get_texure 함수를 부르는 함수에서 open하는 것이 좋겠습니다. 그리고 이 fd를 get_texture에 넘겨주면 됩니다. 그리고 주의해야 할점이 있습니다. 한번 open한 파일은 더 이상 사용하지 않으면 close 해주는 것이 좋습니다. 이는 다음과 같이 구성될 수 있습니다.
t_map *load_file(char *map_path) { t_map *map_info; int fd; fd = open_file(map_path); map_info = init_map_info(); get_texture(map_info, fd); get_map(map_info, fd); close(fd); return (map_info); }
C
복사
get_texture 함수를 만들어 봅시다. texture를 얻기 위해서 해야할 단계들이 있습니다. get_next_line을 이용해 한 줄 씩 읽어들입니다. 그리고 이 한 줄이 어떤 element인지 구분해야 합니다. 이 단계는 다음과 같은 단계를 통해 구분합니다.
is line NULL?
is line “\n” ?
is line map element?
is line texture element?
만약 위의 경우에 아무것도 속하지 않으면 invalid element로 해석해 error 처리합니다.
만약 유니크한 texture path 가 6개가 채워지기 전에 map element가 나온다면 이것은 error 입니다. 혹은 어떤 경우라도 invalid element가 나온다면 이것 또한 error입니다. 이를 처리하며 논리적인 코드를 구성해봅시다. 이는 다음과 같습니다.
void get_texutre(t_map *map_info, int fd) { char *line; while (map_info->texture->count_texture < 6) { line = get_next_line(fd); if (line == NULL) break ; else if (compare_str(line, "\n") == TRUE) ; else if (is_map_element(line) == TRUE) ctrl_error("check element order\n"); else if (is_texture_element(line) == TRUE) set_texture_path(map_info, line); else ctrl_error("invalid element\n"); free(line); } if (map_info != 6) ctrl_error("check texture element\n"); }
C
복사
위 코드를 뜯어봅시다. 먼저 while 문의 반복 조건입니다. 이는 map_info->texture->count_texture 가 6이 될 때까지 반복하고 종료합니다. 이는 map element의 순서와 관계있습니다. 만약 map_element가 texture element가 모두 채워지기 전에 온다면 이를 에러로 처리하기 위한 코드입니다.
그 다음 line을 검사해 NULL인지를 확인하는 코드를 봅시다. 이는 texture가 6개가 채워지기 전에 sd가 끝난다면 충분한 texture를 얻지 못했음을 말하는 것이므로 이를 에러처리 한 것입니다.
그 다음은 “\n”을 뛰어넘는 코드입니다. 개행은 map, texture, invalid에 모두 속하지 않는 것으로 해석합니다. 즉 empty line으로 해석하는 것입니다. empty line == ‘\n’로 해석하고 그냥 넘깁니다.
그리고 map element에 속하는지를 판별합니다. map_element는 다음과 같은 특성을 가집니다.
line의 첫 문자가 다음과 같은 문자 중 하나이다.
space
‘1’
‘0’
정상적인 map이라면 맵 엘레먼트의 첫 문자로 ‘N’ ‘W’ ‘S’ ‘E’ 를 가지지 않는다.
int is_map_element(char *line) { if (line[0] == ' ') return (TRUE); if (line[0] == '1') return (TRUE); if (line[0] == 'N') return (TRUE); if (line[0] == 'W') return (TRUE); if (line[0] == 'E') return (TRUE); if (line[0] == 'S') return (TRUE); return (FALSE); }
C
복사
위 모든 것들에 속하지 않는다면 texture_element인지를 알아볼 시간입니다. 만약 texture_element가 아니라면 map, texture, empty line 모두에 속하지 않으므로 invalid element에 속한다고 판단합니다. 이는 error 처리를 하면 됩니다. texture element는 다음과 같은 속성을 가지고 있습니다. 이를 통해 texture element 에 속하는지를 판단할 수 있습니다.
element 의 첫 번째 문자열이 texture identifier 이면 texture element로 인식한다.
texture identifier 는 “NO”, “SO”, “EA”, “WE”, “F”, “C” 중 하나이다.
texture identifier 는 중복될 수 없다.
texture path 는 중복될 수 없다.
자 이제 texture element 인지를 판단하는 is_texture_element 함수를 만들어 봅시다!!!
int is_texture_element(char *line) { char **splited_line; int result; splited_line = ft_split(line, " "); result = TRUE; if (is_texture_identifier(splited_line[0]) != FALSE) result = FALSE; else if (splited_line[1] == NULL) result = FALSE; else if (splited_line[2] != NULL) result = FALSE; free_two_dimension_array(splited_line); return (result); }
C
복사
위 코드를 하나씩 풀어보겠습니다. map_element 에 속하는지를 확인하는 과정은 다음과 같습니다.
element 의 첫 번째 문자열이 texture identifier 이면 texture element로 인식한다.
texture identifier 는 “NO”, “SO”, “EA”, “WE”, “F”, “C” 중 하나이다.
information은 2개만 존재할 수 있다.
information은 space(s)로 구분된다.
먼저 texutre identier에 속하는지를 확인합니다. 올바른 TI는 NO, SO, EA, WE, F, C 중 하나입니다. 그리고 path에 해당하는 두 번째 informaion이 존재하는지 확인합니다. 이게 없으면 texture element 의 틀에서 벗어난 것으로 판단합니다. 마지막으로 두 개를 초과하는 information이 존재하는지를 확인하는 과정을 거칩니다. 모든 판별 과정을 통과하면 해당 element 는 texture element에 속하는 것으로 판단합니다.
다음과 같은 valid texture element 인지를 확인하는 과정은 set_texture 함수에서 다루겠습니다.
texture identifier 는 중복될 수 없다.
texture path 는 중복될 수 없다.
is_texture_element 에서 texture element에 해당한다고 판단이 들면 이 element에서 path information을 뽑아 우리가 쓸 수 있도록 t_texture 구조체에 담을 차례입니다.
set_texture_path 에서는 우리가 is_texture_element에서 확인하지 않는 두 가지를 확인해야 합니다. 여 두 가지에 더해 해당 path를 열 수 있다면 path를 등록할 수 있습니다. 확인해야 하는 항목은 총 3 개이며 다음과 같습니다.
texture identifer 가 중복되는가?
texture path 가 중복되는가?
path 를 open 할 수 있는가?
int set_texture_path(t_map *map_info, char *line) { char **splited_line; int identifier; splited_line = ft_split(line, ' '); identifier = is_texture_identifier(splited_line[0]); if (identifier == NO && map_info->texture->no_path == NULL) map_info->texture->no_path = ft_strdup(splited_line[1]); else if (identifier == SO && map_info->texture->so_path == NULL) map_info->texture->so_path = ft_strdup(splited_line[1]); else if (identifier == WE && map_info->texture->we_path == NULL) map_info->texture->we_path = ft_strdup(splited_line[1]); else if (identifier == EA && map_info->texture->ea_path == NULL) map_info->texture->ea_path = ft_strdup(splited_line[1]); else if (identifier == C && map_info->texture->c_color == NULL) map_info->texture->c_color = ft_strdup(splited_line[1]); else if (identifier == F && map_info->texture->f_color == NULL) map_info->texture->f_color = ft_strdup(splited_line[1]); else ctrl_error("identifer is duplicated\n"); is_valid_path(identifier, splited_line[1], map_info->texture); free_two_dimension_array(splited_line); return (1); }
C
복사
void is_valid_path(int identifier, char *path, t_texture *texture) { char *trimmed_path; if (identifier == F || identifier == C) return ; else if (identifier == NO) compare_no_path(texture, path); else if (identifier == SO) compare_so_path(texture, path); else if (identifier == WE) compare_we_path(texture, path); else if (identifier == EA) compare_ea_path(texture, path); trimmed_path = trim_path(path); close(open_file(trimmed_path)); return ; }
C
복사
위 코드에서는 texture identifer가 이미 등록되어 있는지를 확인합니다. 만약 등록이 안되어 있다면 identifer가 중복되지 않은 것으로 판단합니다. 그리고 path information을 등록합니다. 그 후 is_valid_path 함수를 통해 path가 다른 path와 중복되는지 검사하며 이상이 없다면 마지막으로 path를 통해 open 할 수 있는지를 확인합니다.
t_map *load_file(char *map_path) { t_map *map_info; int fd; fd = open_file(map_path); map_info = init_map_info(); get_texture(map_info, fd); get_map(map_info, fd); close(fd); return (map_info); }
C
복사

get_map

texture path는 등록을 끝냈습니다. 자 이제, load_file 함수로 돌아갈 차례입니다. load_file의 마지막 순서는 scene descriptor 로부터 map information 을 받을 차례입니다. 이 기능을 함수 get_map 함수를 구성해봅시다.
get_map 함수의 전체적인 흐름에 대해 설명하겠습니다. 이는 다음과 같습니다.
2차원 배열로 할당할 맵의 높이와 넓이를 구한다.
이와 함께 invalid element가 있는지 검사한다.
map element 사이에 empty line이 있는지 검사한다.
map element를 get_next_line으로 읽은 뒤 한 줄 씩 2차원 배열에 할당한다.
이 때 옮길 문자 하나 하나를 map component 에 속하는지 확인한다.
map component: space, ‘0’, ‘1’, ‘N’, ‘S’, ‘W’, ‘E’
플레이어의 위치가 단 하나만 있는지를 확인합니다.
The map must be closed/surrounded by walls, if not the program must return an error: 0이라는 공간이 0 또는 1로 둘러쌓여 있어야 함을 의미합니다. (플레이어 스타팅 포인트는 0과 같다고 판단합니다.)w
첫번째로 만들 함수는 set_map_width_height 입니다. 이 함수는 2차원 배열로 할당할 맵의 높이와 넓이를 구함과 더불어 invalid element가 있는지, map element 사이에 empty line이 있는지를 검사하는 것입니다. SD를 끝까지 읽으며 이 작업을 합니다. 이 함수는 다음과 같이 구성됩니다.
void set_map_width_height(t_map *map_info, int fd) { char *line; int empty_line_flag; line = skip_empty_line(fd); while (TRUE) { if (line == NULL) break ; else if (compare_str(line, "\n") == TRUE) empty_line_flag = TRUE; else if (is_map_element(line) == TRUE) { if (empty_line_flag == TRUE) ctrl_error("empty line between map element\n"); map_info->height = map_info->height + 1; set_map_width(map_info, line); } else ctrl_error("invalid map element\n"); free(line); line = get_next_line(fd); } close(fd); }
C
복사
위 함수는 먼저 texture element 에서 map element로 skip하는 과정을 거칩니다. 그 후 SD파일이 끝날 때 까지 한줄씩 읽어드리며 widht, height를 측정합니다. 이 때 empty_line_flag를 통해 map element 사이에 empty line이 있는지를 검사합니다. 마지막으로 close(fd) 를 합니다. 이는 한번 sd를 끝까지 읽었으로 다시 SD 를 열기 위한 사전 작업입니다. 만약 오프셋을 옮길 수 있는 함수가 허용된다면 그 함수 대신 사용하면 됩니다.
이제 두 번째 기능을 구현할 차례입니다. map element를 get_next_line 으로 읽은 뒤 한 줄 씩 2차원 배열에 할당해야 합니다. 이 때 옮길 문자 하나하나를 검사하며 map componenet에 속하는지 확인합니다. 가장 먼저 해야할 일은 SD를 다시 열어 map element까지 이동하는 것입니다.
char **make_map_array(t_map *map_info, char *map_path) { int fd; char *line; char **map; fd = open_file(map_path); line = move_to_map_element(fd); map = init_map(map_info); pre_set_map(map, map_info); map_info->count_height = map_info->height; while (TRUE) { if (line == NULL) break ; else if (compare_str(line, "\n") == TRUE) ; else if (is_map_element(line) == TRUE) set_map_component(map, line, map_info); else ctrl_error("invalid map element\n"); free (line); line = get_next_line(fd); } close(fd); return (map); }
C
복사
위 코드에 대해 설명하겠습니다. 전체적인 논리 구조는 ‘파일을 열고 한 줄씩 읽어들이며 2차원 배열에 집어 넣는다’ 입니다. 이 일을 하기 위해 SD 를 엽니다. init_map과 pre_set_map 을 통해 2차원 배열의 메모리를 동적 할당한 후 스페이스로 내용을 채웁니다. 그 후 map_elemet가 있는 줄 까지 이동합니다.
이 때 empty line은 다 넘겨줍니다. map element 사이의 공백은 set_map_width_height 함수에서 검출해냈으니 이번에는 하지 않습니다. 만약 map element가 맞다면 element의 한 문자씩 2차원 배열에 이식하는 과정을 거칩니다. 이 때 올바른 map component 인지 작업하는 과정도 함께합니다. 이러한 코드는 다음과 같습니다.
void set_map_component(char **map, char *line, t_map *map_info) { int width; width = 0; while (line[width] != '\0' && line[width] != '\n') { if (is_map_component(line[width]) == FALSE) ctrl_error("check map element's component\n"); map[map_info->count_height - 1][width] = line[width]; width++; } map_info->count_height--; return ; }
C
복사
자 이제 map element 를 2차원 배열로 옮기는 작업을 끝냈습니다. 세 번째로 확인할 것은 과연 플레이어의 위치가 단 하나만 존재하냐 입니다. 이를 위한 함수 check_starting_position 를 한번 구현해봅시다.
void check_starting_position(char **map, t_map *map_info) { int height; int width; int flag_postion; height = 0; flag_postion = FALSE; while (height < map_info->height) { width = 0; while (width < map_info->width) { if (map[height][width] == 'N' || map[height][width] == 'S' || \ map[height][width] == 'E' || map[height][width] == 'W') { if (flag_postion == FALSE) flag_postion = TRUE; else ctrl_error("starting point is duplicated\n"); } ++width; } ++height; } }
C
복사
위 함수는 간단합니다. flag 를 FALSE 상태로 둡니다. 플레이어 포지션을 만나면 TRUE 상태로 바꾼 후 맵을 쭉 읽어나갑니다. 만약 한번 더 포지션을 만나면 error 처리를 합니다.
자 이제 드디어 맵이 벽으로 둘러 쌓여 있는지를 확인해야 합니다. 이 논리는 다음과 같습니다.
0 또는 플레이어 포지션의 ‘동, 서, 남, 북’ 이 ‘0 or 1 or 플레이어 포지션’ 이어야한다.
즉, 0 또는 PP 의 동서남북이 0 도 아니고 1도 아니고 PP도 아니면 뚫려있다.
즉, 주변이 스페이스 혹은 맵의 끝이면 뚫려있는 것이다.
void check_map_is_closed(char **map, t_map *map_info) { int column; int row; column = 0; while (column < map_info->height) { row = 0; while (row < map_info->width) { if (map[column][row] == '0' || map[column][row] == 'N' || \ map[column][row] == 'S' || map[column][row] == 'E' || \ map[column][row] == 'W') check_is_surrouned(map, column, row, map_info); ++row; } ++column; } }
C
복사
map 의 모든 요소가 벽으로 둘러쌓여 있는지를 하나하나 검사합니다. 벽으로 둘러쌓여있다의 정의는 빈 공간 (0 또는 스타팅 포인트) 1 또는 빈 공간으로 둘러쌓여있음을 의미합니다. 위 코드는 이를 그대로 반영하고 있습니다.
check_is_surrounded 는 다음과 같은 코드로 이루어져있습니다.
void check_north_is_empty(t_map *map_info, char **map, int h, int w) { if (h - 1 < 0) ctrl_error("map must be closed by wall\n"); else if (h - 1 >= 0 && (is_empty_space(map[h - 1][w]) == FALSE && map[h - 1][w] != '1')) ctrl_error("map must be closed by wall\n"); return ; } void check_south_is_empty(t_map *map_info, char **map, int h, int w) { if (h + 1 >= map_info->height) ctrl_error("map must be closed by wall\n"); else if (is_empty_space(map[h + 1][w]) == FALSE && map[h + 1][w] != '1') ctrl_error("map must be closed by wall\n"); return ; } void check_east_is_empty(t_map *map_info, char **map, int h, int w) { if (w + 1 >= map_info->width) ctrl_error("map must be closed by wall\n"); else if (is_empty_space(map[h][w + 1]) == FALSE && map[h][w + 1] != '1') ctrl_error("map must be closed by wall\n"); return ; } void check_west_is_empty(t_map *map_info, char **map, int h, int w) { if (w - 1 < 0) ctrl_error("map must be closed by wall\n"); else if (is_empty_space(map[h][w - 1]) == FALSE && map[h][w - 1] != '1') ctrl_error("map must be closed by wall\n"); return ; } void check_is_surrouned(char **map, int h, int w, t_map *map_info) { check_north_is_empty(map_info, map, h, w); check_south_is_empty(map_info, map, h, w); check_east_is_empty(map_info, map, h, w); check_west_is_empty(map_info, map, h, w); }
C
복사

exectue_cub3d

void test_leak(void) { system("leaks cub3D"); } int main(int argc, char **argv) { char *map_path; t_map *map_info; // atexit(test_leak); if (argc != 2) { show_error("need one map file\n"); return (1); } map_path = parse_input(argv[1]); map_info = load_file(map_path); execute_cub3d(map_info); free(map_path); free_map_info(map_info); return (0); }
C
복사
자 이제 화면 띄우기를 시작할 차례입니다. 우리는 아직 레이케스팅의 원리를 잘 알지 못하므로 tutorial을 따라갈 예정입니다. 한번 해봅시다.
가장 먼저 시도할 튜토리얼은 untextured raycasting 입니다. 이 전 단계에서 SD를 파싱해두었으니 그것을 그대로 이용할 예정입니다.
가장 먼저 할 일은 player 벡터 위치, 보는 방향, camera plane 벡터를 초기화 하는 것입니다. 이 정보는 map_info를 기반으로 만들 수 있습니다.
ㄱ t_player *init_player(t_map *map_info) { t_player *player; player = (t_player *)malloc(sizeof(t_player)); if (player == NULL) ctrl_error("faild to malloc\n"); else { init_player_position(map_info, player); init_player_direction(player); init_camera_plane(player); } return (player); } void execute_cub3d(t_map *map_info) { t_player *player; player = init_player(map_info); return ; }
C
복사

execute_cub3D

앞에서 texture, map 파싱이 끝났습니다. 이제는 파싱한 데이터를 토대로 rendering을 시작해야합니다. 우리는 2차원 평면을 토대로 3차원을 구현해야 하며, 키보드로 이동하고 바라보는 방향을 바꿀 수 있어야 합니다. 이 작업을 위해 mlx 라이브러리를 사용해봅시다. 다음은 exexute_cub3D가 추가된 메인 함수입니다.
int main(int argc, char **argv) { char *map_path; t_map *map_info; if (argc != 2) { show_error("need one map file\n"); return (1); } map_path = parse_input(argv[1]); map_info = load_file(map_path); execute_cub3d(map_info); free(map_path); free_map_info(map_info); return (0); }
JavaScript
복사
위 코드를 봅시다. 우리는 parsing이 끝난 후, 렌더링 작업과 key_hook 을 거는 작업을 위한 함수 execute_cub3d 함수를 추가했습니다. 이와 더불어 map_path, map_info 라는 동적 할당된 메로리를 적절히 해제하는 함수 또한 추가했습니다. 이제 드디어 렌더링을 위한 밑 작업이 끝났습니다.
void execute_cub3d(t_map *map_info) { t_player *player; t_window *window; t_render *render; player = init_player(map_info); window = init_window(); render = init_render(player, window, map_info); mlx_hook(window->win, 2, 1L << 0, key_hook, render); mlx_loop_hook(window->mlx, render_graphic, render); mlx_loop(window->mlx); free_render_memory(render); return ; }
JavaScript
복사
execute_cub3d의 역할은 한 가지입니다. player 의 위치와 바라보는 방향에 따라 화면을 rendering하며, player 의 위치와 바라보는 방향을 key_hook 을 통해 업데이트 해주는 것입니다. 이를 이해하려면 mlx_loop(), mlx_loop_hook(), mlx_hook()에 대한 지식이 있어야 합니다.
mlx_loop: 화면을 뛰우고 loop_hook을 지속적으로 실행합니다.
mlx_loop_hook: loop가 지속적으로 실행할 함수를 등록합니다.
mlx_hook: event 를 이용해 사용자가 원하는 작업을 하도록 합니다.
mlx_hook(window->win, 2, 1L << 0, key_hook, render) 을 통해 key_hook 을 등록합니다. key_hook 의 역할은 ‘W, A, S, D, ←, →’ 을 통해 플레이어를 움직이며 보는 방향을 바꾸는 역할을 합니다. (이 때 플레이어의 위치와 보는 방향 변수의 값을 변경합니다.). 그리고 ESC키를 눌렀을 때 window를 종료하도록 해야 합니다. key_hook 함수는 다음과 같습니다.
int key_hook(int keycode, t_render *temp) { t_map *map_info; t_player *player; map_info = temp->map_info; player = temp->player; if (keycode == 53) end_cub3d(); else if (keycode == KEY_W || keycode == KEY_A \ || keycode == KEY_S || keycode == KEY_D) move_player(player, map_info, keycode); else if (keycode == 124 || keycode == 123) rotate_player(player, keycode); return (0); }
JavaScript
복사
key_code 에 맞춰 move_plyer, rotate_player 를 호출합니다. 이 때 player 구조체 변수에 등록된 위치 값과 방향 값이 변경됩니다.
void move_forward(t_player *player, t_map *map_info) { if (is_movable_place(map_info, player, (int)(player->pos_y + \ player->dir_y * MOVE_TEMP), \ (int)(player->pos_x + player->dir_x * MOVE_TEMP)) == TRUE) { player->pos_y += player->dir_y * MOVE_SPEED; player->pos_x += player->dir_x * MOVE_SPEED; } return ; } void move_left(t_player *player, t_map *map_info) { if (is_movable_place(map_info, player, (int)(player->pos_y + \ player->dir_x * MOVE_TEMP), \ (int)(player->pos_x - player->dir_y * MOVE_TEMP)) == TRUE) { player->pos_y += player->dir_x * MOVE_SPEED; player->pos_x -= player->dir_y * MOVE_SPEED; } } void move_back(t_player *player, t_map *map_info) { if (is_movable_place(map_info, player, (int)(player->pos_y - \ player->dir_y * MOVE_TEMP), \ (int)(player->pos_x - player->dir_x * MOVE_TEMP)) == TRUE) { player->pos_y -= player->dir_y * MOVE_SPEED; player->pos_x -= player->dir_x * MOVE_SPEED; } } void move_right(t_player *player, t_map *map_info) { if (is_movable_place(map_info, player, (int)(player->pos_y - \ player->dir_x * MOVE_TEMP), \ (int)(player->pos_x + player->dir_y * MOVE_TEMP)) == TRUE) { player->pos_y -= player->dir_x * MOVE_SPEED; player->pos_x += player->dir_y * MOVE_SPEED; } } void move_player(t_player *player, t_map *map_info, int keycode) { if (keycode == KEY_W) move_forward(player, map_info); else if (keycode == KEY_A) move_left(player, map_info); else if (keycode == KEY_S) move_back(player, map_info); else if (keycode == KEY_D) move_right(player, map_info); return ; } void rotate_player(t_player *p, int keycode) { double old_dir_x; double old_plane_x; if (keycode == 124) { old_dir_x = p->dir_x; p->dir_x = p->dir_x * cos(-ROTSPEED) - p->dir_y * sin(-ROTSPEED); p->dir_y = old_dir_x * sin(-ROTSPEED) + p->dir_y * cos(-ROTSPEED); old_plane_x = p->plane_x; p->plane_x = p->plane_x * cos(-ROTSPEED) - p->plane_y * sin(-ROTSPEED); p->plane_y = old_plane_x * sin(-ROTSPEED) + p->plane_y * cos(-ROTSPEED); } else if (keycode == 123) { old_dir_x = p->dir_x; p->dir_x = p->dir_x * cos(ROTSPEED) - p->dir_y * sin(ROTSPEED); p->dir_y = old_dir_x * sin(ROTSPEED) + p->dir_y * cos(ROTSPEED); old_plane_x = p->plane_x; p->plane_x = p->plane_x * cos(ROTSPEED) - p->plane_y * sin(ROTSPEED); p->plane_y = old_plane_x * sin(ROTSPEED) + p->plane_y * cos(ROTSPEED); } return ; }
JavaScript
복사
move_player, rotate_player 함수는 raycasting tutorial 에서 소개한 방식을 적용했습니다. 이에 대한 정보는 튜터리얼을 참고하시면 좋겠습니다.
mlx_hook(window->win, ON_DESTROY, 0, event_hook, window); int event_hook(t_window *temp) { mlx_destroy_image(temp->mlx, temp->win); end_cub3d(); return (0); }
JavaScript
복사
위 훅과 함수는 윈도우 창의 ‘x’ 키를 눌렀을 때 윈도우를 종료하도록 유도하는 함수 및 훅입니다.
자 이제부터는 cub3D 과제의 핵심인 Ray-casting을 어떻게 구현할지를 생각할 시점입니다. 이에 관한 배경 지식은 raycasting tutorial을 참고하시면 좋겠습니다. 이 글은 앞서 소개한 튜토리얼을 기반으로 작성되었습니다. 이 튜토리얼에서는 raycasting을 DDA 알고리즘을 통해 구현합니다. 그리고 이러한 렌더링 기법은 mlx 라이브러리와 맞물려 동작합니다. DDA 알고리즘은 mlx_loop 와 맞물려 사용되야 하므로 아래와 같이 코드로 묶여 있습니다.
mlx_loop_hook(window->mlx, render_graphic, render);
C
복사
그리고 이러한 mlx_loop_hook 함수의 render_graphic 함수는 DDA 알고리즘을 사용하는 함수인 render_graphic 함수를 지속적으로 호출합니다.
int render_graphic(t_render *render) { draw_screen(render->map_info, render->player, render->window, render->texture_set); return (0); }
C
복사
draw_screen 함수는 DDA로 구현한 raycasting 의 핵심 함수입니다. 이 함수는 DDA 알고리즘을 사용해 mlx window 크기에 맞는 이미지를 생성한 후 DDA 알고리즘으로 벽의 높이를 계산한 후 texture를 입혀 WINDOW_WIDHT 만큼의 수직선을 채우려 합니다. 이와 같은 로직은 아래와 같습니다.
void draw_screen(t_map *map_info, t_player *player, t_window *window, t_texture_set *texture_set) { int x; t_image buffer; t_ray *ray; x = -1; ray = init_ray(); buffer.image = mlx_new_image(window->mlx, WINDOW_WIDTH, WINDOW_HEIGHT); buffer.addr = mlx_get_data_addr(buffer.image, &buffer.bits_per_pixel, &buffer.line_length, &buffer.endian); while (++x <= WINDOW_WIDTH) { calculate_ray_dir(ray, player, map_info, x); calculate_where_is_ray_in(ray, player); calculate_delta_dist(ray); calculate_side_dist(ray, player); find_wall(ray, map_info); calculate_distance_to_wall(ray, player); calculate_wall_height(ray); fill_buffer(&buffer, player, ray, texture_set); } mlx_put_image_to_window(window->mlx, window->win, buffer.image, 0, 0); mlx_destroy_image(window->mlx, buffer.image); free(ray); }
C
복사
DDA 알고리즘은 튜토리얼을 통해 이해할 수 있습니다. 그것을 이해했다고 판단하고 계속 설명하겠습니다. 가장 먼저 할 일은 우리가 찍어낼 화면을 만드는 작업입니다. 이 작업은 buffer를 만드는 작업입니다. mlx_new_image를 통해 window에 딱 맞는 이미지를 생성합니다. 그 다음으로 DDA 알고리즘을 통해 buffer를 채우는 작업을 합니다.
ray를 WINDOW_WIDTH 만큼 쏩니다. 왜냐하면 우리가 채워야할 화면의 넓이는 WINDOW_WIDTH 만큼 존재하기 때문입니다. 플레이어의 위치와 바라보는 방향인 방향 벡터를 기반으로 ray_dir가 어디로 가는지 계산합니다. 그리고 이를 기반으로 언제 벽에 닿았는지, 그리고 벽 까지의 거리가 얼마나 되는지를 계산합니다. 그리고 벽 까지의 거리를 기반으로 벽 수직선의 높이를 얼마로 할지를 정합니다. 이 때 플레이어의 위치와 벽까지의 거리를 기반으로 수직선을 그리게 되면 어안렌즈 효과가 생기므로 이를 보정하기 위해 calculate_distance_to_wall() 함수에서 카메라 플레인과 벽까지의 최단거리를 플레이어 위치와 벽까지의 거리로 대신합니다. 그리고 fill_buffer 함수를 통해 찍어낼 화면의 하나의 수직선을 채웁니다.
ray를 WINDOW_WIDHT만큼 쏘면 image buffer가 가득차게 됩니다. 이를 mlx_put_image_to_window 함수를 사용해 가득찬 buffer를 window에 출력합니다. 이 draw_screen 함수는 mlx_loop 함수를 통해 지속적으로 호출되며 계속해서 화면을 업데이트 합니다. key_hook()에 걸려있는 키보드 감지로 플레이어의 위치와 방향이 업데이트 될 때마다 업데이트 하는 것이 아닌, 화면을 지속적으로 업데이트 하는 과정에서 이 움직임이 반영됩니다.
생각해 볼 항목
1.
움직임을 부드럽게 하는 방법
2.
마우스 움직임을 추가하는 방법
3.
미니맵을 띄우는 방법
4.
플레이어가 움직일 때만 새로 이미지 렌더링을 하는 방법