Unity3d QuickTip – czyli szybkie porady, rozwiązania częstych problemów i sztuczki w Unity3d!
Dzisiejszy odcinek: Jak stworzyć opcję wyboru języka w grze?
[stextbox id=”info” defcaption=”true”]
Uwaga! Jest to poradnik typu QuickTip. Zatem skupia się on na osiągnięciu założonego celu. Zakładamy że użytkownik zna na tyle program Unity3d, aby samodzielnie wykonać najprostsze czynności, jak np. dodanie modelu kostki do sceny czy dodanie modelowi jakiegoś komponentu. Jeżeli brakuje Ci tej podstawowej wiedzy, zapraszam do tutoriala:
Unity Tutorial – Podstawy
[/stextbox]
Teoria
Teoria jest w tym wypadku bardziej niż oczywista. Granie w gry jest fajnie, a jeszcze fajniejsze jak postaci przemawiają w rodzimym języku, a czytając różne teksty w grze, nie musimy mieć w pogotowiu słownika, żeby móc zdecydować, który miecz jest lepszy.
Metod stworzenia opcji wyboru języka jest wiele, ja na przykładzie samego menu, pokażę jak zrobić to dobrze. Pomysł jest zaczerpnięty z rozwiązania jakie stosuje system Android, więc raczej nas nie zawiedzie.
Nie uwzględniłem tutaj linii dialogowych, jednak system zadziała i z nimi.
Przygotowanie
Do działającego przykładu potrzebujemy tylko:
- Kamery głównej – będzie dostępna po utworzeniu sceny
- Skryptu z tekstami – u mnie jest to: GUIStrings.cs
- Skryptu który zarządza wszystkim – u mnie jest to: Menu.cs
- Plików tłumaczeń – u mnie znajdują się w dodatkowym folderze: Languages i są to: pl.txt i en.txt
- Obiektu typu GUISkin – tak naprawdę jest opcjonalny, ale dodajemy dla formalności
Stworzenie Menu
O samym tworzeniu menu już pisałem, dlatego tą część potraktuje bardzo skrótowo i nie będę jej dogłębnie analizował. Jeśli czegoś nie rozumiesz, przejrzyj podlinkowany tutorial, lub zapytaj w komentarzu.
Zaczynamy od dodania pliku Menu.cs do obiektu MainCamera po czym przejdziemy od edycji pliku Menu.cs. Do stworzenia samego menu, potrzebujemy następujących zmiennych pomocniczych:
private Rect normalizedMenuArea; public GUISkin menuSkin; public Rect menuArea; public Rect optionsButton; public Rect quitButton; public Rect header;
Jak widać, są to te same zmienne, które pojawiły się już w QuickTipie o tworzeniu menu. Po ich deklaracji, możemy wrócić do Unity i poustawiać zmienne, oraz przenieść obiekt GUISkin. U mnie wygląda to tak:
Zmienna normalizedMenuArea jest prywatna i póki co nieustawiona. Chcemy, żeby pozycjonowała nam menu na samym środku. O co zadbamy ponownie kodem ze starego tutoriala w funkcji Start:
void Start() { normalizedMenuArea = new Rect(menuArea.x * Screen.width - (menuArea.width * 0.5f), menuArea.y * Screen.height - (menuArea.height * 0.5f), menuArea.width, menuArea.height); _generateLabels(); }
Funkcja _generateLabels, zostanie omówiona później.
Na koniec zostaje nam samo utworzenie menu, czyli kilku przycisków i nagłówka:
void OnGUI() { GUI.skin = menuSkin; GUI.BeginGroup(normalizedMenuArea); if(currentSite == "MainMenu") { _loadMainMenu(); } if(currentSite == "Options") { _loadOptions(); } GUI.EndGroup(); }
Jak widać wewnątrz funkcji OnGUI, wybraliśmy skórkę dla layoutu, oraz utworzyliśmy grupę z dwoma przyciskami. Każdy z nich po kliknięciu wykonuje funkcję, które również zostaną omówione później.
Łatwo zauważyć też nową zmienną currentSite, co jest idealnym momentem, na przybliżenie reszty pomocniczych zmiennych.
private string currentSite = "MainMenu"; private string language = "pl"; private string[] languages = new string[] {"pl", "en"};
currentSite to zmienna do przechowywania informacji, na jakiej podstronie menu obecnie jesteśmy. Przez to, że funkcja OnGUI, wykonuje się “cały czas” wystarczy, że zmienimy wartość tej zmiennej, by if wewnątrz funkcji OnGUI, sprawił, że wyświetli się inna podstrona menu. Zmienna language, określa jakim językiem posługujemy się obecnie, zaś zmienna languages, zbiera informację o wszystkich dostępnych. Kluczowe przy niej są dwa elementy. Nazwy powinny się pokrywać z nazwami plików tekstowych, oraz pierwszym podanym językiem, powinien być język domyślny, czyli ten, który ustawiliśmy w zmiennej language.
Pliki pomocnicze
Skoro już o nich wspomnieliśmy, warto by je pokazać i powiedzieć po co w ogóle nam one są. Zacznijmy jednak od naszego pliku GUIStrings.cs. Wygląda on tak:
public static class GUIStrings { public static string BTN_OPTIONS {get; set;} public static string BTN_QUIT {get; set;} public static string BTN_BACK {get; set;} public static string HEADER_MAIN_MENU {get; set;} public static string HEADER_OPTIONS {get; set;} public static string LANGUAGE_PL {get; set;} public static string LANGUAGE_EN {get; set;} }
Jest to statyczna klasa, zawierająca jedynie publiczne statyczne zmienne typu string. Utworzenie etykiet typu BTN, HEADER, LANGUAGE, ma na celu ułatwienie w późniejszym odnalezieniu się w kodzie. Fragment {get; set;} ustawia dla każdej zmiennej mutatory (gettery i settery), co pozwala nam zmieniać ich wartość.
Sama klasa nie dziedziczy po MonoBehaviour i nie posiada żadnych dodatkowych funkcji.
Jak za to wyglądają pliki pl.txt i en.txt?
// pl.txt BTN_OPTIONS;Opcje BTN_QUIT;Wyjście BTN_BACK;Powrót HEADER_MAIN_MENU;Menu Główne HEADER_OPTIONS;Opcje LANGUAGE_PL;Polski LANGUAGE_EN;Angielski // en.txt BTN_OPTIONS;Options BTN_QUIT;Exit BTN_BACK;Back HEADER_MAIN_MENU;Main Menu HEADER_OPTIONS;Options Menu LANGUAGE_PL;Polish LANGUAGE_EN;English
Mamy tutaj tylko dokładnie te same etykiety, które były w naszej statycznej klasie, oraz po średniku wartość dla każdego z nich, w odpowiednim języku, zależnie od wybranego pliku.
Magia zamiany
Jeśli mamy to wszystko, pytanie – jak dokonać zmiany? Wracamy do pliku Menu.cs. Pojawiło się tam wywołanie funkcji _generateLabels. Oto jak ona wygląda:
private void _generateLabels() { try { string line; StreamReader theReader = new StreamReader(Application.dataPath+"/Languages/"+language+".txt", Encoding.UTF8); using(theReader) { do { line = theReader.ReadLine(); if (line != null) { string[] entries = line.Split(';'); typeof(GUIStrings).GetProperty(entries[0]).SetValue(null, entries[1], null); } } while (line != null); theReader.Close(); } } catch (Exception e) { Console.WriteLine("{0}\n", e.Message); } }
Streszczając: funkcja odpowiada za sczytanie zawartości odpowiedniego pliku tekstowego, po czym wprowadzeniu jego wartości do naszej klasy statycznej. Teraz po kolei:
Najpierw tworzymy StreamReadera. Application.dataPath, prowadzi nas do głównego folderu z assetami, dlatego dokładam do ścieżki mój folder Language, po czym naszą zmienną aktualnie wybranego języka (teraz już wiesz, czemu nazwa zmiennej musiała odpowiadać nazwom plików), po czym dodajemy rozszerzenie. Drugi parametr, to kodowanie. Ustawiamy na UTF-8, dzięki czemu będziemy mieli polskie znaki.
Formuła using(theReader) powoduje, że czyścimy całą zarezerwowaną przez reader pamięć, zaraz po tym jak wykona się blok kodu w klamrach. Jest to bardzo przydatne, ponieważ dbamy o to, żeby nie zapchać pamięci komputera. Oczywiście Garbage Collector wyczyściłby po nas pamięć, jednak zrobiłby to nie wiadomo kiedy i jak. Dzięki tej funkcji wiemy, że pamięć została oczyszczona i gra nie pożre masy zasobów.
Następnie w pętli odczytujemy nasz plik linijka po linijce. Każdą z linijek dzielimy wg. znaku średnika. Po czym korzystając z bardzo dziwnej i rozbudowanej konstrukcji, wstawiamy pobraną wartość do odpowiedniej zmiennej klasy GUIStrings. Jeżeli znacie PHP, to linijka:
typeof(GUIStrings).GetProperty(entries[0]).SetValue(null, entries[1], null);
Będzie odpowiednikiem dla:
$GUIStrings->$entries[0] = $entries[1];
Oczywiście cały kod wstawiamy w blok try-catch, gdyby wystąpiły jakieś wyjątki.
Zostały nam dwie funkcje, których kodu jeszcze nie znamy:
private void _loadMainMenu() { GUI.Label(header, GUIStrings.HEADER_MAIN_MENU); if(GUI.Button(new Rect(optionsButton), GUIStrings.BTN_OPTIONS)) { currentSite = "Options"; } if(GUI.Button(new Rect(quitButton), GUIStrings.BTN_QUIT)) { Debug.Log("Quit!"); } }
Tutaj mamy po prostu narysowanie przycisków w głównym menu. Zrobione w oddzielnej funkcji, tylko dla zachowania porządku. Można tutaj zaobserwować, jak proste, przyjemne i czytelne, jest teraz użycie zmiennej, która “automatycznie” dobierze dla siebie odpowiedni język. Wystarczy odwołanie do GUIStrings, gdzie po kropce wybieramy zmienną. Teraz widać, dlaczego przedrostki BTN czy HEADER były istotne. Jeśli piszesz ten kod i wpiszesz “GUIStrings.BTN_”, wyświetlą Ci się wszystkie dostępne opisy dla przycisków.
Ostatnia funkcja, jest głównie odpowiedzialna za zmianę języka:
private void _loadOptions() { GUI.Label(header, GUIStrings.HEADER_OPTIONS); string[] selStrings = new string[] {GUIStrings.LANGUAGE_PL, GUIStrings.LANGUAGE_EN}; for(int i = 0 ; i < languages.Length ; i++) { float left; float width = 145; float top = (1 + Mathf.Floor(i / 2)) * 40; float height = 30; if(i % 2 == 0) { left = 0; } else { left = normalizedMenuArea.width / 2 + 5; } if(GUI.Button(new Rect(left, top, width, height), selStrings[i])) { language = languages[i]; _generateLabels(); } } if(GUI.Button(new Rect(quitButton), GUIStrings.BTN_BACK)) { currentSite = "MainMenu"; } }
Oczywiście mamy tutaj ponownie wyświetlenie przycisku powrotnego, oraz nagłówka. Jednak bardziej kluczowy jest inny kod, który pozwolę sobie wyróżnić:
string[] selStrings = new string[] {GUIStrings.LANGUAGE_PL, GUIStrings.LANGUAGE_EN}; for(int i = 0 ; i < languages.Length ; i++) { float left; float width = 145; float top = (1 + Mathf.Floor(i / 2)) * 40; float height = 30; if(i % 2 == 0) { left = 0; } else { left = normalizedMenuArea.width / 2 + 5; } if(GUI.Button(new Rect(left, top, width, height), selStrings[i])) { language = languages[i]; _generateLabels(); } }
Pierwszy krok to stworzenie tabeli, gdzie posiadamy pełne nazwy języków. Pobieram je z pliku tekstowego, tak by również były przetłumaczone. Kluczowe tutaj jest to, by języki były w tej samej kolejności co te w zmiennej languages, oraz by było ich tyle samo.
Następnie pojawia się pętla, która wykona się dla każdego języka ze zmiennej languages. Masa skomplikowanych obliczeń ma na celu umiejscowienie przycisków.
- Width – Posiadają stałą szerokość 145 – co stanowi połowę szerokości normalizedMenuArea minus 5 (żeby zrobić przerwę pomiędzy przyciskami – będą pojawiać się w 2 kolumnach)
- Height – Wysokość również jest stała i wynosi 30 – czyli tyle co inne przyciski
- Left – Położenie względem lewej krawędzi ma 2 opcje. 0 – czyli przy krawędzi dla lewych przycisków. 155 – czyli na środku + 5 dla prawych. Z którym przyciskiem mamy do czynienia, określamy na podstawie dzielenia modulo przez 2.
- Top – Położenie od góry. Co dwa przyciski skaczemy o 40 (wysokość poprzedniej pary + 10 przerwy). Musimy dodać jeden po dzieleniu, by uwzględnić nagłówek.
Na koniec zostaje wstawianie przycisków, które otrzymują parametry położenia, oraz opis z naszej pomocniczej, lokalnej tabeli. Zaś po kliknięciu przycisku, zmieniamy wartość zmiennej language, na język odpowiadający wybranemu za pomocą przycisku (teraz widać, dlaczego kluczowa była kolejność w obu tabelach!) Na koniec wykonujemy znaną nam już funkcję, która podmieni wartości stringów, na te, odpowiednie dla wybranego języka.
Ostatnie co musimy zrobić, by całość mogła działać, to dodać kilka importów na samej górze. Poprawna ich lista wygląda następująco:
using UnityEngine; using System.Collections; using System.Text; using System.IO; using System;
I w sumie to tyle. Sposób wygodny dla programisty (łatwe etykiety), nie obciążający komputera (większe obliczenia robione są tylko w momencie zmiany języka), łatwy dla tłumacza (musi tylko dołożyć nowy plik tekstowy i zmieniać odpowiednio po etykietowane linijki tekstu), prosty w rozwoju (wystarczy dodać po linijce do statycznej klasy i do każdego pliku tekstowego).
Download – Gotowe skrypty
[to_like]
[/to_like]