Unity3D用のScriptableObjectベースのローカライズ

はじめに



読者の皆様、ご挨拶。 この記事では、 Unity3D環境で作成されたアプリケーションのローカリゼーションシステムの作成について説明します。このシステムは、 ScriptableObjectクラスの使用に基づいており、テキストだけでなく音声や画像もローカライズし、外部からそのようなデータを読み込むことができます。



伝統によれば、詳細の説明に進む前に、ローカライズとは何か、なぜローカライズが必要なのかについて説明します。



非常に頻繁に、そしてほとんどの場合、ゲーム開発(およびその他のアプリケーション)は複数の市場に焦点を当てています。 各市場は独自の言語グループによって特徴付けられているため、開発者はこれを考慮に入れる必要があります。なぜなら、ゲームをロシア語のみで作成すると、英語を話すユーザーは単に何も理解できないからです。 どうする そうです、ゲーム内の複数の言語のサポートを提供する必要があります。 ほとんどの場合、テキストデータのみが翻訳され、これにはGoogleスプレッドシートなどがよく使用されます。 テーブルからのインポートは難しくないため、非常にシンプルで柔軟です。 しかし、すべてが一見バラ色に見えるわけではありません。 ゲームに多くの音声ガイダンスがある場合はどうなりますか? または、テキストは言語ごとに異なるフォントを使用する必要がありますか? 最後に、画像内の言語に一意性を必要とするテキストや何かがありますか? これらの場合、テーブルはもはや十分ではありません。



それで、あなたは何を尋ねますか(もちろん、あなたがすでに答えを知っているのでない限り)? ScriptableObjectAssetBundleのユースケースを 思いつきました 。 1つ目はデータをAssetの形式で保存する機能を提供し、2つ目はこのデータを外部からロードして保存する機能を提供します。



提案されたアプローチが何であるかをより詳細に検討しましょう。



データを保存する方法



まず、何をどの形式で保存する必要があるかを判断します。そのために、一般から特定に移行します。 ローカリゼーションシステムから取得する必要がある基本データは、サポートされている言語のリストです。



:記事を読み進めながら、必要なクラスを作成して説明します。 したがって、言語:



public class LocalizationData : ScriptableObject { public List<LanguageData> Languages; } [Serializable] public class LanguageData { public string Name { get { return _name; } } [SerializeField] private string _name; }
      
      





サポートされている言語の名前は、ローカライズされた形式で使用し、インターフェイスでの出力に使用できます。 ご覧のとおり、 LocalizationDataScriptableObjectの後継であり、実際、このクラスはメインデータウェアハウスであり、これはAssetの形式でプロジェクトに含まれます。



次は? そして、各言語について、アプリケーションまたはゲームで使用される最終データであるリソースのセットを保存する必要があります。 まず、使用するリソースのタイプを決定し、列挙します( enum ):



 public enum LocalizationResourceType { Text, Image, Texture, Audio }
      
      





画像は、Unity GUIベースのインターフェイスまたは2Dゲームで使用するスプライトです。 なぜTextureと分離されているのですか? 利便性のためだけに。



ここで、リソースへのリンクを直接保存する場所を決定します。
 [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; }
      
      







ご覧のように、クラスにはすべての可能なタイプのリソースへのリンクが含まれていますが、心配する必要はありません。実際には、これらのリンクの1つだけが有効です(もちろん、リソースを結合するためにコードを書くことを妨げるものはありません)。 唯一の例外はテキストとフォントであり、それらは一緒に存在できます。 この動作がデータエディターのレベルに到達するようにします(これについては以下で説明します)。 特に、リソースが属するタグもここに示されます。 タグとは次のとおりです。 上記を考慮して、 LanguageDataクラスを変更しましょう。



 [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; }
      
      





LocalizationDataには言語のリストが格納されているという事実にもかかわらず、それを行う義務はありません。 各言語はAssetに保存できます。 このアプローチでは、サーバーからのユーザーの要求に応じて言語をダウンロードできます。



エディター



ローカリゼーションデータを保存するためのビューを作成しましたが、このデータを作成できるツールが必要になりました。 ここでは、エディターの完全なコードは提供しません。その方法は、チームのニーズと利便性の基準に依存するため、非常に主観的です。 私のバージョンでは、すべてが非常に原始的であり、チームの現在のタスクを満たしています。



まず、上記のLocalizationDataクラスに基づいてAssetを作成する必要があります。 これを行うには2つの方法があります。



  1. 静的関数とMenuItem属性を使用して
  2. ScriptableObjectの下位クラスに直接適用されるCreateAssetMenu属性を介して


最初のオプションを使用しましたが、実際には違いはありません。



ローカリゼーションデータのアセットを作成する機能は次のとおりです。
 [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; } }
      
      







アセットを作成すると彼はプロジェクトに表示され、編集できるようになります。 これを行うには、 LocalizationDataクラスのCustomEditorを作成します。 ローカリゼーションはかなり大量のデータであるため、インスペクターで直接編集することはできませんが、統計情報は次の形式で表示できます。



画像



ここで、 [エディタウィンドウ開く ]ボタンを使用すると、言語、タグ、およびリソースが設定されているエディタウィンドウが開きます。 エディター自体は次のとおりです。



画像



ここでわかるように、すべてが非常に単純ですが、同時に必要なデータをすばやく編集することができます。 タグと言語は互いに別々に編集されますが、言語が既に存在する場合、新しいタグを追加すると、各リソースが追加されます。



エディターのいくつかの重要な点について説明します。



  1. リソースのタイプを変更する場合、リンクが存在する場合はリンクをクリアすることを忘れないでください。そうしないと、リソースに含まれるべきでないものが含まれることになり、これがAssetBundleのサイズの増加につながります。
  2. テキストは非常に小さなウィンドウに表示されますが、それほど不便ではありませんが、編集することはほとんど不可能なので、別のエディターを作成する必要があります。


テキストエディタウィンドウは次のとおりです。



画像



エディターでhtmlマークアップ( Unity3d内のRichText )をサポートすることはできません。これはすべてオプションです。



このエディターのコードは次のとおりです。
 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



アプリケーションで使用されるローカリゼーションシステムのコードを説明する前に、満たす必要がある基本的な要件を定義します。 実際、質問は非常に主観的であり、各開発者は機能とプロジェクトに応じて独自のセットを提示します。 私自身と私の経験に基づいて、次のリストを作成しました。



  1. 言語はその場で変更する必要があります。 つまり、ユーザーが言語を変更するとすぐに、変更がすぐに有効になります。
  2. ローカリゼーションデータは、複数のソースから作成できる必要があります。 つまり、1つのAssetに保存する必要はありません。


これに基づいて、コードの形成を開始し、最初に基本クラスを作成します。



 public class LocalizationController { public delegate void LanguageWasChanged(); public static event LanguageWasChanged OnLanguageWasChanged; }
      
      





LangaungeWasChangedは、さまざまなサブシステムがサブスクライブするイベントです。 このイベントは、言語の変更時にリソースを更新する必要がない場所で必要になります。 LocalizationControllerクラスのインスタンスは、シングルトンバリアントを含め、必要に応じてどこにでも保存できます。



次に、内部データウェアハウスを作成する必要があります。1つ目はタグで、2つ目はそれらに対応するリソースのタイプです。



 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; }
      
      





しかし、言語を変更する際の自動オプションとその場でのデータの更新はどうでしょうか?



これらの目的のために、サブスクライバーリポジトリと2つの方法を開始します
 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); } } }
      
      







次に、アセットからデータを設定するためのメソッドを追加する必要があります
 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); } } }
      
      







AddTagsメソッドは、システム内の既存のタグにタグを追加します。 AddResourcesメソッドは、現在の言語リソースを追加します。 UpdateLocalizeResourcesメソッドは、言語変更イベントのサブスクライバーのメソッドを呼び出します。 最後にやることは、データクリーニングメソッドを追加することです。



AddTagsメソッドとAddResourcesメソッドの両方のエディターモードでは、重複するタグ名のチェックを挿入する必要があります。 これは#if UNITY_EDITOR #endifを使用して実行できます。



 public void ClearResources() { _currentResources.Clear(); } public void Clear() { _resourceTypeByTag.Clear(); _currentResources.Clear(); _tagHandlers.Clear(); }
      
      





したがって、記述されたすべてのコードを見ると、一般に基盤自体は複雑ではなく、すべてが非常に単純です。 ただし、もう1つ、特にタグごとにリソースを更新できるコンポーネントがありません。



 [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を使用して、インスペクター用に別のエディターを作成できます。 このようなエディターは次のようになります。



画像



使い方



そのため、上記では、ローカライズデータとそれらを操作するために必要なコードを保存する方法について説明しました。 ここで、説明したローカリゼーションシステムを使用するための基本的なシナリオを考えてみましょう。



最初のオプションは、複数の言語が保存されている1つのデータセットがある場合のオプションです
 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]; InitLanguage(); bundle.Unload(true); })); }else { InitLanguage(); } } public void ChangeLanguage(int languageId) { Controller.SetLanguage(DefaultLocalization.Languages[languageId]); } public List<string> GetLanguages() { var languages = new List<string>(); for (var i = 0; i < DefaultLocalization.Languages.Count; i++) { languages.Add(DefaultLocalization.Languages[i].Name); } return languages; } IEnumerator LoadLocalizationData(string url, Action<AssetBundle> result) { var request = UnityWebRequestAssetBundle.GetAssetBundle(url); yield return request.SendWebRequest(); var assetBundle = DownloadHandlerAssetBundle.GetContent(request); result(assetBundle); request.Dispose(); } private void InitLanguage() { Controller.AddTags(DefaultLocalization.Tags); Controller.SetLanguage(DefaultLocalization.Languages[DefaultLanguage]); } }
      
      







取得するもの:最初に、ローカライズアセットがインストールされているかどうかを確認し、インストールされている場合は、ローカリゼーションコントローラーを初期化し、デフォルトの言語を設定します。そうでない場合は、サーバーからアセットを読み込みます。 言語を設定し、インターフェースに表示する言語のリストを取得するには、2つの方法があります。 SetLanguageメソッドを呼び出すと、タグによるリソース変更のすべてのサブスクライバーが通知を受け取り、リソースを更新します。



次に、ローカライズデータが複数のAssetsに散在している場合のオプションを検討します。



ここでは、前の例からいくつかの方法を変更する必要があります。
 public LocalizationData LocalizationAudio; public LocalizationData LocalizationImage; public LocalizationData LocalizationText; public int DefaultLanguage; void Start() { Controller.AddTags(LocalizationAudio.Tags); Controller.AddTags(LocalizationImage.Tags); Controller.AddTags(LocalizationText.Tags); ChangeLanguage(DefaultLanguage); } public void ChangeLanguage(int languageId) { Controller.ClearResources(); Controller.AddResources(LocalizationAudio.Languages[languageId].Resources); Controller.AddResources(LocalizationImage.Languages[languageId].Resources); Controller.AddResources(LocalizationText.Languages[languageId].Resources); Controller.UpdateLocalizeResources(); }
      
      







説明なしにすべてが明確になっていると思います。以前のようにタグを追加するだけですが、手動モードでリソースを追加し、その後UpdateLocalizeResourceメソッドを呼び出して、タグのすべてのサブスクライバーに通知をトリガーします。



結論として、エンドポイントでリソースとタグを使用すること、つまり コンテンツレベルで、例としてUnity GUIからImageオブジェクトを取得します。



 public class LocalizeImage : MonoBehaviour { public LocalizationTagDefinition ImageTag; private void OnEnable() { ImageTag.Subsribe((data) => { GetComponent<Image>().sprite = data as Sprite; }); } private void OnDisable() { ImageTag.Unsubscribe(); } }
      
      





ここでは、前述のLocalizationTagDefinitionコンポーネントを使用します。 このスクリプトをオブジェクトに掛けることにより、言語が変更された場合に自動的に画像が変更されます。



おわりに



結論として、私の現在の研究におけるこのアプローチの適用は、ローカライズの生活を大いに促進したと言いたいです。 私のプロジェクトが開発されているセグメントでは、さまざまなデータの量が非常に多くなっています。音声、画像、テキストの両方です。 また、ほとんどの言語はメインアプリケーションに含まれておらず、ユーザーの要求に応じてダウンロードされます。 とりわけ、ゲームは言語によって動作が異なる場合があります(これはローカライズテキストデータにjson文字列を追加することで実現されます)。 もちろん、システムは最適ではない可能性があり、コードとエディターの両方の面で開発の余地があります(特に、たとえば、 Googleスプレッドシートからのテキストデータのインポートを追加し、目を楽しませてくれます)が、私のプロジェクトでは今のところ十分です。



最後に、上記のアプローチが使用された小さな例を示します。 これは、ビジュアルロジックエディタのPanthea VSです (イデオロギーのインスピレーションはPlayMakerでした )。






All Articles