Главная » Статьи » Программирование » Unity

Локализация игры Unity

Каждый разработчик игры в процессе разработки или в ее начале приходит к тому, что в его возникает мысль о том, чтобы в его игре было несколько языков.

Скажу сразу сделать это не сложно и есть много способов для этого. Я поделюсь тем способом, который использую чаще всего. Весь процесс доступа к текстам на разных языках в играх у меня состоит из нескольких частей.

Первое это файл с данными для того или иного языка. Обычно все языки я кладу в один файл потому, что не вижу смысла делить на несколько файлов. Смысл в нескольких файлах возможно будет в играх где очень много диалогов и много текста, разделение на несколько файлов в этом случае будет разумным решением потому, что это ускорит получение текста для этого или иного объекта. Я обычно храню все в xml файлах потому, что мне удобнее всего именно в них размещать информацию.

Обычно мой файл выглядит следующим образом:

<Language>

    <Word key="1">

        <Ru>Продолжить</Ru>

        <Eng>Continue</Eng>

    </Word>

</Language>

В моих играх обычно не используется более двух языков. Обычно русский и английский, если вы хотите 10 и более, то лучше придумайте как сопоставить значения переменной «SystemLanguage» с данными в вашем файле. Если в игре не слишком больше количество фраз, я обычно выгружаю большую часть и храню ее в коллекции для ускорения доступа за исключением длинных фраз, состоящих из двух и более предложений их я подгружаю из файла, если они где-то требуются. За загрузку и доступ к данным из файла у меня обычно отвечает класс, прикреплённый к неуничтожаемому объекту, методы класса, за исключением «void Start()», статические. Далее идет пример класса отвечающего за загрузку языка. Я не стал делать объект класса неуничтожаемым или статическим, это дело уже за вами какой способ вам больше нравится. Я бы остановился на первом так как в случае чего объект и класс можно будет убрать из памяти, а если статический сами понимаете от загрузки до окончания работы вашей игры класс и данные, который он в себе содержит будут висеть в памяти. На самом деле процесс загрузки данный из файла занимает не так много времени, поэтому вы можете создать для каждой из сцен свой собственный языковой файл и загружать допустим по названию сцены или ..... В общем решите эту задачу самостоятельно, она не сложная.

    /// <summary>
    /// Класс хранящий данные по словам
    /// </summary>
    public class LangString
    {
        /// <summary>
        /// Ключ
        /// </summary>
        public string key;
        /// <summary>
        /// Значение
        /// </summary>
        public string value;

        /// <summary>
        /// Инициализировать новый экземпляр класса с ключем и значением
        /// </summary>
        /// <param name="key">Key.</param>
        /// <param name="value">Value.</param>
        public LangString(string key, string value)
        {
            this.key = key;
            this.value = value;
        }

        /// <summary>
        /// Инициализивать новый экземпляр класса с ключем, но без значения
        /// </summary>
        /// <param name="key">Key.</param>
        public LangString(string key)
        {
            this.key = key;
            this.value = "";
        }

        /// <summary>
        /// Проверка ключа на равенство
        /// </summary>
        /// <returns><c>true</c>, if key was proved, <c>false</c> otherwise.</returns>
        /// <param name="key">Key.</param>
        public bool provKey(string key)
        {
            return this.key.Equals (key);
        }

        /// <summary>
        /// Является ли данная фраза длинной
        /// </summary>
        /// <returns><c>true</c>, if long word was ised, <c>false</c> otherwise.</returns>
        public bool isLongWord()
        {
            return value == "";
        }
    }

Я в своем примере я храню значения фраз игры вот в таких классах, это удобнее чем хранить в двух массивах (ключи, фразы) потому, что могут возникать сложности в синхронизации и удалении, поэтому, чтобы это избежать я объединяю связанные между собой коллекции в одну, поместив связанные элементы в класс.

    public LangString[] data;
    public SystemLanguage lang;
    public bool langLoadNow = false;

    private XmlDocument getXml()
    {
        TextAsset textAsset = (TextAsset)Resources.Load ("lang.xml");
        XmlDocument doc = new XmlDocument ();
        doc.LoadXml (textAsset.text);
        return doc;
    }

    private void waitLoadLanguage()
    {
        while (langLoadNow == true) {
            System.Threading.Thread.Sleep (300);
        }
    }

    /// <summary>
    /// Метод загружает данные из Xml файла. Вы можете использовать другой формат.
    /// </summary>
    /// <param name="lang">Язык через который грузим</param>
    public void loadLanguageFromFile(SystemLanguage lang)
    {
        this.lang = lang;
        int j = SystemLanguage.Russian == lang ? 0 : 1;
        XmlDocument doc = getXml ();
        System.Threading.Thread th = new System.Threading.Thread (() => {
            waitLoadLanguage();
            langLoadNow = false;
            var list = doc.DocumentElement.ChildNodes;
            int n = list.Count;
            data = new LangString[n];
            for (int i = 0; i < n; i++) {
                var node = list.Item (0);
                string key = node.Attributes.Item (0).Value;
                string word = node.ChildNodes.Item (j).InnerText;
                /*проверяем насколько длинная наша фраза*/
                data [i] = word.Length < 50 ? new LangString (key, word) : new LangString(key);
            }
            langLoadNow = true;
        })
        {
            Name = "Language from file"
        };
        th.Start ();
    }

    /// <summary>
    /// Запускаем через Awake, чтобы начало грузиться до того момента, когда проснуться функции Start() у других объектов на сцене
    /// </summary>
    void Awake()
    {
        int l = PlayerPrefs.GetInt ("Language", (int)Application.systemLanguage);
        SystemLanguage lang = (SystemLanguage)l;
        loadLanguageFromFile (lang);
    }
        
    /// <summary>
    /// Получаем строку из файла в случае, если она длинная и не храниться в памяти
    /// </summary>
    /// <returns>The word from file.</returns>
    /// <param name="index">Index.</param>
    private string getWordFromFile(int index)
    {
        return getXml ().DocumentElement.ChildNodes.Item (index).ChildNodes.Item (SystemLanguage.Russian == lang ? 0 : 1).InnerText;
    }


    /// <summary>
    /// Вернуть слово по ключу
    /// </summary>
    /// <returns>The word.</returns>
    /// <param name="key">Key.</param>
    public string getWord(string key)
    {
        waitLoadLanguage ();
        int n1 = data.Length, n2 = n1 / 2;
        n1 = n1 - n2;
        for (int i = 0, j = data.Length - 1; i < n1 || j >= n2; i++, j--) {
            LangString item = data [i];
            if (item.provKey (key)) {
                if (item.isLongWord () == false)
                    return item.value;
                else
                    getWordFromFile (i);
            }
            item = data [j];
            if (item.provKey (key)) {
                if (item.isLongWord () == false)
                    return item.value;
                else
                    getWordFromFile (j);
            }
        }
        return "Если код попал сюда, то разработчик балбес";
    }

Вот так собственно будет выглядеть сам класс. Тут есть функции для загрузки данных из файла, а так же функции поиска для получения слова по ключу. Как вы видите в качестве ключа для использовал строковые значения для удобства, но вам я рекомендую использовать числовые, чтобы поиск по ключу происходил быстрее. Разбор данных из файла здесь я убрал в отдельный поток, чтобы если файл был большим не было дополнительных фризов в игре, можно конечно использовать "Coroutine", но они мне не нравятся так, как не смотря на асинхронность они работают в основном потоке. Если в "Coroutine" будет какой-то сложный алгоритм требующий мощностей для вычислений, то будет фриз в игре. 

В своих играх я использую реактивные расширения (UniRx), они упрощают многие задачи, и упрощают процесс локализации, но с учетом того, что не каждый их использует я буду покажу классический вариант скрипта, который прикрепляется к текстовому полю.

 

Public UnityEngine.Text textUi;

Public string keyLang;

void Start() {

     textUi.text = LangStatic.GetStr(keyLang);

}

Public setNewText(string key) {

   keyLang = key;

   textUi.text = LangStatic.GetStr(key);

}

 

Это классический вариант без дополнительных потоков. Однако стоит заметить, если у нас ключ будет изменяться, то это будет тормозить основной поток игры, а он занимается графикой, следовательно, частые запросы фраз может затормозить игровой процесс, что будет очень неприятно для любого игрока. Поэтому стоит усложнить наш скрипт добавив в него поток.

Public UnityEngine.Text textUi;

Public string keyLang;

String valueText;

Public bool destroyObj = false, editText = false;

void Start() {;

Thread th = new Thread(()=> {

valueText = LangStatic.getStr(keyLang);

String oldkey = keyLang;

editText = true;

While(destroyObj == false) {

If (oldkey == keyLang) {

Thread.Sleep(TimeSpan.FromSecond(1));

} else {

valueText = LangStatic.getStr(keyLang);

oldkey = keyLang;

editText = true;

}

}

});

th.Start();

}

 

Void Update() {

If (editText == true) {

textUi.text = valueStr;

editText = false;

}

}

 

Void OnDestroy() {

destroyObj = true;

}

Такой способ даст возможность снять нагрузки с основного процесса. Однако, обратите внимание, что в этом фрагменте будет использоваться случайно выделенный поток, поэтому рекомендую выделить для локализации один отдельный поток, чтобы не размножать из, все-таки количество ядер у нас выше потолка не поднимется, а, следовательно, от большого числа потоков смысла не будет, к тому же язык переключают не так часто, что можно обойтись и одним потоком.

Под конец хочу добавить несколько слов. В своем примере по отображению надписей я не упомянул о том, что необходимо добавить еще механизм перезагрузки текстов во всех надписях. Это сделать очень легко просто в функцию смены языка добавить поиск всех объектов содержащих класс, отвечающий за запрос фразы и отображение ее на сцене, в моем случае вызвать функцию старт "Start", которая заново запросит фразу по ключу. Кроме того, в своих примерах я использовал числа для работы с коллекцией SystemLanguage, но можно пойти и другим путем - приравнивать переменные такого типа к типу int или к строке.

Надеюсь данная статья поможет начинающим разработчикам игр.

 

Категория: Unity | Добавил: Алексей (03.01.2017) | Автор: Фролов Алексей Алексеевич E
Просмотров: 852 | Рейтинг: 4.0/1
Всего комментариев: 0
avatar