{{:raylib:flappybirdlocalization.png?600|플래피 버드 다국어 지원하기}}
===== 필요성 =====
한국은 너무 좁다. 한국어만 지원한다면 한국어 사용인구 5천만(북한까지 7천만)을 제외하고 나머지를 포기하는 것이다.
그렇다고 영어로 앱을 만들자니 모국어를 지원도 하지 않는 것은 말이 안될 것이다.
앱을 2개로 만드는 것은 그다지 좋은 방법은 아닐 것이다. 따라서 설정에 따라 지원되는 언어가 바뀌게 해보자.
===== 스타팅 파일 =====
* [[raylib:flappybird:json_파일_저장_및_읽기|JSON 파일 저장 및 읽기]]가 이전에 했던 강의이다.
* 그 사이에 [[raylib:util:raylib_loadbmfont_extender|한글 비트맵폰트 불러오기]] 헬퍼 유틸리티도 만들었다. 이제 한글을 자유롭게 사용하자
* 스타팅 소스 파일은 {{ :raylib:flappybird_13.zip |다국어 지원하기 스타팅 소스파일}}이다. 기존에 JSON파일 저장 및 읽기에다가 비트맵 폰트 읽기 기능도 같이 넣은 것이다.
===== 개념 =====
JSON 파일 구조를 어떻게 할 것인지를 고민해보자.
한 나라별로 문자열을 만들 것인지 아니면 해당 문자열에 대하여 각 나라별로 정리할 것인지 2가지 방법이 있을 것이다.
일단은 문자열이 많지 않으니 각 나라별로 문자열을 정리하도록 하자
즉, 각 나라가 배열의 첫번째 요소이다. 이런 식으로 localization.json이란 파일을 만들자.
{
"en": {
"Load": "Load",
"Log": "Log",
"MainMenu": "MainMenu",
"Save": "Save",
"Settings": "Settings",
"SettingsScreen": "Settings Screen",
"Start": "Start",
"Title": "DK Flappy Bird!",
"Volume": "Volume",
"exit": "Exit"
},
"ko": {
"Load": "불러오기",
"Log": "로그기록",
"MainMenu": "메인메뉴",
"Save": "저장하기",
"Settings": "환경설정",
"SettingsScreen": "환결설정 화면",
"Start": "시작하기",
"Title": "DK 플래피 버드!",
"Volume": "볼륨",
"exit": "끝내기"
}
}
===== 다국어 매니저 만들기 =====
==== 1. 헤더파일 ====
기존 config.h 헤더파일에 LangManager란 싱글톤 클래스를 만들었다.
class LangManager
{
private:
LangManager() {}
static LangManager* instance;
std::string GetString(const char* lang, std::string _elem);
nlohmann::json outputData;
public:
static LangManager* GetInstance()
{
if (instance != nullptr)
{
return instance;
}else
{
instance = new LangManager();
return instance;
}
}
~LangManager();
void Init();
void LoadStrings(const char* lang);
// 멤버 변수
std::string title, start, mainMenu, settings, exit, settingsScreen, save, load, volume, log;
};
[[https://github.com/nlohmann/json|Niels Lohmann의 JSON라이브러리]]가 c++의 std::string 값을 아웃풋으로 내놓으므로, 저장할 멤버 변수들도 모두 std::string 타입으로 지정하였다.
==== 2. 실행 코드 ====
아래와 같이 config.cpp에 LangManager에 대한 실행코드를 만들었다.
void LangManager::Init()
{
title = "DK Flappy Bird!";
start = "Start";
mainMenu = "MainMenu";
settings = "Settings";
exit = "Exit";
settingsScreen = "Settings Screen";
save = "Save";
load = "Load";
volume = "Volume";
log = "Log";
}
void LangManager::LoadStrings(const char* lang)
{
using namespace nlohmann;
std::ifstream f("localization.json");
outputData = json::parse(f);
// 해당 언어가 있는지 확인하고 없으면 영어를 출력하기
if (!outputData.contains(std::string(lang)))
{
lang = "en";
}
// Strings set up by language localization.
title = GetString(lang, "Title");
start = GetString(lang, "Start");
mainMenu = GetString(lang, "MainMenu");
settings = GetString(lang, "Settings");
exit = GetString(lang,"Exit");
settingsScreen = GetString(lang, "SettingsScreen");
save = GetString(lang, "Save");
load = GetString(lang, "Load");
volume = GetString(lang, "Volume");
log = GetString(lang, "Log");
f.close();
}
// 해당 언어중에 빠트린 문장이 있으면 영어 문장을 출력하기
std::string LangManager::GetString(const char* _lang, std::string _elem)
{
if (!outputData[_lang].contains(_elem)) return outputData["en"][_elem];
else return outputData[_lang][_elem];
}
LangManager::~LangManager()
{
}
=== 가. LoadString() 메서드 ===
json 파일을 불러온 다음에 불러올 언어가 있는지 확인한다. 이를테면 만약 "jp"로 설정된 일본어가 있는지 확인한 후 "jp"가 없으면 "en"으로 설정된 영어를 불러온다.
다음에 각 언어별로 해당하는 문자열 값을 GetString()함수로 불러온다.
=== 나. GetString() 메서드 ===
각 문자열별로도 있는지 없는지를 체크한다. 만약 해당 문자열이 없으면 영어 문자열을 불러오게 하였다.
==== 3. LangManager 셋업 ====
=== 가. 초기화하기 ===
싱글톤으로 만든 클래스는 프로그램 시작점에서 인스턴스의 값을 지정해주어서 초기화를 해야 한다는 것을 잊지 말자.
다음과 같이. main.cpp의 main()함수 바로 앞에 클래스를 정의해주자
//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
SceneManager* SceneManager::instance = nullptr;
LangManager* LangManager::instance = nullptr;
int main(void)
{
=== 나. 불러오기 ===
우리는 이전에 [[raylib:싱글톤과_로고화면|싱글톤과 로고화면]]에서 한번 해본적이 있다.
main.cpp에서는 다음과 같이 싱글톤 클래스를 불러오면 된다.
langs = LangManager::GetInstance();
===== 포인터 버튼 GUI 만들기 =====
==== 1. 문제점 ====
기존처럼 텍스트의 값을 가져오는 방식이라면 언어가 바뀌었다고 하더라도 GUI화면은 바뀌지 않는다. 이미 GUI버튼을 로드할 때에 문자열이 값으로 넘겨졌고, 각 해당 GUI버튼에는 각각의 고유의 문자열 값이 있기 때문이다. 따라서 우리는 GUI버튼에 문자열 값을 따로 주기 보다는 LangManager가 가지고 있는 문자열의 주소를 지시하는 포인터를 저장해 주어야 한다. 아마도 이게 바로 우리가 C와 C++을 사용하는 주된 이유일 것이다. Lua와 같은 언어를 사용하면 우리는 버튼을 다시 리로딩해 주는 수고를 해야만 한다.
포인터를 사용하는 방법에 대해서는 [[raylib:포인터를_이용하여_최적화하기|포인터를 이용하여 최적화하기]]를 참고하자.
==== 2. 포인터 버튼 만들기 ====
문자열을 사용하는 변수는 GUI버튼에서 'title'변수였다. 이를 고치면 된다.
=== 가. GUI.h ===
다음과 같이 기존 GUI버튼을 상속하는 클래스를 하나 만들자.
// Button Pinter. It has pointer to title string
class ButtonPointer : public GUI
{
private:
std::string* titlePointer;
public :
ButtonPointer();
ButtonPointer(int button_x, int button_y, int button_width, int button_height, std::string* button_title, Font* _font);
virtual void Draw();
};
생성자와 Draw()함수를 오버로딩한 것을 알 수 있다.
=== 나. GUI.cpp ===
다음과 같이 값 대신에 포인터를 대입하고, Draw()메서드에서는 포인터의 값을 역참조하여 출력하게 하였다.
ButtonPointer::ButtonPointer() {}
ButtonPointer::ButtonPointer(int button_x, int button_y, int button_width, int button_height, std::string* button_title, Font* _font)
{
x = button_x;
y = button_y;
width = button_width;
height = button_height;
titlePointer = button_title; // 포인터를 대입한다. 버튼 자체는 문자열을 가지고 있지 않고 문자열에 대한 주소만 저장한다.
rect = Rectangle{float(x), float(y), float(width), float(height)};
font = _font;
}
void ButtonPointer::Draw()
{
DrawRectangle(x, y, width, height, DARKGREEN); // 버튼을 그린다.
Vector2 length = MeasureTextEx(*font, (*titlePointer).c_str(), 32, 5); // 글자의 포인트는 32으로 가정할 것이다. 32으로 가정한 글자의 가로 픽셀 크기를 구한다.
// 텍스트 출력
Vector2 pos = {(float)x + (width / 2) - (length.x / 2), (float)y + (height /2) - (length.y / 2)}; // 글자를 가로 및 세로 가운데 정렬한다.
DrawTextEx(*font, (*titlePointer).c_str(), pos, 32, 5, DARKBROWN);
if (isChecked) // 선택된 경우
{
DrawRectangle(x, y, width, height, Color{255, 255, 255, 85}); // 버튼을 그린다.
DrawRectangleLinesEx(rect, 4, Fade(BLACK, 0.3f)); // 버튼 꾸미기
}
}
=== 다. 이미지 버튼 ===
메뉴화면에서 각 나라를 클릭하기 위하여 이미지 버튼도 만들었다. 이 역시 기본 버튼을 상속하면 간단하게 만들 수 있다. 다음은 코드의 일부이다.
// Image Button
ImgButton::ImgButton() {}
ImgButton::ImgButton(int button_x, int button_y, int button_width, int button_height, Texture2D* _image)
{
x = button_x;
y = button_y;
width = button_width;
height = button_height;
boxImage = _image;
rect = Rectangle{float(x), float(y), float(width), float(height)};
}
void ImgButton::Draw()
{
DrawTexture(*boxImage, x, y, WHITE);
if (isChecked) DrawRectangleLinesEx(rect, 4, Fade(BLACK, 0.3f)); // 버튼 꾸미기
}
===== 메뉴 클래스에서 다국어 버튼 만들기 =====
메뉴 클래스에서는 다국어 버튼을 다음과 같이 불러오면 된다.
// 국기 이미지
flagImg[0] = LoadTexture("Assets/flags/us.png");
flagImg[1] = LoadTexture("Assets/flags/KR.png");
flagImg[2] = LoadTexture("Assets/flags/JP.png");
// 각 버튼 선언하기
button[0] = new ImgButton(GetScreenWidth() - 300, 20, 90, 60, &flagImg[0]);
button[1] = new ImgButton(GetScreenWidth() - 200, 20, 90, 60, &flagImg[1]);
button[2] = new ImgButton(GetScreenWidth() - 100, 20, 90, 60, &flagImg[2]);
button[3] = new ButtonPointer(GetScreenWidth() - 200, GetScreenHeight() - 200, 180, 40, &(langs->start), &menuManager->fontStrawberry); // 게임 시작 버튼
button[4] = new ButtonPointer(GetScreenWidth() - 200, GetScreenHeight() - 140, 180, 40, &langs->settings, &menuManager->fontStrawberry); // 환경설정 버튼
button[5] = new ButtonPointer(GetScreenWidth() - 200, GetScreenHeight() - 80, 180, 40, &langs->exit, &menuManager->fontStrawberry); // 게임 종료 버튼
flagImg는 이미지버튼의 각 이미지들의 텍스쳐를 말한다.
이전과 달리 각 다국어 이미지는 config.cpp에 있는 langs클래스 내의 각 멤버변수들이므로, 각 멤버변수들의 주소를
&langs->start
이런 식으로 그 주소를 지정해 주면 된다. 이렇게 포인터로 각 다국어의 메버변수들을 지정해 주면, 언어를 바꾸면 그 해당 언어로 문자열이 바로 바뀌는 것을 알 수 있다. 만약 포인터가 아니고 값으로 지정해주었다면 매번 언어가 바뀔 때마다 새롭게 버튼의 문자열을 바꿔주는 작업을 일일이 다시 했어야 한다.
일본어의 경우 현재 localization.json에 문자열이 없다. 따라서 일본어 대신 영어가 나오는 것을 알 수 있다.
{{:raylib:flappybirdlocalization.png?600|플래피 버드 다국어 지원하기}}
===== 결론 =====
==== 1. 소스 파일 ====
지금까지의 소스파일은 다음이다.
{{ :raylib:flappybird_14.zip |Raylib 다국어 지원하기}}