Локализация игрового меню, HUD и т.п.

Итак, очередное развитие темы, на этот раз касательно поддержки нескольких языков в игре. Обращаем ваше внимание, что данный проект заточен для локализации именно игрового меню, HUD и подобного. В отличии от прошлого проекта, на этот раз мы будем работать с ресурсами Unity, а не внешними файлами, что во много лучше и проще. Основной упор был сделан на удобство в использовании, кроме того, появилась возможность делать настраиваемый текст, для конкретного текстового UI компонента. Иначе говоря, если, например, у нас есть текстовое поле, то для него мы можем назначить сразу несколько вариантов значений, а потом менять их в любое время и всё это будет делаться с учетом текущей локали.


Как это работает? Допустим, у нас есть Canvas в котором мы собрали меню для игры, настройки и т.п. На все текстовые UI объекты, которым мы планируемым делать локали, вешаем специальный компонент. Затем после запуска соответствующей функции в редакторе, все эти объекты будут найдены и определены автоматически. На этой основе генерируется XML файл с языком по умолчанию, остается только сделать копии этого файла и перевести внутренний текст на другой язык. Главное условие, целевой Canvas должен всегда присутствовать на сцене, его можно активировать или деактивировать, но он не должен быть удален со сцены. Это как раз актуально для менюшки или HUD, которые нет смысла удалять/создавать каждый раз, когда игрок открывает/закрывает меню.

Если у нас уже есть готовое меню с текстом по умолчанию, на основе которого будет генерироваться файл шаблон. Тогда для начала, в проекте создаем папку Resources, а в ней папку Localization, где будут находится все локали.

Добавим на сцену пустой объект, а на него вешаем следующий скрипт:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.IO;

public class Localization : MonoBehaviour {

	// обязательная сериализация даных полей
	[SerializeField] private Canvas[] canvas; // здесь указываем Canvas, для дочерних текстовых элементов которых, предусмотрена локаль
	[SerializeField] private Dropdown dropdown; // меню для переключения языка
	[SerializeField] private LocalizationComponent[] source; // базовый массив, заполняется автоматически, после генерирования локали

	private TextAsset[] binary;
	private static Localization _internal;

	public static Localization Internal
	{
		get{ return _internal; }
	}

	void Awake()
	{
		_internal = this;
		StartScene();
	}

	public void Custom(int id, int index) // работа с настраиваемым текстом
	{
		foreach(LocalizationComponent t in source)
		{
			if(t.hash == id) t.SetCustom(index);
		}
	}

	void StartScene()
	{
		Load(); // загружаем все локали в массив
		dropdown.value = -1; // выбор локали на старте сцены, для самого первого элемента списка: -1
	}

	void Load()
	{
		binary = Resources.LoadAll<TextAsset>("Localization"); // папка в Resources, где лежат локали
		dropdown.options = new List<Dropdown.OptionData>();

		if(binary.Length == 0)
		{
			ListData("List empty...");
			dropdown.value = -1;
			Debug.Log(this + " файлы не обнаружены.");
			return;
		}

		for(int i = 0; i < binary.Length; i++)
		{
			ListData(binary[i].name);
		}

		dropdown.onValueChanged.AddListener(delegate{Locale();});
	}

	void ListData(string value) // добавление элемента в выпадающее меню (выбор языка)
	{
		Dropdown.OptionData option = new Dropdown.OptionData();
		option.text = value;
		dropdown.options.Add(option);
	}

	int GetInt(string text)
	{
		int value;
		if(int.TryParse(text, out value)) return value;
		return 0;
	}

	void InnerText(int id, string text)
	{
		foreach(LocalizationComponent t in source)
		{
			if(t.hash == id) t.target.text = text;
		}
	}

	void InnerCustomText(int id, string text)
	{
		foreach(LocalizationComponent t in source)
		{
			if(t.hash == id) t.SetCustomLoad(text);
		}
	}

	void Locale() // чтение XML
	{
		XmlTextReader reader = new XmlTextReader(new StringReader(binary[dropdown.value].text));
		while(reader.Read())
		{
			if(reader.IsStartElement("content")) InnerText(GetInt(reader.GetAttribute("id")), reader.ReadString());
			else if(reader.IsStartElement("custom")) InnerCustomText(GetInt(reader.GetAttribute("id")), reader.ReadString());
		}
		reader.Close();
	}
		
	public void SetComponents() // создание шаблона локали, используется только в редакторе
	{
		source = LocalizationGenerator.GenerateLocale(canvas);
	}
}

Собственно он и отвечает за чтение XML и смену языка.

Дополнительно к нему идут пара вспомогательных классов.

Первый, для создания шаблона локали:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.IO;
using System.Linq;

// инструмент для создания файла локали и определения массива, используется только в редакторе
public class LocalizationGenerator {

	public static LocalizationComponent[] GenerateLocale(Canvas[] canvas)
	{
		if(canvas.Length == 0)
		{
			Debug.Log(" неопределен массив Canvas.");
			return null;
		}

		string path = Application.dataPath + "/Resources/Localization/Default.xml";

		List<LocalizationComponent> list = new List<LocalizationComponent>();

		foreach(Canvas target in canvas)
		{
			if(target)
			{
				LocalizationComponent[] comp = target.GetComponentsInChildren<LocalizationComponent>();
				foreach(LocalizationComponent c in comp)
				{
					c.SetComponent();
					if(c.target) list.Add(c);
				}	
			}
		}

		if(list.Count == 0)
		{
			Debug.Log(" указанный Canvas, не содержит дочернего компонента LocalizationComponent.");
			return null;
		}

		LocalizationComponent[] copy = list.ToArray();

		// раздувать файл одинаковыми текстами нет смысла, поэтому
		// убираем из массива элементы с одинаковым хеш кодом
		// из этого получаем новый массив и его сохраняем
		LocalizationComponent[] result = list.Distinct(new HashComparer()).ToArray(); 

		XmlNode userNode;
		XmlAttribute attribute;
		XmlDocument xmlDoc = new XmlDocument();
		XmlDeclaration declaration = xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", null);
		xmlDoc.AppendChild(declaration);
		XmlNode rootNode = xmlDoc.CreateElement("locale");
		xmlDoc.AppendChild(rootNode);

		for(int i = 0; i < result.Length; i++)
		{
			if(result[i].isCustom)
			{
				userNode = xmlDoc.CreateElement("custom");
				userNode.InnerText = result[i].content;
			}
			else
			{
				userNode = xmlDoc.CreateElement("content");
				userNode.InnerText = result[i].target.text;
			}

			attribute = xmlDoc.CreateAttribute("id");
			attribute.Value = result[i].hash.ToString();
			userNode.Attributes.Append(attribute);
			rootNode.AppendChild(userNode);
		}

		xmlDoc.Save(path);

		Debug.Log(" создан фаил локали: " + path);

		return copy;
	}
}

class HashComparer : IEqualityComparer<LocalizationComponent>
{
	public bool Equals(LocalizationComponent x, LocalizationComponent y)
	{
		return x.hash == y.hash;
	}

	public int GetHashCode(LocalizationComponent obj)
	{
		return obj.hash;
	}
}

Второй, для вывода кнопки в инспекторе:

#if UNITY_EDITOR
using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor(typeof(Localization))]

public class LocalizationEditor : Editor {

	public override void OnInspectorGUI()
	{
		DrawDefaultInspector();
		Localization e = (Localization)target;
		GUILayout.Label("Default Locale:", EditorStyles.boldLabel);
		if(GUILayout.Button("Create / Update"))
		{
			e.SetComponents();
		}
	}
}
#endif

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

Теперь, нам еще нужен класс, который будет связывать UI компоненты с функцией смены локали.

Создаем скрипт:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class LocalizationComponent : MonoBehaviour {

	// обязательная сериализация даных полей
	[SerializeField] private Text _target;
	[SerializeField] private int _hash;
	[SerializeField] private bool _isCustom;
	[Header("Custom Field")]
	[SerializeField] private string _content;

	private string[] custom;
	private int last_id;

	public int hash
	{
		get{ return _hash; }
	}

	public Text target
	{
		get{ return _target; }
	}

	public bool isCustom
	{
		get{ return _isCustom; }
	}

	public string content
	{
		get{ return _content; }
	}

	public void SetComponent()
	{
		Text t = GetComponent<Text>();

		if(t == null)
		{
			_target = null;
			_hash = 0;
			_isCustom = false;
		}
		else
		{
			_target = t;

			if(_content != null && _content.Trim().Length > 0)
			{
				_hash = content.GetHashCode();
				_isCustom = true;
			}
			else
			{
				_hash = t.text.GetHashCode();
				_isCustom = false;
			}
		}
	}

	public void SetCustomLoad(string text)
	{
		_content = text;
		custom = text.Split(new char[]{'|'});
		_target.text = custom[last_id];
	}

	public void SetCustom(int index)
	{
		if(index < 0 || index > custom.Length-1) return;
		_target.text = custom[index];
		last_id = index;
	}
}

Его мы добавляем на текст, который хотим переводить.

Локализация игрового меню, HUD и т.п.

Переменные: target, hash, isCustom - заполнять вручную не нужно.

Поговорим подробнее о поле Custom Field.

Представим такую ситуацию, что у нас в меню есть кнопка, которая открывает и закрывает всплывающее окно. Если окно закрыто, то текст у кнопки должен быть "Открыть", если игрок нажал на кнопку, окно появилось и текст должен измениться на "Закрыть". Как решить эту проблему, с учетом того, что у нас может быть много языков в игре? Вот именно для этого и нужно данное поле.

Нам просто нужно написать так: Открыть|Закрыть


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

Когда мы генерируем XML файл, то можем получить примерно такое:

<locale>
  <custom id="519361337">One|Two|Three</custom>
  <content id="138244472">New Game</content>
</locale>

Тег content отвечает за смену значений по id фильтру.
Тег custom по сути тоже самое, но только с первым текстом, который стоит перед символом "|".

Например: One|Two|Three - будет равно числовым значениям: 0, 1, 2.

Если мы хотим изменить текст, то нам нужно отправить идентификатор и соответствующий числовой номер. То есть, для того, чтобы поменять слово "One" на "Two", тогда пишем такой запрос:

Localization.Internal.Custom(519361337, 1);

Просто берем нужный id из XML и номер текста. Всё очень просто.

Разумеется Custom Field не обязательно для заполнения, если нет нужды в настраиваемом тексте, то его следует оставить пустым.

Примечание:
1. Вызов функции смены текста не рекомендуется делать постоянно, например в Update, а вызывать только по необходимости.
2. Если вы делаете какие-либо изменения в меню, связанные с текстом, то необходимо заново генерировать файл шаблона.

Скачать демо:

Внимание! Посетители, находящиеся в группе Гости, не могут скачивать файлы.
Тестировалось на: Unity 5.3.5

Комментариев 25

Офлайн
Light 28 ноября 2018
monstYT, улучшенная версия данных скриптов есть на нашей страничке Рatreon https://www.patreon.com/posts/22814615 функция авто сохранения там встроена.

После оформления подписки, скрипты можно будет там скачать.
Офлайн
monstYT 28 ноября 2018
{text}
Цитата: monstYT
Я новичок, объясните понятным языком, как сделать, чтобы выбранный язык сохранялся через сцены, т. е. в меню ставишь русский, нажимаешь Играть, и загружается сцена с выбранным в меню языком???
P.S.
Как сделать, чтобы выбранный язык сохранялся при выходе и последующем запуске?

Заранее спасибо.

Написал не туда. Имел ввиду это: https://null-code.ru/scripts/104-podderzhka-neskolkih-yazykov-v-igre.html. Но и на этот способ жду ответа. Как локализовать несколько Canvas'ов?
Офлайн
monstYT 27 ноября 2018
Я новичок, объясните понятным языком, как сделать, чтобы выбранный язык сохранялся через сцены, т. е. в меню ставишь русский, нажимаешь Играть, и загружается сцена с выбранным в меню языком???
P.S.
Как сделать, чтобы выбранный язык сохранялся при выходе и последующем запуске?

Заранее спасибо.
Офлайн
Light 16 ноября 2018
PaNDa11v48, сохранять индекс dropdown.value чтобы на старте включать потом нужную локаль из массива по этому индексу.
Офлайн
PaNDa11v48 14 ноября 2018
Код работает, но нужно добавить сохранение языка. Есть советы какие-либо советы?
Офлайн
Conagher 5 июня 2018
Light,
Canvas указан тот. На всех текстах есть LocalizationComponent
Офлайн
Light 4 июня 2018
Conagher, поиск делается стандартными средствами, либо это глюк, либо указан не тот Canvas.
Офлайн
Conagher 4 июня 2018
Light,
Я поместил его на все тексты которые у меня есть
Офлайн
Light 4 июня 2018
Conagher, это компонент который отвечает за локализацию, конечно он должен быть.
Офлайн
Conagher 4 июня 2018
Light,
У меня тут опять проблемка. Указанный Canvas, не содержит дочернего компонента LicalizationComponent.
Офлайн
Danko 25 февраля 2018
Light,
Спасибо
Офлайн
Light 25 февраля 2018
Danko, подключить библиотеку using UnityEngine.UI;
А потом объявить массив public Text[] text;
Офлайн
Danko 25 февраля 2018
Light,
Я конечно понимаю глупый вопрос , но я новичок , как поместить ui текст в массив?
Офлайн
Light 20 мая 2017
Stason4ikRU, пришли ссылку на архив проекта в ЛС (без лишних файлов, музыки и т.п.). Посмотрим что за проблемы с переводом UI.
Офлайн
Stason4ikRU 20 мая 2017
Как сделать что бы текст скрывался в 1 сцене и появлялся во 2.
Офлайн
Stason4ikRU 20 мая 2017
Light,
не работают анимации HUD. Я лучше сделаю так, уберу анимации в отдельный convas и оставлю во 2 сцене (там где им и место). А то что будет переводиться то в 1 сцене.
Офлайн
Light 19 мая 2017
Stason4ikRU, если переводиться в первой, а затем этот объект переносить в другие, то в чем проблема?
Офлайн
Stason4ikRU 19 мая 2017
Переводится только в 1 сцене. Если удалить из 2 сцены HUD и перенести в 1. То он не правильно работает.
Офлайн
Light 18 мая 2017
Stason4ikRU, Convas нужно делать только в первой сцене, а затем просто переносить его в другие https://docs.unity3d.com/ru/current/ScriptReference/Object.DontDestroyonload.html
Офлайн
Stason4ikRU 17 мая 2017
Не работает. Создал Canvas первой сцене ( там где у меня меню) туда запихнул Convas с меню (он тоже на первой сцене) и Convas HUD из второй сцены.Создаю локаль для общего Convas. Запускаю первую сцену меню переводится, нажимаю новая игра HUD не переводится во второй сцене.
Офлайн
Light 15 мая 2017
Stason4ikRU, делать один Canvas с меню и HUD, затем генерировать локаль для него, а уже в игре, включать или отключать, требуемые элементы. То есть, сделать дочерние группы в Canvas, типа HUD и Menu. Получиться один или два Canvas, которые будут кочевать из сцены в сцену, так будет намного проще и удобней.
Офлайн
Stason4ikRU 15 мая 2017
Как сделать чтобы сразу 2 сцены переводились ? Например: 1 Сцена это главное меню. 2 Сцена это HUD уже в игровом процессе.
Офлайн
Light 15 мая 2017
Stason4ikRU, если пишет что не определен, значит так и есть. Проверь массивы и переменные.
Офлайн
Stason4ikRU 14 мая 2017
Нажимаю на Create/update и пишет что не определён массив Canvas. Canvas на сцене есть с Dropdown.
Офлайн
Stason4ikRU 12 мая 2017
как с генерировать XML фай,что бы узнать id ?
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика