간단한 2D 게임 만들기
MiniLibx는 그래픽라이브러리로 그래픽에 대한 지식 없이도 렌더링할 수 있게 해주는 라이브러리.
기본적으로 해당 라이브러리를 사용하기 위해선 해당 과제란에서 다운 받아서 사용한다.
#include "./mlx/mlx.h"
int main()
{
void *mlx_ptr;
void *win_ptr;
mlx_ptr = mlx_init();
win_ptr = mlx_new_window(mlx_ptr, 300, 300, "mlx_test");
mlx_loop(mlx_ptr);
}
// 컴파일: cc -L./mlx -lmlx -framework OpenGL -framework AppKit main.c
void *mlx_init(void)
void *mlx_new_window(void mlx_ptr, int size_x, int size_y, char *title)
int mlx_loop(void *mlx_ptr)
int mlx_hook(void win_ptr, int x_event, int x_mask, int (funct)(), void *param)
int mlx_loop_hook(void *win_ptr, int (*funct_ptr)(), void *param)
void *mlx_xpm_file_to_image(void *mlx_ptr, char *filename, int *width, int *height)
int mlx_put_image_to_window(void *mlx_ptr, void *win_ptr, void *img_ptr, int x, int y);
char *mlx_get_data_addr(void *img_ptr, int bits_per_pixel, int *size_line, int *endian);
int mlx_string_put(void *mlx_ptr, void *win_ptr, int x, int y, int color, char *string)
int mlx_destroy_image(void *mlx_ptr, void *img_ptr)
적당한 png파일을 구해서 64 x 64 픽셀로 이미지를 조정한다.
이미지 변환 사이트 에서 png 파일을 xpm파일로 변환한다.
#ifndef SO_LONG_H
# define SO_LONG_H
# include <fcntl.h>
# include <stdlib.h>
# include <unistd.h>
# include "./get_next_line/get_next_line.h"
# include "./mlx/mlx.h"
# define TILES 64
# define X_EVENT_KEYPRESS 2
# define X_EVENT_EXIT 17
# define KEY_W 13
# define KEY_A 0
# define KEY_S 1
# define KEY_D 2
# define KEY_ESC 53
typedef struct s_param // 이미지를 출력하기 위한 구조체
{
int x;
int y;
int tile_x;
int tile_y;
void *img_ptr;
} t_param;
typedef struct s_map // 맵의 구성요소에 대한 구조체
{
int wall;
int player;
int collectible;
int exit;
} t_map;
typedef struct s_img // 이미지에 대한 구조체
{
void *img_ptr;
int *data;
int bpp;
int size_line;
int endian;
} t_img;
typedef struct s_game // 게임을 진행하는 데 필요한 구조체
{
void *mlx_ptr;
void *win_ptr;
int width;
int height;
int collected;
int move;
t_param position;
t_img img;
t_map map_textures;
char **map;
int valid_path;
} t_game;
typedef struct s_check // 맵 유효성 검증을 위한 구조체
{
int y;
int x;
int collectible;
int **visited;
} t_check;
void ft_putchar_fd(char c, int fd);
void ft_putstr_fd(char *s, int fd);
void ft_putnbr_fd(int n, int fd);
char *ft_itoa(int n);
int check_valid_move(t_game *game, int x, int y, int keycode);
int check_player_move(int keycode, t_game *game);
void passing_exit(t_game *game, int prev_x, int prev_y);
void draw_updated_player(t_game *game, int prev_x, int prev_y);
int press_key(int keycode, t_game *game);
void init_minilibx(t_game *game);
void init_param(t_game *game);
void get_map_col(t_game *game, char *line, int l);
void init_map(t_game *game, int fd);
int check_last_line(char *line);
void set_map_value(t_game *game, char component);
void check_map_line(t_game *game, char *line, int check_wall);
void check_map_components(t_game *game);
void get_map(t_game *game, int fd);
void draw_pixels_of_tile(t_game *game, char texture);
void draw_map(t_game *game, char *line, int l);
int close_game(t_game *game, int type);
int close_game_with_error(int type);
void print_move(char c);
void check_path(t_game *game);
#endif
void ft_putchar_fd(char c, int fd)
{
write(fd, &c, 1);
}
void ft_putstr_fd(char *s, int fd)
{
int i;
i = 0;
while (s[i])
write(fd, &s[i++], 1);
}
void rec_putnbr(int nb, int fd)
{
char c;
if (nb == 0)
return ;
c = '0' + nb % 10;
rec_putnbr(nb / 10, fd);
ft_putchar_fd(c, fd);
}
void ft_putnbr_fd(int n, int fd)
{
char c;
if (n < 0)
{
write(fd, "-", 1);
c = '0' - n % 10;
rec_putnbr(-(n / 10), fd);
}
else
{
c = '0' + n % 10;
rec_putnbr(n / 10, fd);
}
ft_putchar_fd(c, fd);
}
// 정상적으로 게임이 끝났을 때 프로그램 종료 함수
int close_game(t_game *game, int type)
{
int i;
i = 0;
while (i < game->height)
{
free(game->map[i]);
i++;
}
free(game->map);
mlx_destroy_image(game->mlx_ptr, game->img.img_ptr);
free(game->mlx_ptr);
if (type)
{
ft_putstr_fd("------------------\n", 1);
ft_putstr_fd(" The Game Is Over \n", 1);
ft_putstr_fd("------------------\n", 1);
}
exit(0);
}
// 에러 메시지 출력 함수
int close_game_with_error(int type)
{
ft_putstr_fd("[Error]\n", 1);
if (type == 0)
{
ft_putstr_fd("The map must be composed of only ", 1);
ft_putstr_fd("5 possible characters(0, 1, C, E, P) !\n", 1);
}
else if (type == 1)
ft_putstr_fd("There must be only one player !\n", 1);
else if (type == 2)
ft_putstr_fd("Theere must be only one exit !\n", 1);
else if (type == 3)
ft_putstr_fd("The map must be rectangular !\n", 1);
else if (type == 4)
ft_putstr_fd("The map must be closed or surrounded by walls !\n", 1);
else if (type == 5)
{
ft_putstr_fd("Map must have at least one exit, one collectible, ", 1);
ft_putstr_fd("and one starting position !\n", 1);
}
else if (type == 6)
ft_putstr_fd("The map must have valid path !\n", 1);
else if (type == -1)
ft_putstr_fd("The files does not exist !", 1);
exit(1);
}
// 몇 번 움직였는지 출력하는 함수
void print_move(char c)
{
ft_putstr_fd("Move : ", 1);
ft_putnbr_fd(c, 1);
ft_putchar_fd('\n', 1);
}
int main(int argc, char *argv[])
{
int fd;
t_game game;
if (argc != 2) // 인자가 잘못 됐을 때
{
ft_putstr_fd("[Error]\n", 1);
ft_putstr_fd("Try './so_long [Map_name.ber]'\n", 1);
return (0);
}
fd = open(argv[1], O_RDONLY); // 맵 열기
if (fd == -1)
close_game_with_error(-1);
init_param(&game); // 구조체 초기화
get_map(&game, fd); // 맵의 요소 입력
init_minilibx(&game); // mlx를 위한 초기화
close(fd);
fd = open(argv[1], O_RDONLY); // 맵을 새로 읽어 오기 위해서 한번 더 open
if (fd == -1)
close_game_with_error(-1);
init_map(&game, fd); // 맵 입력후 유효성 검증 및 초기 맵 그리기
close(fd);
mlx_hook(game.win_ptr, X_EVENT_KEYPRESS, 0, &press_key, &game); // 플레이어 움직임에 따라 맵 그리기
mlx_hook(game.win_ptr, X_EVENT_EXIT, 0, &close_game, &game); // 게임 종료
mlx_loop(game.mlx_ptr);
return (0);
}
void set_map_value(t_game *game, char component)
{
if (component == '1' && !game->map_textures.wall) // '1'이고 map_textures에 wall이 없는 경우
game->map_textures.wall = 1;
else if (component == 'C') // 'C'인 경우 콜렉터블 1증가
game->map_textures.collectible += 1;
else if (component == 'P') // 'P'인 경우
{
if (game->map_textures.player > 0) // 플레이어가 존재한다면 에러
close_game_with_error(1);
game->map_textures.player = 1;
}
else if (component == 'E') // 'E' 인경우
{
if (game->map_textures.exit > 0) // 출구가 존재한다면 에러
close_game_with_error(2);
game->map_textures.exit = 1;
}
}
void check_map_line(t_game *game, char *line, int check_wall)
{
int i;
int len;
i = -1;
len = ft_strlen(line) - 1;
if (len != game->width) // 읽어온 길이와 가로길이가 다를경우
close_game_with_error(3);
while (line[++i] && line[i] != '\n') // 개행문자 전까지
{
if ((i == 0 || i == len - 1) && line[i] != '1') // 양 끝이 벽이 아닐경우
close_game_with_error(4);
if (check_wall && line[i] != '1') // 첫 번째 줄이 벽이 아닌경우
close_game_with_error(4);
if (line[i] != '1' && line[i] != '0' && line[i] != 'P' && \
line[i] != 'C' && line[i] != 'E')
{
close_game_with_error(0); // 다른 문자가 있는 경우
}
set_map_value(game, line[i]); // game 구조체에 맵의 구성요소 입력
}
}
void check_map_components(t_game *game)
{
if (!game->map_textures.exit) // 출구가 없다면
close_game_with_error(5);
if (!game->map_textures.player) // 플레이어가 없다면
close_game_with_error(5);
if (!game->map_textures.collectible) // 콜렉터블이 없다면
close_game_with_error(5);
}
void get_map(t_game *game, int fd)
{
char *line;
int h;
int check_wall;
h = 0;
check_wall = 1;
while (1)
{
// 파일을 한 줄 씩 읽어오기
line = get_next_line(fd);
if (!line)
break ;
if (h == 0) // 첫 번째 줄일경우
{
game->width = ft_strlen(line) - 1; // 가로길이 입력
check_map_line(game, line, check_wall); // 해당 줄 유효성 검증, 모두 벽이어야함
}
else // 아닐 경우
check_map_line(game, line, !check_wall); // 해당 줄 유효성 검증
h += 1;
free(line); // 다음을 위해 free
}
game->height = h; // 최종적으로 세로길이 입력
check_map_components(game); // 맵의 요소에 대한 유효성 검증
}
// mlx를 init
void init_minilibx(t_game *game)
{
int w;
int h;
w = game->width; // 맵의 가로 입력
h = game->height; // 맵의 세로 입력
game->mlx_ptr = mlx_init(); // mlx 초기화
game->win_ptr = mlx_new_window(game->mlx_ptr, w * TILES, \
h * TILES, "[so_long]"); // mlx 윈도우 초기화, 이름은 so_long으로
}
// game 구조체를 초기화
void init_param(t_game *game)
{
game->width = 0;
game->height = 0;
game->map_textures.wall = 0;
game->map_textures.player = 0;
game->map_textures.collectible = 0;
game->map_textures.exit = 0;
game->collected = 0;
game->move = 0;
game->map = NULL;
game->position.x = 0;
game->position.y = 0;
game->valid_path = 0;
}
void get_map_col(t_game *game, char *line, int l)
{
int i;
i = 0;
game->map[l] = (char *)malloc(sizeof(char) * game->width); // 가로길이 만큼 동적 할당
while (line[i])
{
if (line[i] == 'P') // 'P', 플레이어 라면
{
game->position.y = l;
game->position.x = i;
// game.position에 좌표 입력
}
game->map[l][i] = line[i]; // 맵 입력
i++;
}
}
// map의 마지막 줄을 체크
int check_last_line(char *line)
{
int i;
i = 0;
while (line[i] && line[i] != '\n')
{
if (line[i] != '1') // 마지막 줄이 벽이 아니라면 에러
{
close_game_with_error(4);
return (0);
}
i++;
}
return (1);
}
void init_map(t_game *game, int fd)
{
int i;
int is_valid;
char *line;
i = 0;
is_valid = 1;
game->map = (char **)malloc(sizeof(char *) * game->height); // 맵을 입력하기 위해 동적 세로길이만큼 동적 할당
while (1)
{
line = get_next_line(fd);
if (!line)
break ;
get_map_col(game, line, i); // 맵 입력
if (i + 1 == game->height) // 마지막 줄이라면
is_valid = check_last_line(line); // 마지막 줄 체크
if (!is_valid) // 마지막 줄에 대한 타당성 검증
close_game(game, 0);
draw_map(game, line, i); // 맵 그리기
free(line);
i++;
}
check_path(game); // 맵에 가능한 경로 체크
ft_putstr_fd("------------------\n", 1);
ft_putstr_fd(" Game start ! \n", 1);
ft_putstr_fd("------------------\n", 1);
}
void free_visited(int **visited) // visited 배열 free
{
int i;
i = -1;
while (visited[++i])
{
free(visited[i]);
visited[i] = 0;
}
free(visited);
visited = 0;
}
int **visited_init(t_game *game) // visited 배열 생성
{
int **visited;
int i;
int j;
i = -1;
visited = (int **)malloc(sizeof(int *) * game->height);
if (!visited)
return(0);
while (++i < game->height - 1)
{
visited[i] = (int *)malloc(sizeof(int) * (game->width + 1));
if (!visited[i])
{
free_visited(visited);
return (0);
}
j = -1;
while (j < game->width)
visited[i][++j] = 0; // visited 배열을 0으로 초기화
}
return (visited);
}
void dfs(t_game *game, t_check *check, int y, int x)
{
const int dy[4] = {1, -1, 0, 0};
const int dx[4] = {0, 0, 1, -1}; // 네 방향 탐색
int ny;
int nx;
int i;
check->visited[y][x] = 1;
if (game->map[y][x] == 'C')
check->collectible -= 1;
if (game->map[y][x] == 'E')
{
game->valid_path = 1; // 출구가 존재하므로 유효한 경로로 임시 지정
return ;
}
i = -1;
while (++i < 4)
{
ny = y + dy[i];
nx = x + dx[i];
if (game->map[ny][nx] != '1' && !check->visited[ny][nx]) // 새로운 좌표가 벽이 아니고 방문하지 않았을 떄 dfs 탐색
dfs(game, check, ny, nx);
}
}
void init_check(t_game *game, t_check *check)
{
check->visited = visited_init(game); // visited 배열 입력
check->y = game->height; // 세로길이 입력
check->x = game->width; // 가로길이 입력
check->collectible = game->map_textures.collectible; // 콜렉터블의 개수 입력
}
void check_path(t_game *game)
{
t_check check;
init_check(game, &check); // check 구조체 초기화
dfs(game, &check, game->position.y, game->position.x); // dfs 깊이우선탐색 실행 (완탐 한번 걸기)
if (!game->valid_path || check.collectible > 0) // 탈출구를 못찾았거나 존재하는 콜렉터블을 모두 지우지 못했을 시 경로가 존재 x
close_game_with_error(6);
free_visited(check.visited); // visited 배열 free
}
void draw_pixels_of_tile(t_game *game, char texture)
{
int w;
int h;
if (texture == '1') // '1'일 경우 벽
game->img.img_ptr = mlx_xpm_file_to_image(game->mlx_ptr, \
"imgs/wall.xpm", &w, &h);
else if (texture == 'C') // 'C'일 경우 콜렉터블
game->img.img_ptr = mlx_xpm_file_to_image(game->mlx_ptr, \
"imgs/collectible.xpm", &w, &h);
else if (texture == 'E') // 'E'일 경우 출구
game->img.img_ptr = mlx_xpm_file_to_image(game->mlx_ptr, \
"imgs/exit.xpm", &w, &h);
else if (texture == '0') // '0'일 경우 빈공간
game->img.img_ptr = mlx_xpm_file_to_image(game->mlx_ptr, \
"imgs/empty.xpm", &w, &h);
else if (texture == 'P') // 'P'인 경우 플레이어
{
game->img.img_ptr = mlx_xpm_file_to_image(game->mlx_ptr, \
"imgs/player.xpm", &w, &h);
game->position.img_ptr = game->img.img_ptr; // 플레이어 위치에 대한 이미지 포인터도 저장해줌
}
}
void draw_map(t_game *game, char *line, int l)
{
int i;
i = 0;
while (line[i])
{
if (line[i] == '0') // '0'일 경우에 빈 공간이므로 pass
{
i++;
continue ;
}
if (line[i] == 'P') // 'P'일 경우 플레이어
{
game->position.tile_x = i * TILES; // 플레이어의 x좌표 * 64
game->position.tile_y = l * TILES; // 플레이어의 y좌표 * 64 (64픽셀로 그림 그리기 때문) -> 가장 오른쪽 하단 꼭짓점이 좌표가 됨
}
draw_pixels_of_tile(game, line[i]);
mlx_put_image_to_window(game->mlx_ptr, game->win_ptr, \
game->img.img_ptr, i * TILES, l * TILES); // 해당 위치의 이미지 저장값을 출력
i++;
}
}
int check_valid_move(t_game *game, int y, int x, int keycode)
{
char cur;
cur = game->map[y][x]; // cur은 플레이여의 현재 위치
if (keycode != KEY_W && keycode != KEY_S && \
keycode != KEY_A && keycode != KEY_D)
return (0); // 허용된 키가 아닌경우 0반환
if (cur == '1')
return (0); // 벽인 경우 0반환
else if (cur == 'C') // 콜렉터블 인경우
{
game->collected += 1; // 콜렉터블 1증가
game->map[y][x] = '0'; // 콜렉터블 위치 빈공간으로 변환
}
else if (cur == 'E') // 탈출구인 경우
{
if (game->collected == game->map_textures.collectible) // 전체 맵의 콜렉터블과 모은 콜렉터블이 같을경우
close_game(game, 1); // 정상적으로 게임 종료
else
ft_putstr_fd("There's still a collectible left!\n", 1); // 아닐 경우 콜렉터블이 더 남았다는 메시지
}
return (1); // 빈공간이거나 콜렉터블이거나 콜렉터블이 남았을 때 출구라면 1 반환
}
int check_player_move(int keycode, t_game *game)
{
int nx;
int ny;
int flag;
flag = 1;
ny = game->position.y; // 플레이어의 y좌표
nx = game->position.x; // 플레이어의 x좌표
if (keycode == KEY_W) // 'W'를 눌렀을 때 y 감소, 위로 올라가기
ny--;
else if (keycode == KEY_S) // 'S'를 눌렀을 때 y 증가, 아래로 내려가기
ny++;
else if (keycode == KEY_A) // 'A'를 눌렀을 때 x 감소, 왼쪽 방향
nx--;
else if (keycode == KEY_D) // 'D'를 눌렀을 때 x 증가, 오른쪽 방향
nx++;
else // 아닐경우 flag = 0
flag = 0;
flag = check_valid_move(game, ny, nx, keycode); // 빈공간이거나 콜렉터블이거나 콜렉터블이 남았을 때 출구라면 1 반환
if (!flag) // 잘못된 경우라면 해당 플레그 반환
return (flag);
if (game->map[game->position.y][game->position.x] == 'E')
flag = 2;
game->position.y = ny; // 플레이어의 y좌표 업데이트
game->position.x = nx; // 플레이어의 x좌표 업데이트
return (flag); // 출구라면 2반환, 뭔가 남았는데 올바른 경우 1반환
}
void passing_exit(t_game *game, int prev_y, int prev_x)
{
draw_pixels_of_tile(game, 'E'); // 출구에 대한 이미지 업데이트
mlx_put_image_to_window(game->mlx_ptr, game->win_ptr, \
game->img.img_ptr, prev_x, prev_y);
}
void draw_updated_player(t_game *game, int prev_y, int prev_x)
{
int h;
int w;
game->img.data = (int *)mlx_get_data_addr(game->position.img_ptr, \
&game->img.bpp, &game->img.size_line, &game->img.endian);
h = 0;
while (h < TILES) // 원래 있던 플레이어의 공간의 데이터를 빈 공간으로 교체
{
w = 0;
while (w < TILES)
{
game->img.data[h * TILES + w] = 0;
w++;
}
h++;
}
mlx_put_image_to_window(game->mlx_ptr, game->win_ptr, \
game->position.img_ptr, prev_x, prev_y); // 데이터에 맞게 빈 공간으로 그리기
draw_pixels_of_tile(game, 'P'); // 플레이어에 대한 이미지 업데이트
mlx_put_image_to_window(game->mlx_ptr, game->win_ptr, \
game->position.img_ptr, game->position.tile_x, game->position.tile_y); // 새롭게 움직인 위치에 대한 플레이어의 그림 업데이트
game->move += 1; // 움직인 횟수 증가
}
int press_key(int keycode, t_game *game)
{
int prev_x;
int prev_y;
int flag;
if (keycode == KEY_ESC) //'ESC'를 눌렀을 때 게임 종료
close_game(game, 1);
flag = check_player_move(keycode, game); // 끝나면 2반환, 뭔가 남았는데 올바른 경우 1반환
if (flag)
{
prev_y = game->position.tile_y; // 플레이어의 이전 픽셀 y좌표를 가져옴
prev_x = game->position.tile_x; // 플레이어의 이전 픽셀 x좌표를 가져옴
if (keycode == KEY_W) // W를 눌렀을 떄
game->position.tile_y -= TILES; // y--이므로 64만큼 감소
else if (keycode == KEY_S) // S를 눌렀을 때
game->position.tile_y += TILES; // y++이므로 64만큼 증가
else if (keycode == KEY_A) // A를 눌렀을 때
game->position.tile_x -= TILES; // x--이므로 64만큼 감소
else if (keycode == KEY_D) // D를 눌렀을 때
game->position.tile_x += TILES; // x++이므로 64만큼 증가
draw_updated_player(game, prev_y, prev_x); // 플레이어의 그림 업데이트
if (flag == 2) // 플레그가 2일경우 출구로 이동
passing_exit(game, prev_y, prev_x);
print_move(game->move); // 움직임 업데이트
}
return (0);
}