목차
스타팅 파일
- 이전 강의는 GUI와 메인메뉴였다.
- 스타팅 파일은 GUI메인메뉴를 만들고 난 결과물이다.
아래에서는 벡터폰트(ttf, otf)를 사용하는 것을 설명한 것이다. fnt 폰트를 로딩하는 것에 대해서는 이동근이라는 변호사가 라이브러리를 따로 만들었다. Raylib Bitmapfont Loader
목적
한글등 유니코드를 지원하지 않는다면, 게임의 표현력은 한계가 있을 수 밖에 없다. 특히 한국에서 게임을 출시한다면 한글은 필수 일 것이다.
그런데 raylib는 불행히도 drawtext()함수만으로는 한글을 제대로 입력할 수 없다.
이는 love2d와 다른 점이다. 그런걸 보면 love2d가 매우 강력함을 알 수 있다. love2d는 산업표준인 FreeType2 라이브러리를 이용해서 벡터 폰트인 ttf와 otf도 그냥 폰트로 지정한 후 print함수로 써주면 한글 등의 출력이 바로 된다1).
그런데 raylib의 경우 FreeType2 대신에 stb_truetype이라는 라이브러리를 이용하여 ttf를 rasterization2) 한다3).
이 stb_truetype의 문제인지, 아니면 단순히 raylib를 만든 raysan의 개인취향인지 모르겠지만
Freetype2를 이용하는 love2d와 sdl은 폰트를 한번 래스터화(벡터값을 형상화)하면 계속하여 그 폰트를 사용할 수 있지만,
raylib 에서는 문자열에 포함된 글자만 래스터화하기 때문에 미리 문자열이 무엇인지를 알아야 한다.
이러한 raylib의 방식은 성능을 염두에 둔 것으로 보인다. 필요한 글자만 형상화해서 램에 저장한 후 불러오므로 가벼울 수 밖에 없다.
다만 램 용량이 넘쳐나는 요새에도 과연 이런 방식으로 할 필요가 있을까 의문이긴 하다.
raylib의 텍스트 출력 개념
폰트에는 비트맵이지미폰트가 있고, 벡터 폰트가 있다.
1. 비트맵이지미 폰트
비트맵 픽셀로 이미지를 만들어서 그 이미지를 글자로 쓰는 방식이다. 픽셀폰트라고도 한다.
비트맵이지미 폰트는 예전 90년대 윈도우가 쓰던 방식으로 파일확장자가 fnt이다. 별도의 png 그림파일에 문자의 그림을 그려놓은 후, 그 그림의 위치를 fnt 파일에 저장해 놓고 해당 그림만 불러온느 방식이 fnt 폰트 방식이다.
쉽게 말해서 2d에서 게임캐릭터를 애니메이션화할 때, 이미지 스프라이트에서 해당구간만 가져와서 이미지를 출력하는 것과 동일한 개념인 것이다.
비트맵방식이므로 확대를 하면 글자가 이쁘지 않다. 따라서 요새는 잘 안 쓰인다.
유니티의 경우 textmeshpro에서 이러한 비트맵 폰트 방식을 이용한 적이 있다. 벡터로 저장된 ttf의 글자들을 32혹은 64픽셀 크기로 일일이 불러온 후 해당 글자마다 다시 그림을 쫙 그려준 것이다. 글자를 비트맵으로 만들면 아무래도 그림을 수정하듯 직관적으로 수정할 수 있으니 이렇게 비트맵으로 전환하는 수요가 아직도 가끔 있다.
TTF를 비트맵폰트인 fnt로 전환하는 가장 유명한 프로그램으로는 angelcode에서 만든 bmfont 가 있다.
2. 벡터 폰트
트루타입폰트(ttf)와 오픈타입폰트(otf)가 바로 벡터 폰트이다.
트루타입폰트는 2차 베지어 곡선을 이용하고 오픈타입폰트는 3차 베지어 곡선을 이용한다고 한다4).
재밌는 사실은, ttf는 애플이 만들었지만 현재 윈도우에서 가장 많이 쓰이고 있고(물론 윈도우도 지금은 오픈타입으로 많이 넘어가고 있다),
otf규격은 마이크로소프트가 만든 것이지만 지금 현재 애플의 주력이라는 것이다5).
3. raylib의 폰트 처리 방식
LoadFontEx는 벡터 폰트인 ttf 와 otf를 처리하는 함수이다. 앞서 언급하였듯 stb_truetype 라이브러리를 이용하여 필요한 벡터 글자들을 메모리에 불러와서 글자 형상화(래스터레이션) 한 후에 메모리에 저장한다.
LoadBMFont는 비트맵폰트인 fnt파일을 불러와서 각 이미지 마다 코드포인트(글자의 위치)를 지정한 후에 메모리에 저장한다.
rtext.c를 뜯어보면 알겠지만, loadBMFont는 숨겨져 있다. LoadFont()함수에서 확장자가 fnt면 LoadBMFont()를 불러오고, 확장자가 ttf나 otf면 loadfontEX()를 불러오기 때문이다.
참고로, Loadfontex()에는 폰트 크기 등의 다른 인자들도 있으므로 ttf나 otf 폰트를 불러올 때에는 아주 특별한 경우가 아니면 LoadFont()로 불러오지 않고 LoadFontEx()함수를 불러오게 된다.
그런데 내부적으로 벡터값을 메모리에 래스터레이징화하는 이 LoadFontEx()가 참 고약하다. 내부적으로 512글자만 쓰게 되어 있다.
더 가관인건 LoadBMFont()함수 역시 fnt파일이 참조하는 png 이미지 파일을 딱 하나만 상정하고 있다. 한글 폰트는 11000개 이상의 글자가 나오므로 png이미지 파일이 여러개가 나오며, 이미지 파일의 인덱스도 생성되는데, LoadBMFont()는 매우 제한된 글자만 읽게 되어 있는 것이다.
아마도 퍼포먼스를 위하여 이렇게 제한을 걸어 놓은 것 같은데, 그렇기 때문에 raylib에서 한글을 입력하는 것은 매우 짜증나고 성가신 일이다.
따라서 다음과 같이 별도의 Font 헬퍼 유틸리티를 만들자.
벡터 폰트에 대한 헬퍼 유틸리티
1. Fontutil.h
다음과 같이 함수를 선언하였다.
- fontutil.h
#pragma once #include <string> #include "raylib.h" #include <vector> // Remove codepoint duplicates if requested int *CodepointRemoveDuplicates(int *codepoints, int codepointCount, int *codepointResultCount); // 문자열에서 필요한 유니코드 문자를 뽑아서, 이 필요한 문자만 폰트 텍스쳐로 래스터라이징한 후 메모리에 저장함 Font GetFont(std::vector<std::string> _texts); // 폰트 파일을 지정할 수 있는 함수 Font GetFont(std::vector<std::string> _texts, char* _fontFile);
CodepointRemoveDuplicates는 해당 문자열에서 중복된 글자들을 뽑아내어서 해당 문장을 구성하는 요소 글자를 가려내는 작업이다.
해당 함수의 소스코드는 깃헙에 raysan이 올려놓은 것을 그대로 가져왔다.
2. fontutil.cpp
실제 구현하는 소스 코드는 다음과 같다.
- fontutil.cpp
#ifndef __FONTUTIL__ #define __FONTUTIL__ #include "fontutil.h" // Remove codepoint duplicates if requested // WARNING: This process could be a bit slow if there text to process is very long int *CodepointRemoveDuplicates(int *codepoints, int codepointCount, int *codepointsResultCount) { int codepointsNoDupsCount = codepointCount; int *codepointsNoDups = (int *)calloc(codepointCount, sizeof(int)); memcpy(codepointsNoDups, codepoints, codepointCount*sizeof(int)); // Remove duplicates for (int i = 0; i < codepointsNoDupsCount; i++) { for (int j = i + 1; j < codepointsNoDupsCount; j++) { if (codepointsNoDups[i] == codepointsNoDups[j]) { for (int k = j; k < codepointsNoDupsCount; k++) codepointsNoDups[k] = codepointsNoDups[k + 1]; codepointsNoDupsCount--; j--; } } } // NOTE: The size of codepointsNoDups is the same as original array but // only required positions are filled (codepointsNoDupsCount) *codepointsResultCount = codepointsNoDupsCount; return codepointsNoDups; } // 한글 폰트를 그리는데 필요한 글자만 전체 문자열에서 가지고 온 후에, 필요한 글자만 폰트로 로딩한다. Font GetFont(std::vector<std::string> _texts) { std::string totalText; // for (int i = 0; i < text.size();i++) for (unsigned int i = 0; i < _texts.size();i++) { totalText += _texts[i].c_str(); } // Get codepoints from text int codepointCount = 0; int *codepoints = LoadCodepoints(totalText.c_str(), &codepointCount); // Removed duplicate codepoints to generate smaller font atlas int codepointsNoDupsCount = 0; int *codepointsNoDups = CodepointRemoveDuplicates(codepoints, codepointCount, &codepointsNoDupsCount); UnloadCodepoints(codepoints); Font _font = LoadFontEx("Assets/Cafe24Ohsquare.ttf", 36, codepointsNoDups, codepointsNoDupsCount) ; // Set bilinear scale filter for better font scaling SetTextureFilter(_font.texture, TEXTURE_FILTER_BILINEAR); // Free codepoints, atlas has already been generated free(codepointsNoDups); return _font; } // 폰트 파일을 지정할 수 있는 함수 Font GetFont(std::vector<std::string> _texts, char* _fontFile) { std::string totalText; // for (int i = 0; i < text.size();i++) for (unsigned int i = 0; i < _texts.size();i++) { totalText += _texts[i].c_str(); } // Get codepoints from text int codepointCount = 0; int *codepoints = LoadCodepoints(totalText.c_str(), &codepointCount); // Removed duplicate codepoints to generate smaller font atlas int codepointsNoDupsCount = 0; int *codepointsNoDups = CodepointRemoveDuplicates(codepoints, codepointCount, &codepointsNoDupsCount); UnloadCodepoints(codepoints); Font _font = LoadFontEx(_fontFile, 36, codepointsNoDups, codepointsNoDupsCount) ; // Set bilinear scale filter for better font scaling SetTextureFilter(_font.texture, TEXTURE_FILTER_BILINEAR); // Free codepoints, atlas has already been generated free(codepointsNoDups); return _font; } #endif
해당 문자열을 구성하는 글자 요소를 가져 온 후에 이에 대하여 우리가 앞으로 사용할 벡터 폰트 파일에서 글자 형태를 가져와서 형상화한 후 메모리에 저장하여 그 폰트를 반환하는 것이다.
헬퍼파일 사용하기
1. 문자열 선언하기
우리는 menu.cpp에서 한글 폰트를 사용할 것이다. 최상단에 다음과 같이 문자열과 폰트를 선언하자
std::vector<std::string> texts { "한글로 인사드립니다. 플래피버드입니다! ", "게임 시작", // 버튼1 "점수판", // 버튼 2 "게임끝" // 버튼3 }; Font font;
2. 폰트 가져오기
init() 함수 내에서 다음과 같이 문자열과 폰트파일 경로를 넣어주면 문자열에 필요한 글자를 형상화한 폰트가 메모리에 저장된다.
// font = GetFont(texts); font = GetFont(texts, "Assets/SDKukdetopokki-bBd.otf");
3. 텍스트 그리기
지정된 폰트로 텍스트를 그려야 하므로 DrawTextEX()라는 raylib 함수를 이용한다.
Draw()메서드 내에서 다음과 같이 구현한다.
// Draw provided text with loaded font, containing all required codepoint glyphs DrawTextEx(font, texts[0].c_str(), (Vector2) { 100, 110 }, 32, 5, WHITE); DrawTextEx(font, texts[1].c_str(), (Vector2) { 100, 160 }, 32, 5, WHITE); DrawTextEx(font, texts[2].c_str(), (Vector2) { 100, 210 }, 32, 5, WHITE);
텍스트를 가운데로 정렬하는 방법을 참고하면 가운데 정렬을 할 수 있다.
결론
스크린 샷 및 프로젝트 파일
위와 같이 한글이 잘 입력되는 것을 알 수 있다.
GUI버튼에도 똑같이 폰트를 지정하여 글자를 그리게 해주어야 할 것이다.
GUI버튼까지 수정한 소스파일은 다음과 같다.
다음에는 이제 다시 게임 화면 구동 로직으로 돌아가자
과제 및 향후 해야 할 일
1. fnt폰트의 로딩 문제 해결
fnt폰트는 rtext.c에서 LoadBMFont() 함수로 읽는다. 그런데 여기에는 치명적인 단점이 있다.
rayilb에서는 fnt폰트를 읽을 때 다음의 순서를 해당 글자의 비트맵 위치를 읽는다.
sscanf(buffer, "char id=%i x=%i y=%i width=%i height=%i xoffset=%i yoffset=%i xadvance=%i",
그런데 angelcode에서 만든 bmfont로 한글이 들어가 있는 ttf 파일을 변환하면 마지막에 그림파일의 위치를 넘겨주는 인자도 같이 저장되어 있다.
raylib에서는 몇번째 그림파일인지를 읽지 않으니 당연히 fnt폰트를 읽을 수 없다.
따라서 한글과 같이 코드 수가 많은 유니코드는 raylib의 LoadBMFont() 메서드 부분을 고칠 필요가 있다.
그래서 이를 고친 후
Github에 공유하였다.
2. 벡터 폰트를 모두 읽어 오기
ttf나 otf 함수를 읽을 때도 비슷한 문제가 발생한다.
raylib에서는 퍼포먼스를 위해 문자열에 포함된 글자 코드만 래스터화 해주고 폰트로 저장한다.
글자가 적을 때야 위와 같이 하면 되겠지만, RPG 게임과 같이 글자가 많은 경우에는 미리 한글코드를 모두 불러올 필요가 있다.
이 역시 향후에 고쳐야 할 과제이다.
나중에 시간되면 이러한 문제들을 고쳐보도록 하자
더보기
다음에는 게임 장애물 만들기를 해보자
로그인