서론
오랜만에 자는 시간까지 쪼개서 과제를 했다. 7월 4일에 시작해서 7월 21일에 끝냈으니 14일이 걸린건데, 3일은 팀원의 일정으로 인해, 1일은 내 일정으로 인해 출근하지 않았으니 10일동안 쉬지않고 달려서 완성했다.
특히 이번에는 파트를 나누지않고 계속 둘이 같이 보면서 코드를 작성했는데, 생각한거보다 코드가 더 빠르게 나왔다. 둘이 각자 만든것보다 속도가 별로 느리지는 않으면서 코드에 대한 이해는 동일하게 상승하는걸 느끼고 짝 프로그래밍을 이래서 하는구나 하고 알게되었음.
공부의 순서는 맵 파싱 -> 레이캐스팅 공부 -> 레이캐스팅을 적용하여 완성 순으로 진행했는데, 다른 방식으로 공부해보진 않았지만 이게 가장 합리적인 공부 방식이었던것 같다. 맵 파싱을 하면서 만든 코드를 레이캐스팅 튜토리얼에 우겨넣었더니 더 공부가 잘 되었는데, 단점은 이걸 다시 구현하는 과정에서 처음에 맵 파싱 까지 한 코드로 되돌아갔을때 무언가를 만들어놓고 까먹고 지나가는 경우가 있었다. (비슷한 이유로 리트 한번 했음.)
최근에 내가 쓴 코드를 그대로 사용한 평가자를 만난 이후로 블로그 글에서는 모든 내용을 다 설명하지 않기로 결정했다. 원래 내 글 하나만 보고도 과제를 완성할수있게 돕는 글을 목표로 했지만, 누군가의 공부에 방해가 되는 길이라는걸 깨달았다. 아마 의도적으로 무언가를 생략하는 형태일텐데, 사실 생각해보면 깃허브의 코드는 그대로니까 큰 차이가 없는것 같기도 합니다...
서브젝트 정리
사용 가능한 함수
open, close, read, write, printf, malloc, free, perror, strerror, exit
All functions of the math library (-lm man man 3 math)
All functions of the MinilibX
레이캐스팅을 이용하여 1인칭 시점의 미로를 구현하는 과제
miniLibX를 반드시 사용해야함. 사용할때는 Libft와 동일한 규칙을 적용함.
최소화 등의 창을 제어하는 기능이 부드럽게 동작해야함
벽의 사방이 직접 고른 다른 텍스쳐로 구성되어 있어야함.
천장과 바닥의 색을 다르게 설정할수있어야함.
왼쪽 오른쪽 화살표 키를 눌러서 좌우로 돌아볼수있어야함
WASD 키를 눌러서 카메라를 이동할수있어야함
창 상단의 X키나 ESC를 누르면 창이 닫히고 프로그램이 정상적으로 종료되어야함
프로그램이 받는 인자는 .cub 확장자를 가진 맵 파일
cub 파일 안에는 지도의 구성요소 정보와 지도의 모습이 담겨있음.
지도가 마지막에, 다른 구성요소들은 순서 상관없이 등장 가능.
다른 구성요소들은 하나 이상의 빈줄로 구분됨.
구성요소의 종류
북쪽 벽 텍스쳐 : NO 경로 남쪽 벽 텍스쳐 : SO 경로
서쪽 벽 텍스쳐 : WE 경로
동쪽 벽 텍스쳐 : EA 경로
바닥 색상 : F 0~255 사이의 RGB값 3개
천장 색상 : C 0~255 사이의 RGB값 3개
지도에서는 6개의 문자만 허용됨(0, 1, NSEW). 0은 바닥, 1은 벽, NSEW는 플레이어의 시작 위치와 바라보고있는 시점
공백은 지도의 유효한 부분이며, 눈에 보이는대로 불러올수있어야함.
NO ./path_to_the_north_texture
SO ./path_to_the_south_texture
WE ./path_to_the_west_texture
EA ./path_to_the_east_texture
F 220,100,0
C 225,30,0
1111111111111111111111111
1000000000110000000000001
1011000001110000000000001
1001000000000000000000001
111111111011000001110000000000001
100000000011000001110111111111111
11110111111111011100000010001
11110111111111011101010010001
11000000110101011100000010001
10000000000000001100000010001
10000000000000001101010010001
11000001110101011111011110N0111
11110111 1110101 101111010001
11111111 1111111 111111111111
지도는 벽으로 둘러쌓여있어야하며 그렇지 않을경우 에러 반환
맵 파싱
맵 파싱이 so_long에 비해 좀 더 복잡하다. 그때는 맵이 직사각형이기때문에 한줄로 파싱할수있어서 편했는데, 지금은 한줄로 파싱을 하려면... 사실 뭐 못할건 아니지만 더 어려운 방식일것같아서 줄마다 따로 가져와서 저장했다. 맵 파싱을 주로 다룬 가이드가 없어서 헤매면서 했습니다. 사실 이것도 예외처리를 다 얘기하진 않았으니 가이드라고 부르기는 어렵습니다...
void file_parsing(char *file_name, t_map *map)
{
char *line;
int fd;
fd = open(file_name, O_RDONLY);
if (fd < 0)
ft_exit("file open error\n");
initialization_map(map);
while (map->ret == 1)
{
map->ret = ft_get_next_line(fd, &line);
if (map->ret == -1)
ft_exit("error");
map->mcount += 1;
file_check(map, line);
free(line);
line = NULL;
}
map_dub(file_name, map);
print_struct(map); // 잘 입력되었는지 확인하는 구조체 출력 함수
close (fd);
map->ret = 1;
}
기본 구조는 단순하다. 파일을 열어서 해당 파일의 내용이 정상적인지 체크하면서 구성요소들을 구조체에 담고, 전체에 이상이 없는걸 확인했으면 맵을 마저 저장한다.
int direction_deep_cheak(t_map *map, char *line, char **tmp)
{
int fd;
line += 2;
while (ft_isspace(*line))
line++;
fd = open(line, O_RDONLY);
if (fd == -1)
ft_exit("texture errer");
close(fd);
*tmp = ft_strdup(line);
}
void direction_check(t_map *map, char *line, char c)
{
char *tmp;
if (c == 'N')
{
if (direction_deep_cheak(map, line, &tmp))
map->no_path = tmp;
}
else if (c == 'S')
{
if (direction_deep_cheak(map, line, &tmp))
map->so_path = tmp;
}
else if (c == 'W')
{
if (direction_deep_cheak(map, line, &tmp))
map->we_path = tmp;
}
else if (c == 'E')
{
if (direction_deep_cheak(map, line, &tmp))
map->ea_path = tmp;
}
}
void file_check(t_map *map, char *line)
{
int i;
if (!ft_strncmp("NO ", line, 3) || !ft_strncmp("SO ", line, 3)
|| !ft_strncmp("WE ", line, 3) || !ft_strncmp("EA ", line, 3))
direction_check(map, line, line[0]);
else if (!ft_strncmp("F ", line, 2) || !ft_strncmp("C ", line, 2))
color_check(map, line, line[0]);
i = 0;
while (ft_isspace(line[i]))
i++;
if (line[i] == '0' || line[i] == '1')
{
if (!map->no_path || !map->so_path || !map->we_path || !map->ea_path
|| (map->floor == -1) || (map->celling == -1))
ft_exit("something missing or file order is worng");
map_check(map, line);
}
}
맨 앞글자가 방향을 가르키면 방향을 구조체에 저장하는 함수로 보낸다.
앞 세글자를 기준으로 해서 이미 조건문에 거른 상태이기 때문에 다시 첫글자만을 기준으로 삼아도 문제가 생기지 않는다.
파일을 읽어서 정상적으로 읽어지는지 확인하고, 그렇다면 해당 파일의 경로를 알맞은 변수에 담아둔다.
void range_number_check(char *line)
{
int i;
i = 0;
if (!ft_isdigit(*line))
ft_exit("rgb digit error");
while (*line)
{
if (*line == ',')
{
i++;
line++;
while (ft_isspace(*line))
line++;
}
if (!ft_isdigit(*line))
ft_exit("rgb error");
line++;
}
if (i != 2)
ft_exit("range error");
}
int set_rgb(char **line)
{
int rt;
int i;
rt = ft_atoi(*line);
i = 1;
while (rt > i)
{
(*line)++;
i *= 10;
}
while ((**line) && (!ft_strncmp(",", (*line), ft_strlen(*line))
|| ft_isspace(**line)))
(*line)++;
return (rt);
}
void color_check(t_map *map, char *line, char c)
{
t_range rgb;
line += 2;
while (ft_isspace(*line))
line++;
range_number_check(line);
rgb.r = set_rgb(&line);
rgb.g = set_rgb(&line);
rgb.b = set_rgb(&line);
if (!(rgb.r >= 0 && rgb.r <= 255) || !(rgb.g >= 0 && rgb.g <= 255)
|| !(rgb.b >= 0 && rgb.b <= 255))
ft_exit("it is nor 0 ~ 255");
if (c == 'F')
{
map->floor = (rgb.r << 16 | rgb.g << 8 | rgb.b);
}
else if (c == 'C')
{
map->celling = (rgb.r << 16 | rgb.g << 8 | rgb.b);
}
}
맨 앞이 F나 C면 RGB를 체크하는 함수로 보낸다.
일단 문자 바로 뒤 공백은 필요없으니 삭제해주고, 정상적으로 숫자들이 들어왔는지를 확인한다. 숫자가 아닌 값이나 ‘ , ’가 두개가 아니라면 무언가 비정상적인 값이 들어온것이기 때문에 바로 에러처리를 해준다.
그다음 rgb를 각각 함수에 넣어서 앞에서부터 순서대로 숫자를 입력해준다. 해당 값이 한덩어리의 문자열로 들어왔기때문에 이중포인터를 사용하여 상위 함수에서의 문자열도 전진시킨다.
그 다음 구조체에 RGB를 저장할때 비트연산을 이용하여 숫자를 저장한다. mlx에서의 rgb를 읽을때 8비트씩 읽기때문에, 비트연산을 이용하여 각 변수를 8비트씩 저장한다.(맨 앞 8비트는 투명도로 여기에서는 사용하지않음)
int set_player_dir(char c)
{
if (c == 'W')
return (W);
else if (c == 'N')
return (N);
else if (c == 'S')
return (S);
else if (c == 'E')
return (E);
return (0);
}
void map_check(t_map *map, char *line)
{
int i;
char c;
i = 0;
while (line[i])
{
c = line[i];
if (!(c == ' ') && c != '0' && c != '1' && c != 'W'
&& c != 'N' && c != 'S' && c != 'E')
ft_exit("map argv error");
else if (c == 'W' || c == 'N' || c == 'S' || c == 'E')
{
if (map->player.status != 0)
ft_exit("its more than one player error");
map->player.status = set_player_dir(c);
}
i++;
}
if (map->width < i)
map->width = i;
if (map->start == 0)
map->start = map->mcount;
}
상위함수에서 구조체에 저장하지 않은 값이 있는지 확인한다음, 맵에 에러가 있는지 확인하는 함수에 들어온다.
여기에서 적절하지않은 어떤 값이든 바로 에러처리를 해주고, 플레이어의 위치를 저장하는 문자가 나오면 이전에 문자가 나온적이 있는지를 확인해서 에러처리 하고 저장해둔다. 매크로로 WASD 를 1234로 바꿔서 저장해두고 사용했다.
그리고 가로 크기는 가장 큰 값을 저장해두고, 시작 지점을 따로 저장해둔다.
void map_gnl_forward(int fd, t_map *map)
{
int i;
char *line;
i = 0;
while (i < map->start - 1)
{
map->ret = ft_get_next_line(fd, &line);
if (map->ret == -1)
ft_exit("gnl error");
free(line);
line = NULL;
i++;
}
}
void map_set_str(int fd, t_map *map, char **tmp)
{
int i;
char *line;
char *str;
i = 0;
while (i < map->height + 1)
{
map->ret = ft_get_next_line(fd, &line);
if (map->ret == -1)
ft_exit("gnl error");
str = (char *)malloc(sizeof(char) * (map->width + 1));
if (!str)
ft_exit("map_dup malloc error");
ft_memset(str, ' ', map->width);
str[map->width] = '\0';
ft_strcpy(str, line);
tmp[i] = str;
free(line);
line = NULL;
i++;
}
}
void map_dub(char *file_name, t_map *map)
{
char **tmp;
int fd;
map->height = map->mcount - map->start;
tmp = (char **)malloc(sizeof(char *) * (map->height + 2));
if (!tmp)
ft_exit("map_dup malloc error");
fd = open(file_name, O_RDONLY);
if (fd == -1)
ft_exit("map_dip open error");
map_gnl_forward(fd, map);
map_set_str(fd, map, tmp);
tmp[map->height + 1] = 0;
map->map = tmp;
surround_wall_check(map, map->map);
set_player_pos(map, map->map);
close(fd);
}
기존에 구한 파일 전체 길이와 지도 시작지점을 이용하여 지도의 높이를 저장한 다음 그만큼의 문자열 배열을 malloc 해준다.
맵을 다시 open 한 다음, 맵에서 이미 저장한 구성요소들은 다 지나치고 지도 부분까지 gnl을 사용하여 전진한다.
맵에서 가장 가로크기가 긴 부분만큼씩 배열을 만든다음 해당 부분을 공백으로 채워주고, 맵 파일의 문자열을 복사해서 구조체에 저장한다.
void surround_wall_check(t_map *map, char **str)
{
int i;
int j;
i = 0;
while (str[i])
{
j = 0;
while (str[i][j])
{
if (str[i][j] == '0')
{
if (i == 0 || j == 0) // 맨 앞이거나 맨 위일때는 0이 나오면 안됨
ft_exit("1");
else if (i == map->height || !str[i][j + 1]) // 맨 아래거나 맨 뒤일때도 0이 나오면 안됨
ft_exit("2");
else if (str[i][j - 1] == ' ' || str[i][j + 1] == ' ') // 0의 앞이나 뒤가 공백인지 확인
ft_exit("3");
else if (str[i + 1][j] == ' ' || str[i - 1][j] == ' ') // 0의 위나 아래가 공백인지 확인
ft_exit("4");
}
j++;
}
i++;
}
}
void set_player_pos(t_map *map, char **str)
{
int i;
int j;
i = 0;
while (str[i])
{
j = 0;
while (str[i][j])
{
if (str[i][j] == 'W' || str[i][j] == 'E' || str[i][j] == 'S'
|| str[i][j] == 'N')
{
map->player.y = i;
map->player.x = j;
}
j++;
}
i++;
}
}
마지막으로 지도가 벽으로 둘러쌓여있는지를 점검하고, 플레이어의 위치 좌표를 저장한다.
벽으로 둘러쌓인걸 확인하는 방법은 0이 나왔을때 사방이 공백인지를 확인해주면 된다. 이때 반드시 현재 0 의 위치가 처음이거나 마지막인지를 조건문에서 먼저 확인해줘야한다. 그렇지않으면 해당 부분에서 참조해야하는 곳이 접근할수없는 곳이어서 세그폴트가 난다.
같은 이유로 문자열을 malloc 할때 쓰레기값을 참조하는 경우를 피하기위해 가장 긴 길이만큼 공백을 채워준다.
레이캐스팅 공부
https://github.com/l-yohai/cub3d/blob/master/mlx_example/03_img_textured_raycast.c
위의 코드를 가져와서 나의 맵 파싱에 맞게 수정한다음 코드를 이해하면서 레이캐스팅 공부를 했다.
이 코드를 컴파일해보면 움직일때 천장이나 바닥이 이상하게 보이는 현상이 있는데, 이전에 그린 내용을 지우거나 수정하는 부분이 없어서 그렇다. 서브젝트에서 천장과 바닥 색을 지정해서 수정하도록 되어있기 때문에 그 과정에서 자연스럽게 수정하게된다.
원래 글을 쭉 쓰다가 보니 맵 파싱을 한 내용을 수정하는 과정에서 많은걸 배웠다는걸 알게되었다. 그래서 그 부분은 지우고 다른 글을 첨부하고 이 파트를 끝낸다... 아마 레이캐스팅을 설명하지 않은 유일한 큡디 가이드이지 않을까?
쓰다보니 귀찮아서 그런게 아닙니다. 정말로요. 저보다 설명을 잘하신 분들이있는데 설명을 하려니 부끄러워서 그렇습니다.
https://github.com/365kim/raycasting_tutorial
https://blog.chichoon.com/category/42/42s%20Cursus
코드 작성
mlx_hook(info.win, 17, 0, &exit_game, &info);
mlx_hook(info.win, 2, 0, &key_press, &info);
mlx_hook(info.win, 3, 0, &key_release, &info);
mlx_loop_hook(info.mlx, &main_loop, &info);
mlx_loop(info.mlx);
순서대로 설명하면 키 프레스와 릴리즈를 먼저 설명해야겠지만, main_loop를 먼저 설명해야겠다.
int main_loop(t_info *info)
{
paint_floor(info);
raycasting(info);
paint_img(info);
key_hook(info);
return (0);
}
메인에서 mlx_loop_hook 함수를 호출하면서 그 안에서 천장과 바닥을 그리고, 레이캐스팅을 이용해 그릴것을 계산한 다음, 화면에 그려주고, 누른 키에 따라 움직이도록 함수를 넣어뒀다.
void paint_floor(t_info *info)
{
int i;
int j;
int celling;
int floor;
i = 0;
celling = info->map.celling;
floor = info->map.floor;
while (i < SCHEIGHT / 2)
{
j = 0;
while (j < SCWIDTH)
{
info->img.buffer[i][j] = celling;
info->img.buffer[SCHEIGHT - i - 1][j] = floor;
j++;
}
i++;
}
}
파싱 과정에서 저장한 천장과 바닥의 색을 버퍼에 담아둔다. 이 위에 벽을 그린다음 유저에게 보여줄 예정이다.
void dda_calc(t_ray *ray, t_map *map)
{
int hit;
hit = 0;
while (hit != 1)
{
if (ray->sidedist_x < ray->sidedist_y)
{
ray->sidedist_x += ray->deltadist_x;
ray->map_x += ray->step_x;
ray->side = X;
}
else
{
ray->sidedist_y += ray->deltadist_y;
ray->map_y += ray->step_y;
ray->side = Y;
}
if (map->map[ray->map_y][ray->map_x] == '1')
hit = 1;
}
if (ray->side == X)
ray->perpwalldist = ray->sidedist_x - ray->deltadist_x;
else
ray->perpwalldist = ray->sidedist_y - ray->deltadist_y;
}
void raycasting(t_info *info)
{
int x;
x = 0;
while (x < SCWIDTH)
{
ray_init(info, x);
dda_calc(&info->ray, &info->map);
mapping_buff(&info->ray, &info->player);
set_buff(&info->ray, &info->img, x);
x++;
}
}
레이캐스팅을 시작하면 광선에 필요한 변수들을 초기화한 다음, dda 알고리즘에 따라 광선이 벽에 닿을때까지 step과 광선을 전진시키면서 거리를 계산한다.
(mapX - info->posX + (1 - stepX) / 2) / rayDirX
(sideDistX - deltaDistX)
어안렌즈 부분이 로데브에서는 위처럼 되어있는데 이게 아래 식과 같다는걸 우연히 알게되고 그 이유를 못찾아서 15시간 넘게 헤맸는데 그냥 계산을 해보니 원래 같은 값이었다.
delta = 1/ray
step = 1
side = (map + 1 - pos) * delta
(map - pos + (1 - 1) / 2) / rayDirX = side - delta
map - pos = (side - delta) * ray
map - pos = ((map + 1 - pos) * 1/ray * ray) - (1/ray * ray)
map - pos = map - pos + 1 - 1
map - pos = map - pos
step = -1
side = (pos - map) * delta;
(map - pos + (1 - -1) / 2) / rayDir = side - delta
map - pos + 1 = (side - delta) * ray
map - pos + 1 = ((pos - map) * delta - delta) * ray
step = -1 이면 ray는 음수
map - pos + 1 = ((pos - map) * 1/ray * ray) - (1/ray * ray)
map - pos + 1 = -pos + map + 1
void mapping_buff(t_ray *ray, t_player *p)
{
double wall;
if (ray->side == X)
wall = p->pos_y + (ray->perpwalldist * ray->raydir_y);
else
wall = p->pos_x + (ray->perpwalldist * ray->raydir_x);
wall = wall - floor(wall);
ray->tex_x = (int)(wall * (double)TEXWIDTH);
if (ray->side == X && ray->raydir_x < 0)
ray->tex_x = TEXWIDTH - ray->tex_x - 1;
if (ray->side == Y && ray->raydir_y > 0)
ray->tex_x = TEXWIDTH - ray->tex_x - 1;
ray->line_h = (int)(SCHEIGHT / ray->perpwalldist);
ray->start = SCHEIGHT / 2 - ray->line_h / 2;
ray->end = SCHEIGHT / 2 + ray->line_h / 2;
if (ray->start < 0)
ray->start = 0;
if (ray->end >= SCHEIGHT)
ray->end = SCHEIGHT;
ray->ratio = 1.0 * TEXHEIGHT / ray->line_h;
ray->texpos = (ray->start - SCHEIGHT / 2 + ray->line_h / 2) * ray->ratio;
}
void set_buff(t_ray *ray, t_img *img, int x)
{
int color;
int y;
y = ray->start;
while (y < ray->end)
{
ray->tex_y = (int)ray->texpos & (TEXHEIGHT - 1);
if (ray->side == X)
{
if (ray->raydir_x >= 0)
color = img->arr_img[E][TEXHEIGHT * ray->tex_y + ray->tex_x];
else
color = img->arr_img[W][TEXHEIGHT * ray->tex_y + ray->tex_x];
}
else if (ray->side == Y)
{
if (ray->raydir_y >= 0)
color = img->arr_img[S][TEXHEIGHT * ray->tex_y + ray->tex_x];
else
color = img->arr_img[N][TEXHEIGHT * ray->tex_y + ray->tex_x];
}
img->buffer[y][x] = color;
ray->texpos += ray->ratio;
y++;
}
}
그런 다음 벽과의 거리를 이용하여 저장해둔 벽의 텍스쳐를 가져와서 천장과 바닥이 입력되어있는 버퍼에 덮어 씌우면서 벽을 그려주고, 그 버퍼를 mlx의 img에 넣어줘서 유저에게 보여준다.
참고로 메모리를 아끼지않기위해 버퍼를 사용하지않을수도있긴한데, 코드가 지저분해지고 알아보기 어렵게 되기때문에 가급적 쓰는걸 추천합니다.
int key_press(int keycode, t_info *info)
{
if (keycode == K_W)
info->move.key_w = 1;
if (keycode == K_S)
info->move.key_s = 1;
if (keycode == K_A)
info->move.key_a = 1;
if (keycode == K_D)
info->move.key_d = 1;
if (keycode == K_AR_R)
info->move.arr_r = 1;
if (keycode == K_AR_L)
info->move.arr_l = 1;
if (keycode == K_ESC || keycode == 65307)
exit (0);
return (0);
}
int key_release(int keycode, t_info *info)
{
if (keycode == K_W)
info->move.key_w = 0;
if (keycode == K_S)
info->move.key_s = 0;
if (keycode == K_A)
info->move.key_a = 0;
if (keycode == K_D)
info->move.key_d = 0;
if (keycode == K_AR_R)
info->move.arr_r = 0;
if (keycode == K_AR_L)
info->move.arr_l = 0;
return (0);
}
int key_hook(t_info *info)
{
if (info->move.key_w && !info->move.key_s)
key_hook_w(&info->player, &info->map);
if (info->move.key_s && !info->move.key_w)
key_hook_s(&info->player, &info->map);
if (info->move.key_a && !info->move.key_d)
key_hook_a(&info->player, &info->map);
if (info->move.key_d && !info->move.key_a)
key_hook_d(&info->player, &info->map);
if (info->move.arr_l && !info->move.arr_r)
key_hook_l(&info->player);
if (info->move.arr_r && !info->move.arr_l)
key_hook_r(&info->player);
return (1);
}
키 프레스와 키 릴리즈에서 구조체를 하나 만들어서 그 구조체를 1과 0으로 변환해주면서 움직임을 만들어줬다. mlx_hook에서 움직임을 바로 구현하면 움직임이 부드럽지않고, 한번에 여러개의 키를 인식하지 못하는 문제가 생기기때문에 이렇게 처리해줘야했다.
서브젝트에서는 별다른 말이 없었지만 W를 누른 상태에서 S를 누르는 상황에 어떻게 동작하도록 만들것인지 고민을 조금 했는데, 계속 전진을 하게 만드는것도 어렵지는 않았겠지만, 서브젝트에서 주어진 울펜슈타인을 해보니 같은 상황에서 멈추는걸 보고 그냥 쉽게 구현하였다.
void key_hook_w(t_player *p, t_map *map)
{
double x_vec;
double y_vec;
x_vec = p->dir_x * p->move_speed;
y_vec = p->dir_y * p->move_speed;
if (map->map[(int)(p->pos_y)][(int)(p->pos_x + x_vec * WALLDIST)] == '0')
p->pos_x += x_vec;
if (map->map[(int)(p->pos_y + y_vec * WALLDIST)][(int)(p->pos_x)] == '0')
p->pos_y += y_vec;
}
void key_hook_l(t_player *p)
{
double olddir;
double oldplane;
olddir = p->dir_y;
oldplane = p->plane_y;
p->dir_y = p->dir_x * sin(-p->rot_speed) + p->dir_y * cos(-p->rot_speed);
p->dir_x = p->dir_x * cos(-p->rot_speed) - olddir * sin(-p->rot_speed);
p->plane_y = p->plane_x * sin(-p->rot_speed)
+ p->plane_y * cos(-p->rot_speed);
p->plane_x = p->plane_x * cos(-p->rot_speed)
- oldplane * sin(-p->rot_speed);
}
캐릭터의 움직임은 해당 위치에 벽이 있는지를 확인하고 벽이 없는 경우에만 움직이도록 구현하였다. WALLDIST에 1.05 혹은 1.1을 넣어서 벽과에 거리에 곱해서 벽과 조금 떨어지도록 했는데, 이게 없으면 벽을 수직으로 보면서 접근하면 벽 너머를 보게되면서 세그폴트가 난다.
좌우 회전은 삼각함수를 이용해서 구현하였는데, https://www.youtube.com/watch?v=OVPyMijFiEQ 이걸 보면 바로 이해할수있다.
결론
시작할때는 이것도 얘기하고 저것도 얘기하고.. 하면서 생각해둔게 많은데, 레이캐스팅 얘기를 빼버리고 나니 할말이 확 줄은 느낌이다.
지금도 다시 쓸지말지 고민중인데, 우선 노션에 정리해두고 나중에 다시 생각해봐야겠다.
'프로그래밍' 카테고리의 다른 글
[42서울] CPP Module 01 - 클래스와 레퍼런스 (0) | 2022.08.10 |
---|---|
[42서울] CPP Module 00 - c++ 과제의 시작 (0) | 2022.07.28 |
[42서울] 넷프랙티스(netpractice). 놀랄 만큼 쉽고 믿기 힘들 만큼 재미있는 과제. (0) | 2022.06.27 |
[42서울] 미니쉘(minishell) 파싱, 그 후회와 참회의 기록. (0) | 2022.06.19 |
[42서울] 철학자(Philosophers)에게 밥을 먹이자 (0) | 2022.05.13 |