Здравствуй, игродел!
В этой статье ты узнаешь как сделать удобную локализацию в своей игре.

Проблема:

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

Пути решения:

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

1) Assets, мощный инструмент Unity, позволяющий использовать реализованный кем-то функционал в своём проекте, локализация - не исключение, например simple localization, однако использование такого решения может быть сложным, если проект небольшой и займет больше времени чем собственная реализация, к тому же оно не дает понимания того, как работает локализация в играх.

2) Разные версии игры на разных языках. Сомнительный метод, однако раньше его часто использовали для экономии памяти и упрощения самой игры, однако то время прошло и сейчас дополнительный скрипт и файл в ресурсах игры - мелочь, способная сэкономить массу времени на поиске в коде текста подлежащего переводы.

3) Перевод по файлу, на мой взгляд лучший способ, который мы и реализуем ниже своими руками. Переводчику будет просто работать с одним текстовым файлом.

Решение:

Для начала нам нужно создать csv файл, в котором будем хранить перевод всех текстовых надписей в игре. Сделать это можно в Exсel или просто в блокноте (ведь csv - текстовый файл):

Структура файла такова: первая строка - наименования столбцов, " ; " - разделитель элементов разных столбцов, находящихся на одной строке. id - уникальный индикатор текста, переводы которого хранятся в соответствующих столбцах той же строки.

Когда вы разместите Localization.csv в Resources (там я создал папку "Localization" и уже в неё поместил сsv) своей игры вы увидите, что Unity воспринимает ваш файл как Text Asset, так и должно быть, для него в Unity есть свои методы загрузки, потому нам не придётся заниматься парсингом.

Итак, перед непосредственной локализацией текста нам нужно реализовать выбор языка:

// Language_changer.cs
using UnityEngine;

public class Language_changer : MonoBehaviour
{
    public void Set_RU()
    {
    		// сохраняем пару ключ-значение
        PlayerPrefs.SetString("GameLanguage", "RU");
        // выведем сведетельство того, что игра увидела смену языка
        Debug.Log("Language changed to RUSSIAN");
    }
    public void Set_EN()
    {
        PlayerPrefs.SetString("GameLanguage", "EN");
        Debug.Log("Language changed to ENGLISH");
    }
}

Этот скрипт надо прикрепить к префабу, а его повесить на кнопки, отвечающие за выбор языка:

Наконец можно писать сам скрипт-локализатор, который в последствии мы будем вешать на всё, что имеет поле Text...

// Localizator.cs
using UnityEngine;
using UnityEngine.UI;
using System.Text.RegularExpressions;

public class Localizator : MonoBehaviour{
    public string id;
    void Awake(){  // если язык выбран...
        if (PlayerPrefs.HasKey("GameLanguage")){
            string GameLanguage = PlayerPrefs.GetString("GameLanguage");  //  RU/EN
            change_text(localized_text(id, GameLanguage));
        }else{  // если язык не выбран то английский по умолчанию
            change_text(localized_text(id, "EN"));
        }
    }

    private void change_text(string new_text){
        // вставляем текст в текстовое поле объекта на котором висит скрипт
        GetComponent<Text>().text = new_text;
    }
  
    private string localized_text(string id, string lang){  // вытаскиваем из таблицы значение
      // читаем из Resources/Localization/Localization.csv
      TextAsset mytxtData=(TextAsset)Resources.Load("localization/localization");
        string loc_txt=mytxtData.text;
        string[] rows = loc_txt.Split('\n');
        for (int i = 1; i < rows.Length; i++);
            string[] cuted_row = Regex.Split(rows[i], ";");
            if(id == cuted_row[0]){
                if(lang == "EN"){
                    return cuted_row[1];
                }else if(lang == "RU"){
                    return cuted_row[2];
                }
                break;
            }
        }
        return "translation not found";  // если перевод не найден в таблице
    }
}

Готово! Все что нужно для локализации - просто прикрепить скрипт к объекту (имеющему поле Text), указывать id (публичную переменную скрипта) и внести id с соответствующими переводами в файл локализации.

пример использования
пример использования

Комментарии (14)


  1. ShadowTheAge
    10.12.2021 19:00
    +3

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


    Что если в строке есть символ ";" или "\n"? Что если в строку нужно подставить цифру? Что если язык меняется в процессе игры?


    1. Leopotam
      10.12.2021 19:20
      +1

      Да ладно, достаточно было спросить "что если я поменяю местами столбцы EN и RU".


      1. TimPavlenko Автор
        10.12.2021 19:24
        -1

        Но вы же не меняете местами кнопки на клавиатуре...
        Дурдом будет


        1. Leopotam
          10.12.2021 19:27

          Серьезно, я не могу столбики данных менять местами? А если у меня не будет русской локализации, а будет, допустим французская или немецкая - мне код потребуется переписывать? Это удобная локализация?


          1. TimPavlenko Автор
            10.12.2021 19:34
            -1

            Достаточно вписать в конец Localizator.cs возвращение cuted_row[i], где i - номер столбца в файле перевода.


            1. Leopotam
              10.12.2021 19:37
              +1

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


    1. TimPavlenko Автор
      10.12.2021 19:30

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


  1. wett1988
    10.12.2021 20:27
    +2

    У Unity недавно вышел в релиз официальный пакет для локализации

    https://docs.unity3d.com/Packages/com.unity.localization@1.0/manual/index.html

    Я не знаю насколько он хорош в настоящем продакшене, но выглядит удобно и продуманно.

    На мой взгляд лучше попробовать его, чем отвлекаться от основной разработки на кодинг костылей, которые потом наверняка начнут любить голову.


    1. Suvitruf
      11.12.2021 05:30
      +1

      Пока не очень хорош. Лучше уж I2 Localization использовать.


    1. Ka33yC
      11.12.2021 12:06
      +2

      Я пусть и джун, но решил опробовать этот новый способ локализации. И весь мой опыт хочу изложить. Во-первых, о ней мало где сказано, казалось бы, почему бы не говорить о таком хорошем инструменте на каждом шагу? Потому что он сырой, потому что документация скудная и запутанная. Потому что приложения, который билдились на андроид за 2 минуты стали билдиться по 7-10 минут после установки этого чуда. Во-вторых, там функционал полурабочий. Что я имею ввиду? Например, удобно было бы тексту на английском и русском языках менять шрифты при изменении языка, менять размер шрифта, и т.п. Они это сделали, да. Но. ТОЛЬКО КОГДА ОБЪЕКТ Active. Если он выключен, то текст и остальное внутри не изменится. С этим можно смириться изменением альфы и отлючением блокинг рейкастов(в канвасе, больше нигде не тестил). Но и эксепшены выпадают на каждом шагу, потому что плагин сырой из-за чего он работать нормально не хочет. Там есть костыль - localization String, по-моему и внутри ты указываешь таблицу, по которой происходит локализация и компоненты который изменятся при изменении языка(надеюсь понятно объяснил) ну и соответственно ячейку в этой таблице, тогда всё заработает как надо. Но оно того не стоит, потому что время билда так возросло, потому что не ловимые эксепшены вылязят, потому что ошибки в непонятных местах.
      В общем. Мне не понравилось, не рекомендую.


  1. roman-ilyin
    10.12.2021 20:45

    https://romanilyin.com/unity-localization/ просто оставлю это здесь :)

    Автор, видимо, не работал с реальной локализацией если называет описанное выше — удобным.


  1. Suvitruf
    11.12.2021 05:35
    +1

    1. Погодите-ка, у вас для каждого объекта, которому нужна локализация, обращение к диску и чтение файла (в худшем случае, до самого конца), пока не совпадёт id? Как минимум, лучше считать один раз и положить в хешмепу.
    2. GetComponent нужно один раз в Эвейке брать, а потом к локальной переменной обращаться.
    3. Текст с подстановкой не поддерживается.
    4. Возьмите I2 Localization и не мучайтесь.


  1. Myxach
    13.12.2021 03:42
    +1

    Читать файл не лучше ли сразу? Или при первом использование только открывать.

    Не проще ли сразу при запуска сохранять перевод в двух словарях(Английский и русский естественно), а не каждый раз перечитывать файл?

    И да у тебя в коде, после for двоеточие....


  1. Myxach
    13.12.2021 04:00
    +1

            Dictionary<string, string> locRus;
            Dictionary<string, string> locEng;
            public string id;
            void Awake()
            {  // если язык выбран...
                LoadLocal();
                if (PlayerPrefs.HasKey("GameLanguage"))
                {
                    string GameLanguage = PlayerPrefs.GetString("GameLanguage");  //  RU/EN
                    change_text(localized_text(id, GameLanguage));
                }
                else
                {  // если язык не выбран то английский по умолчанию
                    change_text(localized_text(id, "EN"));
                }
            }
    
            private void change_text(string new_text)
            {
                // вставляем текст в текстовое поле объекта на котором висит скрипт
                GetComponent<Text>().text = new_text;
            }
            private void LoadLocal()
            {
                // вытаскиваем из таблицы значение
                // читаем из Resources/Localization/Localization.csv
          			TextAsset mytxtData=(TextAsset)Resources.Load("localization/localization");
                string loc_txt = mytxtData.text;
                string[] rows = loc_txt.Split('\n');
                locEng = new Dictionary<string, string>();
                locRus = new Dictionary<string, string>();
                for (int i = 1; i < rows.Length; i++)
                {
                    string[] cuted_row = Regex.Split(rows[i], ";");
                    //загружаем английский перевод
                    locEng.Add(cuted_row[0], cuted_row[1]);
                    //русский
    
                    if(cuted_row.Length>=3)
                        locRus.Add(cuted_row[0], cuted_row[2]);
                }
            }
            private string localized_text(string id, string lang)
            {  
                Dictionary<string, string> realLoc;
                if (lang == "EN")
                {
                    realLoc = locEng;
                }
                else if (lang == "RU")
                {
                    realLoc = locRus;
                }
                else return "ERROR load local";
                if (realLoc.ContainsKey(id))
                    return realLoc[id];
                else if (locEng.ContainsKey(id))
                    return locEng[id];
                return "translation not found";  // если перевод не найден в таблице
            }

    Так получше будет, я так думаю