C++/Game

[게임 프레임워크 개발 일지] #5 Multi Style이 적용된 Text

Kareus 2022. 2. 18. 01:50

SFML에서의 텍스트는 Text 클래스를 사용하면 되지만, 그대로 쓰기에는 문제점이 있습니다.

 

첫 번째 문제는, 스타일이나 색깔을 객체 하나 당 하나씩만 쓸 수 있다는 점입니다.

보통 여러 개의 스타일을 혼합해서 쓸 수 있게 해주는 텍스트를 RichText라고 부르는 데,

SFML에는 기본적으로 이런 RichText가 존재하지 않습니다.

 

그러니 직접 만들어야 합니다.

Text에 스타일을 하나만 적용할 수 있다고 했으니, Text 객체를 여러 개 만들어서 처리해야 합니다.

여간 귀찮은 일이 아니군요. 그래서 전지전능한 구글에 검색해서 누군가가 미리 만든 것을 github에서 찾을 수 있었습니다.

source: https://github.com/skyrpex/RichText

 

한 텍스트 두 스타일

이 RichText는 스타일과 텍스트를 스트림 형식으로 받아서 각각 처리합니다.

입력할 텍스트만 잘 처리하면 됩니다. 벌써 문제 하나가 해결이 됐네요.

저는 게임에 사용할 dialogue를 txt 파일로 수정할 수 있게 하고 싶었기 때문에

RichTextFormat이라는 클래스를 하나 만들고 이를 읽어들여서 출력하는 함수를 추가했습니다.

여기서 거슬렸던 것이, 텍스트에 적용할 스타일이나 색깔의 키워드가 맞는지 검사하는 부분이었는데

if-else 문이 너무 많이 연달아 나오는 것이었습니다.

 

텍스트가 많을 때 일일이 검사하면 비용이 커질 것이라 생각해서 조금이라도 줄여보고자 Trie를 구현하기로 했습니다.

메모리를 좀 먹겠지만... 그래픽이나 사운드에 비하면 별 거 아닙니다.

자료 구조를 직접 정의해서 써먹어 보는 것은 자료 구조 수강할 때 이후로는 이번이 처음입니다.

게다가 Trie는 자료 구조가 아니라 PS 공부할 때 배웠습니다.

 

아무튼, 여기서 사용할 Trie는 복잡할 것 없이 문자열의 존재 여부만 판단하면 됩니다.

그런데 구현하다보니 생긴 문제가, 존재할 경우에 각 문자열마다 지정된 명령을 수행해야 하는데, 이게 좀 복잡했습니다.

람다식 객체를 끼워놓자니 파라미터에 의존적이라 (람다식에 Text 객체를 넘겨줘야 되니) 타입 정의하기가 이상해지고,

지금 찾는 키워드가 존재하는지, 무엇인지도 모르니 find 함수의 파라미터로도 못 쓴다는 문제였습니다.

 

그래서 좀 돌아가는 길이지만 등록하는 키워드마다 번호를 지정하게 했고

그 키워드를 찾으면 해당 번호를, 아니면 -1를 반환하게 했습니다.

탐색을 실행하는 쪽에서는 키워드마다 고유 번호를 지정하고, 반환값에 따라 알아서 코드를 실행하면 됩니다.

 

조금 뒤에 이야기가 나오겠지만, 문자열 종류만 해도 대표적으로 string, wstring (u16string, u32string)이 있으므로 Trie를 template class로 만들어야 했습니다.

 

#ifndef _TRIE_H
#define _TRIE_H

#include <string>
#include <unordered_map>

template <typename Char=char>
class Trie
{
private:
	std::unordered_map<Char, Trie*> next;
	int finish;

	void _insert(std::basic_string_view<Char> str, int idx, int finish_code);

	Trie<Char>* _remove(Trie* root, std::basic_string_view<Char> str, int idx);

public:
	Trie();

	~Trie();

	void insert(std::basic_string_view<Char> str, int finish_code = 0);

	void remove(std::basic_string_view<Char> str);

	void clear();
    
	int find(std::basic_string_view<Char> str);

	bool empty() const;
};

template <typename Char>
Trie<Char>::Trie() : finish(0)
{
	clear();
}

template <typename Char>
Trie<Char>::~Trie()
{
	clear();
}

template <typename Char>
void Trie<Char>::_insert(std::basic_string_view<Char> str, int idx, int finish_code)
{
	if (idx == str.size())
	{
		finish = finish_code;
		return;
	}

	char key = str.at(idx);
	for (auto& p : next)
		if (p.first == key)
		{
			p.second->_insert(str, idx + 1, finish_code);
			return;
		}

	Trie<Char>* node = new Trie<Char>;
	next.emplace(key, node);
	node->_insert(str, idx + 1, finish_code);
}

template <typename Char>
Trie<Char>* Trie<Char>::_remove(Trie<Char>* root, std::basic_string_view<Char> str, int idx)
{
	if (!root) return nullptr;

	if (idx == str.size())
	{
		if (root->finish) root->finish = 0;
		if (root->empty())
		{
			delete root;
			root = nullptr;
		}

		return root;
	}

	char key = str.at(idx);
	root->next[key] = _remove(root->next[key], str, idx + 1);

	if (root->empty() && !root->finish)
	{
		delete root;
		root = nullptr;
	}

	return root;
}

template <typename Char>
void Trie<Char>::insert(std::basic_string_view<Char> str, int finish_code)
{
	if (finish_code == -1) {} //warn something?
	_insert(str, 0, finish_code);
}

template <typename Char>
void Trie<Char>::remove(std::basic_string_view<Char> str)
{
	if (!find(str)) return; //warn soemthing [2]?
	_remove(this, str, 0);
}

template <typename Char>
void Trie<Char>::clear()
{
	for (auto& p : next) delete p.second;
	next.clear();
}

template <typename Char>
int Trie<Char>::find(std::basic_string_view<Char> str)
{
	Trie<Char>* current = this;
	for (auto& key : str)
	{
		if (current->next.find(key) == current->next.end()) return -1;
		current = current->next[key];
	}

	return current->finish;
}

template <typename Char>
bool Trie<Char>::empty() const
{
	return next.empty();
}
#endif

 

아래는 사용 예시입니다.

Trie<char> trie;
trie.insert("asdfasdf", 1);
trie.insert("asdfaaaa", 2);

std::string test = "asdfaaaa";

int code = trie.find(test);

switch (code)
{
case 1:
    std::cout << "You are testing 1.";
    break;

case 2:
    std::cout << "You are testing 2.";
    break;

default:
    std::cout << "No keyword found.";
    break;
}

 

단순히 Text에서뿐만 아니라, 문자열 처리쪽에서는 고질적인 문제가 하나 있습니다.

한글, 정확히는 Non-ASCII 문자의 처리 문제인데요.

 

인코딩 상에서 문자 하나에 할당되는 바이트 수가 다르다보니 출력에서 문제가 발생합니다.

예를 들어 string에서 한 글자씩 출력한다는 말은 1바이트씩 출력한다는 것과 같은 말입니다.

그런데 한글은 2바이트로 표현하므로, 1바이트마다 끊어서 출력하면 글자가 제대로 나오지 않습니다.

길이 계산에서도 한글 글자가 포함되어 있으면 제대로 구할 수 없습니다.

문자열이 "abc가나다" 였으면, 6이 아니라 9로 계산됩니다.

 

한 글자씩 출력하는 효과 (흔히 게임 대화에서 한 글자씩 주루룩 나오는 그 효과 말입니다)를 만들려고 보니

글자 출력도 제대로 안되고, 예외 처리하기도 복잡해지더군요.

 

그래서 wstring으로 바꿨습니다. 얘는 영어든 한글이든 같은 바이트로 표현할 수 있으니 한결 편합니다.

sf::String은 string이든 wstring이든 안 가리고 잘 먹지만, 어느 한 타입으로 변환해서 출력하려 하면 (예를 들어 string을 sf::String 변수에 대입하고, 이를 wstring으로 변환한다면) 한글이 깨지는 문제가 발생해서, 텍스트 파일을 불러올 때는 wstring으로만 불러오게 했습니다. 프로그램 내에서는 sf::String에 입력한 원본 문자열이 string인지 wstring인지 알 수 없었습니다.

 

그렇게 돌고 돌아서, 나온 결과물은 다음과 같습니다.

아주 잘 된다

이거 하나를 위해 할 일이 참 많군요