Введение Приветствую вас, дорогие читатели.
В данной статье речь пойдет о создании системы локализации приложений, созданных в среде Юнити3D , основанный на использовании класса СкриптаблеОбъект , который позволяет локализовать не только текст, но также звуки и изображения, а также загружать такие данные извне.
По традиции, прежде чем начать описывать детали, остановимся на том, что такое локализация и зачем она нам нужна.
Очень часто и почти всегда разработка игр (и любых других приложений) ориентирована более чем на один рынок.
Поскольку для каждого рынка характерна своя языковая группа, разработчикам приходится это учитывать, ведь если делать игру только на русском языке, то англоязычные пользователи просто ничего не поймут. Что делать? Правильно, игра должна поддерживать несколько языков.
В большинстве случаев переводятся только текстовые данные и часто используются для этой цели.
Google Таблицы или что-то подобное.
Это довольно просто и гибко, поскольку импорт из таблиц не представляет затруднений.
Однако не все так радужно, как может показаться на первый взгляд. Что делать, если в игре много озвучки? Или текст должен иметь разный шрифт для разных языков? И наконец, есть ли еще и текст или что-то требует уникальности языка в изображениях? В этих случаях таблиц уже недостаточно.
Так что же делать, спросите вы (если, конечно, вы уже не знаете ответ)? Я придумал вариант использования СкриптаблеОбъект И AssetBundle .
Первый дает нам возможность хранить данные в виде Объект ’a, а второй — загружать и хранить эти данные извне.
Рассмотрим подробнее, в чем заключается предлагаемый подход.
Как хранить данные
Для начала определимся, что и в каком виде необходимо хранить, для этого будем двигаться от общего к частному.Основные данные, которые мы должны получить от любой системы локализации, — это список поддерживаемых ею языков.
Примечание : по ходу статьи я буду формировать необходимые классы и описывать их.
Итак, языки:
Название поддерживаемого языка можно локализовать и использовать для отображения в интерфейсе.public class LocalizationData : ScriptableObject { public List<LanguageData> Languages; } [Serializable] public class LanguageData { public string Name { get { return _name; } } [SerializeField] private string _name; }
Как видно Данные локализации является наследником СкриптаблеОбъект , по сути, этот наш класс является основным хранилищем данных, которые будут в проекте в виде Объект 'А.
Что дальше? А дальше нам нужно хранить набор ресурсов для каждого языка, конечные данные, которые будут использоваться в приложении или игре.
Для начала давайте определим типы ресурсов, которые мы будем использовать, и создадим для них перечисление ( перечисление ): public enum LocalizationResourceType
{
Text,
Image,
Texture,
Audio
}
Изображение - Этот Спрайт для использования в интерфейсе на основе графического интерфейса Unity или в 2D-играх.
Почему он отделен от Текстура ? Просто ради удобства.
Теперь определимся с местом, где мы будем непосредственно хранить ссылки на ресурсы.
[Serializable]
public class LocalizationResource
{
public string Tag
{
get
{
return _tag;
}
}
public string StringData
{
get
{
return _stringData;
}
}
public Font FontData
{
get
{
return _fontData;
}
}
public Sprite SpriteData
{
get
{
return _spriteData;
}
}
public Texture TextureData
{
get
{
return _textureData;
}
}
public AudioClip AudioData
{
get
{
return _audioData;
}
}
[SerializeField]
private string _tag;
[SerializeField]
private string _stringData;
[SerializeField]
private Font _fontData;
[SerializeField]
private Sprite _spriteData;
[SerializeField]
private Texture _textureData;
[SerializeField]
private AudioClip _audioData;
}
Как видите, класс содержит ссылки на все возможные типы ресурсов, но не пугайтесь, на самом деле действительна только одна из этих ссылок (хотя, конечно, ничто не мешает вам написать код так, чтобы ресурс был сборным).
).
Единственными исключениями являются текст и шрифт; они могут существовать вместе.
Обеспечение такого поведения осуществляется на уровне редактора данных (об этом будет сказано ниже).
Помимо прочего, здесь также указывается тег, которому принадлежат ресурсы.
Что такое тег, будет описано ниже.
Давайте изменим наш класс Языковые данные учитывая вышеизложенное.
[Serializable]
public class LanguageData
{
public string Name
{
get
{
return _name;
}
}
public List<LocalizationResource> Resources;
[SerializeField]
private string _name;
}
Последней задачей хранилища данных локализации является интерпретация и идентификация ресурсов независимо от языка.
Решить эту проблему можно введением в систему тегов, которые будут храниться независимо и позволят решать проблемы.
Мы опишем это на занятии.
[Serializable]
public class LocalizationTag
{
public string Name
{
get
{
return _name;
}
}
public LocalizationResourceType ResourceType
{
get
{
return _resourceType;
}
}
[SerializeField]
private string _name;
[SerializeField]
private LocalizationResourceType _resourceType;
}
Как видите, тег — это имя, которое будет использоваться для идентификации ресурса в системе, и тип ресурса для его интерпретации в окончательные данные.
Таким образом, хранилище данных примет следующий вид. public class LocalizationData : ScriptableObject
{
public List<LanguageData> Languages;
public List<LocalizationTag> Tags;
}
Примечание : несмотря на то, что Данные локализации хранит список языков, это не обязательно.
Каждый язык может быть сохранен в своем собственном Объект 'е.
При таком подходе языки можно загружать по запросу пользователя с сервера.
редактор
Мы создали представление для хранения данных локализации, теперь нам нужен инструмент, который позволит нам создавать эти данные.Я не буду приводить здесь полный код редактора, так как то, как это сделать, зависит от потребностей команды и критериев удобства, которые достаточно субъективны.
В моей версии всё довольно примитивно и отвечает текущим задачам команды.
Сначала нам нужно создать Объект на основе класса, описанного выше Данные локализации .
Это можно сделать двумя способами:
- За счет использования статической функции и атрибута Пункт меню
- Через атрибут СоздатьАсетМеню применяется непосредственно к классу-потомку СкриптаблеОбъект
[MenuItem("Assets/Create/Localization Data")]
public static void CreateLocalizationDataAsset()
{
var selectionPath = AssetDatabase.GetAssetPath(Selection.activeObject);
if (string.IsNullOrEmpty(selectionPath))
{
selectionPath = Application.dataPath;
}
var path = EditorUtility.SaveFilePanelInProject(
"Create Localization Data",
"NewLocalizationData",
"asset",
string.Empty,
selectionPath);
if (path.Length > 0)
{
var asset = ScriptableObject.CreateInstance<LocalizationData>();
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.SaveAssets();
EditorUtility.FocusProjectWindow();
Selection.activeObject = asset;
}
}
После создания Объект ’а он появится в проекте и теперь его можно будет редактировать.
Для этого вам необходимо создать Пользовательский редактор для нашего класса Данные локализации .
Поскольку локализация — это достаточно большой объем данных, их нельзя редактировать непосредственно в инспекторе, но статистическую информацию можно отобразить в следующем виде.
Нажмите здесь на кнопку Открыть окно редактора Откроется окно редактора, где указываются языки, теги и ресурсы.
Сам редактор выглядит так:
Как видите, здесь все довольно просто, но в то же время позволяет быстро редактировать необходимые данные.
Теги и языки редактируются отдельно друг от друга, но если языки уже присутствуют, то при добавлении нового тега к каждому добавляется соответствующий ресурс.
Остановлюсь на нескольких важных моментах в редакторе:
- При изменении типа ресурса необходимо не забыть очистить ссылки, если они были, иначе может оказаться, что ресурс будет содержать то, чего не должно быть, а это в свою очередь приведет к увеличению размера.
AssetBundle 'А.
- Текст представлен в очень маленьком окне, в котором его не только неудобно, но и практически невозможно редактировать, поэтому для него необходимо писать отдельный редактор.
Редактор не обязательно должен поддерживать HTML-разметку ( Богатый текст в пределах Юнити3д ), это все необязательно.
Код этого редактора выглядит следующим образом:
public class LocalizationTextEditorWindow : EditorWindow
{
public SerializedProperty CurrentTextProperty;
public Font TextFont;
private GenericMenu _copyPasteMenu;
private GUIStyle _textStyle;
public static void Show(string tag, string language, SerializedProperty textProperty, Font textFont)
{
var instance = (LocalizationTextEditorWindow)EditorWindow.GetWindow(typeof(LocalizationTextEditorWindow), true);
instance.titleContent = new GUIContent("[{0}: {1}]".
Fmt(language, tag), string.Empty);
instance.CurrentTextProperty = textProperty;
instance.TextFont = textFont;
}
private void OnEnable()
{
_copyPasteMenu = new GenericMenu();
_copyPasteMenu.AddItem(new GUIContent("Copy"), false, () =>
{
EditorGUIUtility.systemCopyBuffer = CurrentTextProperty.stringValue;
});
_copyPasteMenu.AddItem(new GUIContent("Paste"), false, () =>
{
CurrentTextProperty.stringValue = EditorGUIUtility.systemCopyBuffer;
CurrentTextProperty.serializedObject.ApplyModifiedProperties();
});
}
private void OnGUI()
{
if (CurrentTextProperty == null) return;
if (_textStyle == null)
{
_textStyle = new GUIStyle(EditorStyles.textArea);
_textStyle.font = TextFont;
}
if (Event.current.type == EventType.MouseDown && Event.current.button == 1)
{
_copyPasteMenu.ShowAsContext();
}
CurrentTextProperty.stringValue = GUI.TextArea(new Rect(0f, 0f, position.width, position.height), CurrentTextProperty.stringValue, _textStyle);
CurrentTextProperty.serializedObject.ApplyModifiedProperties();
}
}
Самое главное в этом коде — возможность копировать и вставлять текст из буфера обмена, в остальном все довольно просто.
API
Прежде чем описывать код системы локализации, который будет использоваться в приложении, определим основные требования, которым он должен соответствовать.На самом деле вопрос весьма субъективный; каждый разработчик представляет свой набор, в зависимости от возможностей и проекта.
Я сформировал для себя следующий список, основываясь на своем опыте:
- Языки должны меняться на лету.
Это означает, что как только пользователь захочет сменить язык, изменения вступят в силу немедленно.
- Данные локализации должны иметь возможность генерироваться из нескольких источников.
Это означает, что их не обязательно хранить в одном Объект ’е.
public class LocalizationController
{
public delegate void LanguageWasChanged();
public static event LanguageWasChanged OnLanguageWasChanged;
}
ЛангаунгБылоИзменено — это событие, на которое подписываются разные подсистемы.
Событие нужно для тех мест, где обновление ресурсов при смене языка не нужно производить автоматически.
Ээкземпляр класса Контроллер локализации можно хранить где угодно и самое главное любым способом, включая вариант Singleton.
Теперь нам нужно создать внутренние хранилища данных, первое — это теги и второе — соответствующие им типы ресурсов: private Dictionary<string, LocalizationResourceType> _resourceTypeByTag = new Dictionary<string, LocalizationResourceType>();
И сами ресурсы: private Dictionary<string, LocalizationResource> _currentResources = new Dictionary<string, LocalizationResource>();
Теперь нам нужна функция, с помощью которой мы будем получать ресурс локализации по тегу.
Это необходимо для получения данных вручную.
public object GetResourceByTag(string tag)
{
if (_resourceTypeByTag.ContainsKey(tag))
{
var resourceType = _resourceTypeByTag[tag];
var resource = _currentResources[tag];
switch (resourceType)
{
case LocalizationResourceType.Text:
return new KeyValuePair<string, Font>(resource.StringData, resource.FontData);
case LocalizationResourceType.Image:
return resource.SpriteData;
case LocalizationResourceType.Texture:
return resource.TextureData;
case LocalizationResourceType.Audio:
return resource.AudioData;
}
}
return null;
}
А как насчет автоматической опции и обновления данных на лету при смене языка? Для этих целей создадим абонентское хранилище и два метода
private Dictionary<string, List<Action<object>>> _tagHandlers = new Dictionary<string, List<Action<object>>>();
public void SubscribeTag(string tag, Action<object> handler)
{
if (!_tagHandlers.ContainsKey(tag))
{
_tagHandlers.Add(tag, new List<Action<object>>());
}
_tagHandlers[tag].
Add(handler);
}
public void UnsubscribeTag(string tag, Action<object> handler)
{
if (_tagHandlers.ContainsKey(tag))
{
var handlers = _tagHandlers[tag];
if (handlers.Contains(handler))
{
handlers.Remove(handler);
}
}
}
Теперь нам нужно добавить методы для установки данных из Asset public void SetLanguage(LanguageData language)
{
ClearResources();
AddResources(language.Resources);
UpdateLocalizeResources();
OnLanguageWasChanged?.
Invoke();
}
public void AddTags(IList<LocalizationTagParameter> tags)
{
for (var i = 0; i < tags.Count; i++)
{
var tag = tags[i];
_resourceTypeByTag.Add(tag.Name, tag.ResourceType);
}
}
public void AddResources(IList<LocalizationResource> resources)
{
foreach (var resource in resources)
{
_currentResources.Add(resource.Tag, resource);
}
}
public void UpdateLocalizeResources()
{
foreach (var tag in _tagHandlers.Keys)
{
var resource = GetResourceByTag(tag);
var handlers = _tagHandlers[tag];
foreach (var handler in handlers)
{
handler(resource);
}
}
}
Метод Добавить теги добавляет теги к существующим в системе.
Метод АддРесаурцес добавляет текущие языковые ресурсы.
Метод ОбновлениеLocalizeResources вызывает методы подписчиков события смены языка.
Последнее, что осталось сделать, это добавить методы очистки данных.
Примечание : Для режима редактора и в методе Добавить теги и в метод АддРесаурцес вы можете/должны вставлять проверки на наличие повторяющихся имен тегов.
Это можно сделать через #if UNITY_EDITOR #endif .
public void ClearResources()
{
_currentResources.Clear();
}
public void Clear()
{
_resourceTypeByTag.Clear();
_currentResources.Clear();
_tagHandlers.Clear();
}
Итак, если посмотреть весь написанный код, то в целом сам базис не представляет никакой сложности, все очень просто.
Однако нам не хватает еще одного, а в частности компонента, который позволит нам обновлять ресурсы по тегу.
[Serializable]
public class LocalizationTagDefinition
{
public string Tag;
private Action<object> _languageChangedHandler;
public void Subsribe (Action<object> handler)
{
_languageChangedHandler = handler;
LocalizationController.SubscribeTag(Tag, handler);
}
public void Unsubscribe()
{
LocalizationController.UnsubscribeTag(Tag, _languageChangedHandler);
}
}
Экземпляр этого класса можно создать в любом скрипте, работающем с интерфейсом или данными, требующими локализации.
Для удобства можно создать для него отдельный редактор для инспектора, используя CustomPropertyDrawer .
Такой редактор может выглядеть примерно так:
Как использовать
Итак, выше описано, как мы храним данные локализации и код, необходимый для работы с ними.Рассмотрим теперь основные сценарии использования описанной системы локализации.
И первый вариант будет, когда у нас будет один набор данных, хранящий несколько языков.
public class GameLocalization : MonoBehaviour
{
public static LocalizationController Controller
{
get
{
if (_localizationController == null)
{
_localizationController = new LocalizationController();
}
return _localizationController;
}
}
public LocalizationData DefaultLocalization;
public int DefaultLanguage;
private static LocalizationController _localizationController;
void Start()
{
if (DefaultLocalization == null)
{
StartCoroutine(LoadLocalizationData(" http://myserver.ru/localization ", (bundle) =>
{
DefaultLocalization = bundle.LoadAllAssets<LocalizationData>()[0];
Теги: #unity3d #локализация #scriptableobject #расширения редактора Unity #Разработка игр #Разработка игр #unity
-
Компьютерный Сервис Спешит На Помощь
19 Oct, 24 -
Типы Хостинга Серверов
19 Oct, 24 -
Не Катайтесь На Космическом Лифте
19 Oct, 24 -
Глюк С «Новым» Блогом, Который Стал «Старым»
19 Oct, 24