Unicode w programowaniu gier - podstawy

Właśnie skończyłem ulepszać framework nad którym pracuję do postaci wspierającej wewnętrznie Unicode. W tej chwili framework kompiluje się i działa zarówno w trybie Unicode jak i ASCII. Była to momentami ciężka walka i bardzo dziękuję tutaj Spax'owi za pomoc (z resztą to on mnie na Unicode nawrócił w pierwszej kolejności :) ). Dlatego chciałem podzielić się kilkoma doświadczeniami, które mogą przydać się początkującym w tej dziedzinie. 1. Nie mieszaj w projekcie różnych standardów kodowania. Ciężko potem nad tym zapanować. Dlatego od razu na początku warto zrobić sobie kilka typedef'ów, na przykład w taki sposób:
#ifdef _UNICODE
typedef wchar_t achar;
typedef std::wstring astring;
typedef std::wstringstream astringstream;
typedef boost::wformat aformat;
//(...)
#else
typedef char achar;
typedef std::string astring;
typedef std::stringstream astringstream;
typedef boost::format aformat;
#endif
I potem używać tych typedef'ów w całym kodzie. Ponieważ stałe znakowe / tekstowe w C++ należy poprzedzać literką L żeby oznaczyć, że są to wielobajtowe znaki, dlatego przydają się też makra:
#ifdef _UNICODE
#define ATEXT(x) L##x
#else
#define ATEXT(x) x
#endif
Każdą pojawiającą się w kodzie stałą tekstową lub znakową zapisujemy z użyciem tego makra, np. ATEXT('a') albo ATEXT("Hello"). WinAPI zapewnia makro TEXT, które w uproszczeniu wygląda podobnie, ale osobiście stosuję własne makro ATEXT, by oddzielić kod frameworka (w szczególności interfejsy i późniejszą logikę gry) od platformy. 2. Unikaj konwersji. Zamiana sposobu kodowania tekstu jest wolna i nierzadko skomplikowana, dlatego należy jak najbardziej jej unikać. Może się zdarzyć, że będziemy musieli korzystać z funkcji bibliotecznych języka C (takich jak printf, strcpy, _strdate). Funkcje te na szczęście mają swoje odpowiedniki dla Unikodu (np. wprintf, wcscpy, _wstrdate). Zamiast jednak używać jednej z tych rodzin i przygotowywać sobie konwerter ASCII/Unicode na nasze wewnętrzne stringi, lepiej jest stworzyć specjalne makrodefinicje, na przykład:
#ifdef _UNICODE
#define astrdate _wstrdate
#define astrcpy wcscpy
//(...)
#else
#define astrdate _strdate
#define astrcpy strcpy
//(...)
#endif
Następnie w kodzie można już używać tych makr (astrcpy, astrdate). Zauważmy, że podobnie skonstruowane jest WinAPI, w którym praktycznie każda funkcja przyjmująca parametr tekstowy (np. MessageBox) to tak na prawdę makrodefinicja na funkcje przyjmujące tekst ASCII/Unicode (MessageBoxA, MessageBoxW), zależnie od ustawień kompilacji projektu. 3. Oddzielaj mocno interfejs od implementacji. Jest to ogólna rada inżynierii oprogramowania, która także tutaj ma znaczenie. Dobrze napisane elementy framework'a powinny komunikować się z innymi za pośrednictwem wewnętrznych reprezentacji stringów (np. nasze astring) zupełnie nie przejmując się ich typem, a wszelkie konwersje Unicode/ASCII należy ograniczać tylko do strony implementacji. W szczególności kod gry opartej na takim framework'u nie powinien w ogóle mieć do czynienia z konwersjami. Wsparcie dla Unicode na różnych platformach jest wciąż skomplikowane, więc jeśli programiście zależy na przenośności kodu to musi zadbać, żeby zmian w kodzie związanym z konwersjami było jak najmniej, i żeby te zmiany były jak najbardziej wyizolowane. 4. Uważaj na pułapki przy wczytywaniu plików. Może się zdarzyć, że wczytywane pliki (np. konfiguracja gry) okażą się mieć kodowanie niezgodne z kodowaniem gry (np. pliki w ASCII podczas, gdy gra używa Unicode). Należy wtedy rozpoznać używane przez plik kodowanie i dokonać stosownej konwersji. Sprawa troszkę się komplikuje, gdy gra korzysta z formatów binarnych przechowujących w swej strukturze tekst (na przykład w implementacji VFS). Warto zachować szczególną ostrożność w tych miejscach. Na zakończenie chciałbym tylko wskazać osobom korzystającym z VC++ 2005 Express (w nowych IDE Visual C++ powinno być podobnie), w którym miejscu można włączyć/wyłączyć obsługę Unicode w projekcie. Wybieramy menu Project->[nazwa projektu] Properties (albo wciskamy ALT+F7). Tam z lewej strony rozwijamy Configuration Properties i klikamy na gałęzi General. Z prawej strony pod Project Defaults znajdujemy opcję Character Set. Może ona przyjmować następujące wartości: Not Set - projekt korzysta z kodowania ASCII, Use Unicode Character Set - projekt korzysta z Unicode, Use Multi-Byte Character Set - projekt korzysta z kodowania multibajtowego (to kodowanie to już osobny temat). Mam nadzieję, że te kilka rad pomoże osobom, które zastanawiają się nad zapewnieniem wsparcia dla Unicode w swoich projektach. Polecam też zapoznać się z tekstem na Warsztatowym Wiki dotyczącym łańcuchów znakowych w C++, w tym m.in. Unikodowych.