이전 포스트인 cub3D(1) 포스트를 참고하여 레이캐스트
가 무엇인지? DDA알고리즘
이 무엇인지?
이전 포스팅 fractal 에서 사용한 그래픽라이브러리 mlx
를 다시 활용하여 이미지를 표현해줍니다.
삼각함수
와 백터
, 행렬
에 대해 기본지식 탑재 필수
튜토리얼 해상 사이트를 참조하여 레이캐스팅의 큰 틀을 공부합니다.
텍스처로 사용될 이미지 파일은 .xpm, 맵 파일은 .cub 파일로 받습니다.
팀 프로젝트로 진행하였으며, 저는 실행파트를 맡았기 때문에 맵 파싱 부분은 간략하게 설명하고 넘어가겠습니다.
NAME = cub3D
CFLAGS = -I $(INCLUDES) -MMD -Wall -Wextra -Werror
LFLAGS = -Lmlx -lmlx -framework OpenGL -framework AppKit
SRCS_DIR = ./mandatory/
SRCS_B_DIR = ./bonus/
SRCS = main.c parse.c parse_map.c parse_elements.c run.c hook.c key.c paint.c loop.c free.c setting.c
SRCS_B = main_bonus.c parse_bonus.c parse_map_bonus.c parse_elements_bonus.c run_bonus.c hook_bonus.c key_bonus.c paint_bonus.c loop_bonus.c free_bonus.c minimap_bonus.c setting_bonus.c
SRCS_NAME = $(if $(filter bonus, $(MAKECMDGOALS)), $(addprefix $(SRCS_B_DIR), $(SRCS_B)), $(addprefix $(SRCS_DIR), $(SRCS)))
INCLUDES = ./includes/
SHELL = bash
OBJ_DIR = objs
OBJS = $(addprefix $(OBJ_DIR)/, $(notdir $(SRCS_NAME:.c=.o)))
DEPS = $(addprefix $(OBJ_DIR)/, $(notdir $(SRCS_NAME:.c=.d)))
vpath %.c $(SRCS_DIR) $(SRCS_B_DIR)
all : $(NAME)
bonus : $(NAME)
$(NAME) : $(OBJ_DIR) $(OBJS)
@echo -n "Making game... "
@make all -s -C libft
@cc $(OBJS) $(CFLAGS) $(LFLAGS) ./libft/libft.a -o $@
@echo -e "\033[32;1m"complete"\033[0m"
@sleep 0.2
$(OBJ_DIR) :
@mkdir $@
$(OBJ_DIR)/%.o : %.c
@cc $(CFLAGS) -c $< -o $@
clean :
@echo -n "Deleting object files : "
@make clean -s -C libft
@rm -rf $(OBJ_DIR)
@echo -e "\033[31;1m"delete"\033[0m"
@sleep 0.2
fclean : clean
@echo -n "Deleting cub3D : "
@make fclean -s -C libft
@rm -f cub3D
@echo -e "\033[31;1m"delete"\033[0m"
@sleep 0.2
re :
@make fclean
@make all
.PHONY : all clean fclean re bonus
-include $(DEPS)
Makefile은 팀원이 작성했는데 저와는 완전 다른방식으로 작성하는 스타일이여서 인상깊었습니다.
# define TEXWIDTH 256
# define TEXHEIGHT 256
# define WIDTH 1920
# define HEIGHT 1080
// 파싱 파트의 헤더
typedef struct s_parse
{
int fd;
char *line;
int element_cnt;
int line_cnt;
int stop;
} t_parse;
// 실행 파일의 해더
typedef struct s_map
{
char **grid; // 파싱으로 받은 맵 정보
char **minimap; // 미니맵을 사용하기 위한 2차원 배열
int width; // 맵 너비
int height; // 맵 높이
int map_x; // 맵에서의 플레이어 x위치
int map_y; // 맵에서의 플레이어 y위치
int side; // 벽면의 정보 x축과 수직인 면 = 0, y축과 수직인 면 = 1
} t_map;
typedef struct s_key
{
int w;
int s;
int a;
int d; // 이동 방향키
int left;
int right; // 회전 방향키
} t_key; // 방향키 입력값
typedef struct s_player
{
double x; // 플레이어의 x위치
double y; // 플레이어의 y위치
double dir_x; // 방향벡터 x
double dir_y; // 방향벡터 y
double plane_x; // 카메라 평면 x
double plane_y; // 카메라 평면 y
} t_player;
typedef struct s_ray
{
int step_x; // x방향으로 이동할 때의 방향
int step_y; // y방향으로 이동할 때의 방향
double ray_dir_x; // 빛의 방향벡터 x
double ray_dir_y; // 빛의 방향벡터 y
double size_dist_x; // 플레이어의 x위치 시작점에서 벽까지의 첫 x면까지 거리
double size_dist_y; // 플레이어의 y위치 시작점에서 벽까지의 첫 y면까지 거리
double delta_dist_x; // 첫 x면에서 다음 x면까지의 거리
double delta_dist_y; // 첫 y면에서 다음 y축 까지의 거리
double wall_dist; // 벽까지 광선의 이동거리
} t_ray;
typedef struct s_draw
{
int draw_height; // 그리는 부분의 높이
int draw_start; // 그리는 부분의 시작점
int draw_end; // 그리는 부분의 끝점
int color; // rgb 색상 값
int texture_number; // 4개의 텍스쳐 파일을 구분하기 위한 번호
int texture_x; // 텍스쳐의 x좌표
double wall_x; // x면에서 텍스쳐 x좌표 까지의 거리
} t_draw;
typedef struct s_mlx
{
void *mlx;
void *win;
void *img;
int *addr;
int bits_per_pixel;
int line_length;
int img_width;
int img_height;
int endian;
} t_mlx; // mlx정보
typedef struct s_game
{
t_map map; // 맵 정보
t_player player; // 플레이어 정보
t_ray ray; // 빛 정보
t_draw draw; // 그리기 위한 정보
t_mlx mlx; // mlx 정보
t_key key; // 방향키 누른 정보
char *tex_path[4]; // 이미지가 존재하는 경로
int *tex[4]; // 텍스쳐 xpm 정보
int floor_color; // 바닥 색상
int ceiling_color; // 천장 색상
int **arr_temp; // 임시로 color를 담을 배열
} t_game;
다룰 인자들이 많아 헤더 구조체 양이 많습니다.
실행에서 활용하기 위해 맵 파일을 파싱 한 정보를 담은 t_map
플레이어의 좌표와 방향벡터, 그리고 화면에 보여줄 카메라 벡터를 담은 t_player
DDA 알고리즘을 활용하기 위한 빛의 정보를 담은 t_ray
위의 정보를 활용하여 이미지를 표현하기 위해 사용되는 t_draw
mlx를 활용하기 위해 내장 함수의 정보를 담은 t_mlx
다양한 구조체들이 있지만, 편하게 사용하기 위해 t_game
구조체 안에 담아서 해당 구조체만 인자로 가지고 다닙니다.
int main(int acgc, char *argv[])
{
t_game cub;
if (argc != 2) // 들어오는 인자 확인
return (ft_putstr_fd("Error: invalid arguments\n", 2), EXIT_FAILURE);
ft_memset(&cub, 0, sizeof(t_game));
if (parse(argv[1], &cub)) // 파싱
return (ft_putstr_fd("Error: wrong map\n", 2), EXIT_FAILURE);
cub.mlx.mlx = mlx_init(); // mlx 생성
setting_all(&cub); // 파싱받은 결과를 가지고 기본값 설정
cub.mlx.win = mlx_new_window(cub.mlx.mlx, WIDTH, HEIGHT, "cub3d");
cub.mlx.img = mlx_new_image(cub.mlx.mlx, WIDTH, HEIGHT);
cub.mlx.addr = (int *)mlx_get_data_addr(cub.mlx.img,
&cub.mlx.bits_per_pixel, &cub.mlx.line_length, &cub.mlx.endian);
mlx_hook(cub.mlx.win, 17, 0, x_exit, &cub);
mlx_hook(cub.mlx.win, 2, 1, key_press, &cub);
mlx_hook(cub.mlx.win, 3, 2, key_release, &cub);
mlx_loop_hook(cub.mlx.mlx, &loop, &cub); // loop 함수를 계속 반복합니다.
mlx_loop(cub.mlx.mlx);
return (EXIT_SUCCESS);
}
우선 인자가 2개가 아니면 에러를 리턴합니다.
제대로 된 값이 들어오면 argv[1]
을 open 해서 맵 파일 정보를 파싱 해서 값을 전달받습니다.
파싱 받은 값을 t_map 구조체에 넣고 mlx_loop_hook 함수에 loop 함수
를 인자로 넣어 해당 함수를 계속 반복합니다.
int parse(char *file, t_game *game)
{
t_parse p;
if (name_check(file)) // .cub 파일 형식으로 들어오는지 확인
return (EXIT_FAILURE);
p.fd = open(file, O_RDONLY);
if (p.fd < 0 || read(p.fd, NULL, 0) < 0)
{
ft_putstr_fd("Error: fail to open\n", 2);
exit(1);
} // 파일을 열어줍니다.
ft_memset(game->tex_path, 0, sizeof(char *) * 4);
if (scan_map(&p, game)) // 맵 파일 안에 유효한 값들이 있는지 확인합니다.
return (EXIT_FAILURE);
game->map.grid = malloc(sizeof(char *) * (game->map.height + 1));
if (!game->map.grid)
exit(1);
ft_memset(game->map.grid, 0, sizeof(char *) * (game->map.height + 1));
if (parse_map(file, game, p.line_cnt)) // 제대로 파싱된 값을 t_game 안에있는 t_map 구조체에 넣습니다.
return (free_map(game), EXIT_FAILURE);
return (EXIT_SUCCESS);
}
우선 맵의 파일명을 확인합니다. 오직. cub 파일만 읽을 수 있으며, 다른 확장명의 파일이 들어온다면 에러를 출력해 줍니다.
제대로 된 파일이 들어올 경우, 해당 파일은 아래와 같은 형식으로 들어와야 합니다.
NO ./temp/eme.xpm SO ./temp/iron.xpm WE ./temp/coal.xpm EA ./temp/diamond.xpm F 144,144,144 C 128,128,128 1111111111111111111111111 1000000000110000000000001 1011000001110000000000001 100100000000000000E000001 111111111011000001110000000000001 100000000011000001110111111111111 11110111111111011100000010001 11110111111111011101010010001 11000000110101011100000010001 10000000000000001100000010001 10000000000000001101010010001 1100000111010101111101111000111 11110111 1110101 101111000001 11111111 1111111 111111111111
- 동, 서, 남, 북 텍스처의 경로가 담겨있어야 합니다.
- 바닥(Floor)와 천장(Ceiling)의 RGB 값이 담겨있어야 합니다.
- 맵은 0, 1, 플레이어 방향으로 이루어져 있고 반드시 벽을 나타내는 1로 둘러싸여 있어야 합니다.
- 맵 안에는 플레이어가 바라보는 방향 N, S, W, E 중 하나만 적혀있어야 합니다.
제대로 된 형식으로 들어왔으면 맵을 2차원 배열로 표현하여 t_map
에 담고,
플레이어의 좌표와 방향벡터, 카메라 벡터 등은 t_player
에 담아줍니다.
int loop(t_game *cub)
{
int x;
x = 0;
mlx_clear_window(cub->mlx.mlx, cub->mlx.win); // 이미지 클리어
painting_floor(cub); // 바닥, 천장 색상칠하기
while (x < WIDTH) // x를 WIDTH가 될때까지 세로로 칠하기
{
ray_setting(cub, x); // 광선 셋팅
ray_size_dist_setting(cub); // 광선 거리 셋팅
hit_check(cub); // 벽에 광선이 닿았는지 확인
draw_point_check(cub); // 그리는 지점 확인
put_texture_color(cub, x); // 텍스쳐 입히는 위치 등 확인
x++;
}
key_check(cub); // 키보드 입력값이 들어왔는지 확인
painting(cub); // 그리기
return (0);
}
실행부분에서는 해당 loop 함수를 반복합니다.
메인 동작전에 이미지를 초기화해주고 바닥과 천장 색상을 화면의 중간을 기준으로 위, 아래에 칠해줍니다.
x
가 WIDTH
값이 될 때까지 세로로 한 줄씩 그림을 그려줍니다.
전부 그렸으면 화면에 띄워주며 해당 동작을 계속 반복해 줍니다.
while() 내부의 동작들이 실행 파트의 핵심 부분입니다. 해당 반복문 안에서 모든 계산을 처리해 줍니다.
void ray_setting(t_game *cub, int x)
{
double camera_x;
camera_x = 2 * x / (double)WIDTH - 1; // 카메라 비율 조정
cub->ray.ray_dir_x = cub->player.dir_x + cub->player.plane_x * camera_x;
cub->ray.ray_dir_y = cub->player.dir_y + cub->player.plane_y * camera_x;
cub->map.map_x = (int)cub->player.x; // double인 플레이어 위치를 int형으로 맵에 적어줍니다
cub->map.map_y = (int)cub->player.y;
if (cub->ray.ray_dir_x == 0) // 빛의 x축 방향벡터가 0이면 delta_x는 무한대까지 갑니다.
cub->ray.delta_dist_x = DBL_MAX;
else
cub->ray.delta_dist_x = fabs(1 / cub->ray.ray_dir_x);
if (cub->ray.ray_dir_y == 0)
cub->ray.delta_dist_y = DBL_MAX;
else
cub->ray.delta_dist_y = fabs(1 / cub->ray.ray_dir_y);
}
우선 카메라 비율을 설정해 주기 위해 WIDTH의 길이를 -1 ~ 1까지의 값이 나오도록 비율을 조정해 줍니다.
빛의 방향벡터를 구하기 위해 플레이어의 방향벡터에 플레이어 평면 벡터에 카메라 비율을 적용한 값을 더해줍니다.
빛의 방향벡터는 위와 같은 식으로 구해줍니다.
map_x, map_y 는 플레이어의 좌표를 int로 변환한 값을 넣어줍니다.
헤더에 주석을 달아놨지만 다시한번 설명드리자면
delta_dist를 구하는 식은 위와 같습니다. 삼각형의 비례를 활용하여 delta_dist_x를 기준으로 x길이가 1이라는 점을 활용하여 구하면 됩니다.
delta_dist_x = |1 / ray_dir_x|
void ray_size_dist_setting(t_game *cub)
{
if (cub->ray.ray_dir_x < 0)
{
cub->ray.step_x = -1; // x가 음수방향
cub->ray.size_dist_x = (cub->player.x - cub->map.map_x)
* cub->ray.delta_dist_x;
}
else
{
cub->ray.step_x = 1; // x가 양수방향
cub->ray.size_dist_x = (cub->map.map_x + 1.0 - cub->player.x)
* cub->ray.delta_dist_x;
}
if (cub->ray.ray_dir_y < 0)
{
cub->ray.step_y = -1; // y가 음수방향
cub->ray.size_dist_y = (cub->player.y - cub->map.map_y)
* cub->ray.delta_dist_y;
}
else
{
cub->ray.step_y = 1; // y가 양수방향
cub->ray.size_dist_y = (cub->map.map_y + 1.0 - cub->player.y)
* cub->ray.delta_dist_y;
}
}
size_dist는 광선이 나아가는 방향 (↖ ↗ ↘ ↙) 에 따라 결정됩니다
가장 이해하기 쉬운 step_x가 1인 방향으로 수식을 구하면 위 이미지처럼 구할 수 있습니다.
(보드에 수식을 증명할 때 코드를 안 보고 그리느라 pos_x 라 표현했는데 position으로 player_x로 봐주세요.)
void hit_check(t_game *cub)
{
while (1)
{
if (cub->ray.size_dist_x < cub->ray.size_dist_y)
{
cub->ray.size_dist_x += cub->ray.delta_dist_x; // size_dist에 delta_dist를 더하고 반복문
cub->map.map_x += cub->ray.step_x; // 맵의 x좌표도 광선의 방향에 따라 값을 더해줍니다.
cub->map.side = 0;
}
else
{
cub->ray.size_dist_y += cub->ray.delta_dist_y;
cub->map.map_y += cub->ray.step_y;
cub->map.side = 1;
}
if (cub->map.grid[cub->map.map_y][cub->map.map_x] == '1') // 벽인지 확인
break ;
}
if (cub->map.side == 0) // y축의 벽일경우
cub->ray.wall_dist = (cub->map.map_x - cub->player.x
+ ((1.0 - cub->ray.step_x) / 2)) / cub->ray.ray_dir_x;
else
cub->ray.wall_dist = (cub->map.map_y - cub->player.y
+ ((1.0 - cub->ray.step_y) / 2)) / cub->ray.ray_dir_y;
}
해당 코드에선 광선이 닿는 벽의 모든 면 을 검사하는 방법인 DDA알고리즘
을 활용합니다.
size_dist_x
와 size_dist_y
를 비교하여 더 작은 size_dist
에 delta_dist
를 더해 해당 위치가 벽인지 아닌지 확인하는 방법입니다.
이렇게 x면과 y면을 한 칸 한 칸 벽인지 아닌지 확인하며 벽이 나타나면 반복문을 탈출하는 방식입니다.
if (cub->map.side == 0)
cub->ray.wall_dist = (cub->map.map_x - cub->player.x
+ ((1.0 - cub->ray.step_x) / 2)) / cub->ray.ray_dir_x;
else
cub->ray.wall_dist = (cub->map.map_y - cub->player.y
+ ((1.0 - cub->ray.step_y) / 2)) / cub->ray.ray_dir_y;
wall_dist
는 player 위치에서 벽을 확인하면 벽 끝점들은 광선의 길이가 길어져 왼쪽 파란색 그림과 같이 표현됩니다.
이를 어안렌즈 효과 (fisheye effect)
라고 하며, 실제 거리 값을 사용했을 때 모든 벽이 둥글게 보여서 회전할 때 울렁거릴 수도 있는 현상을 말합니다.
이러한 어안렌즈 효과
를 피해서 오른쪽 그림과 같이 표현하기 위해, 플레이어 위치까지의 유클리드 거리
대신 카메라 평면까지의 거리
를 사용합니다.
즉 플레이어에서 벽을 바라보는 유클리드 거리 Player -> H
가 아닌, A -> H
로 표현을 할 겁니다.
해당 길이를 구하기 위해선 위와 같은 방법으로 수식을 도출했습니다.
void draw_point_check(t_game *cub)
{
cub->draw.draw_height = (int)(HEIGHT / cub->ray.wall_dist); // 맵의 높이와 거리의 비례로 계산합니다.
cub->draw.draw_start = HEIGHT / 2 - cub->draw.draw_height / 2; // 그리기 시작점
cub->draw.draw_end = HEIGHT / 2 + cub->draw.draw_height / 2; // 그리기 끝점
if (cub->draw.draw_start < 0)
cub->draw.draw_start = 0;
if (cub->draw.draw_end >= HEIGHT)
cub->draw.draw_end = HEIGHT - 1;
if (cub->map.side == 1 && cub->ray.ray_dir_y < 0) // 동서남북에 따른 텍스쳐 flag
cub->draw.texture_number = 0;
else if (cub->map.side == 1 && cub->ray.ray_dir_y > 0)
cub->draw.texture_number = 1;
else if (cub->map.side == 0 && cub->ray.ray_dir_x < 0)
cub->draw.texture_number = 2;
else
cub->draw.texture_number = 3;
if (cub->map.side == 0) // 플레이어 위치에서 현재 wall_dist에 따른 텍스쳐의 위치를 구해줍니다.
cub->draw.wall_x = cub->player.y + cub->ray.wall_dist * cub->ray.ray_dir_y;
else
cub->draw.wall_x = cub->player.x + cub->ray.wall_dist * cub->ray.ray_dir_x;
cub->draw.wall_x -= floor(cub->draw.wall_x); // 내림으로 빼서 소수점만 구해줍니다.
}
광선이 벽의 어느 지점에 닿았는지 구해야 텍스쳐 상의 어떤 픽셀 색을 사용할지 알 수 있습니다.
텍스쳐의 크기에 비례해서 어느 지점의 색을 가져와야 하는지 계산하기 쉽기 때문에 벽이 1 X 1 크기라고 가정하여 비율로 계산합니다.
wall_x = player.x + wall_dist * ray_dir_x;
wall_x -= floor(wall_x);
구한 값에 내림해서 뺀 후 소수점만 얻습니다.
1 X 1 크기라고 가정했기 때문에 맞는 비율을 구하기 위한 과정입니다.
void put_texture_color(t_game *cub, int x)
{
int temp;
int texture_y;
double step;
double texture_position;
cub->draw.texture_x = (int)(cub->draw.wall_x * TEXWIDTH);
if (cub->map.side == 0 && cub->ray.ray_dir_x > 0) // x면일 경우 보정
cub->draw.texture_x = TEXWIDTH - cub->draw.texture_x - 1;
if (cub->map.side == 1 && cub->ray.ray_dir_y < 0) // y면일 경우 보정
cub->draw.texture_x = TEXWIDTH - cub->draw.texture_x - 1;
step = (double)TEXHEIGHT / (double)cub->draw.draw_height; // 텍스쳐에서 색을 가져올 때 텍스쳐의 원래 크기를 실제 벽으로 나눈 값
texture_position = (cub->draw.draw_start - (int)(HEIGHT / 2)
+ (int)(cub->draw.draw_height / 2)) * step;
temp = cub->draw.draw_start; // 임시로 그리기 시작점을 temp에 담아두고
while (temp <= cub->draw.draw_end) // 그리기 끝점까지 반복문을 실행합니다.
{
texture_y = (int)texture_position & (TEXHEIGHT - 1);
texture_position += step;
cub->draw.color = cub->tex[cub->draw.texture_number] \
[TEXHEIGHT * texture_y + cub->draw.texture_x]; // 색상에 현재 텍스쳐의 값을 넣어두고
cub->arr_temp[temp][x] = cub->draw.color; // 임시 배열에 해당 색상을 담아줍니다.
temp++;
}
}
실제 텍스쳐의 값을 구하기 위해 위에서 얻은 비율을 활용합니다.
1 : wall_x = TEXWIDTH : texture_x
(가정한 텍스쳐의 가로 길이 '1' : 광선을 쏠 때 텍스쳐의 위치 = 실제 텍스쳐의 가로길이 : 구하고자 하는 위치)
위와 같은 수식으로 얻어낸 위치입니다.
cub->draw.texture_x = (int)(cub->draw.wall_x * TEXWIDTH)
반복문을 돌면서 그리는 과정에서 임시 배열 arr_temp에 담아두는 이유는 mlx 내장 pixel_put을 사용하면 너무 많은 작업으로 인해 처리 속도가 느려집니다.
이를 해결하기 위해 임시 배열에 담고 x가 WIDTH가 되어 전체 반복문이 끝났을 때 그리는 방식으로 동작합니다.
void key_check(t_game *cub)
{
double move_speed;
double rotate_speed;
move_speed = 0.03; // 방향키를 눌렀을때 해당 방향으로의 증가량
rotate_speed = 0.03; // 삼각함수에 넣을 각도 (직관상 M_PI가 아닌 소수점 사용)
key_w_and_s(cub, move_speed); // 앞뒤 움직임
key_a_and_d(cub, move_speed); // 좌우 움직임
key_left(cub, rotate_speed); // 왼쪽으로 회전
key_right(cub, rotate_speed); // 오른쪽으로 회전
}
void key_w_and_s(t_game *cub, double move_speed)
{
if (cub->key.w) // w키로 앞으로 이동 가중치만큼 더해줍니다.
{
if (cub->map.grid[(int)cub->player.y][(int)(cub->player.x + (cub->player.dir_x * move_speed))] == '0')
cub->player.x += cub->player.dir_x * move_speed;
if (cub->map.grid[(int)(cub->player.y + (cub->player.dir_y * move_speed))][(int)cub->player.x] == '0')
cub->player.y += cub->player.dir_y * move_speed;
}
if (cub->key.s) // s키로 뒤로 이동 가중치만큼 빼줍니다.
{
if (cub->map.grid[(int)cub->player.y][(int)(cub->player.x - (cub->player.dir_x * move_speed))] == '0')
cub->player.x -= cub->player.dir_x * move_speed;
if (cub->map.grid[(int)(cub->player.y - (cub->player.dir_y * move_speed))][(int)cub->player.x] == '0')
cub->player.y -= cub->player.dir_y * move_speed;
}
}
void key_a_and_d(t_game *cub, double move_speed)
{
if (cub->key.a) // a키로 왼쪽으로 이동 가중치만큼 빼줍니다.
{
if (cub->map.grid[(int)cub->player.y][(int)(cub->player.x - (cub->player.plane_x * move_speed))] == '0')
cub->player.x -= cub->player.plane_x * move_speed;
if (cub->map.grid[(int)(cub->player.y - (cub->player.plane_y * move_speed))][(int)cub->player.x] == '0')
cub->player.y -= cub->player.plane_y * move_speed;
}
if (cub->key.d) // d키로 오른쪽으로 이동 가중치만큼 더해줍니다.
{
if (cub->map.grid[(int)cub->player.y][(int)(cub->player.x + (cub->player.plane_x * move_speed))] == '0')
cub->player.x += cub->player.plane_x * move_speed;
if (cub->map.grid[(int)(cub->player.y + (cub->player.plane_y * move_speed))][(int)cub->player.x] == '0')
cub->player.y += cub->player.plane_y * move_speed;
}
}
void key_left(t_game *cub, double rotate_speed)
{
double temp_dir_x;
double temp_plane_x;
if (cub->key.left)
{
temp_dir_x = cub->player.dir_x;
cub->player.dir_x = cub->player.dir_x * cos(-rotate_speed)
- (cub->player.dir_y * sin(-rotate_speed));
cub->player.dir_y = temp_dir_x * sin(-rotate_speed)
+ (cub->player.dir_y * cos(-rotate_speed));
temp_plane_x = cub->player.plane_x;
cub->player.plane_x = cub->player.plane_x * cos(-rotate_speed)
- (cub->player.plane_y * sin(-rotate_speed));
cub->player.plane_y = temp_plane_x * sin(-rotate_speed)
+ (cub->player.plane_y * cos(-rotate_speed));
}
}
void key_right(t_game *cub, double rotate_speed)
{
double temp_dir_x;
double temp_plane_x;
if (cub->key.right)
{
temp_dir_x = cub->player.dir_x;
cub->player.dir_x = cub->player.dir_x * cos(rotate_speed)
- (cub->player.dir_y * sin(rotate_speed));
cub->player.dir_y = temp_dir_x * sin(rotate_speed)
+ (cub->player.dir_y * cos(rotate_speed));
temp_plane_x = cub->player.plane_x;
cub->player.plane_x = cub->player.plane_x * cos(rotate_speed)
- (cub->player.plane_y * sin(rotate_speed));
cub->player.plane_y = temp_plane_x * sin(rotate_speed)
+ (cub->player.plane_y * cos(rotate_speed));
}
}
앞뒤로 움직일 때 행렬 y축 위치에 가중치만큼,
좌우로 움직일때는 행렬 x축 위치에 가중치만큼 움직입니다.
방향키로 회전을 할때는 이전 포스트에서 언급한 회전행렬을 적용해줍니다.
학부시절 배웠던 선형대수를 마음껏 쓸 수 있던 프로젝트였습니다.
직각 삼각형의 닮음, 삼각함수, 행렬과 벡터까지 예전에 배웠던 내용들을 보드에 직접 써가며 하나하나 수식을 증명하는 과정에서 수학적 지식을 늘렸습니다.
또한 맵 사이즈를 정하는 과정에서 메모리 누수 처리를 하기 싫어 지역변수로 배열을 만들었지만, 1920 x 1080등 큰 배열은 용량 초과로 오버플로우가 발생하여 어쩔 수 없이 동적할당하여 힙영역에서 동작시켰습니다.
위와 같은 시행착오와 팀원과의 잦은 소통을 통해 해당 프로젝트를 빠른 시간 안에 완성시켰고,
두 번의 팀 프로젝트를 진행하면서 다양한 방식의 협업을 배웠습니다.
안녕하세요, 질문이 있는데 드려도 될까요?