개요 시작은 분명 예전에 만들어둔 게임 소스를 빌드했던 것일테죠. 5년전에 C언어로 만든 게임을 보니 추억이 새록새록 피어오르면서 욕심이 나기 시작합니다.
지금 만든다면 더 잘 만들 수 있는데
그래서 새로운 게임을 간단하게 구상해 봤습니다. 오목같이 간단한 보드게임은 게임의 구현보단 알고리즘의 공부가 아닐까 싶었고, 리듬게임이나 슈팅게임은 만들어 보았습니다. 물론 지금 만들면 더 잘 만들겠지만. 이전에 미로찾기도 만들어 봤습니다.
결국, 도달한 것은 지금껏 실패만 했었던 2D 횡스크롤 게임입니다.
그냥 간단하게 만드는 것 보단 CUI라도 조금은 보는 게 즐거웠으면 하는 마음에 아스키 아트로 이루어진 애니메이션을 만들어보자! 그렇게 탄생한 것이 이것입니다.
구현 방법 사실 위 애니메이션은 원본이 있습니다. itch.io 에서 무료 게임 에셋 중 하나를 다운받은 것입니다.
https://rvros.itch.io/animated-pixel-hero
해당 이미지를 다운받을 경우, 스프라이트 이미지들이 제공됩니다. 저는 이걸 아스키 아트로 바꾼 다음에 이어서 출력하도록 했을 뿐입니다.
물론, 이렇게도 가능하지만 문든 든 생각이 있었죠.
자동으로 아스키 아트로 변환하면 파일만 준비해도 되지 않을까?
게다가 알아보니 PNG 를 사용하면 이후 맵 관련해서 만들 때 더 괜찮게 만들 수 있을 것 같았습니다. 원랜 PNG를 해석하는 걸 직접 만들까 했지만 이미 libpng 란 좋은 라이브러리가 있으니 그걸 사용하는 것으로 했죠.
프로젝트 준비 제가 라이브러리를 빌드하고 포함시키는 데 8시간이나 걸려버렸습니다.
리눅스는 많이 다뤄봤어도 윈도우에서 프로그래밍 하는 건 이전 미로찾기 글을 쓴 이후로 3년정도 됐군요. VS는 안 된다는 게 뭐가 그리 많은지… 고생 좀 했습니다.
일단, libpng 는 zlib 을 사용하므로, zlib 소스를 먼저 다운받습니다.
https://zlib.net/
비주얼 스튜디오에서 새로운 빈 프로젝트를 생성하고, 다음과 같은 구성으로 만듭니다.
파일들은 다운받은 소스에서 해당 부분만 가져오면 됩니다.
프로젝트 속성에서 여러가지를 바꿔줄 것입니다.
1 2 3 4 5 6 타겟: Release (x86) 구성 속성 -> 일반 -> 구성 형석: 정적 라이브러리(.lib) 구성 속성 -> 고급 -> 대상 파일 확장명: .lib 구성 속성 -> C/C++ -> 일반 -> 경고 수준: 모든 경고 해제(/W0) 구성 속성 -> C/C++ -> 코드 생성 -> 스펙터 완화: 사용 안 함 구성 속성 -> C/C++ -> 고급 -> 특정 경고 사용 안 함: 4996
이렇게 바꾸고 빌드하면, 프로젝트 폴더에 Release 에 zlib.lib 와 zlib.bsc 파일이 생성됩니다.
이제 필요한 걸 챙겼으니, libpng 를 솔루션 내에서 새 프로젝트 생성으로 프로젝트를 추가해 줍니다.
http://www.libpng.org/pub/png/libpng.html
파일 목록은 다음과 같습니다.
pngconf.h 파일은 pngconf.h.prebuilt 파일을 가져다가 복사해서 이름만 바꿔주면 됩니다.
해당 프로젝트의 속성도 바꿔줄 것입니다.
추가 포함 디렉터리를 zlib 폴더로 지정을 해줘야 합니다.
1 2 3 4 5 6 7 타겟: Release (x86) 구성 속성 -> 일반 -> 구성 형석: 정적 라이브러리(.lib) 구성 속성 -> 고급 -> 대상 파일 확장명: .lib 구성 속성 -> C/C++ -> 일반 -> 추가 포함 디렉터리: $(ProjectDir)..\zlib 구성 속성 -> C/C++ -> 일반 -> 경고 수준: 모든 경고 해제(/W0) 구성 속성 -> C/C++ -> 코드 생성 -> 스펙터 완화: 사용 안 함 구성 속성 -> C/C++ -> 고급 -> 특정 경고 사용 안 함: 4996
이제, libpng 를 사용할 프로젝트를 솔루현에 추가해 줍니다.
프로젝트의 속성을 변경합니다.
1 2 3 4 5 6 7 타겟: Release (x86) 구성 속성 -> C/C++ -> 일반 -> 추가 포함 디렉터리: $(ProjectDir)..\libpng 구성 속성 -> C/C++ -> 일반 -> 경고 수준: 모든 경고 해제(/W0) 구성 속성 -> C/C++ -> 코드 생성 -> 스펙터 완화: 사용 안 함 구성 속성 -> C/C++ -> 고급 -> 특정 경고 사용 안 함: 4996 구성 속성 -> 링커 -> 일반 -> 추가 라이브러리 디렉터리: $(ProjectDir)..\Release; 구성 속성 -> 링커 -> 입력 -> 추가 종속성: zlib.lib;libpng.lib
위와 같이 설정하고, 아래 코드를 실행시키면 정상적으로 함수가 실행되는 것을 확인할 수 있습니다.
프로젝트 준비는 끝났습니다.
1 2 3 4 5 6 7 8 9 #include <stdio.h> #include <png.h> int main ( void ) { png_structp png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING, NULL , NULL , NULL ); printf ( "Hello World!\n" ); return 0 ; }
PNG 파일 읽기 인터넷과 libpng 의 소스와 메뉴얼을 뒤적거리면서, 파일을 읽어들여 사진의 데이터를 배열로 만들 수 있었습니다. 예를 들어 3x2 크기를 가진 파일에 각각 [#ff0000, #00ff00, #0000ff, #ff0000, #00ff00, #0000ff] 코드를 가졌다면 그림은 다음과 같겠죠.
그랬을 때, 만들어질 배열의 구조는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 [ [ 255 , 0 , 0 , 255 , 0 , 255 , 0 , 255 , 0 , 0 , 255 , 255 , ] , [ 255 , 0 , 0 , 255 , 0 , 255 , 0 , 255 , 0 , 0 , 255 , 255 , ] , ]
(R, G, B, A) 가 한 픽셀의 정보를 가진 것으로, 무조건 4바이트씩 한 픽셀의 데이터를 쭉 잇고, 2차원 배열로 높이 만큼 만든 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 png_minfo_t * read_png ( char *file ) { png_byte signature[8 ] = { 0 , }; png_minfo_t *pinfo = NULL ; if ( PathFileExistsA( file ) ) { FILE *fp = fopen( file, "rb" ); if ( !fp ) { printf ( "Can not file open. [%s]\n" , file ); return NULL ; } fread( signature, 1 , sizeof ( signature ), fp ); if ( png_sig_cmp( signature, 0 , sizeof ( signature )) != 0 ) { printf ("File [%s] is not png.\n" , file); fclose( fp ); return NULL ; } png_structp png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING, NULL , user_error_fn, user_warning_fn ); if ( !png_ptr ) { printf ( "png_create_read_struct faild\n" ); fclose( fp ); return NULL ; } png_infop info_ptr = png_create_info_struct( png_ptr ); if ( !info_ptr ) { printf ( "png_create_info_struct faild\n" ); png_destroy_read_struct( &png_ptr, NULL , NULL ); fclose( fp ); return NULL ; } png_infop end_info = png_create_info_struct( png_ptr ); if ( !end_info ) { printf ( "png_create_info_struct [end_info]\n" ); png_destroy_read_struct( &png_ptr, &info_ptr, NULL ); } if ( setjmp( png_jmpbuf( png_ptr ) ) ) { printf ( "Error during init_io\n" ); png_destroy_read_struct( &png_ptr, &info_ptr, &end_info ); fclose( fp ); return NULL ; } png_init_io( png_ptr, fp ); png_set_sig_bytes( png_ptr, sizeof ( signature ) ); png_read_info( png_ptr, info_ptr ); png_uint_32 width = png_get_image_width( png_ptr, info_ptr ); png_uint_32 height = png_get_image_height( png_ptr, info_ptr ); png_byte color_type = png_get_color_type( png_ptr, info_ptr ); png_byte bit_depth = png_get_bit_depth( png_ptr, info_ptr ); int pass = png_set_interlace_handling( png_ptr ); png_read_update_info( png_ptr, info_ptr ); if ( png_get_color_type( png_ptr, info_ptr ) != PNG_COLOR_TYPE_RGBA ) { printf ( "File color type must be PNG_COLOR_TYPE_RGBA\n" ); png_destroy_read_struct( &png_ptr, &info_ptr, &end_info ); fclose( fp ); return NULL ; } pinfo = (png_minfo_t *)malloc ( sizeof ( png_minfo_t ) ); pinfo->width = width; pinfo->height = height; pinfo->buf = (png_bytepp)png_malloc( png_ptr, sizeof ( png_bytep ) * height ); if ( setjmp( png_jmpbuf( png_ptr ) ) ) { printf ( "Error during read image\n" ); png_destroy_read_struct( &png_ptr, &info_ptr, &end_info ); fclose( fp ); return NULL ; } png_size_t wsize = width * sizeof ( png_rgba_pixel_t ); for ( int y=0 ; y < height; y++ ) { pinfo->buf[y] = (png_bytep)png_malloc(png_ptr, png_get_rowbytes(png_ptr, info_ptr)); } png_read_image( png_ptr, pinfo->buf ); png_read_end( png_ptr, end_info ); png_destroy_read_struct( &png_ptr, &info_ptr, &end_info ); fclose( fp ); return pinfo; } else { printf ( "No such file [%s]\n" , file ); } return NULL ; }
위 함수의 인자로 파일 이름을 주고 실행하면, 아래 구조체로 압축이 풀린 정보를 얻을 수 있습니다.
1 2 3 4 5 typedef struct png_minimal_info { png_uint_32 width; png_uint_32 height; png_bytepp buf; } png_minfo_t ;
동적할당 된 정보를 해제할 땐 아래 함수를 실행하면 됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void free_png_minfo ( png_minfo_t *minfo ) { if ( minfo ) { if ( minfo->buf ) { for ( int y = 0 ; y < minfo->height; y++ ) { if ( minfo->buf[y] ) { free ( minfo->buf[y] ); } } free ( minfo->buf ); } free ( minfo ); } }
정보를 출력해 보면 아래와 같은 내용을 얻을 수 있었습니다.
x: 24, y: 14 위치엔 (201, 149, 127) 색이 불투명도 100%로 존재한다는 뜻입니다.
해당 정보를 가지고 아스키 아트를 만들어 보았습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const char PREFIX_ASCII[] = "#,.0123456789:;@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$" ;typedef struct _ascii_info { int width; int height; char **buf; } ascii_info_t ; ascii_info_t * png2ascii ( png_minfo_t *minfo ) { if ( minfo == NULL ) { printf ( "Info struct is null.\n" ); return NULL ; } ascii_info_t *info = (ascii_info_t *)malloc ( sizeof ( ascii_info_t ) ); info->width = ( ( minfo->width * 2 ) + 1 ); info->height = minfo->height; info->buf = (char **)malloc ( sizeof ( char * ) * minfo->height ); for ( int y = 0 ; y < minfo->height; y++ ) { png_bytep row = minfo->buf[y]; info->buf[y] = (char *)malloc ( sizeof ( char ) * info->width ); for ( int x = 0 ; x < minfo->width; x++ ) { png_rgba_pixel_t pixel; memcpy ( &pixel, ( row +(x * sizeof ( png_rgba_pixel_t )) ), sizeof ( png_rgba_pixel_t ) ); if ( pixel.alpha == 0 ) { snprintf ( info->buf[y]+(x*2 ), ( info->width - (x*2 )), " " ); } else { png_byte grey = ( pixel.red + pixel.green + pixel.blue ) / 3 ; char c = PREFIX_ASCII[grey * ( strlen ( PREFIX_ASCII ) - 1 ) / 256 ]; snprintf ( info->buf[y] + ( x * 2 ), ( info->width - ( x * 2 ) ), "%c%c" , c, c ); } } info->buf[y][info->width - 1 ] = '\0' ; } return info; }
위 함수를 실행시키면 ascii_info_t
구조체에 정보가 담기게 되는데, 출력해 보면 아래 결과가 보이게 됩니다.
마무리 게임을 위해 올린 코드에서 색깔을 입히도록 했습니다.
깃허브에서 프로젝트 전체를 확인하실 수 있습니다.
https://github.com/raravel/lib-png2ascii
읽어주셔서 감사합니다.