Здравствуй, игродел!
В этой статье ты узнаешь как сделать удобную локализацию в своей игре.
Проблема:
Язык - пожалуй, один из главных порогов для игроков, если ваша игра поддерживает английский, многие смогут понимать, что в ней происходит, но как быть с аудиторией не владеющей этим языком? Да и играть в игру с поддержкой родного языка всегда приятнее.
Для малоопытного разработчика может быть весьма неочевидно, как реализовать многоязычность своей игры, порой этот вопрос возникает, когда часть игры уже закончена, в таком случаи система локализации должна быть "надстройкой" к уже существующей базе кода, а не перестраивать её в связи с новой задачей.
Пути решения:
Как и с любой задачей в программировании игры, мы можем найти массу путей решения, так и здесь.
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)
wett1988
10.12.2021 20:27+2У Unity недавно вышел в релиз официальный пакет для локализации
https://docs.unity3d.com/Packages/com.unity.localization@1.0/manual/index.html
Я не знаю насколько он хорош в настоящем продакшене, но выглядит удобно и продуманно.
На мой взгляд лучше попробовать его, чем отвлекаться от основной разработки на кодинг костылей, которые потом наверняка начнут любить голову.Ka33yC
11.12.2021 12:06+2Я пусть и джун, но решил опробовать этот новый способ локализации. И весь мой опыт хочу изложить. Во-первых, о ней мало где сказано, казалось бы, почему бы не говорить о таком хорошем инструменте на каждом шагу? Потому что он сырой, потому что документация скудная и запутанная. Потому что приложения, который билдились на андроид за 2 минуты стали билдиться по 7-10 минут после установки этого чуда. Во-вторых, там функционал полурабочий. Что я имею ввиду? Например, удобно было бы тексту на английском и русском языках менять шрифты при изменении языка, менять размер шрифта, и т.п. Они это сделали, да. Но. ТОЛЬКО КОГДА ОБЪЕКТ Active. Если он выключен, то текст и остальное внутри не изменится. С этим можно смириться изменением альфы и отлючением блокинг рейкастов(в канвасе, больше нигде не тестил). Но и эксепшены выпадают на каждом шагу, потому что плагин сырой из-за чего он работать нормально не хочет. Там есть костыль - localization String, по-моему и внутри ты указываешь таблицу, по которой происходит локализация и компоненты который изменятся при изменении языка(надеюсь понятно объяснил) ну и соответственно ячейку в этой таблице, тогда всё заработает как надо. Но оно того не стоит, потому что время билда так возросло, потому что не ловимые эксепшены вылязят, потому что ошибки в непонятных местах.
В общем. Мне не понравилось, не рекомендую.
roman-ilyin
10.12.2021 20:45https://romanilyin.com/unity-localization/ просто оставлю это здесь :)
Автор, видимо, не работал с реальной локализацией если называет описанное выше — удобным.
Suvitruf
11.12.2021 05:35+1- Погодите-ка, у вас для каждого объекта, которому нужна локализация, обращение к диску и чтение файла (в худшем случае, до самого конца), пока не совпадёт id? Как минимум, лучше считать один раз и положить в хешмепу.
- GetComponent нужно один раз в Эвейке брать, а потом к локальной переменной обращаться.
- Текст с подстановкой не поддерживается.
- Возьмите I2 Localization и не мучайтесь.
Myxach
13.12.2021 03:42+1Читать файл не лучше ли сразу? Или при первом использование только открывать.
Не проще ли сразу при запуска сохранять перевод в двух словарях(Английский и русский естественно), а не каждый раз перечитывать файл?
И да у тебя в коде, после for двоеточие....
Myxach
13.12.2021 04:00+1Dictionary<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"; // если перевод не найден в таблице }
Так получше будет, я так думаю
ShadowTheAge
Сделайте файл с локализацией ну хотя бы тысяч на 10 строк, проверьте производительность и число аллокаций, и ужаснитесь.
Что если в строке есть символ ";" или "\n"? Что если в строку нужно подставить цифру? Что если язык меняется в процессе игры?
Leopotam
Да ладно, достаточно было спросить "что если я поменяю местами столбцы EN и RU".
TimPavlenko Автор
Но вы же не меняете местами кнопки на клавиатуре...
Дурдом будет
Leopotam
Серьезно, я не могу столбики данных менять местами? А если у меня не будет русской локализации, а будет, допустим французская или немецкая - мне код потребуется переписывать? Это удобная локализация?
TimPavlenko Автор
Достаточно вписать в конец Localizator.cs возвращение cuted_row[i], где i - номер столбца в файле перевода.
Leopotam
Я согласен, что достаточно взять нормальную реализацию локализации, чтобы она работала, только в статье ее нет, раз ее уже надо подпиливать на самых простых кейсах.
TimPavlenko Автор
При смене языка в процессе он сменится как и надо, но при перезапуске сцены, таким образом в скрипт кнопки смены языка можно запихать и перезапуск сцены.
А вот по поводу производительности вы правы, однако фишка в удобстве и простоте реализации, речь не идет о супер-пупер играх, хотя оптимизацию можно подтянуть уменьшив кол-во обращений к файлу, например разовое чтение всего что необходимо при загрузке сцены.