Repository
Mandatory
MinilibX를 사용하여 2D 게임을 만드는 프로젝트입니다. 플레이어가 맵에서 모든 수집품을 모으고 출구로 탈출하는 게임을 구현합니다.
과제 요구사항
게임 규칙:
- 플레이어는 맵에서 모든 수집품(C)을 모아야 합니다
- 모든 수집품을 모은 후에만 출구(E)로 탈출 가능
- 이동 횟수를 터미널에 출력해야 합니다
- W, A, S, D 키로 상하좌우 이동
- ESC 키 또는 창의 X 버튼으로 게임 종료
맵 구성 요소:
0: 빈 공간 (Empty space)1: 벽 (Wall)C: 수집품 (Collectible)E: 출구 (Exit)P: 플레이어 시작 위치 (Player)
맵 규칙:
- 맵은 직사각형이어야 함
- 맵은 벽(1)로 완전히 둘러싸여야 함
- 유효한 경로가 존재해야 함 (수집품과 출구 모두 도달 가능)
- 플레이어(P) 1개, 출구(E) 최소 1개, 수집품(C) 최소 1개 필요
.ber확장자를 가진 파일로 맵 저장
맵 예시
1111111111111
10010000000C1
1000011111001
1P0011E000001
1111111111111
설명:
P: 플레이어 시작 위치C: 수집해야 할 아이템E: 모든 수집품을 모은 후 탈출할 출구1: 벽 (지나갈 수 없음)0: 이동 가능한 빈 공간
MinilibX 기초
초기화 및 윈도우 생성
#include <mlx.h>
typedef struct s_game
{
void *mlx; // MLX 인스턴스
void *win; // 윈도우 포인터
int width; // 윈도우 너비
int height; // 윈도우 높이
} t_game;
int main(void)
{
t_game game;
// MLX 초기화
game.mlx = mlx_init();
// 윈도우 생성
game.width = 800;
game.height = 600;
game.win = mlx_new_window(game.mlx, game.width, game.height, "So Long");
// 이벤트 루프 시작
mlx_loop(game.mlx);
return (0);
}
이미지 로드 및 출력
typedef struct s_image
{
void *img; // 이미지 포인터
int width; // 이미지 너비
int height; // 이미지 높이
} t_image;
// 이미지 로드
t_image load_image(void *mlx, char *path)
{
t_image img;
img.img = mlx_xpm_file_to_image(mlx, path, &img.width, &img.height);
if (!img.img)
{
printf("Error\n이미지 로드 실패: %s\n", path);
exit(1);
}
return (img);
}
// 이미지 화면에 출력
void render_image(t_game *game, t_image img, int x, int y)
{
mlx_put_image_to_window(game->mlx, game->win, img.img, x, y);
}
이벤트 처리
// 키보드 이벤트
int key_press(int keycode, t_game *game)
{
if (keycode == ESC_KEY) // 53 (macOS)
{
mlx_destroy_window(game->mlx, game->win);
exit(0);
}
else if (keycode == W_KEY) // 13 (macOS)
move_player(game, 0, -1); // 위로 이동
else if (keycode == A_KEY) // 0
move_player(game, -1, 0); // 왼쪽
else if (keycode == S_KEY) // 1
move_player(game, 0, 1); // 아래
else if (keycode == D_KEY) // 2
move_player(game, 1, 0); // 오른쪽
return (0);
}
// 윈도우 닫기 이벤트
int close_window(t_game *game)
{
mlx_destroy_window(game->mlx, game->win);
exit(0);
return (0);
}
int main(void)
{
t_game game;
// ... 초기화 ...
// 이벤트 핸들러 등록
mlx_key_hook(game.win, key_press, &game);
mlx_hook(game.win, 17, 0, close_window, &game); // 17 = DestroyNotify
mlx_loop(game.mlx);
return (0);
}
게임 구조
맵 구조체
typedef struct s_map
{
char **grid; // 2D 맵 배열
int width; // 맵 너비
int height; // 맵 높이
int collectibles; // 수집품 개수
int collected; // 수집한 개수
int exit_count; // 출구 개수
int player_count; // 플레이어 개수
} t_map;
플레이어 구조체
typedef struct s_player
{
int x; // X 좌표 (타일 단위)
int y; // Y 좌표 (타일 단위)
int moves; // 이동 횟수
} t_player;
게임 전체 구조체
typedef struct s_game
{
void *mlx;
void *win;
t_map map;
t_player player;
t_image img_wall;
t_image img_floor;
t_image img_collectible;
t_image img_exit;
t_image img_player;
int tile_size; // 타일 크기 (예: 64픽셀)
} t_game;
맵 파싱
맵 파일 읽기
t_map read_map(char *filename)
{
int fd;
char *line;
t_map map;
int i;
// 파일 확장자 검사
if (!ft_strnstr(filename, ".ber", ft_strlen(filename)))
{
printf("Error\n잘못된 파일 확장자\n");
exit(1);
}
fd = open(filename, O_RDONLY);
if (fd < 0)
{
printf("Error\n파일을 열 수 없습니다\n");
exit(1);
}
// 맵 높이 계산
map.height = 0;
while (get_next_line(fd, &line) > 0)
{
map.height++;
free(line);
}
close(fd);
// 맵 데이터 저장
map.grid = malloc(sizeof(char *) * (map.height + 1));
fd = open(filename, O_RDONLY);
i = 0;
while (get_next_line(fd, &line) > 0)
{
map.grid[i] = line;
if (i == 0)
map.width = ft_strlen(line);
i++;
}
map.grid[i] = NULL;
close(fd);
return (map);
}
맵 유효성 검사
int validate_map(t_map *map)
{
// 1. 직사각형 검사
for (int i = 0; i < map->height; i++)
{
if (ft_strlen(map->grid[i]) != map->width)
return (0);
}
// 2. 벽으로 둘러싸여 있는지 검사
for (int i = 0; i < map->width; i++)
{
if (map->grid[0][i] != '1' || map->grid[map->height - 1][i] != '1')
return (0);
}
for (int i = 0; i < map->height; i++)
{
if (map->grid[i][0] != '1' || map->grid[i][map->width - 1] != '1')
return (0);
}
// 3. 필수 요소 개수 검사
map->collectibles = 0;
map->exit_count = 0;
map->player_count = 0;
for (int y = 0; y < map->height; y++)
{
for (int x = 0; x < map->width; x++)
{
char c = map->grid[y][x];
if (c == 'C')
map->collectibles++;
else if (c == 'E')
map->exit_count++;
else if (c == 'P')
map->player_count++;
else if (c != '0' && c != '1')
return (0); // 잘못된 문자
}
}
if (map->player_count != 1 || map->exit_count < 1 || map->collectibles < 1)
return (0);
return (1);
}
경로 유효성 검사 (DFS/BFS)
모든 수집품과 출구에 도달 가능한지 확인합니다.
void flood_fill(char **grid, int x, int y, int *collectibles, int *exits)
{
if (grid[y][x] == '1' || grid[y][x] == 'V') // 벽 또는 방문함
return;
if (grid[y][x] == 'C')
(*collectibles)++;
else if (grid[y][x] == 'E')
(*exits)++;
grid[y][x] = 'V'; // 방문 표시
// 4방향 재귀 탐색
flood_fill(grid, x + 1, y, collectibles, exits);
flood_fill(grid, x - 1, y, collectibles, exits);
flood_fill(grid, x, y + 1, collectibles, exits);
flood_fill(grid, x, y - 1, collectibles, exits);
}
int check_path(t_map *map, t_player *player)
{
char **copy;
int found_collectibles = 0;
int found_exits = 0;
// 맵 복사
copy = copy_map(map->grid, map->height);
// 플레이어 위치에서 flood fill
flood_fill(copy, player->x, player->y, &found_collectibles, &found_exits);
// 복사본 해제
free_map(copy, map->height);
return (found_collectibles == map->collectibles && found_exits >= 1);
}
게임 로직
플레이어 이동
void move_player(t_game *game, int dx, int dy)
{
int new_x = game->player.x + dx;
int new_y = game->player.y + dy;
// 벽 충돌 검사
if (game->map.grid[new_y][new_x] == '1')
return;
// 수집품 수집
if (game->map.grid[new_y][new_x] == 'C')
{
game->map.collected++;
game->map.grid[new_y][new_x] = '0'; // 수집품 제거
}
// 출구 도달
if (game->map.grid[new_y][new_x] == 'E')
{
if (game->map.collected == game->map.collectibles)
{
printf("축하합니다! %d번 만에 탈출했습니다!\n", game->player.moves + 1);
close_window(game);
}
else
{
printf("모든 수집품을 모아야 합니다! (%d/%d)\n",
game->map.collected, game->map.collectibles);
return;
}
}
// 이동 수행
game->player.x = new_x;
game->player.y = new_y;
game->player.moves++;
// 이동 횟수 출력
printf("Moves: %d\n", game->player.moves);
// 화면 다시 그리기
render_game(game);
}
화면 렌더링
void render_game(t_game *game)
{
for (int y = 0; y < game->map.height; y++)
{
for (int x = 0; x < game->map.width; x++)
{
int pixel_x = x * game->tile_size;
int pixel_y = y * game->tile_size;
// 바닥 먼저 그리기
render_image(game, game->img_floor, pixel_x, pixel_y);
// 타일 타입에 따라 이미지 그리기
char tile = game->map.grid[y][x];
if (tile == '1')
render_image(game, game->img_wall, pixel_x, pixel_y);
else if (tile == 'C')
render_image(game, game->img_collectible, pixel_x, pixel_y);
else if (tile == 'E')
render_image(game, game->img_exit, pixel_x, pixel_y);
// 플레이어 그리기
if (x == game->player.x && y == game->player.y)
render_image(game, game->img_player, pixel_x, pixel_y);
}
}
}
보너스
추가 기능:
- 애니메이션 추가 (플레이어, 수집품 등)
- 적(Enemy) 추가 - 적과 닿으면 게임 오버
- 화면에 이동 횟수 표시 (터미널이 아닌 게임 창에)
- 스프라이트 애니메이션
// 애니메이션 예시
int animate(t_game *game)
{
static int frame = 0;
frame++;
if (frame % 10 == 0) // 10프레임마다 업데이트
{
// 애니메이션 로직
render_game(game);
}
return (0);
}
int main(void)
{
t_game game;
// ... 초기화 ...
mlx_loop_hook(game.mlx, animate, &game);
mlx_loop(game.mlx);
return (0);
}
주의사항
- 메모리 관리
- 모든 이미지는
mlx_destroy_image()로 해제 - 맵 데이터 메모리 해제
- 윈도우는
mlx_destroy_window()로 해제
- 모든 이미지는
- 오류 처리
- 잘못된 맵 파일
- 유효하지 않은 맵 구조
- 이미지 로드 실패
- 모든 오류는 “Error\n” 출력 후 종료
- 키 코드 (macOS/Linux 차이)
- macOS와 Linux에서 키 코드가 다를 수 있음
- 조건부 컴파일 사용 또는
mlx_key_hook()대신mlx_hook()사용
컴파일 예시
# macOS
gcc -Wall -Wextra -Werror *.c -Lmlx -lmlx -framework OpenGL -framework AppKit -o so_long
# Linux
gcc -Wall -Wextra -Werror *.c -Lmlx_linux -lmlx -lXext -lX11 -lm -o so_long
실행 예시
./so_long maps/map.ber